0.7.4 • Published 9 months ago

@frontside/backstage-plugin-graphql v0.7.4

Weekly downloads
-
License
Apache-2.0
Repository
-
Last release
9 months ago

@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,

  1. Graph schema - easily query relationships between data in the catalog.
  2. Schema-based resolvers - add field resolvers using directives without requiring JavaScript.
  3. Modular schema definition - allows organizing related schema into graphql-modules
  4. Connection - based on GraphQL Cursor Connections Specification. (see #68)
  5. Strives to support - GraphQL Server Specification

Some key features are currently missing. These features may change the schema in backward-incompatible ways.

  1. 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.

  1. Run yarn add @frontside/backstage-plugin-graphql in packages/backend
  2. Create packages/backend/src/plugins/graphql.ts file with the following content

    import { 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,
      });
    }
  3. 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.

  1. Add graphql-modules and @graphql-tools/load-files to your backend packages in packages/backend with yarn add graphql-modules @graphql-tools/load-files
  2. Create packages/backend/src/graphql directory that will contain your modules
  3. Create a file for your first GraphQL module called packages/backend/src/graphql/my-module.ts with the following content

    import { 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'
        }
      }
    })
  4. Create GraphQL module's schema called packages/backend/src/graphql/my-module.graphql with the following content

    type Query {
      hello: String!
    }
  5. 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 using modules: [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,
      });
    }
  6. Start your backend and you should be able to query your API with { hello } query to get { data: { hello: 'world' } }

  7. (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")
    }
  8. (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')
    )
  )
);
  1. Then replace schema field in your codegen.yml file to use schema.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.

  1. Once you installed @backstage/plugin-graphiql plugin with these instructions
  2. Modify packages/app/src/apis.ts to add your GraphQL API as an endpoint

    factory: ({ 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.

  1. 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
  2. Modify app-config.yaml to allow reading urls from localhost:7007

    backend:
      ...
    
      reading:
        allow:
          - host: localhost:7007
0.7.4

9 months ago

0.7.3

12 months ago

0.7.2

1 year ago

0.7.1

1 year ago

0.7.0

1 year ago

0.6.1

1 year ago

0.5.4

1 year ago

0.5.3

1 year ago

0.5.0

2 years ago

0.5.2

1 year ago

0.5.1

2 years ago

0.6.0

1 year ago

0.4.2

2 years ago

0.4.1

2 years ago

0.4.0

2 years ago

0.1.0

2 years ago

0.3.0

2 years ago

0.2.1

2 years ago

0.2.0

2 years ago