Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improving documentation of @frontside/backstage-plugin-graphql #69

Merged
merged 10 commits into from
Aug 11, 2022
5 changes: 5 additions & 0 deletions .changeset/gentle-shoes-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@frontside/backstage-plugin-graphql': patch
---

Allow importing GraphQL Modules into Backstage GraphQL Plugin
5 changes: 5 additions & 0 deletions .changeset/yellow-houses-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@frontside/backstage-plugin-graphql': patch
---

Adding README for `@frontside/backstage-plugin-graphql`
1 change: 1 addition & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@frontside/backstage-plugin-effection-inspector-backend": "0.1.1",
"@frontside/backstage-plugin-humanitec-backend": "^0.3.0",
"@frontside/backstage-plugin-graphql": "^0.2.0",
"graphql-modules": "^2.1.0",
"@gitbeaker/node": "^34.6.0",
"@internal/plugin-healthcheck": "0.1.0",
"@octokit/rest": "^18.5.3",
Expand Down
18 changes: 18 additions & 0 deletions packages/backend/src/graphql/my-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createModule, gql } from 'graphql-modules'

export const myModule = createModule({
id: 'my-module',
dirname: __dirname,
typeDefs: [
gql`
type Query {
hello: String!
}
`
],
resolvers: {
Query: {
hello: () => 'world'
}
}
})
2 changes: 2 additions & 0 deletions packages/backend/src/plugins/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { createRouter } from '@frontside/backstage-plugin-graphql';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
import { myModule } from '../graphql/my-module';

export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createRouter({
modules: [myModule],
logger: env.logger,
catalog: env.catalog,
});
Expand Down
210 changes: 206 additions & 4 deletions plugins/graphql/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,211 @@
# graphql
# @frontside/backstage-plugin-graphql

