@frontside/backstage-plugin-graphql v0.7.4
@frontside/backstage-plugin-graphql
Status: Alpha - this plugin is in early stage of code maturity. It includes features that were battle tested on client's projects, but require some time in open source to settle. You should expect the schema provided by this plugin to change because we're missing a number of important features.
Backstage GraphQL Plugin adds a GraphQL API to a Backstage developer portal. The GraphQL API behaves like a gateway to provide a single API for accessing data from the Catalog and other plugins. Currently, it only supports the Backstage catalog.
It includes the following features,
- Graph schema - easily query relationships between data in the catalog.
- Schema-based resolvers - add field resolvers using directives without requiring JavaScript.
- Modular schema definition - allows organizing related schema into graphql-modules
- Connection - based on GraphQL Cursor Connections Specification. (see #68)
- Strives to support - GraphQL Server Specification
Some key features are currently missing. These features may change the schema in backward-incompatible ways.
viewer
query for retrieving data for the current user. (see #67)
We plan to add these over time. If you're interested in contributing to this plugin, feel free to message us in #graphql
channel in Backstage Discord.
Getting started
You can install the GraphQL Plugin using the same process that you would use to install other backend Backtstage plugins.
- Run
yarn add @frontside/backstage-plugin-graphql
inpackages/backend
Create
packages/backend/src/plugins/graphql.ts
file with the following contentimport { createRouter } from '@frontside/backstage-plugin-graphql'; import { CatalogClient } from '@backstage/catalog-client'; import { Router } from 'express'; import { Logger } from 'winston'; interface PluginEnvironment { logger: Logger; catalog: CatalogClient } export default async function createPlugin( env: PluginEnvironment, ): Promise<Router> { return await createRouter({ logger: env.logger, catalog: env.catalog, }); }
Add plugin's router to your backend API router in
packages/backend/src/index.ts
// import the graphql plugin import graphql from './plugins/graphql'; // create the graphql plugin environment const graphqlEnv = useHotMemoize(module, () => createEnv('graphql')); // add `/graphql` route to your apiRouter apiRouter.use('/graphql', await graphql(graphqlEnv));
See packages/backend/src/index.ts for an example.
Extending Schema
Backstage GraphQL Plugin allows developers to extend the schema provided by the plugin. Extending the schema allows you to query additional information for existing types or add new types. GraphQL is often used as a gateway to many different APIs. It's reasonable and expected that you may want to add custom types and fields. This section will tell you what you need to know to extend the schema.
In Backstage Backend
You can extend the schema from inside of Backstage Backend by creating a GraphQL Module that you can pass to the GraphQL API plugin's router. Here are step-by-step instructions on how to set up your GraphQL API plugin to provide a custom GraphQL Module.
- Add
graphql-modules
and@graphql-tools/load-files
to your backend packages inpackages/backend
withyarn add graphql-modules @graphql-tools/load-files
- Create
packages/backend/src/graphql
directory that will contain your modules Create a file for your first GraphQL module called
packages/backend/src/graphql/my-module.ts
with the following contentimport { resolvePackagePath } from '@backstage/backend-common' import { loadFilesSync } from '@graphql-tools/load-files'; import { createModule } from 'graphql-modules' export const myModule = createModule({ id: 'my-module', dirname: resolvePackagePath('backend', 'src/graphql'), typeDefs: loadFilesSync(resolvePackagePath('backend', 'src/graphql/my-module.graphql')), resolvers: { Query: { hello: () => 'world' } } })
Create GraphQL module's schema called
packages/backend/src/graphql/my-module.graphql
with the following contenttype Query { hello: String! }
Register your GraphQL module with the GraphQL API plugin by modifying
packages/backend/src/plugins/graphql.ts
. You must import your new module and pass it to the router usingmodules: [myModule]
. Here is what the result should look like.import { createRouter } from '@frontside/backstage-plugin-graphql'; import { Router } from 'express'; import { Logger } from 'winston'; import { DatabaseManager } from '@backstage/backend-common'; import { myModule } from '../graphql/my-module'; interface PluginEnvironment { logger: Logger; databaseManager: DatabaseManager; } export default async function createPlugin( env: PluginEnvironment, ): Promise<Router> { return await createRouter({ modules: [myModule], logger: env.logger, database: env.databaseManager, }); }
Start your backend and you should be able to query your API with
{ hello }
query to get{ data: { hello: 'world' } }
(Optional) You can also extend existing basic node types. For example, you can add a new field to the
Component
interface. Here is what the result should look like.// ./src/graphql/extensions.ts import { resolvePackagePath } from '@backstage/backend-common' import { loadFilesSync } from '@graphql-tools/load-files'; import { createModule } from 'graphql-modules' export const extensionsModule = createModule({ id: 'extensions', dirname: resolvePackagePath('backend', 'src/graphql'), typeDefs: loadFilesSync(resolvePackagePath('backend', 'src/graphql/extensions.graphql')), });
# ./src/graphql/extensions.graphql interface Component { type: String @field(at: "spec.type") }
(Optional) If you use TypeScript and generate types from your GraphQL modules you should create
packages/backend/src/schema.ts
file:
import { resolvePackagePath } from '@backstage/backend-common';
import { transformSchema } from '@frontside/backstage-plugin-graphql';
import { loadFilesSync } from '@graphql-tools/load-files';
import { printSchemaWithDirectives } from '@graphql-tools/utils';
export default printSchemaWithDirectives(
transformSchema(
loadFilesSync(
resolvePackagePath('backend', 'src/graphql/*.graphql')
)
)
);
- Then replace
schema
field in yourcodegen.yml
file to useschema.ts
as a input schema:
schema: "src/schema.ts"
Directives API
Every GraphQL API consists of two things - a schema and resolvers. The schema describes relationships and fields that you can retrieve from the API. The resolvers describe how you retrieve the data described in the schema. The Backstage GraphQL Plugin provides several directives to help write a GraphQL schema and resolvers for Backstage. These directives take into account some specificities for Backstage APIs to make it easier to write schema and implement resolvers. This section will explain each directive and the assumptions they make about the developer's intention.
@field
@field
directive allows you to access properties on the object using a given path. It allows you to specify a resolver for a field from the schema without actually writing a real resolver. Under the hood, it's creating the resolver for you. It's used extensively in the catalog.graphql
module to retrieve properties like namespace
, title
and others. For example, here is how we define the resolver for the Entity#name
field name: String! @field(at: "metadata.name")
. It also accepts an array of strings, it useful when path tokens contain dots: label: String! @field(at: ["spec", "data.label"])
. You can specify default value as a fallback if the field is not found: label: String! @field(at: "spec.label", default: "N/A")
.
@relation
@relation
directive allows you to resolve relationships between entities. Similar to @field
directive, it provides the resolver from the schema so you do not have to write a resolver yourself. It assumes that relationships are defined as standard Entity
relationships. The type
argument allows you to specify the type of the relationship. It will automatically look up the entity in the catalog. For example, here is how we define owner
of an Component - owner: Owner @relation(type: "ownedBy")
. If you have more than one relationship of specific type you might want to use a relay connection. In that case you specify the Connection
type as a field type and use interface
argument to specify which connection type you would like to resolve to. contributors: Connection @relation(type: "contributedBy", interface: "User")
. Or you can just use an array of entities contributors: [User] @relation(type: "contributedBy")
. If you have different kinds of relationships with the same type you can filter them by kind
argument. For example, resources: Connection @relation(type: "hasPart", interface: "Resource", kind: "resource")
.
@extend
@extend
directive allows you to inherit fields from another entity. We created this directive to make it easier to implement interfaces that extend from Entity
and other interfaces. It makes GraphQL types similar to extending types in TypeScript. In TypeScript, when a class extends another class, the child class automatically inherits properties and methods of the parent class. This functionality doesn't have an equivalent in GraphQL. Without this directive, the Component
interface in GraphQL would need to reimplement many fields that are defined on Entity which leads to lots of duplication. Using this directive, you can easily create a new interface that includes all of the properties of the parent. For example, if you wanted to create a Repository
interface, you can do the following,
interface Repository @extends(interface: "Entity", when: "kind", is: "Repository") {
languages: [String] @field(at: "spec.languages")
}
Your Repository
interface will automatically get all of the properties from Entity
. You might notice that we don't declare object types and @extend
can be used only on interfaces. That's because you can't extend from object types, only interfaces can be used for implementations. But for making queries you must have object types, so we're creating object types for you automatically. And for Repository
interface we're creating RepositoryImpl
.
Integrations
Backstage GraphiQL Plugin
It's convenient to be able to query the Backstage GraphQL API from inside of Backstage App. You can accomplish this by installing the Backstage GraphiQL Plugin and adding the GraphQL API endpoint to the GraphiQL Plugin API factory.
- Once you installed
@backstage/plugin-graphiql
plugin with these instructions Modify
packages/app/src/apis.ts
to add your GraphQL API as an endpointfactory: ({ errorApi, githubAuthApi, discovery }) => GraphQLEndpoints.from([ { id: 'backstage-backend', title: 'Backstage GraphQL API', // we use the lower level object with a fetcher function // as we need to `await` the backend url for the graphql plugin fetcher: async (params: any) => { const graphqlURL = await discovery.getBaseUrl('graphql'); return fetch(graphqlURL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params), }).then(res => res.json()); }, }, ])
Checkout this example
packages/app/src/apis.ts
.
Backstage API Docs
You might want to show the schema from your GraphQL API in API definition section of an API entity in Backstage. You can use the /api/graphql/schema
endpoint to read the schema provided by your GraphQL API. Here is how you can accomplish this.
Create API entity and reference
definition.$text: http://localhost:7007/api/graphql/schema
apiVersion: backstage.io/v1alpha1 kind: API metadata: name: backstage-graphql-api description: GraphQL API provided by GraphQL Plugin spec: type: graphql owner: engineering@frontside.com lifecycle: production definition: $text: http://localhost:7007/api/graphql/schema
Modify
app-config.yaml
to allow reading urls fromlocalhost:7007
backend: ... reading: allow: - host: localhost:7007
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago