From fce4dccfb9ac15b215ee4357e7c684bae510f0c2 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Mon, 27 Jan 2025 08:29:59 +0530 Subject: [PATCH 01/14] feat(marketplace): add pagination Signed-off-by: its-mitesh-kumar --- .../plugins/marketplace-backend/src/router.ts | 6 +++++- .../src/api/MarketplaceCatalogClient.ts | 15 ++++++++++++--- .../plugins/marketplace-common/src/types.ts | 17 ++++++++++++++++- .../components/MarketplaceCatalogContent.tsx | 2 +- .../src/components/MarketplaceCatalogGrid.tsx | 4 ++-- .../components/MarketplaceEntryAboutDrawer.tsx | 2 +- .../MarketplaceEntryInstallDrawer.tsx | 2 +- 7 files changed, 38 insertions(+), 10 deletions(-) diff --git a/workspaces/marketplace/plugins/marketplace-backend/src/router.ts b/workspaces/marketplace/plugins/marketplace-backend/src/router.ts index a4ea9ce58..5424fa960 100644 --- a/workspaces/marketplace/plugins/marketplace-backend/src/router.ts +++ b/workspaces/marketplace/plugins/marketplace-backend/src/router.ts @@ -38,7 +38,11 @@ export async function createRouter({ router.use(express.json()); router.get('/plugins', async (_req, res) => { - const plugins = await marketplaceApi.getPlugins(); + const { cursor, limit } = _req.query; + const plugins = await marketplaceApi.getPlugins( + cursor as string | undefined, + limit as string | undefined, + ); res.json(plugins); }); diff --git a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts index 2e722256e..0fcc6027a 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts @@ -23,6 +23,7 @@ import { MarketplaceKinds, MarketplacePlugin, MarketplacePluginList, + MarketplacePluginWithPageInfo, } from '../types'; /** @@ -55,18 +56,26 @@ export class MarketplaceCatalogClient implements MarketplaceApi { }); } - async getPlugins(): Promise { + async getPlugins( + cursor: string, + limit: string, + ): Promise { const token = await this.getServiceToken(); const result = await this.catalog.queryEntities( { filter: { kind: 'plugin', }, + limit: limit ? parseInt(limit as string, 10) : 20, + cursor: cursor as string, }, token, ); - - return result.items as MarketplacePlugin[]; + return { + items: result.items as MarketplacePlugin[], + totalItems: result.totalItems, + pageInfo: result.pageInfo, + }; } async getPluginLists(): Promise { diff --git a/workspaces/marketplace/plugins/marketplace-common/src/types.ts b/workspaces/marketplace/plugins/marketplace-common/src/types.ts index 01d7a9a4f..410a853f6 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/types.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/types.ts @@ -25,6 +25,18 @@ export interface MarketplacePlugin extends Entity { spec?: MarketplacePluginSpec; } +/** + * @public + */ +export interface MarketplacePluginWithPageInfo { + items: MarketplacePlugin[]; + totalItems?: Number; + pageInfo?: { + nextCursor?: string; + prevCursor?: string; + }; +} + /** * @public */ @@ -88,7 +100,10 @@ export interface MarketplacePluginSpec extends JsonObject { * @public */ export interface MarketplaceApi { - getPlugins(): Promise; + getPlugins( + limit?: string, + cursor?: string, + ): Promise; getPluginByName(name: string): Promise; getPluginLists(): Promise; getPluginListByName(name: string): Promise; diff --git a/workspaces/marketplace/plugins/marketplace/src/components/MarketplaceCatalogContent.tsx b/workspaces/marketplace/plugins/marketplace/src/components/MarketplaceCatalogContent.tsx index 79b5d9516..c51c1ebbc 100644 --- a/workspaces/marketplace/plugins/marketplace/src/components/MarketplaceCatalogContent.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/components/MarketplaceCatalogContent.tsx @@ -37,7 +37,7 @@ export const MarketplaceCatalogContent = () => { > All plugins - {plugins.data ? ` (${plugins.data?.length})` : null} + {plugins.data ? ` (${plugins.data?.items?.length})` : null} diff --git a/workspaces/marketplace/plugins/marketplace/src/components/MarketplaceCatalogGrid.tsx b/workspaces/marketplace/plugins/marketplace/src/components/MarketplaceCatalogGrid.tsx index 407a68ad1..5a2b955cf 100644 --- a/workspaces/marketplace/plugins/marketplace/src/components/MarketplaceCatalogGrid.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/components/MarketplaceCatalogGrid.tsx @@ -171,10 +171,10 @@ export const MarketplaceCatalogGrid = () => { const filteredEntries = React.useMemo(() => { if (!search || !plugins.data) { - return plugins.data; + return plugins.data?.items; } const lowerCaseSearch = search.toLocaleLowerCase('en-US'); - return plugins.data.filter(entry => { + return plugins.data.items.filter(entry => { const lowerCaseValue = entry.metadata?.title?.toLocaleLowerCase('en-US'); return lowerCaseValue?.includes(lowerCaseSearch); }); diff --git a/workspaces/marketplace/plugins/marketplace/src/components/MarketplaceEntryAboutDrawer.tsx b/workspaces/marketplace/plugins/marketplace/src/components/MarketplaceEntryAboutDrawer.tsx index 068aa3a8f..28fec9a46 100644 --- a/workspaces/marketplace/plugins/marketplace/src/components/MarketplaceEntryAboutDrawer.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/components/MarketplaceEntryAboutDrawer.tsx @@ -183,7 +183,7 @@ const EntryContent = ({ entry }: { entry: MarketplacePlugin }) => { const Entry = ({ entryName }: { entryName: string }) => { const plugins = usePlugins(); - const entry = plugins.data?.find(e => e.metadata.name === entryName); + const entry = plugins.data?.items?.find(e => e.metadata.name === entryName); if (plugins.isLoading) { return ; diff --git a/workspaces/marketplace/plugins/marketplace/src/components/MarketplaceEntryInstallDrawer.tsx b/workspaces/marketplace/plugins/marketplace/src/components/MarketplaceEntryInstallDrawer.tsx index 514b342d9..4bdffec6f 100644 --- a/workspaces/marketplace/plugins/marketplace/src/components/MarketplaceEntryInstallDrawer.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/components/MarketplaceEntryInstallDrawer.tsx @@ -104,7 +104,7 @@ const EntryContent = ({ entry }: { entry: MarketplacePlugin }) => { const Entry = ({ entryName }: { entryName: string }) => { const plugins = usePlugins(); - const entry = plugins.data?.find(e => e.metadata.name === entryName); + const entry = plugins.data?.items?.find(e => e.metadata.name === entryName); if (plugins.isLoading) { return ; From 98856130acd5f64ef37dc633d40b8d1c1ca76142 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Mon, 27 Jan 2025 12:41:07 +0530 Subject: [PATCH 02/14] feat(marketplace): adding sort , filter Signed-off-by: its-mitesh-kumar --- .../plugins/marketplace-backend/src/router.ts | 16 ++++-- .../src/api/MarketplaceCatalogClient.test.ts | 6 +-- .../src/api/MarketplaceCatalogClient.ts | 52 +++++++++++++------ .../plugins/marketplace-common/src/types.ts | 19 +++++-- .../src/api/MarketplaceBackendClient.tsx | 3 +- .../marketplace/src/hooks/usePlugins.ts | 2 +- 6 files changed, 69 insertions(+), 29 deletions(-) diff --git a/workspaces/marketplace/plugins/marketplace-backend/src/router.ts b/workspaces/marketplace/plugins/marketplace-backend/src/router.ts index 5424fa960..46ff01f35 100644 --- a/workspaces/marketplace/plugins/marketplace-backend/src/router.ts +++ b/workspaces/marketplace/plugins/marketplace-backend/src/router.ts @@ -24,6 +24,7 @@ import { MarketplaceAggregationApi, MarketplaceApi, MarketplaceKinds, + SortOrder, } from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; export async function createRouter({ @@ -38,11 +39,16 @@ export async function createRouter({ router.use(express.json()); router.get('/plugins', async (_req, res) => { - const { cursor, limit } = _req.query; - const plugins = await marketplaceApi.getPlugins( - cursor as string | undefined, - limit as string | undefined, - ); + const { cursor, limit, sortByField, sortOrder, searchText } = _req.query; + const plugins = await marketplaceApi.getPlugins({ + cursor: typeof cursor === 'string' ? cursor : undefined, + limit: typeof limit === 'string' ? limit : undefined, + sortByField: typeof sortByField === 'string' ? sortByField : undefined, + sortOrder: + sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : SortOrder.asc, + searchText: typeof searchText === 'string' ? searchText : undefined, + }); + res.json(plugins); }); diff --git a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.test.ts b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.test.ts index 37adaa44c..51227a606 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.test.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.test.ts @@ -78,7 +78,7 @@ describe('MarketplaceCatalogClient', () => { it('should call queryEntities function', async () => { const api = new MarketplaceCatalogClient(options); - await api.getPlugins(); + await api.getPlugins({}); expect(mockQueryEntities).toHaveBeenCalledTimes(1); expect(mockQueryEntities).toHaveBeenCalledWith( @@ -91,13 +91,13 @@ describe('MarketplaceCatalogClient', () => { it('should return the plugins', async () => { const api = new MarketplaceCatalogClient(options); - const plugins = await api.getPlugins(); + const plugins = await api.getPlugins({}); expect(plugins).toHaveLength(2); }); it('should return the plugins when the auth options is not passed', async () => { const api = new MarketplaceCatalogClient({ ...options, auth: undefined }); - const plugins = await api.getPlugins(); + const plugins = await api.getPlugins({}); expect(plugins).toHaveLength(2); }); }); diff --git a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts index 0fcc6027a..88c5bae07 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts @@ -16,7 +16,7 @@ import { AuthService } from '@backstage/backend-plugin-api'; import { stringifyEntityRef } from '@backstage/catalog-model'; -import { CatalogApi } from '@backstage/catalog-client'; +import { CatalogApi, QueryEntitiesRequest } from '@backstage/catalog-client'; import { NotFoundError } from '@backstage/errors'; import { MarketplaceApi, @@ -24,6 +24,7 @@ import { MarketplacePlugin, MarketplacePluginList, MarketplacePluginWithPageInfo, + SortOrder, } from '../types'; /** @@ -34,6 +35,9 @@ export type MarketplaceCatalogClientOptions = { catalogApi: CatalogApi; }; +type ExtendedQueryEntitiesRequest = QueryEntitiesRequest & { + cursor?: string; // Add cursor as an optional property +}; /** * @public */ @@ -56,21 +60,39 @@ export class MarketplaceCatalogClient implements MarketplaceApi { }); } - async getPlugins( - cursor: string, - limit: string, - ): Promise { + async getPlugins({ + cursor, + limit = '20', // Default value for limit + sortByField = 'metadata.name', // Default value for sortByField + sortOrder = SortOrder.asc, // Default to 'asc' + searchText, + }: { + cursor?: string; + limit?: string; + sortByField?: string; + sortOrder?: 'asc' | 'desc'; // Enforcing correct sortOrder values + searchText?: string; + }): Promise { const token = await this.getServiceToken(); - const result = await this.catalog.queryEntities( - { - filter: { - kind: 'plugin', - }, - limit: limit ? parseInt(limit as string, 10) : 20, - cursor: cursor as string, - }, - token, - ); + + const payload: ExtendedQueryEntitiesRequest = { + filter: { kind: 'plugin' }, + orderFields: { field: sortByField, order: sortOrder }, + limit: parseInt(limit, 10), // Convert limit to number + }; + + // Add optional properties only if they are provided + if (cursor) { + payload.cursor = cursor; + } + + if (searchText) { + payload.fullTextFilter = { + term: searchText, + }; + } + const result = await this.catalog.queryEntities(payload, token); + return { items: result.items as MarketplacePlugin[], totalItems: result.totalItems, diff --git a/workspaces/marketplace/plugins/marketplace-common/src/types.ts b/workspaces/marketplace/plugins/marketplace-common/src/types.ts index 410a853f6..e91184904 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/types.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/types.ts @@ -59,6 +59,14 @@ export enum MarketplaceKinds { pluginList = 'PluginList', } +/** + * @public + */ +export enum SortOrder { + asc = 'asc', + desc = 'desc', +} + /** * @public */ @@ -100,10 +108,13 @@ export interface MarketplacePluginSpec extends JsonObject { * @public */ export interface MarketplaceApi { - getPlugins( - limit?: string, - cursor?: string, - ): Promise; + getPlugins(params: { + limit?: string; + cursor?: string; + sortByField?: string; + sortOrder?: 'asc' | 'desc'; + searchText?: string; + }): Promise; getPluginByName(name: string): Promise; getPluginLists(): Promise; getPluginListByName(name: string): Promise; diff --git a/workspaces/marketplace/plugins/marketplace/src/api/MarketplaceBackendClient.tsx b/workspaces/marketplace/plugins/marketplace/src/api/MarketplaceBackendClient.tsx index 6ab8d2972..c6f86410b 100644 --- a/workspaces/marketplace/plugins/marketplace/src/api/MarketplaceBackendClient.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/api/MarketplaceBackendClient.tsx @@ -20,6 +20,7 @@ import { MarketplaceApi, MarketplacePlugin, MarketplacePluginList, + MarketplacePluginWithPageInfo, } from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; export type MarketplaceBackendClientOptions = { @@ -36,7 +37,7 @@ export class MarketplaceBackendClient implements MarketplaceApi { this.fetchApi = options.fetchApi; } - async getPlugins(): Promise { + async getPlugins(): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('marketplace'); const url = `${baseUrl}/plugins`; diff --git a/workspaces/marketplace/plugins/marketplace/src/hooks/usePlugins.ts b/workspaces/marketplace/plugins/marketplace/src/hooks/usePlugins.ts index 347f2ff43..c25d99f17 100644 --- a/workspaces/marketplace/plugins/marketplace/src/hooks/usePlugins.ts +++ b/workspaces/marketplace/plugins/marketplace/src/hooks/usePlugins.ts @@ -23,6 +23,6 @@ export const usePlugins = () => { const marketplaceApi = useApi(marketplaceApiRef); return useQuery({ queryKey: ['plugins'], - queryFn: () => marketplaceApi.getPlugins(), + queryFn: () => marketplaceApi.getPlugins({}), }); }; From d2f0d8cf3bae7a1cb464b9ed4a4b82c68e62d601 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Mon, 27 Jan 2025 12:49:28 +0530 Subject: [PATCH 03/14] feat(marketplace): adding changeset Signed-off-by: its-mitesh-kumar --- workspaces/marketplace/.changeset/quiet-kiwis-admire.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 workspaces/marketplace/.changeset/quiet-kiwis-admire.md diff --git a/workspaces/marketplace/.changeset/quiet-kiwis-admire.md b/workspaces/marketplace/.changeset/quiet-kiwis-admire.md new file mode 100644 index 000000000..f7c6d9648 --- /dev/null +++ b/workspaces/marketplace/.changeset/quiet-kiwis-admire.md @@ -0,0 +1,7 @@ +--- +'@red-hat-developer-hub/backstage-plugin-marketplace-backend': patch +'@red-hat-developer-hub/backstage-plugin-marketplace-common': patch +'@red-hat-developer-hub/backstage-plugin-marketplace': patch +--- + +add sorting , filtering , pagination in /plugins api From 481db499ab5cfb31bc4a543da12fd87696dc24fa Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Wed, 29 Jan 2025 14:04:47 +0530 Subject: [PATCH 04/14] feat(marketplace): parsing payload in backend Signed-off-by: its-mitesh-kumar --- .../plugins/marketplace-backend/src/router.ts | 59 +++++++++++++++---- .../src/api/MarketplaceCatalogClient.ts | 35 ++--------- .../plugins/marketplace-common/src/types.ts | 33 ++++++++--- 3 files changed, 78 insertions(+), 49 deletions(-) diff --git a/workspaces/marketplace/plugins/marketplace-backend/src/router.ts b/workspaces/marketplace/plugins/marketplace-backend/src/router.ts index 46ff01f35..45d82af84 100644 --- a/workspaces/marketplace/plugins/marketplace-backend/src/router.ts +++ b/workspaces/marketplace/plugins/marketplace-backend/src/router.ts @@ -21,11 +21,13 @@ import { HttpAuthService } from '@backstage/backend-plugin-api'; import { InputError, NotFoundError } from '@backstage/errors'; import { AggregationsSchema, + GetPluginsRequest, MarketplaceAggregationApi, MarketplaceApi, MarketplaceKinds, SortOrder, } from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; +import { QueryEntitiesRequest } from '@backstage/catalog-client/index'; export async function createRouter({ marketplaceApi, @@ -38,18 +40,53 @@ export async function createRouter({ const router = Router(); router.use(express.json()); - router.get('/plugins', async (_req, res) => { - const { cursor, limit, sortByField, sortOrder, searchText } = _req.query; - const plugins = await marketplaceApi.getPlugins({ - cursor: typeof cursor === 'string' ? cursor : undefined, - limit: typeof limit === 'string' ? limit : undefined, - sortByField: typeof sortByField === 'string' ? sortByField : undefined, - sortOrder: - sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : SortOrder.asc, - searchText: typeof searchText === 'string' ? searchText : undefined, + router.get('/plugins', async (req, res) => { + const query = req.query as Partial; + console.log('query1', query.limit); + // Parse and validate query params + const parseJSON = (param: string | undefined, defaultValue: any) => { + console.log('typeof param', param, typeof param); + if (param && typeof param === 'string') { + try { + return JSON.parse(param); + } catch (error) { + throw new Error( + `Invalid parameter ${param}. Must be a valid JSON string.`, + ); + } + } + return defaultValue; + }; + + const orderFields = parseJSON(query.orderFields as unknown as string, [ + { field: 'metadata.name', order: 'asc' }, + ]); + const filter = parseJSON(query.filter as unknown as string, { + kind: 'plugin', }); - - res.json(plugins); + const fullTextFilter = parseJSON( + query.fullTextFilter as unknown as string, + undefined, + ); + + // Construct payload with default values + const payload: QueryEntitiesRequest = { + filter: { kind: 'plugin', ...filter }, // Default to 'plugin' filter + orderFields: orderFields, // Default orderFields + limit: query.limit ? Number(query.limit) : 20, // Default limit to 20 + offset: query.offset ? Number(query.offset) : undefined, // Optional + fullTextFilter: fullTextFilter, // Optional full text search + }; + + // Call the marketplace API with the constructed payload + try { + const plugins = await marketplaceApi.getPlugins(payload); + res.json(plugins); + } catch (error) { + res + .status(500) + .json({ message: 'Failed to fetch plugins', error: error.message }); + } }); router.get('/plugins/:name', async (req, res) => { diff --git a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts index 88c5bae07..de70ad4e6 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts @@ -19,6 +19,7 @@ import { stringifyEntityRef } from '@backstage/catalog-model'; import { CatalogApi, QueryEntitiesRequest } from '@backstage/catalog-client'; import { NotFoundError } from '@backstage/errors'; import { + GetPluginsRequest, MarketplaceApi, MarketplaceKinds, MarketplacePlugin, @@ -60,38 +61,12 @@ export class MarketplaceCatalogClient implements MarketplaceApi { }); } - async getPlugins({ - cursor, - limit = '20', // Default value for limit - sortByField = 'metadata.name', // Default value for sortByField - sortOrder = SortOrder.asc, // Default to 'asc' - searchText, - }: { - cursor?: string; - limit?: string; - sortByField?: string; - sortOrder?: 'asc' | 'desc'; // Enforcing correct sortOrder values - searchText?: string; - }): Promise { + async getPlugins( + query?: GetPluginsRequest, + ): Promise { const token = await this.getServiceToken(); - const payload: ExtendedQueryEntitiesRequest = { - filter: { kind: 'plugin' }, - orderFields: { field: sortByField, order: sortOrder }, - limit: parseInt(limit, 10), // Convert limit to number - }; - - // Add optional properties only if they are provided - if (cursor) { - payload.cursor = cursor; - } - - if (searchText) { - payload.fullTextFilter = { - term: searchText, - }; - } - const result = await this.catalog.queryEntities(payload, token); + const result = await this.catalog.queryEntities(query, token); return { items: result.items as MarketplacePlugin[], diff --git a/workspaces/marketplace/plugins/marketplace-common/src/types.ts b/workspaces/marketplace/plugins/marketplace-common/src/types.ts index e91184904..af644bfa5 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/types.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityFilterQuery } from '@backstage/catalog-client'; +import { EntityFilterQuery, EntityOrderQuery } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/types'; @@ -104,17 +104,34 @@ export interface MarketplacePluginSpec extends JsonObject { appconfig?: string; }; } + +/** + * @public + */ +export type FullTextFilter = { + term: string; + fields?: string[]; +}; + +/** + * @public + */ +export type GetPluginsRequest = { + limit?: number; + offset?: number; + filter?: EntityFilterQuery; + orderFields?: EntityOrderQuery; + fullTextFilter?: { + term: string; + fields?: string[]; + }; +}; + /** * @public */ export interface MarketplaceApi { - getPlugins(params: { - limit?: string; - cursor?: string; - sortByField?: string; - sortOrder?: 'asc' | 'desc'; - searchText?: string; - }): Promise; + getPlugins(query?: GetPluginsRequest): Promise; getPluginByName(name: string): Promise; getPluginLists(): Promise; getPluginListByName(name: string): Promise; From 8e17e8d78ead06e70cc65ed105c423fb97e45cd7 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Fri, 31 Jan 2025 14:42:59 +0530 Subject: [PATCH 05/14] feat(marketplace): displaying query params in the network Signed-off-by: its-mitesh-kumar --- .../plugins/marketplace-backend/src/router.ts | 43 +----- .../src/api/MarketplaceCatalogClient.ts | 11 +- .../plugins/marketplace-common/src/index.ts | 1 + .../plugins/marketplace-common/src/types.ts | 11 +- .../plugins/marketplace-common/src/utils.ts | 136 ++++++++++++++++++ .../src/api/MarketplaceBackendClient.tsx | 9 +- .../marketplace/src/hooks/usePlugins.ts | 12 +- 7 files changed, 164 insertions(+), 59 deletions(-) create mode 100644 workspaces/marketplace/plugins/marketplace-common/src/utils.ts diff --git a/workspaces/marketplace/plugins/marketplace-backend/src/router.ts b/workspaces/marketplace/plugins/marketplace-backend/src/router.ts index 45d82af84..92e3a4860 100644 --- a/workspaces/marketplace/plugins/marketplace-backend/src/router.ts +++ b/workspaces/marketplace/plugins/marketplace-backend/src/router.ts @@ -25,9 +25,7 @@ import { MarketplaceAggregationApi, MarketplaceApi, MarketplaceKinds, - SortOrder, } from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; -import { QueryEntitiesRequest } from '@backstage/catalog-client/index'; export async function createRouter({ marketplaceApi, @@ -41,46 +39,9 @@ export async function createRouter({ router.use(express.json()); router.get('/plugins', async (req, res) => { - const query = req.query as Partial; - console.log('query1', query.limit); - // Parse and validate query params - const parseJSON = (param: string | undefined, defaultValue: any) => { - console.log('typeof param', param, typeof param); - if (param && typeof param === 'string') { - try { - return JSON.parse(param); - } catch (error) { - throw new Error( - `Invalid parameter ${param}. Must be a valid JSON string.`, - ); - } - } - return defaultValue; - }; - - const orderFields = parseJSON(query.orderFields as unknown as string, [ - { field: 'metadata.name', order: 'asc' }, - ]); - const filter = parseJSON(query.filter as unknown as string, { - kind: 'plugin', - }); - const fullTextFilter = parseJSON( - query.fullTextFilter as unknown as string, - undefined, - ); - - // Construct payload with default values - const payload: QueryEntitiesRequest = { - filter: { kind: 'plugin', ...filter }, // Default to 'plugin' filter - orderFields: orderFields, // Default orderFields - limit: query.limit ? Number(query.limit) : 20, // Default limit to 20 - offset: query.offset ? Number(query.offset) : undefined, // Optional - fullTextFilter: fullTextFilter, // Optional full text search - }; - - // Call the marketplace API with the constructed payload try { - const plugins = await marketplaceApi.getPlugins(payload); + const query = req.query as Partial; + const plugins = await marketplaceApi.getPlugins(query); res.json(plugins); } catch (error) { res diff --git a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts index de70ad4e6..eecd358dd 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts @@ -16,7 +16,7 @@ import { AuthService } from '@backstage/backend-plugin-api'; import { stringifyEntityRef } from '@backstage/catalog-model'; -import { CatalogApi, QueryEntitiesRequest } from '@backstage/catalog-client'; +import { CatalogApi } from '@backstage/catalog-client'; import { NotFoundError } from '@backstage/errors'; import { GetPluginsRequest, @@ -25,8 +25,8 @@ import { MarketplacePlugin, MarketplacePluginList, MarketplacePluginWithPageInfo, - SortOrder, } from '../types'; +import { convertGetPluginsRequestToQueryEntitiesRequest } from '../utils'; /** * @public @@ -36,9 +36,6 @@ export type MarketplaceCatalogClientOptions = { catalogApi: CatalogApi; }; -type ExtendedQueryEntitiesRequest = QueryEntitiesRequest & { - cursor?: string; // Add cursor as an optional property -}; /** * @public */ @@ -65,8 +62,8 @@ export class MarketplaceCatalogClient implements MarketplaceApi { query?: GetPluginsRequest, ): Promise { const token = await this.getServiceToken(); - - const result = await this.catalog.queryEntities(query, token); + const payload = convertGetPluginsRequestToQueryEntitiesRequest(query); + const result = await this.catalog.queryEntities(payload, token); return { items: result.items as MarketplacePlugin[], diff --git a/workspaces/marketplace/plugins/marketplace-common/src/index.ts b/workspaces/marketplace/plugins/marketplace-common/src/index.ts index a11375b37..d78a2bd8c 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/index.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/index.ts @@ -22,3 +22,4 @@ export * from './types'; export * from './api'; +export * from './utils'; diff --git a/workspaces/marketplace/plugins/marketplace-common/src/types.ts b/workspaces/marketplace/plugins/marketplace-common/src/types.ts index af644bfa5..7701938a5 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/types.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/types.ts @@ -119,19 +119,18 @@ export type FullTextFilter = { export type GetPluginsRequest = { limit?: number; offset?: number; - filter?: EntityFilterQuery; + filter?: Record; orderFields?: EntityOrderQuery; - fullTextFilter?: { - term: string; - fields?: string[]; - }; + searchTerm?: string; }; /** * @public */ export interface MarketplaceApi { - getPlugins(query?: GetPluginsRequest): Promise; + getPlugins( + request?: GetPluginsRequest, + ): Promise; getPluginByName(name: string): Promise; getPluginLists(): Promise; getPluginListByName(name: string): Promise; diff --git a/workspaces/marketplace/plugins/marketplace-common/src/utils.ts b/workspaces/marketplace/plugins/marketplace-common/src/utils.ts new file mode 100644 index 000000000..ea22c1f16 --- /dev/null +++ b/workspaces/marketplace/plugins/marketplace-common/src/utils.ts @@ -0,0 +1,136 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + EntityFilterQuery, + EntityOrderQuery, + QueryEntitiesRequest, +} from '@backstage/catalog-client/index'; +import { GetPluginsRequest, SortOrder } from './types'; + +const DefaultOrderFields: EntityOrderQuery = [ + { field: 'metadata.name', order: 'asc' }, +]; +const DefaultLimit = 20; +const RequiredFilter = { kind: 'plugin' }; + +export const convertGetPluginsRequestToQueryEntitiesRequest = ( + query?: GetPluginsRequest, +): QueryEntitiesRequest => { + let orderFields: EntityOrderQuery = DefaultOrderFields; + + if (query?.orderFields) { + if (Array.isArray(query.orderFields)) { + orderFields = query.orderFields.map(field => { + if (typeof field === 'string') { + const [key, order] = (field as string).split(','); + return { field: key, order: order as SortOrder }; + } + return field; + }); + } else { + if (typeof query.orderFields === 'string') { + const [key, order] = (query.orderFields as string).split(','); + orderFields = { field: key, order: order as SortOrder }; + } + } + } + + const filter: EntityFilterQuery = query?.filter + ? { + ...(typeof query.filter === 'string' + ? JSON.parse(query.filter) + : query.filter), + ...RequiredFilter, + } + : RequiredFilter; + const payload: QueryEntitiesRequest = { + filter: filter, + orderFields: orderFields, + limit: query?.limit ? Number(query.limit) : DefaultLimit, + offset: query?.offset ? Number(query.offset) : undefined, + }; + if (query?.searchTerm) { + payload.fullTextFilter = { term: query.searchTerm }; + } + return payload; +}; + +export const convertGetPluginRequestToSearchParams = ( + query?: GetPluginsRequest, +): URLSearchParams => { + const params = new URLSearchParams(); + if (query?.limit) params.append('limit', String(query.limit)); + if (query?.offset) params.append('offset', String(query.offset)); + if (query?.searchTerm) params.append('searchTerm', query.searchTerm); + if (query?.orderFields) { + if (Array.isArray(query.orderFields)) { + query.orderFields.forEach(({ field, order }) => { + params.append('orderFields', `${field},${order}`); + }); + } else { + const { field, order } = query.orderFields; + params.append('orderFields', `${field},${order}`); + } + } + if (query?.filter) params.append('filter', JSON.stringify(query.filter)); + return params; +}; + +export const convertSearchParamsToGetPluginsRequest = ( + params?: URLSearchParams, +): GetPluginsRequest => { + const request: GetPluginsRequest = {}; + + // Convert 'limit' parameter + const limit = params?.get('limit'); + if (limit) { + request.limit = Number(limit); + } + + // Convert 'offset' parameter + const offset = params?.get('offset'); + if (offset) { + request.offset = Number(offset); + } + + // Convert 'searchTerm' parameter + const searchTerm = params?.get('searchTerm'); + if (searchTerm) { + request.searchTerm = searchTerm; + } + + const orderFields = params?.getAll('orderFields'); + + if (Array.isArray(orderFields) && orderFields?.length > 0) { + request.orderFields = orderFields.map(field => { + const [key, order] = field.split(','); + return { field: key, order: order as SortOrder }; + }); + } + + const filter: Record = {}; + params?.forEach((value, key) => { + if (key.startsWith('filter[') && key.endsWith(']')) { + const filterKey = key.slice(7, -1); + filter[filterKey] = value; + } + }); + + if (Object.keys(filter).length > 0) { + request.filter = filter; + } + return request; +}; diff --git a/workspaces/marketplace/plugins/marketplace/src/api/MarketplaceBackendClient.tsx b/workspaces/marketplace/plugins/marketplace/src/api/MarketplaceBackendClient.tsx index c6f86410b..5168aef0d 100644 --- a/workspaces/marketplace/plugins/marketplace/src/api/MarketplaceBackendClient.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/api/MarketplaceBackendClient.tsx @@ -17,10 +17,12 @@ import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; import { + GetPluginsRequest, MarketplaceApi, MarketplacePlugin, MarketplacePluginList, MarketplacePluginWithPageInfo, + convertGetPluginRequestToSearchParams, } from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; export type MarketplaceBackendClientOptions = { @@ -37,9 +39,12 @@ export class MarketplaceBackendClient implements MarketplaceApi { this.fetchApi = options.fetchApi; } - async getPlugins(): Promise { + async getPlugins( + request?: GetPluginsRequest, + ): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('marketplace'); - const url = `${baseUrl}/plugins`; + const params = convertGetPluginRequestToSearchParams(request); + const url = `${baseUrl}/plugins?${params.toString()}`; const response = await this.fetchApi.fetch(url); if (!response.ok) { diff --git a/workspaces/marketplace/plugins/marketplace/src/hooks/usePlugins.ts b/workspaces/marketplace/plugins/marketplace/src/hooks/usePlugins.ts index c25d99f17..494388e36 100644 --- a/workspaces/marketplace/plugins/marketplace/src/hooks/usePlugins.ts +++ b/workspaces/marketplace/plugins/marketplace/src/hooks/usePlugins.ts @@ -18,11 +18,17 @@ import { useApi } from '@backstage/core-plugin-api'; import { useQuery } from '@tanstack/react-query'; import { marketplaceApiRef } from '../api'; +import { + GetPluginsRequest, + convertSearchParamsToGetPluginsRequest, +} from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; -export const usePlugins = () => { +export const usePlugins = (params?: URLSearchParams) => { const marketplaceApi = useApi(marketplaceApiRef); + const request: GetPluginsRequest = + convertSearchParamsToGetPluginsRequest(params); return useQuery({ - queryKey: ['plugins'], - queryFn: () => marketplaceApi.getPlugins({}), + queryKey: ['plugins', params], + queryFn: () => marketplaceApi.getPlugins(request), }); }; From 9334e0ddf73dbb6a838d54f162693b0ac0469427 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Fri, 31 Jan 2025 17:24:00 +0530 Subject: [PATCH 06/14] feat(marketplace): handling ts error after merging main Signed-off-by: its-mitesh-kumar --- .../src/api/MarketplaceCatalogClient.test.ts | 6 +++--- .../src/api/MarketplaceCatalogClient.ts | 2 +- .../plugins/marketplace-common/src/index.ts | 2 +- .../plugins/marketplace-common/src/types.ts | 21 ++++--------------- .../src/{utils.ts => util.ts} | 10 ++++----- 5 files changed, 14 insertions(+), 27 deletions(-) rename workspaces/marketplace/plugins/marketplace-common/src/{utils.ts => util.ts} (95%) diff --git a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.test.ts b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.test.ts index 829b01eda..9695a9420 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.test.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.test.ts @@ -80,7 +80,7 @@ describe('MarketplaceCatalogClient', () => { it('should call queryEntities function', async () => { const api = new MarketplaceCatalogClient(options); - await api.getPlugins({}); + await api.getPlugins(); expect(mockQueryEntities).toHaveBeenCalledTimes(1); expect(mockQueryEntities).toHaveBeenCalledWith( @@ -93,13 +93,13 @@ describe('MarketplaceCatalogClient', () => { it('should return the plugins', async () => { const api = new MarketplaceCatalogClient(options); - const plugins = await api.getPlugins({}); + const plugins = await api.getPlugins(); expect(plugins).toHaveLength(2); }); it('should return the plugins when the auth options is not passed', async () => { const api = new MarketplaceCatalogClient({ ...options, auth: undefined }); - const plugins = await api.getPlugins({}); + const plugins = await api.getPlugins(); expect(plugins).toHaveLength(2); }); }); diff --git a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts index 7754dbd29..11dd4a086 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts @@ -30,7 +30,7 @@ import { MarketplacePluginList, MarketplacePluginWithPageInfo, } from '../types'; -import { convertGetPluginsRequestToQueryEntitiesRequest } from '../utils'; +import { convertGetPluginsRequestToQueryEntitiesRequest } from '../util'; /** * @public diff --git a/workspaces/marketplace/plugins/marketplace-common/src/index.ts b/workspaces/marketplace/plugins/marketplace-common/src/index.ts index ad5b66ad2..6466a61cb 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/index.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/index.ts @@ -23,4 +23,4 @@ export * from './types'; export * from './utils'; export * from './api'; -export * from './utils'; +export * from './util'; diff --git a/workspaces/marketplace/plugins/marketplace-common/src/types.ts b/workspaces/marketplace/plugins/marketplace-common/src/types.ts index 925f584d7..cc492ee87 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/types.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/types.ts @@ -15,7 +15,6 @@ */ import { - EntityFilterQuery, EntityOrderQuery, GetEntityFacetsRequest, GetEntityFacetsResponse, @@ -35,7 +34,7 @@ export interface MarketplacePlugin extends Entity { */ export interface MarketplacePluginWithPageInfo { items: MarketplacePlugin[]; - totalItems?: Number; + totalItems?: number; pageInfo?: { nextCursor?: string; prevCursor?: string; @@ -118,7 +117,6 @@ export interface MarketplacePluginSpec extends JsonObject { }; } - /** * @public */ @@ -138,19 +136,6 @@ export type GetPluginsRequest = { searchTerm?: string; }; -/** - * @public - */ -export interface MarketplaceApi { - getPlugins( - request?: GetPluginsRequest, - ): Promise; - getPluginByName(name: string): Promise; - getPluginLists(): Promise; - getPluginListByName(name: string): Promise; - getPluginsByPluginListName(name: string): Promise; -} - /** * @public */ @@ -190,7 +175,9 @@ export type { * @public */ export interface MarketplaceApi { - getPlugins(): Promise; + getPlugins( + request?: GetPluginsRequest, + ): Promise; getPluginByName(name: string): Promise; getPluginLists(): Promise; getPluginListByName(name: string): Promise; diff --git a/workspaces/marketplace/plugins/marketplace-common/src/utils.ts b/workspaces/marketplace/plugins/marketplace-common/src/util.ts similarity index 95% rename from workspaces/marketplace/plugins/marketplace-common/src/utils.ts rename to workspaces/marketplace/plugins/marketplace-common/src/util.ts index ea22c1f16..9eb5db49d 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/utils.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/util.ts @@ -23,8 +23,8 @@ import { GetPluginsRequest, SortOrder } from './types'; const DefaultOrderFields: EntityOrderQuery = [ { field: 'metadata.name', order: 'asc' }, ]; -const DefaultLimit = 20; -const RequiredFilter = { kind: 'plugin' }; +const defaultLimit = 20; +const requiredFilter = { kind: 'plugin' }; export const convertGetPluginsRequestToQueryEntitiesRequest = ( query?: GetPluginsRequest, @@ -53,13 +53,13 @@ export const convertGetPluginsRequestToQueryEntitiesRequest = ( ...(typeof query.filter === 'string' ? JSON.parse(query.filter) : query.filter), - ...RequiredFilter, + ...requiredFilter, } - : RequiredFilter; + : requiredFilter; const payload: QueryEntitiesRequest = { filter: filter, orderFields: orderFields, - limit: query?.limit ? Number(query.limit) : DefaultLimit, + limit: query?.limit ? Number(query.limit) : defaultLimit, offset: query?.offset ? Number(query.offset) : undefined, }; if (query?.searchTerm) { From d3114061ab227169dd133cad5c989efb35c37039 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Fri, 31 Jan 2025 17:32:33 +0530 Subject: [PATCH 07/14] feat(marketplace): resolving comments on PR Signed-off-by: its-mitesh-kumar --- .../plugins/marketplace-backend/src/router.ts | 12 +++--------- .../plugins/marketplace-common/src/types.ts | 3 ++- .../marketplace/src/api/MarketplaceBackendClient.tsx | 3 ++- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/workspaces/marketplace/plugins/marketplace-backend/src/router.ts b/workspaces/marketplace/plugins/marketplace-backend/src/router.ts index fe057e953..39f24006d 100644 --- a/workspaces/marketplace/plugins/marketplace-backend/src/router.ts +++ b/workspaces/marketplace/plugins/marketplace-backend/src/router.ts @@ -38,15 +38,9 @@ export async function createRouter({ router.use(express.json()); router.get('/plugins', async (req, res) => { - try { - const query = req.query as Partial; - const plugins = await marketplaceApi.getPlugins(query); - res.json(plugins); - } catch (error) { - res - .status(500) - .json({ message: 'Failed to fetch plugins', error: error.message }); - } + const query = req.query as Partial; + const plugins = await marketplaceApi.getPlugins(query); + res.json(plugins); }); router.get('/plugins/:name', async (req, res) => { diff --git a/workspaces/marketplace/plugins/marketplace-common/src/types.ts b/workspaces/marketplace/plugins/marketplace-common/src/types.ts index cc492ee87..fdeda2438 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/types.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/types.ts @@ -15,6 +15,7 @@ */ import { + EntityFilterQuery, EntityOrderQuery, GetEntityFacetsRequest, GetEntityFacetsResponse, @@ -131,7 +132,7 @@ export type FullTextFilter = { export type GetPluginsRequest = { limit?: number; offset?: number; - filter?: Record; + filter?: EntityFilterQuery; orderFields?: EntityOrderQuery; searchTerm?: string; }; diff --git a/workspaces/marketplace/plugins/marketplace/src/api/MarketplaceBackendClient.tsx b/workspaces/marketplace/plugins/marketplace/src/api/MarketplaceBackendClient.tsx index 160d255e4..e6daccebf 100644 --- a/workspaces/marketplace/plugins/marketplace/src/api/MarketplaceBackendClient.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/api/MarketplaceBackendClient.tsx @@ -47,7 +47,8 @@ export class MarketplaceBackendClient implements MarketplaceApi { ): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('marketplace'); const params = convertGetPluginRequestToSearchParams(request); - const url = `${baseUrl}/plugins?${params.toString()}`; + const query = params.toString(); + const url = `${baseUrl}/plugins${query ? '?' : ''}${query}`; const response = await this.fetchApi.fetch(url); if (!response.ok) { From 1390685ba103c21903a6024a3805afa5709f45cc Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Fri, 31 Jan 2025 17:54:24 +0530 Subject: [PATCH 08/14] feat(marketplace): updating changeset Signed-off-by: its-mitesh-kumar --- workspaces/marketplace/.changeset/quiet-kiwis-admire.md | 2 +- .../marketplace/plugins/marketplace-common/src/util.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/workspaces/marketplace/.changeset/quiet-kiwis-admire.md b/workspaces/marketplace/.changeset/quiet-kiwis-admire.md index f7c6d9648..b504c0f4d 100644 --- a/workspaces/marketplace/.changeset/quiet-kiwis-admire.md +++ b/workspaces/marketplace/.changeset/quiet-kiwis-admire.md @@ -4,4 +4,4 @@ '@red-hat-developer-hub/backstage-plugin-marketplace': patch --- -add sorting , filtering , pagination in /plugins api +add optional sorting, filtering, pagination to marketplace api diff --git a/workspaces/marketplace/plugins/marketplace-common/src/util.ts b/workspaces/marketplace/plugins/marketplace-common/src/util.ts index 9eb5db49d..c4f0fad8e 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/util.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/util.ts @@ -26,6 +26,9 @@ const DefaultOrderFields: EntityOrderQuery = [ const defaultLimit = 20; const requiredFilter = { kind: 'plugin' }; +/** + * @public + */ export const convertGetPluginsRequestToQueryEntitiesRequest = ( query?: GetPluginsRequest, ): QueryEntitiesRequest => { @@ -68,6 +71,9 @@ export const convertGetPluginsRequestToQueryEntitiesRequest = ( return payload; }; +/** + * @public + */ export const convertGetPluginRequestToSearchParams = ( query?: GetPluginsRequest, ): URLSearchParams => { @@ -89,6 +95,9 @@ export const convertGetPluginRequestToSearchParams = ( return params; }; +/** + * @public + */ export const convertSearchParamsToGetPluginsRequest = ( params?: URLSearchParams, ): GetPluginsRequest => { From b74cb295c0f3730be0e9e8eef032fef09154812b Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Fri, 31 Jan 2025 18:00:52 +0530 Subject: [PATCH 09/14] feat(marketplace): adding api report Signed-off-by: its-mitesh-kumar --- .../plugins/marketplace-common/report.api.md | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/workspaces/marketplace/plugins/marketplace-common/report.api.md b/workspaces/marketplace/plugins/marketplace-common/report.api.md index 5e8a894a1..bc3e372ed 100644 --- a/workspaces/marketplace/plugins/marketplace-common/report.api.md +++ b/workspaces/marketplace/plugins/marketplace-common/report.api.md @@ -9,9 +9,11 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { EntityFilterQuery } from '@backstage/catalog-client'; import { EntityFilterQuery as EntityFilterQuery_2 } from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; +import { EntityOrderQuery } from '@backstage/catalog-client'; import { GetEntityFacetsRequest } from '@backstage/catalog-client'; import { GetEntityFacetsResponse } from '@backstage/catalog-client'; import { JsonObject } from '@backstage/types'; +import { QueryEntitiesRequest } from '@backstage/catalog-client/index'; import { z } from 'zod'; // @public (undocumented) @@ -22,6 +24,15 @@ export interface AppConfigExample extends JsonObject { title: string; } +// @public (undocumented) +export const convertGetPluginRequestToSearchParams: (query?: GetPluginsRequest) => URLSearchParams; + +// @public (undocumented) +export const convertGetPluginsRequestToQueryEntitiesRequest: (query?: GetPluginsRequest) => QueryEntitiesRequest; + +// @public (undocumented) +export const convertSearchParamsToGetPluginsRequest: (params?: URLSearchParams) => GetPluginsRequest; + // @public (undocumented) export const decodeFacetParams: (searchParams: URLSearchParams) => string[]; @@ -63,10 +74,25 @@ export { EntityFilterQuery } // @public (undocumented) export const EntityFilterQuerySchema: z.ZodRecord]>>; +// @public (undocumented) +export type FullTextFilter = { + term: string; + fields?: string[]; +}; + export { GetEntityFacetsRequest } export { GetEntityFacetsResponse } +// @public (undocumented) +export type GetPluginsRequest = { + limit?: number; + offset?: number; + filter?: EntityFilterQuery; + orderFields?: EntityOrderQuery; + searchTerm?: string; +}; + // @public (undocumented) export enum InstallStatus { // (undocumented) @@ -89,7 +115,7 @@ export interface MarketplaceApi { // (undocumented) getPluginLists(): Promise; // (undocumented) - getPlugins(): Promise; + getPlugins(request?: GetPluginsRequest): Promise; // (undocumented) getPluginsByPluginListName(name: string): Promise; } @@ -106,7 +132,7 @@ export class MarketplaceCatalogClient implements MarketplaceApi { // (undocumented) getPluginLists(): Promise; // (undocumented) - getPlugins(): Promise; + getPlugins(query?: GetPluginsRequest): Promise; // (undocumented) getPluginsByPluginListName(name: string): Promise; } @@ -207,4 +233,25 @@ export interface MarketplacePluginSpec extends JsonObject { packages?: (string | MarketplacePluginPackage)[]; } +// @public (undocumented) +export interface MarketplacePluginWithPageInfo { + // (undocumented) + items: MarketplacePlugin[]; + // (undocumented) + pageInfo?: { + nextCursor?: string; + prevCursor?: string; + }; + // (undocumented) + totalItems?: number; +} + +// @public (undocumented) +export enum SortOrder { + // (undocumented) + asc = "asc", + // (undocumented) + desc = "desc" +} + ``` From 7dbfd2da2226997adda8e53826d7b9a25fb32909 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Sat, 1 Feb 2025 01:49:41 +0530 Subject: [PATCH 10/14] feat(marketplace): adding generic code for encoding and decoding Signed-off-by: its-mitesh-kumar --- .../plugins/marketplace-backend/src/router.ts | 6 +- .../src/api/MarketplaceCatalogClient.ts | 12 +- .../plugins/marketplace-common/src/index.ts | 1 - .../plugins/marketplace-common/src/util.ts | 145 ------------------ .../src/utils/decodeQueryParams.ts | 73 +++++++++ .../src/utils/encodeQueryParams.ts | 56 ++++++- .../src/api/MarketplaceBackendClient.tsx | 4 +- .../marketplace/src/hooks/usePlugins.ts | 11 +- 8 files changed, 145 insertions(+), 163 deletions(-) delete mode 100644 workspaces/marketplace/plugins/marketplace-common/src/util.ts diff --git a/workspaces/marketplace/plugins/marketplace-backend/src/router.ts b/workspaces/marketplace/plugins/marketplace-backend/src/router.ts index 39f24006d..5f085cfa4 100644 --- a/workspaces/marketplace/plugins/marketplace-backend/src/router.ts +++ b/workspaces/marketplace/plugins/marketplace-backend/src/router.ts @@ -20,6 +20,7 @@ import Router from 'express-promise-router'; import { HttpAuthService } from '@backstage/backend-plugin-api'; import { InputError, NotFoundError } from '@backstage/errors'; import { + decodeGetPluginsRequest, decodeQueryParams, EntityFacetSchema, GetEntityFacetsRequest, @@ -38,8 +39,9 @@ export async function createRouter({ router.use(express.json()); router.get('/plugins', async (req, res) => { - const query = req.query as Partial; - const plugins = await marketplaceApi.getPlugins(query); + const query = req.url.split('?')[1] || ''; + const request: GetPluginsRequest = decodeGetPluginsRequest(query); + const plugins = await marketplaceApi.getPlugins(request); res.json(plugins); }); diff --git a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts index 11dd4a086..7ac6e65e7 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.ts @@ -30,7 +30,7 @@ import { MarketplacePluginList, MarketplacePluginWithPageInfo, } from '../types'; -import { convertGetPluginsRequestToQueryEntitiesRequest } from '../util'; +import { convertGetPluginsRequestToQueryEntitiesRequest } from '../utils'; /** * @public @@ -63,11 +63,15 @@ export class MarketplaceCatalogClient implements MarketplaceApi { } async getPlugins( - query?: GetPluginsRequest, + request?: GetPluginsRequest, ): Promise { const token = await this.getServiceToken(); - const payload = convertGetPluginsRequestToQueryEntitiesRequest(query); - const result = await this.catalog.queryEntities(payload, token); + const queryEntitiesRequest = + convertGetPluginsRequestToQueryEntitiesRequest(request); + const result = await this.catalog.queryEntities( + queryEntitiesRequest, + token, + ); return { items: result.items as MarketplacePlugin[], diff --git a/workspaces/marketplace/plugins/marketplace-common/src/index.ts b/workspaces/marketplace/plugins/marketplace-common/src/index.ts index 6466a61cb..b6bd1a24e 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/index.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/index.ts @@ -23,4 +23,3 @@ export * from './types'; export * from './utils'; export * from './api'; -export * from './util'; diff --git a/workspaces/marketplace/plugins/marketplace-common/src/util.ts b/workspaces/marketplace/plugins/marketplace-common/src/util.ts deleted file mode 100644 index c4f0fad8e..000000000 --- a/workspaces/marketplace/plugins/marketplace-common/src/util.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { - EntityFilterQuery, - EntityOrderQuery, - QueryEntitiesRequest, -} from '@backstage/catalog-client/index'; -import { GetPluginsRequest, SortOrder } from './types'; - -const DefaultOrderFields: EntityOrderQuery = [ - { field: 'metadata.name', order: 'asc' }, -]; -const defaultLimit = 20; -const requiredFilter = { kind: 'plugin' }; - -/** - * @public - */ -export const convertGetPluginsRequestToQueryEntitiesRequest = ( - query?: GetPluginsRequest, -): QueryEntitiesRequest => { - let orderFields: EntityOrderQuery = DefaultOrderFields; - - if (query?.orderFields) { - if (Array.isArray(query.orderFields)) { - orderFields = query.orderFields.map(field => { - if (typeof field === 'string') { - const [key, order] = (field as string).split(','); - return { field: key, order: order as SortOrder }; - } - return field; - }); - } else { - if (typeof query.orderFields === 'string') { - const [key, order] = (query.orderFields as string).split(','); - orderFields = { field: key, order: order as SortOrder }; - } - } - } - - const filter: EntityFilterQuery = query?.filter - ? { - ...(typeof query.filter === 'string' - ? JSON.parse(query.filter) - : query.filter), - ...requiredFilter, - } - : requiredFilter; - const payload: QueryEntitiesRequest = { - filter: filter, - orderFields: orderFields, - limit: query?.limit ? Number(query.limit) : defaultLimit, - offset: query?.offset ? Number(query.offset) : undefined, - }; - if (query?.searchTerm) { - payload.fullTextFilter = { term: query.searchTerm }; - } - return payload; -}; - -/** - * @public - */ -export const convertGetPluginRequestToSearchParams = ( - query?: GetPluginsRequest, -): URLSearchParams => { - const params = new URLSearchParams(); - if (query?.limit) params.append('limit', String(query.limit)); - if (query?.offset) params.append('offset', String(query.offset)); - if (query?.searchTerm) params.append('searchTerm', query.searchTerm); - if (query?.orderFields) { - if (Array.isArray(query.orderFields)) { - query.orderFields.forEach(({ field, order }) => { - params.append('orderFields', `${field},${order}`); - }); - } else { - const { field, order } = query.orderFields; - params.append('orderFields', `${field},${order}`); - } - } - if (query?.filter) params.append('filter', JSON.stringify(query.filter)); - return params; -}; - -/** - * @public - */ -export const convertSearchParamsToGetPluginsRequest = ( - params?: URLSearchParams, -): GetPluginsRequest => { - const request: GetPluginsRequest = {}; - - // Convert 'limit' parameter - const limit = params?.get('limit'); - if (limit) { - request.limit = Number(limit); - } - - // Convert 'offset' parameter - const offset = params?.get('offset'); - if (offset) { - request.offset = Number(offset); - } - - // Convert 'searchTerm' parameter - const searchTerm = params?.get('searchTerm'); - if (searchTerm) { - request.searchTerm = searchTerm; - } - - const orderFields = params?.getAll('orderFields'); - - if (Array.isArray(orderFields) && orderFields?.length > 0) { - request.orderFields = orderFields.map(field => { - const [key, order] = field.split(','); - return { field: key, order: order as SortOrder }; - }); - } - - const filter: Record = {}; - params?.forEach((value, key) => { - if (key.startsWith('filter[') && key.endsWith(']')) { - const filterKey = key.slice(7, -1); - filter[filterKey] = value; - } - }); - - if (Object.keys(filter).length > 0) { - request.filter = filter; - } - return request; -}; diff --git a/workspaces/marketplace/plugins/marketplace-common/src/utils/decodeQueryParams.ts b/workspaces/marketplace/plugins/marketplace-common/src/utils/decodeQueryParams.ts index 971d812e4..04c84258a 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/utils/decodeQueryParams.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/utils/decodeQueryParams.ts @@ -14,6 +14,12 @@ * limitations under the License. */ +import { + EntityOrderQuery, + QueryEntitiesRequest, +} from '@backstage/catalog-client/index'; +import { GetPluginsRequest, SortOrder } from '../types'; + /** * * @public @@ -32,6 +38,73 @@ export const decodeFilterParams = (searchParams: URLSearchParams) => { return filter; }; +/** + * + * @public + */ +export const decodeOrderFields = (searchParams: URLSearchParams) => { + const orderFields = searchParams.getAll('orderFields'); + const decodedOrderFields: EntityOrderQuery = orderFields.map(field => { + const [key, order] = field.split(','); + return { field: key, order: order as SortOrder }; + }); + return decodedOrderFields; +}; + +/** + * + * @public + */ +export const decodeGetPluginsRequest = ( + queryString: string, +): GetPluginsRequest => { + const searchParams = new URLSearchParams(queryString); + return { + orderFields: + searchParams.getAll('orderFields').length > 0 + ? decodeOrderFields(searchParams) + : undefined, + searchTerm: searchParams.get('searchTerm') || undefined, + limit: searchParams.get('limit') + ? Number(searchParams.get('limit')) + : undefined, + offset: searchParams.get('offset') + ? Number(searchParams.get('offset')) + : undefined, + filter: + searchParams.getAll('filter').length > 0 + ? decodeFilterParams(searchParams) + : undefined, + }; +}; + +/** + * @public + */ +export const convertGetPluginsRequestToQueryEntitiesRequest = ( + query?: GetPluginsRequest, +): QueryEntitiesRequest => { + const entitiesRequest: QueryEntitiesRequest = {}; + if (query?.filter) { + entitiesRequest.filter = query.filter; + } + if (query?.orderFields) { + entitiesRequest.orderFields = query.orderFields; + } + if (query?.limit) { + entitiesRequest.limit = query.limit; + } + if (query?.offset) { + entitiesRequest.offset = query.offset; + } + if (query?.searchTerm) { + entitiesRequest.fullTextFilter = { + term: query.searchTerm, + }; + } + return entitiesRequest; +}; + /** * @public */ diff --git a/workspaces/marketplace/plugins/marketplace-common/src/utils/encodeQueryParams.ts b/workspaces/marketplace/plugins/marketplace-common/src/utils/encodeQueryParams.ts index 4a360597a..0f6d7b742 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/utils/encodeQueryParams.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/utils/encodeQueryParams.ts @@ -13,7 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityFilterQuery } from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; +import { EntityOrderQuery } from '@backstage/catalog-client/index'; +import { + EntityFilterQuery, + GetPluginsRequest, +} from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; + +const requiredFilter = { kind: 'plugin' }; /** * @public @@ -35,6 +41,54 @@ export const encodeFilterParams = (filter: EntityFilterQuery) => { return params; }; +/** + * @public + */ +export const encodeOrderFieldsParams = (orderFields: EntityOrderQuery) => { + const params = new URLSearchParams(); + if (Array.isArray(orderFields)) { + orderFields.forEach(({ field, order }) => { + params.append( + 'orderFields', + `${encodeURIComponent(field)},${encodeURIComponent(order)}`, + ); + }); + } else { + const { field, order } = orderFields; + params.append('orderFields', `${field},${order}`); + } + return params; +}; + +/** + * @public + */ +export const encodeGetPluginsQueryParams = ( + options?: GetPluginsRequest, +): URLSearchParams => { + const params = new URLSearchParams(); + const { searchTerm, limit, offset, orderFields, filter } = options || {}; + if (limit) { + params.append('limit', String(limit)); + } + if (offset) { + params.append('offset', String(offset)); + } + if (searchTerm) { + params.append('searchTerm', encodeURIComponent(searchTerm)); + } + if (orderFields) { + encodeOrderFieldsParams(orderFields).forEach((value, key) => { + params.append(key, value); + }); + } + + encodeFilterParams({ ...filter, ...requiredFilter }).forEach((value, key) => + params.append(key, value), + ); + return params; +}; + /** * @public */ diff --git a/workspaces/marketplace/plugins/marketplace/src/api/MarketplaceBackendClient.tsx b/workspaces/marketplace/plugins/marketplace/src/api/MarketplaceBackendClient.tsx index e6daccebf..db40a3088 100644 --- a/workspaces/marketplace/plugins/marketplace/src/api/MarketplaceBackendClient.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/api/MarketplaceBackendClient.tsx @@ -25,7 +25,7 @@ import { MarketplacePlugin, MarketplacePluginList, MarketplacePluginWithPageInfo, - convertGetPluginRequestToSearchParams, + encodeGetPluginsQueryParams, } from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; export type MarketplaceBackendClientOptions = { @@ -46,7 +46,7 @@ export class MarketplaceBackendClient implements MarketplaceApi { request?: GetPluginsRequest, ): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('marketplace'); - const params = convertGetPluginRequestToSearchParams(request); + const params = encodeGetPluginsQueryParams(request); const query = params.toString(); const url = `${baseUrl}/plugins${query ? '?' : ''}${query}`; diff --git a/workspaces/marketplace/plugins/marketplace/src/hooks/usePlugins.ts b/workspaces/marketplace/plugins/marketplace/src/hooks/usePlugins.ts index 494388e36..0c897176c 100644 --- a/workspaces/marketplace/plugins/marketplace/src/hooks/usePlugins.ts +++ b/workspaces/marketplace/plugins/marketplace/src/hooks/usePlugins.ts @@ -18,17 +18,12 @@ import { useApi } from '@backstage/core-plugin-api'; import { useQuery } from '@tanstack/react-query'; import { marketplaceApiRef } from '../api'; -import { - GetPluginsRequest, - convertSearchParamsToGetPluginsRequest, -} from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; +import { GetPluginsRequest } from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; -export const usePlugins = (params?: URLSearchParams) => { +export const usePlugins = (request?: GetPluginsRequest) => { const marketplaceApi = useApi(marketplaceApiRef); - const request: GetPluginsRequest = - convertSearchParamsToGetPluginsRequest(params); return useQuery({ - queryKey: ['plugins', params], + queryKey: ['plugins', request], queryFn: () => marketplaceApi.getPlugins(request), }); }; From 3e0c781e6891c8588a4962b4b1d6a0615dfb9459 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Sat, 1 Feb 2025 01:55:11 +0530 Subject: [PATCH 11/14] feat(marketplace): updating api report Signed-off-by: its-mitesh-kumar --- .../plugins/marketplace-common/report.api.md | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/workspaces/marketplace/plugins/marketplace-common/report.api.md b/workspaces/marketplace/plugins/marketplace-common/report.api.md index bc3e372ed..cb9d85f8b 100644 --- a/workspaces/marketplace/plugins/marketplace-common/report.api.md +++ b/workspaces/marketplace/plugins/marketplace-common/report.api.md @@ -10,8 +10,10 @@ import { Entity } from '@backstage/catalog-model'; import { EntityFilterQuery } from '@backstage/catalog-client'; import { EntityFilterQuery as EntityFilterQuery_2 } from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; import { EntityOrderQuery } from '@backstage/catalog-client'; +import { EntityOrderQuery as EntityOrderQuery_2 } from '@backstage/catalog-client/index'; import { GetEntityFacetsRequest } from '@backstage/catalog-client'; import { GetEntityFacetsResponse } from '@backstage/catalog-client'; +import { GetPluginsRequest as GetPluginsRequest_2 } from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; import { JsonObject } from '@backstage/types'; import { QueryEntitiesRequest } from '@backstage/catalog-client/index'; import { z } from 'zod'; @@ -25,19 +27,22 @@ export interface AppConfigExample extends JsonObject { } // @public (undocumented) -export const convertGetPluginRequestToSearchParams: (query?: GetPluginsRequest) => URLSearchParams; +export const convertGetPluginsRequestToQueryEntitiesRequest: (query?: GetPluginsRequest) => QueryEntitiesRequest; // @public (undocumented) -export const convertGetPluginsRequestToQueryEntitiesRequest: (query?: GetPluginsRequest) => QueryEntitiesRequest; +export const decodeFacetParams: (searchParams: URLSearchParams) => string[]; // @public (undocumented) -export const convertSearchParamsToGetPluginsRequest: (params?: URLSearchParams) => GetPluginsRequest; +export const decodeFilterParams: (searchParams: URLSearchParams) => Record; // @public (undocumented) -export const decodeFacetParams: (searchParams: URLSearchParams) => string[]; +export const decodeGetPluginsRequest: (queryString: string) => GetPluginsRequest; // @public (undocumented) -export const decodeFilterParams: (searchParams: URLSearchParams) => Record; +export const decodeOrderFields: (searchParams: URLSearchParams) => { + field: string; + order: "desc" | "asc"; +}[]; // @public (undocumented) export const decodeQueryParams: (queryString: string) => { @@ -51,6 +56,12 @@ export const encodeFacetParams: (facets: string[]) => URLSearchParams; // @public (undocumented) export const encodeFilterParams: (filter: EntityFilterQuery_2) => URLSearchParams; +// @public (undocumented) +export const encodeGetPluginsQueryParams: (options?: GetPluginsRequest_2) => URLSearchParams; + +// @public (undocumented) +export const encodeOrderFieldsParams: (orderFields: EntityOrderQuery_2) => URLSearchParams; + // @public (undocumented) export const encodeQueryParams: (options?: { filter?: EntityFilterQuery_2; @@ -132,7 +143,7 @@ export class MarketplaceCatalogClient implements MarketplaceApi { // (undocumented) getPluginLists(): Promise; // (undocumented) - getPlugins(query?: GetPluginsRequest): Promise; + getPlugins(request?: GetPluginsRequest): Promise; // (undocumented) getPluginsByPluginListName(name: string): Promise; } From 527487a3efeaf741fe4307c829b8fa3efeba79f9 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Sat, 1 Feb 2025 02:21:07 +0530 Subject: [PATCH 12/14] feat(marketplace): fixing failing unit tests Signed-off-by: its-mitesh-kumar --- .../src/api/MarketplaceCatalogClient.test.ts | 4 ++-- .../marketplace-common/src/utils/decodeQueryParams.ts | 8 +++++--- .../marketplace-common/src/utils/encodeQueryParams.ts | 11 +++++------ 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.test.ts b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.test.ts index 9695a9420..b2de6790d 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.test.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/api/MarketplaceCatalogClient.test.ts @@ -94,13 +94,13 @@ describe('MarketplaceCatalogClient', () => { it('should return the plugins', async () => { const api = new MarketplaceCatalogClient(options); const plugins = await api.getPlugins(); - expect(plugins).toHaveLength(2); + expect(plugins?.items).toHaveLength(2); }); it('should return the plugins when the auth options is not passed', async () => { const api = new MarketplaceCatalogClient({ ...options, auth: undefined }); const plugins = await api.getPlugins(); - expect(plugins).toHaveLength(2); + expect(plugins?.items).toHaveLength(2); }); }); diff --git a/workspaces/marketplace/plugins/marketplace-common/src/utils/decodeQueryParams.ts b/workspaces/marketplace/plugins/marketplace-common/src/utils/decodeQueryParams.ts index 04c84258a..e24efccd7 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/utils/decodeQueryParams.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/utils/decodeQueryParams.ts @@ -20,6 +20,8 @@ import { } from '@backstage/catalog-client/index'; import { GetPluginsRequest, SortOrder } from '../types'; +const requiredFilter = { kind: 'plugin' }; + /** * * @public @@ -85,9 +87,9 @@ export const convertGetPluginsRequestToQueryEntitiesRequest = ( query?: GetPluginsRequest, ): QueryEntitiesRequest => { const entitiesRequest: QueryEntitiesRequest = {}; - if (query?.filter) { - entitiesRequest.filter = query.filter; - } + + entitiesRequest.filter = { ...query?.filter, ...requiredFilter }; + if (query?.orderFields) { entitiesRequest.orderFields = query.orderFields; } diff --git a/workspaces/marketplace/plugins/marketplace-common/src/utils/encodeQueryParams.ts b/workspaces/marketplace/plugins/marketplace-common/src/utils/encodeQueryParams.ts index 0f6d7b742..cd6a867d1 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/utils/encodeQueryParams.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/utils/encodeQueryParams.ts @@ -19,8 +19,6 @@ import { GetPluginsRequest, } from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; -const requiredFilter = { kind: 'plugin' }; - /** * @public */ @@ -82,10 +80,11 @@ export const encodeGetPluginsQueryParams = ( params.append(key, value); }); } - - encodeFilterParams({ ...filter, ...requiredFilter }).forEach((value, key) => - params.append(key, value), - ); + if (filter) { + encodeFilterParams(filter).forEach((value, key) => + params.append(key, value), + ); + } return params; }; From ceaad31cb9535c183a8ecab3b62da57f22afd561 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Sat, 1 Feb 2025 02:33:43 +0530 Subject: [PATCH 13/14] feat(marketplace): fix backend unit test fail Signed-off-by: its-mitesh-kumar --- .../marketplace/plugins/marketplace-backend/src/router.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspaces/marketplace/plugins/marketplace-backend/src/router.test.ts b/workspaces/marketplace/plugins/marketplace-backend/src/router.test.ts index 73cf551a0..a05e9eb57 100644 --- a/workspaces/marketplace/plugins/marketplace-backend/src/router.test.ts +++ b/workspaces/marketplace/plugins/marketplace-backend/src/router.test.ts @@ -140,7 +140,7 @@ describe('createRouter', () => { '/api/marketplace/plugins', ); expect(response.status).toEqual(200); - expect(response.body).toHaveLength(2); + expect(response.body.items).toHaveLength(2); }); it('should get the plugin by name', async () => { From d214aad928aea8f4ec741f5d26776955a5d9d333 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Sat, 1 Feb 2025 03:10:39 +0530 Subject: [PATCH 14/14] feat(marketplace): adding unit tests for encoding decoding Signed-off-by: its-mitesh-kumar --- .../src/utils/decodeQueryParams.test.ts | 41 +++++++++++++++ .../src/utils/encodeQueryParams.test.ts | 50 ++++++++++++++++++- 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/workspaces/marketplace/plugins/marketplace-common/src/utils/decodeQueryParams.test.ts b/workspaces/marketplace/plugins/marketplace-common/src/utils/decodeQueryParams.test.ts index e22f5e3cb..125140512 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/utils/decodeQueryParams.test.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/utils/decodeQueryParams.test.ts @@ -13,13 +13,54 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { GetPluginsRequest } from '../types'; import { decodeFacetParams, decodeFilterParams, decodeQueryParams, + decodeGetPluginsRequest, + decodeOrderFields, } from './decodeQueryParams'; describe('decodeFilterParams', () => { + it('should decode single orderFields', () => { + const searchParams = new URLSearchParams( + 'orderFields=metadata.title%2Casc', + ); + expect(decodeOrderFields(searchParams)).toEqual([ + { field: 'metadata.title', order: 'asc' }, + ]); + }); + + it('should decode multiple orderFields', () => { + const searchParams = new URLSearchParams( + 'orderFields=metadata.title%2Cdesc&orderFields=metadata.name%2Casc', + ); + expect(decodeOrderFields(searchParams)).toEqual([ + { field: 'metadata.title', order: 'desc' }, + { field: 'metadata.name', order: 'asc' }, + ]); + }); + + it('should decode GetPlugins Request', () => { + const encodedString = + 'filter=metadata.name%3Dsearch&filter=spec.type%3Dbackend-plugin&orderFields=metadata.title%2Cdesc&orderFields=metadata.name%2Casc&searchTerm=search&limit=2&offset=1'; + const params: GetPluginsRequest = { + filter: { + 'metadata.name': ['search'], + 'spec.type': ['backend-plugin'], + }, + orderFields: [ + { field: 'metadata.title', order: 'desc' }, + { field: 'metadata.name', order: 'asc' }, + ], + searchTerm: 'search', + limit: 2, + offset: 1, + }; + expect(decodeGetPluginsRequest(encodedString)).toEqual(params); + }); + it('should decode single filter', () => { const searchParams = new URLSearchParams('filter=kind%3Dplugin'); expect(decodeFilterParams(searchParams)).toEqual({ kind: ['plugin'] }); diff --git a/workspaces/marketplace/plugins/marketplace-common/src/utils/encodeQueryParams.test.ts b/workspaces/marketplace/plugins/marketplace-common/src/utils/encodeQueryParams.test.ts index f1dce0a3e..63a14c46b 100644 --- a/workspaces/marketplace/plugins/marketplace-common/src/utils/encodeQueryParams.test.ts +++ b/workspaces/marketplace/plugins/marketplace-common/src/utils/encodeQueryParams.test.ts @@ -13,14 +13,62 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityFilterQuery } from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; +import { + EntityFilterQuery, + GetPluginsRequest, + SortOrder, +} from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; import { encodeFilterParams, encodeFacetParams, encodeQueryParams, + encodeGetPluginsQueryParams, + encodeOrderFieldsParams, } from './encodeQueryParams'; +import { EntityOrderQuery } from '@backstage/catalog-client/index'; describe('encodeFilterParams', () => { + it('should encode single orderFields correctly', () => { + const orderFields: EntityOrderQuery = { + field: 'metadata.title', + order: 'asc' as SortOrder, + }; + const params = encodeOrderFieldsParams(orderFields).toString(); + expect(params).toBe('orderFields=metadata.title%2Casc'); + }); + + it('should encode multiple orderFields correctly', () => { + const orderFields: EntityOrderQuery = [ + { field: 'metadata.title', order: 'desc' }, + { field: 'metadata.name', order: 'asc' }, + ]; + const params = encodeOrderFieldsParams(orderFields).toString(); + expect(params).toBe( + 'orderFields=metadata.title%2Cdesc&orderFields=metadata.name%2Casc', + ); + }); + + it('should encode GetPluginsRequest correctly', () => { + const params: GetPluginsRequest = { + filter: { + 'metadata.name': 'search', + 'spec.type': 'backend-plugin', + }, + orderFields: [ + { field: 'metadata.title', order: 'desc' }, + { field: 'metadata.name', order: 'asc' }, + ], + searchTerm: 'search', + limit: 2, + offset: 1, + }; + + const encodedParams = encodeGetPluginsQueryParams(params).toString(); + expect(encodedParams).toBe( + 'limit=2&offset=1&searchTerm=search&orderFields=metadata.title%2Cdesc&orderFields=metadata.name%2Casc&filter=metadata.name%3Dsearch&filter=spec.type%3Dbackend-plugin', + ); + }); + it('should encode single filter correctly', () => { const filter: EntityFilterQuery = { kind: 'component' }; const params = encodeFilterParams(filter).toString();