Welcome to the plugin for Backstage that exposes a GraphQL API for data stored in the Backstage catalog.
> **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](https://www.graphql-modules.com/docs)
4. Strives to support - [GraphQL Server Specification]

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

1. [`Connection`](https://relay.dev/docs/guides/graphql-server-specification/#connections) based on [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm).(see [#68](https://github.com/thefrontside/backstage/issues/68))
2. `viewer` query for retrieving data for the current user. (see [#67](https://github.com/thefrontside/backstage/issues/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](https://discord.gg/yXEYX2h7Ed).

- [@frontside/backstage-plugin-graphql](#frontsidebackstage-plugin-graphql)
- [Getting started](#getting-started)
- [Extending Schema](#extending-schema)
- [In Backstage Backend](#in-backstage-backend)
- [Directives API](#directives-api)
- [`@field(at: String!)`](#fieldat-string)
- [`@relation(type: String!)`](#relationtype-string)
- [`@extend(type: String!)`](#extendtype-string)
- [Integrations](#integrations)
- [Backstage GraphiQL Plugin](#backstage-graphiql-plugin)
- [Backstage API Docs](#backstage-api-docs)

## 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

```ts
import { createRouter } from '@frontside/backstage-plugin-graphql';
import { Router } from 'express';
import { PluginEnvironment } from '../types';

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`

```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](https://github.com/thefrontside/backstage/blob/main/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](https://www.graphql-modules.com) 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` to your backend packages in `packages/backend` with `yarn add graphql-modules`
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

```ts
import { createModule, gql } from 'graphql-modules'

export const myModule = createModule({
id: 'my-module',
dirname: __dirname,
typeDefs: [
gql`
type Query {
hello: String!
}
`
],
resolvers: {
Query: {
hello: () => 'world'
}
}
})
```

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

```ts
import { createRouter } from '@frontside/backstage-plugin-graphql';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
import { myModule } from '../graphql/my-module';

export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createRouter({
modules: [myModule],
logger: env.logger,
catalog: env.catalog,
});
}
```

5. Start your backend and you should be able to query your API with `{ hello }` query to get `{ data: { hello: 'world' } }`

### 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(at: String!)`

`@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`](https://github.com/thefrontside/backstage/blob/main/plugins/graphql/src/app/modules/catalog/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")`

#### `@relation(type: String!)`

`@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 name of the relationship. It will automatically look up the entity in the catalog. For example, here is how we define `consumers` of an API - `consumers: [Component] @relation(type: "apiConsumedBy")`.

#### `@extend(type: String!)`

`@extend` directive allows you to inherit fields from another entity. We created this directive to make it easier to implement types that extend from `Entity` and other types. 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` type in GraphQL would need to reimplement many fields that are defined on Entity which leads to lots of duplication. Using this type, you can easily create a new type that includes all of the properties of the parent. For example, if you wanted to create a `Repository` type, you can do the following,

```graphql
type Repository @extends(type: "Entity") {
languages: [String] @field('spec.languages')
}
```
yarn add @frontside/backstage-plugin-graphql
```

Your `Repository` type will automatically get all of the properties from `Entity`.

## 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](https://roadie.io/backstage/plugins/graphiQL/) and adding the GraphQL API endpoint to the GraphiQL Plugin API factory.

1. Once you installed `@backstage/plugin-graphiql` plugin [with these instructions](https://roadie.io/backstage/plugins/graphiQL/)
2. Modify `packages/app/src/apis.ts` to add your GraphQL API as an endpoint

```ts
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`](https://github.com/thefrontside/backstage/blob/main/packages/app/src/apis.ts#L35).

### 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`

```yaml
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: backstage-graphql-api
description: GraphQL API provided by GraphQL Plugin
spec:
type: graphql
owner: [email protected]
lifecycle: production
definition:
$text: http://localhost:7007/api/graphql/schema
```
2. Modify `app-config.yaml` to allow reading urls from `localhost:7007`

```yaml
backend:
...

reading:
allow:
- host: localhost:7007
```

3 changes: 2 additions & 1 deletion plugins/graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
},
"files": [
"dist",
"src/app/modules/**/*.graphql"
"src/app/modules/**/*.graphql",
"README.md"
],
"jest": {
"testTimeout": 15000
Expand Down
31 changes: 17 additions & 14 deletions plugins/graphql/src/app/app.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,42 @@
import type { CatalogApi, ResolverContext } from './types';
import { GetEnvelopedFn, envelop, useExtendContext } from '@envelop/core';
import type { CatalogApi } from './types';
import { envelop, useExtendContext } from '@envelop/core';
import { useGraphQLModules } from '@envelop/graphql-modules';
import { Application, createApplication } from 'graphql-modules';
import { createApplication, Module } from 'graphql-modules';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { createLoader } from './loaders';
import { Catalog } from './modules/catalog/catalog';
import { Core } from './modules/core/core';
import { transform } from './schema-mapper';

export interface App {
(): ReturnType<GetEnvelopedFn<ResolverContext>>;
export interface createGraphQLAppOptions {
catalog: CatalogApi;
modules: Module[]
}

export const schema = create().schema;

export function createApp(catalog: CatalogApi): App {
const application = create();
const loader = createLoader({ catalog });
export function createGraphQLApp(options: createGraphQLAppOptions) {
const application = create(options);
const loader = createLoader(options);

const run = envelop({
plugins: [
useExtendContext(() => ({ catalog, loader })),
useExtendContext(() => ({ catalog: options.catalog, loader })),
useGraphQLModules(application),
],
});

return run;
return { run, application };
}

interface CreateOptions {
modules: Module[]
}

function create(): Application {
function create(options: CreateOptions) {
return createApplication({
schemaBuilder: ({ typeDefs, resolvers }) =>
transform(makeExecutableSchema({
typeDefs, resolvers
})),
modules: [Core, Catalog],
modules: [Core, Catalog, ...options.modules],
});
}
15 changes: 10 additions & 5 deletions plugins/graphql/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { Logger } from 'winston';
import { graphqlHTTP } from 'express-graphql';
import { CatalogClient } from '@backstage/catalog-client';
import { printSchema } from 'graphql';
import type { Module } from 'graphql-modules';

export interface RouterOptions {
logger: Logger;
catalog: CatalogClient;
modules?: Module[]
}

import { schema, createApp } from './app';
import { createGraphQLApp } from './app';

export * from './app';

Expand All @@ -20,7 +22,10 @@ export async function createRouter(
): Promise<express.Router> {
const { logger } = options;

const app = createApp(options.catalog);
const { run, application } = createGraphQLApp({
catalog: options.catalog,
modules: options.modules ?? []
});

const router = Router();
router.use(express.json());
Expand All @@ -30,13 +35,13 @@ export async function createRouter(
});

router.get('/schema', (_, response) => {
response.send(printSchema(app().schema))
response.send(printSchema(application.schema))
});

router.use('/', graphqlHTTP(async () => {
const { parse, validate, contextFactory, execute } = app();
const { parse, validate, contextFactory, execute } = run();
return {
schema,
schema: application.schema,
graphiql: true,
customParseFn: parse,
customValidateFn: validate,
Expand Down
2 changes: 0 additions & 2 deletions plugins/graphql/src/tests/graphql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ describe('querying the graphql API', () => {
harness.create("User", {
displayName: "Janelle Dawe"
})
let users = [...harness.all('User')];
console.dir({ users }, { depth: 5 });
});


Expand Down
Loading