diff --git a/.changeset/nasty-zebras-dress.md b/.changeset/nasty-zebras-dress.md new file mode 100644 index 0000000000..223c4e930e --- /dev/null +++ b/.changeset/nasty-zebras-dress.md @@ -0,0 +1,5 @@ +--- +'@frontside/backstage-plugin-graphql-backend-module-catalog': patch +--- + +Add `encodeEntityId/decodeEntityId` helpers diff --git a/plugins/graphql-backend-module-catalog/README.md b/plugins/graphql-backend-module-catalog/README.md index 03704d76a0..5ee1d1d488 100644 --- a/plugins/graphql-backend-module-catalog/README.md +++ b/plugins/graphql-backend-module-catalog/README.md @@ -17,10 +17,10 @@ Some key features are currently missing. These features may change the schema in - [GraphQL modules](#graphql-modules) - [Backstage Plugins](#backstage-plugins) - - [Experimental Backend System](#experimental-backend-system) + - [Backend System](#backend-system) - [Directives API](#directives-api) - [`@relation` directive](#relation-directive) -- [Catalog Data loader](#catalog-data-loader-advanced) +- [Custom GraphQL Resolvers](#custom-graphql-resolvers) ## GraphQL modules @@ -46,13 +46,16 @@ export default async function createPlugin( logger: env.logger, modules: [Catalog], loaders: { ...createCatalogLoader(env.catalogClient) }, + // You might want to pass catalog client to the context + // and use it in resolvers, but it's not required + context: ctx => ({ ...ctx, catalog: env.catalogClient }), }); } ``` -### Experimental Backend System +### Backend System -For the [experimental backend system](https://backstage.io/docs/plugins/experimental-backend), +For the [backend system](https://backstage.io/docs/backend-system/), you can add them as a plugin modules: - To use `Catalog` GraphQL module @@ -122,6 +125,46 @@ type System { } ``` +## Custom GraphQL Resolvers + +If you need to implement complicated logic for some fields and can't be +achieved with available [directives][directives-api], you can write +your own resolvers. To do this, you need to define a resolver function +in your [GraphQL module](../graphql-backend/README.md#custom-graphql-module): + +```ts +import { createModule } from "graphql-modules"; +import type { CatalogClient, QueryEntitiesRequest } from '@backstage/catalog-client'; +import { encodeEntityId } from '@frontside/backstage-plugin-graphql-backend-module-catalog'; + +export const myModule = createModule({ + /* ... */ + resolvers: { + Task: { + // This resolver utilize 3rd party api to get entity ref and then encodes it to NodeId + // Which will be resolved to an entity + entity: async (_, args, { taskAPI }) => { + const response = await taskAPI.getTask(args.taskId); + return { id: encodeEntityId(response.entityRef) }; + }, + }, + Query: { + // Here you can use catalog client to query entities + entities: async ( + _: any, + args: QueryEntitiesRequest, + // If you aren't using Backstage Backend System https://backstage.io/docs/backend-system/ + // This requires you to pass catalog client to the context + { catalog }: { catalog: CatalogClient } + ): Promise<{ id: string }[]> => { + const { items: entities } = await catalog.queryEntities(args); + return entities.map(entity => ({ id: encodeEntityId(entity) })); + }, + }, + }, +}); +``` + [graphql-backend]: ../graphql-backend/README.md [graphql-modules]: https://the-guild.dev/graphql/modules [relay]: https://relay.dev/docs/guides/graphql-server-specification diff --git a/plugins/graphql-backend-module-catalog/src/catalogModule.ts b/plugins/graphql-backend-module-catalog/src/catalogModule.ts index 681f8a99c9..6b16913e77 100644 --- a/plugins/graphql-backend-module-catalog/src/catalogModule.ts +++ b/plugins/graphql-backend-module-catalog/src/catalogModule.ts @@ -1,6 +1,7 @@ import { createBackendModule } from '@backstage/backend-plugin-api'; import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; import { + graphqlContextExtensionPoint, graphqlLoadersExtensionPoint, graphqlModulesExtensionPoint, } from '@frontside/backstage-plugin-graphql-backend'; @@ -17,10 +18,12 @@ export const graphqlModuleCatalog = createBackendModule({ catalog: catalogServiceRef, modules: graphqlModulesExtensionPoint, loaders: graphqlLoadersExtensionPoint, + context: graphqlContextExtensionPoint, }, - async init({ catalog, modules, loaders }) { + async init({ catalog, modules, loaders, context }) { modules.addModules([Catalog]); loaders.addLoaders(createCatalogLoader(catalog)); + context.setContext(ctx => ({ ...ctx, catalog })); }, }); }, diff --git a/plugins/graphql-backend-module-catalog/src/helpers.ts b/plugins/graphql-backend-module-catalog/src/helpers.ts new file mode 100644 index 0000000000..645fa63556 --- /dev/null +++ b/plugins/graphql-backend-module-catalog/src/helpers.ts @@ -0,0 +1,18 @@ +import { CompoundEntityRef, Entity, parseEntityRef, stringifyEntityRef } from "@backstage/catalog-model"; +import { decodeId, encodeId } from "@frontside/hydraphql"; +import { CATALOG_SOURCE } from "./constants"; + +export function encodeEntityId(entityOrRef: Entity | CompoundEntityRef | string): string { + const ref = typeof entityOrRef === 'string' ? entityOrRef : stringifyEntityRef(entityOrRef); + return encodeId({ + source: CATALOG_SOURCE, + typename: 'Node', + query: { ref }, + }); +} + +export function decodeEntityId(id: string): CompoundEntityRef { + const { query: { ref = '' } } = decodeId(id); + + return parseEntityRef(ref); +} diff --git a/plugins/graphql-backend-module-catalog/src/index.ts b/plugins/graphql-backend-module-catalog/src/index.ts index 0e70e46ee1..efedbcebf4 100644 --- a/plugins/graphql-backend-module-catalog/src/index.ts +++ b/plugins/graphql-backend-module-catalog/src/index.ts @@ -1,3 +1,4 @@ +export * from './helpers'; export * from './catalog'; export * from './relation'; export * from './catalogModule'; diff --git a/plugins/graphql-backend-module-catalog/src/relationModule.ts b/plugins/graphql-backend-module-catalog/src/relationModule.ts index 9e53826f9a..57a7d5bd7a 100644 --- a/plugins/graphql-backend-module-catalog/src/relationModule.ts +++ b/plugins/graphql-backend-module-catalog/src/relationModule.ts @@ -1,6 +1,7 @@ import { createBackendModule } from '@backstage/backend-plugin-api'; import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; import { + graphqlContextExtensionPoint, graphqlLoadersExtensionPoint, graphqlModulesExtensionPoint, } from '@frontside/backstage-plugin-graphql-backend'; @@ -17,10 +18,12 @@ export const graphqlModuleRelationResolver = createBackendModule({ catalog: catalogServiceRef, modules: graphqlModulesExtensionPoint, loaders: graphqlLoadersExtensionPoint, + context: graphqlContextExtensionPoint, }, - async init({ catalog, modules, loaders }) { + async init({ catalog, modules, loaders, context }) { modules.addModules([Relation]); loaders.addLoaders(createCatalogLoader(catalog)); + context.setContext(ctx => ({ ...ctx, catalog })); }, }); }, diff --git a/plugins/graphql-backend/README.md b/plugins/graphql-backend/README.md index ca2869ebbb..5d6bfc76e0 100644 --- a/plugins/graphql-backend/README.md +++ b/plugins/graphql-backend/README.md @@ -12,7 +12,7 @@ At a minimum, you should install the [graphql-backend-module-catalog][] which ad schema elements to access the [Backstage Catalog][backstage-catalog] via GraphQL - [Backstage Plugins](./docs/backend-plugins.md#getting-started) -- [Experimental Backend System](#experimental-backend-system) +- [Backend System](#backend-system) - [Getting started](#getting-started) - [GraphQL Modules](#graphql-modules) - [Custom GraphQL Module](#custom-graphql-module) @@ -25,7 +25,7 @@ schema elements to access the [Backstage Catalog][backstage-catalog] via GraphQL - [Backstage API Docs](#backstage-api-docs) - [Questions](#questions) -## Experimental Backend System +## Backend System This approach is suitable for the new [Backstage backend system](https://backstage.io/docs/backend-system/). For the current [Backstage plugins system](https://backstage.io/docs/plugins/backend-plugin) see [Backstage Plugins](./docs/backend-plugins.md#getting-started)