Merge pull request #1914 from spotify/mob/union-types
Add Catalog API + Union Types to GraphQL
This commit is contained in:
@@ -64,7 +64,6 @@ async function main() {
|
||||
const configs = await loadBackendConfig();
|
||||
const configReader = ConfigReader.fromConfigs(configs);
|
||||
const createEnv = makeCreateEnv(configs);
|
||||
|
||||
const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck'));
|
||||
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
|
||||
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
|
||||
|
||||
@@ -30,10 +30,14 @@
|
||||
*/
|
||||
|
||||
import { createRouter } from '@backstage/plugin-graphql-backend';
|
||||
import type { PluginEnvironment } from '../types';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({ logger }: PluginEnvironment) {
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
}: PluginEnvironment) {
|
||||
return await createRouter({
|
||||
logger,
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
# Catalog GraphQL Plugin
|
||||
|
||||
## Getting Started
|
||||
|
||||
This is the Catalog GraphQL plugin.
|
||||
|
||||
It provides the `catalog` part of the GraphQL schema.
|
||||
|
||||
To register it with the GraphQL backend, be sure to follow the [Getting Started](../graphql/README.md#getting-started) guide of the GraphQL plugin.
|
||||
|
||||
<!-- TODO: Need to make the GraphQL plugin compatible with non forked repos >
|
||||
@@ -0,0 +1,15 @@
|
||||
overwrite: true
|
||||
generates:
|
||||
./src/graphql/types.ts:
|
||||
schema: ./src/schema.js
|
||||
plugins:
|
||||
- typescript
|
||||
- typescript-resolvers
|
||||
hooks:
|
||||
afterOneFileWrite:
|
||||
- eslint --fix
|
||||
config:
|
||||
contextType: '@graphql-modules/core#ModuleContext'
|
||||
allowParentTypeOverride: true
|
||||
useIndexSignature: true
|
||||
defaultMapper: Partial<{T}>
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@backstage/plugin-catalog-graphql",
|
||||
"version": "0.1.1-alpha.22",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli backend:dev",
|
||||
"build": "backstage-cli backend:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"generate:types": "graphql-codegen",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.22",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.22",
|
||||
"@backstage/config": "^0.1.1-alpha.22",
|
||||
"@graphql-modules/core": "^0.7.17",
|
||||
"apollo-server": "^2.16.1",
|
||||
"graphql": "^15.3.0",
|
||||
"graphql-tag": "^2.11.0",
|
||||
"graphql-type-json": "^0.3.2",
|
||||
"node-fetch": "^2.6.0",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.22",
|
||||
"@types/express": "^4.17.7",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"@graphql-codegen/cli": "^1.17.7",
|
||||
"@graphql-codegen/typescript": "^1.17.7",
|
||||
"@graphql-codegen/typescript-resolvers": "^1.17.7",
|
||||
"ts-node": "^8.10.2",
|
||||
"eslint-plugin-graphql": "^4.0.0",
|
||||
"msw": "^0.19.5",
|
||||
"supertest": "^4.0.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { createModule } from './module';
|
||||
import { execute } from 'graphql';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { ReaderEntity } from '../service/client';
|
||||
import { createLogger } from 'winston';
|
||||
import { gql } from 'apollo-server';
|
||||
|
||||
describe('Catalog Module', () => {
|
||||
const worker = setupServer();
|
||||
const mockCatalogBaseUrl = 'http://im.mock';
|
||||
const mockConfig = ConfigReader.fromConfigs([
|
||||
{
|
||||
context: '',
|
||||
data: {
|
||||
backend: {
|
||||
baseUrl: mockCatalogBaseUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
beforeAll(() => worker.listen());
|
||||
afterAll(() => worker.close());
|
||||
|
||||
afterEach(() => worker.resetHandlers());
|
||||
|
||||
describe('Default Entity', () => {
|
||||
beforeEach(() => {
|
||||
const mockResponse: ReaderEntity[] = [
|
||||
{
|
||||
apiVersion: 'something',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
annotations: {},
|
||||
etag: '123',
|
||||
generation: 1,
|
||||
labels: {},
|
||||
name: 'Ben',
|
||||
namespace: 'Blames',
|
||||
uid: '123',
|
||||
},
|
||||
spec: {
|
||||
type: 'thing',
|
||||
lifecycle: 'something',
|
||||
owner: 'auser',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
worker.use(
|
||||
rest.get(`${mockCatalogBaseUrl}/catalog/entities`, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockResponse)),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should call the catalog client when requesting entities', async () => {
|
||||
const { schema } = await createModule({
|
||||
config: mockConfig,
|
||||
logger: createLogger(),
|
||||
});
|
||||
|
||||
const result = await execute({
|
||||
schema,
|
||||
document: gql`
|
||||
query {
|
||||
catalog {
|
||||
list {
|
||||
kind
|
||||
apiVersion
|
||||
metadata {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const [catalogItem] = result.data?.catalog.list;
|
||||
expect(catalogItem.kind).toBe('Component');
|
||||
expect(catalogItem.apiVersion).toBe('something');
|
||||
expect(catalogItem.metadata.name).toBe('Ben');
|
||||
});
|
||||
|
||||
it('Defaults to empty annotations when none are provided', async () => {
|
||||
const mockResponse: ReaderEntity[] = [
|
||||
{
|
||||
apiVersion: 'something',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
annotations: null as any,
|
||||
etag: '123',
|
||||
generation: 1,
|
||||
labels: {},
|
||||
name: 'Ben',
|
||||
namespace: 'Blames',
|
||||
uid: '123',
|
||||
},
|
||||
spec: {
|
||||
type: 'thing',
|
||||
lifecycle: 'something',
|
||||
owner: 'auser',
|
||||
},
|
||||
},
|
||||
];
|
||||
worker.use(
|
||||
rest.get(`${mockCatalogBaseUrl}/catalog/entities`, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
const { schema } = await createModule({
|
||||
config: mockConfig,
|
||||
logger: createLogger(),
|
||||
});
|
||||
|
||||
const result = await execute({
|
||||
schema,
|
||||
document: gql`
|
||||
query {
|
||||
catalog {
|
||||
list {
|
||||
metadata {
|
||||
annotations
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const [catalogItem] = result.data?.catalog.list;
|
||||
expect(catalogItem.metadata.annotations).toEqual({});
|
||||
});
|
||||
|
||||
it('Defaults to empty labels when none are provided', async () => {
|
||||
const mockResponse: ReaderEntity[] = [
|
||||
{
|
||||
apiVersion: 'something',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
annotations: {},
|
||||
etag: '123',
|
||||
generation: 1,
|
||||
labels: null as any,
|
||||
name: 'Ben',
|
||||
namespace: 'Blames',
|
||||
uid: '123',
|
||||
},
|
||||
spec: {
|
||||
type: 'thing',
|
||||
lifecycle: 'something',
|
||||
owner: 'auser',
|
||||
},
|
||||
},
|
||||
];
|
||||
worker.use(
|
||||
rest.get(`${mockCatalogBaseUrl}/catalog/entities`, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
const { schema } = await createModule({
|
||||
config: mockConfig,
|
||||
logger: createLogger(),
|
||||
});
|
||||
|
||||
const result = await execute({
|
||||
schema,
|
||||
document: gql`
|
||||
query {
|
||||
catalog {
|
||||
list {
|
||||
metadata {
|
||||
labels
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const [catalogItem] = result.data?.catalog.list;
|
||||
expect(catalogItem.metadata.labels).toEqual({});
|
||||
});
|
||||
it('Returns the correct record from the annotation dictionary', async () => {
|
||||
const mockResponse: ReaderEntity[] = [
|
||||
{
|
||||
apiVersion: 'something',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
annotations: { lob: 'bloben' },
|
||||
etag: '123',
|
||||
generation: 1,
|
||||
labels: {},
|
||||
name: 'Ben',
|
||||
namespace: 'Blames',
|
||||
uid: '123',
|
||||
},
|
||||
spec: {
|
||||
type: 'thing',
|
||||
lifecycle: 'something',
|
||||
owner: 'auser',
|
||||
},
|
||||
},
|
||||
];
|
||||
worker.use(
|
||||
rest.get(`${mockCatalogBaseUrl}/catalog/entities`, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
const { schema } = await createModule({
|
||||
config: mockConfig,
|
||||
logger: createLogger(),
|
||||
});
|
||||
|
||||
const result = await execute({
|
||||
schema,
|
||||
document: gql`
|
||||
query {
|
||||
catalog {
|
||||
list {
|
||||
metadata {
|
||||
test: annotation(name: "lob")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const [catalogItem] = result.data?.catalog.list;
|
||||
expect(catalogItem.metadata.test).toEqual('bloben');
|
||||
});
|
||||
it('Returns the correct record from the labels dictionary', async () => {
|
||||
const mockResponse: ReaderEntity[] = [
|
||||
{
|
||||
apiVersion: 'something',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
annotations: {},
|
||||
etag: '123',
|
||||
generation: 1,
|
||||
labels: { lob2: 'bloben' },
|
||||
name: 'Ben',
|
||||
namespace: 'Blames',
|
||||
uid: '123',
|
||||
},
|
||||
spec: {
|
||||
type: 'thing',
|
||||
lifecycle: 'something',
|
||||
owner: 'auser',
|
||||
},
|
||||
},
|
||||
];
|
||||
worker.use(
|
||||
rest.get(`${mockCatalogBaseUrl}/catalog/entities`, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
const { schema } = await createModule({
|
||||
config: mockConfig,
|
||||
logger: createLogger(),
|
||||
});
|
||||
|
||||
const result = await execute({
|
||||
schema,
|
||||
document: gql`
|
||||
query {
|
||||
catalog {
|
||||
list {
|
||||
metadata {
|
||||
test: label(name: "lob2")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const [catalogItem] = result.data?.catalog.list;
|
||||
expect(catalogItem.metadata.test).toEqual('bloben');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { Logger } from 'winston';
|
||||
import { makeExecutableSchema } from 'apollo-server';
|
||||
import { GraphQLModule } from '@graphql-modules/core';
|
||||
import { Resolvers, CatalogQuery } from './types';
|
||||
import { Config } from '@backstage/config';
|
||||
import { CatalogClient } from '../service/client';
|
||||
import GraphQLJSON, { GraphQLJSONObject } from 'graphql-type-json';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
export interface ModuleOptions {
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
export async function createModule(
|
||||
options: ModuleOptions,
|
||||
): Promise<GraphQLModule> {
|
||||
const { default: typeDefs } = require('../schema');
|
||||
|
||||
const catalogClient = new CatalogClient(
|
||||
options.config.getString('backend.baseUrl'),
|
||||
);
|
||||
|
||||
const resolvers: Resolvers = {
|
||||
JSON: GraphQLJSON,
|
||||
JSONObject: GraphQLJSONObject,
|
||||
DefaultEntitySpec: {
|
||||
raw: rootValue => {
|
||||
const { entity } = rootValue as { entity: Entity };
|
||||
return entity.spec ?? null;
|
||||
},
|
||||
},
|
||||
Query: {
|
||||
catalog: () => ({} as CatalogQuery),
|
||||
},
|
||||
CatalogQuery: {
|
||||
list: async () => {
|
||||
return await catalogClient.list();
|
||||
},
|
||||
},
|
||||
CatalogEntity: {
|
||||
metadata: entity => ({ ...entity.metadata!, entity }),
|
||||
spec: entity => ({ ...entity.spec!, entity }),
|
||||
},
|
||||
EntityMetadata: {
|
||||
__resolveType: rootValue => {
|
||||
const {
|
||||
entity: { kind },
|
||||
} = rootValue as { entity: Entity };
|
||||
switch (kind) {
|
||||
case 'Component':
|
||||
return 'ComponentMetadata';
|
||||
case 'Template':
|
||||
return 'TemplateMetadata';
|
||||
default:
|
||||
return 'DefaultEntityMetadata';
|
||||
}
|
||||
},
|
||||
annotation: (e, { name }) => e.annotations?.[name] ?? null,
|
||||
labels: e => e.labels ?? {},
|
||||
annotations: e => e.annotations ?? {},
|
||||
label: (e, { name }) => e.labels?.[name] ?? null,
|
||||
},
|
||||
EntitySpec: {
|
||||
__resolveType: rootValue => {
|
||||
const {
|
||||
entity: { kind },
|
||||
} = rootValue as { entity: Entity };
|
||||
|
||||
switch (kind) {
|
||||
case 'Component':
|
||||
return 'ComponentEntitySpec';
|
||||
case 'Template':
|
||||
return 'TemplateEntitySpec';
|
||||
default:
|
||||
return 'DefaultEntitySpec';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const schema = makeExecutableSchema({
|
||||
typeDefs,
|
||||
resolvers,
|
||||
inheritResolversFromInterfaces: true,
|
||||
});
|
||||
|
||||
const module = new GraphQLModule({
|
||||
extraSchemas: [schema],
|
||||
logger: options.logger as any,
|
||||
});
|
||||
|
||||
return module;
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 {
|
||||
GraphQLResolveInfo,
|
||||
GraphQLScalarType,
|
||||
GraphQLScalarTypeConfig,
|
||||
} from 'graphql';
|
||||
import { ModuleContext } from '@graphql-modules/core';
|
||||
|
||||
export type Maybe<T> = T | null;
|
||||
export type Exact<T extends { [key: string]: unknown }> = {
|
||||
[K in keyof T]: T[K];
|
||||
};
|
||||
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||
export type RequireFields<T, K extends keyof T> = {
|
||||
[X in Exclude<keyof T, K>]?: T[X];
|
||||
} &
|
||||
{ [P in K]-?: NonNullable<T[P]> };
|
||||
/** All built-in and custom scalars, mapped to their actual values */
|
||||
export type Scalars = {
|
||||
ID: string;
|
||||
String: string;
|
||||
Boolean: boolean;
|
||||
Int: number;
|
||||
Float: number;
|
||||
JSON: any;
|
||||
JSONObject: any;
|
||||
};
|
||||
|
||||
export type EntityMetadata = {
|
||||
name: Scalars['String'];
|
||||
annotations: Scalars['JSONObject'];
|
||||
annotation?: Maybe<Scalars['JSON']>;
|
||||
labels: Scalars['JSONObject'];
|
||||
label?: Maybe<Scalars['JSON']>;
|
||||
uid: Scalars['String'];
|
||||
etag: Scalars['String'];
|
||||
generation: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type EntityMetadataAnnotationArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type EntityMetadataLabelArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type DefaultEntityMetadata = EntityMetadata & {
|
||||
__typename?: 'DefaultEntityMetadata';
|
||||
name: Scalars['String'];
|
||||
annotations: Scalars['JSONObject'];
|
||||
annotation?: Maybe<Scalars['JSON']>;
|
||||
labels: Scalars['JSONObject'];
|
||||
label?: Maybe<Scalars['JSON']>;
|
||||
uid: Scalars['String'];
|
||||
etag: Scalars['String'];
|
||||
generation: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type DefaultEntityMetadataAnnotationArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type DefaultEntityMetadataLabelArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ComponentMetadata = EntityMetadata & {
|
||||
__typename?: 'ComponentMetadata';
|
||||
name: Scalars['String'];
|
||||
annotations: Scalars['JSONObject'];
|
||||
annotation?: Maybe<Scalars['JSON']>;
|
||||
labels: Scalars['JSONObject'];
|
||||
label?: Maybe<Scalars['JSON']>;
|
||||
uid: Scalars['String'];
|
||||
etag: Scalars['String'];
|
||||
generation: Scalars['Int'];
|
||||
relationships?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ComponentMetadataAnnotationArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ComponentMetadataLabelArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type TemplateMetadata = EntityMetadata & {
|
||||
__typename?: 'TemplateMetadata';
|
||||
name: Scalars['String'];
|
||||
annotations: Scalars['JSONObject'];
|
||||
annotation?: Maybe<Scalars['JSON']>;
|
||||
labels: Scalars['JSONObject'];
|
||||
label?: Maybe<Scalars['JSON']>;
|
||||
uid: Scalars['String'];
|
||||
etag: Scalars['String'];
|
||||
generation: Scalars['Int'];
|
||||
updatedBy?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type TemplateMetadataAnnotationArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type TemplateMetadataLabelArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type TemplateEntitySpec = {
|
||||
__typename?: 'TemplateEntitySpec';
|
||||
type: Scalars['String'];
|
||||
path?: Maybe<Scalars['String']>;
|
||||
schema: Scalars['JSONObject'];
|
||||
templater: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ComponentEntitySpec = {
|
||||
__typename?: 'ComponentEntitySpec';
|
||||
title: Scalars['String'];
|
||||
lifecycle: Scalars['String'];
|
||||
owner: Scalars['String'];
|
||||
};
|
||||
|
||||
export type LocationEntitySpec = {
|
||||
__typename?: 'LocationEntitySpec';
|
||||
type: Scalars['String'];
|
||||
target?: Maybe<Scalars['String']>;
|
||||
targets: Array<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type DefaultEntitySpec = {
|
||||
__typename?: 'DefaultEntitySpec';
|
||||
raw?: Maybe<Scalars['JSONObject']>;
|
||||
};
|
||||
|
||||
export type EntitySpec =
|
||||
| DefaultEntitySpec
|
||||
| TemplateEntitySpec
|
||||
| LocationEntitySpec
|
||||
| ComponentEntitySpec;
|
||||
|
||||
export type CatalogEntity = {
|
||||
__typename?: 'CatalogEntity';
|
||||
apiVersion: Scalars['String'];
|
||||
kind: Scalars['String'];
|
||||
metadata?: Maybe<EntityMetadata>;
|
||||
spec: EntitySpec;
|
||||
};
|
||||
|
||||
export type CatalogQuery = {
|
||||
__typename?: 'CatalogQuery';
|
||||
list: Array<CatalogEntity>;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
catalog: CatalogQuery;
|
||||
};
|
||||
|
||||
export type WithIndex<TObject> = TObject & Record<string, any>;
|
||||
export type ResolversObject<TObject> = WithIndex<TObject>;
|
||||
|
||||
export type ResolverTypeWrapper<T> = Promise<T> | T;
|
||||
|
||||
export type LegacyStitchingResolver<TResult, TParent, TContext, TArgs> = {
|
||||
fragment: string;
|
||||
resolve: ResolverFn<TResult, TParent, TContext, TArgs>;
|
||||
};
|
||||
|
||||
export type NewStitchingResolver<TResult, TParent, TContext, TArgs> = {
|
||||
selectionSet: string;
|
||||
resolve: ResolverFn<TResult, TParent, TContext, TArgs>;
|
||||
};
|
||||
export type StitchingResolver<TResult, TParent, TContext, TArgs> =
|
||||
| LegacyStitchingResolver<TResult, TParent, TContext, TArgs>
|
||||
| NewStitchingResolver<TResult, TParent, TContext, TArgs>;
|
||||
export type Resolver<TResult, TParent = {}, TContext = {}, TArgs = {}> =
|
||||
| ResolverFn<TResult, TParent, TContext, TArgs>
|
||||
| StitchingResolver<TResult, TParent, TContext, TArgs>;
|
||||
|
||||
export type ResolverFn<TResult, TParent, TContext, TArgs> = (
|
||||
parent: TParent,
|
||||
args: TArgs,
|
||||
context: TContext,
|
||||
info: GraphQLResolveInfo,
|
||||
) => Promise<TResult> | TResult;
|
||||
|
||||
export type SubscriptionSubscribeFn<TResult, TParent, TContext, TArgs> = (
|
||||
parent: TParent,
|
||||
args: TArgs,
|
||||
context: TContext,
|
||||
info: GraphQLResolveInfo,
|
||||
) => AsyncIterator<TResult> | Promise<AsyncIterator<TResult>>;
|
||||
|
||||
export type SubscriptionResolveFn<TResult, TParent, TContext, TArgs> = (
|
||||
parent: TParent,
|
||||
args: TArgs,
|
||||
context: TContext,
|
||||
info: GraphQLResolveInfo,
|
||||
) => TResult | Promise<TResult>;
|
||||
|
||||
export interface SubscriptionSubscriberObject<
|
||||
TResult,
|
||||
TKey extends string,
|
||||
TParent,
|
||||
TContext,
|
||||
TArgs
|
||||
> {
|
||||
subscribe: SubscriptionSubscribeFn<
|
||||
{ [key in TKey]: TResult },
|
||||
TParent,
|
||||
TContext,
|
||||
TArgs
|
||||
>;
|
||||
resolve?: SubscriptionResolveFn<
|
||||
TResult,
|
||||
{ [key in TKey]: TResult },
|
||||
TContext,
|
||||
TArgs
|
||||
>;
|
||||
}
|
||||
|
||||
export interface SubscriptionResolverObject<TResult, TParent, TContext, TArgs> {
|
||||
subscribe: SubscriptionSubscribeFn<any, TParent, TContext, TArgs>;
|
||||
resolve: SubscriptionResolveFn<TResult, any, TContext, TArgs>;
|
||||
}
|
||||
|
||||
export type SubscriptionObject<
|
||||
TResult,
|
||||
TKey extends string,
|
||||
TParent,
|
||||
TContext,
|
||||
TArgs
|
||||
> =
|
||||
| SubscriptionSubscriberObject<TResult, TKey, TParent, TContext, TArgs>
|
||||
| SubscriptionResolverObject<TResult, TParent, TContext, TArgs>;
|
||||
|
||||
export type SubscriptionResolver<
|
||||
TResult,
|
||||
TKey extends string,
|
||||
TParent = {},
|
||||
TContext = {},
|
||||
TArgs = {}
|
||||
> =
|
||||
| ((
|
||||
...args: any[]
|
||||
) => SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>)
|
||||
| SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>;
|
||||
|
||||
export type TypeResolveFn<TTypes, TParent = {}, TContext = {}> = (
|
||||
parent: TParent,
|
||||
context: TContext,
|
||||
info: GraphQLResolveInfo,
|
||||
) => Maybe<TTypes> | Promise<Maybe<TTypes>>;
|
||||
|
||||
export type IsTypeOfResolverFn<T = {}> = (
|
||||
obj: T,
|
||||
info: GraphQLResolveInfo,
|
||||
) => boolean | Promise<boolean>;
|
||||
|
||||
export type NextResolverFn<T> = () => Promise<T>;
|
||||
|
||||
export type DirectiveResolverFn<
|
||||
TResult = {},
|
||||
TParent = {},
|
||||
TContext = {},
|
||||
TArgs = {}
|
||||
> = (
|
||||
next: NextResolverFn<TResult>,
|
||||
parent: TParent,
|
||||
args: TArgs,
|
||||
context: TContext,
|
||||
info: GraphQLResolveInfo,
|
||||
) => TResult | Promise<TResult>;
|
||||
|
||||
/** Mapping between all available schema types and the resolvers types */
|
||||
export type ResolversTypes = ResolversObject<{
|
||||
JSON: ResolverTypeWrapper<Partial<Scalars['JSON']>>;
|
||||
JSONObject: ResolverTypeWrapper<Partial<Scalars['JSONObject']>>;
|
||||
EntityMetadata:
|
||||
| ResolversTypes['DefaultEntityMetadata']
|
||||
| ResolversTypes['ComponentMetadata']
|
||||
| ResolversTypes['TemplateMetadata'];
|
||||
String: ResolverTypeWrapper<Partial<Scalars['String']>>;
|
||||
Int: ResolverTypeWrapper<Partial<Scalars['Int']>>;
|
||||
DefaultEntityMetadata: ResolverTypeWrapper<Partial<DefaultEntityMetadata>>;
|
||||
ComponentMetadata: ResolverTypeWrapper<Partial<ComponentMetadata>>;
|
||||
TemplateMetadata: ResolverTypeWrapper<Partial<TemplateMetadata>>;
|
||||
TemplateEntitySpec: ResolverTypeWrapper<Partial<TemplateEntitySpec>>;
|
||||
ComponentEntitySpec: ResolverTypeWrapper<Partial<ComponentEntitySpec>>;
|
||||
LocationEntitySpec: ResolverTypeWrapper<Partial<LocationEntitySpec>>;
|
||||
DefaultEntitySpec: ResolverTypeWrapper<Partial<DefaultEntitySpec>>;
|
||||
EntitySpec: Partial<
|
||||
| ResolversTypes['DefaultEntitySpec']
|
||||
| ResolversTypes['TemplateEntitySpec']
|
||||
| ResolversTypes['LocationEntitySpec']
|
||||
| ResolversTypes['ComponentEntitySpec']
|
||||
>;
|
||||
CatalogEntity: ResolverTypeWrapper<
|
||||
Partial<
|
||||
Omit<CatalogEntity, 'spec'> & { spec: ResolversTypes['EntitySpec'] }
|
||||
>
|
||||
>;
|
||||
CatalogQuery: ResolverTypeWrapper<Partial<CatalogQuery>>;
|
||||
Query: ResolverTypeWrapper<{}>;
|
||||
Boolean: ResolverTypeWrapper<Partial<Scalars['Boolean']>>;
|
||||
}>;
|
||||
|
||||
/** Mapping between all available schema types and the resolvers parents */
|
||||
export type ResolversParentTypes = ResolversObject<{
|
||||
JSON: Partial<Scalars['JSON']>;
|
||||
JSONObject: Partial<Scalars['JSONObject']>;
|
||||
EntityMetadata:
|
||||
| ResolversParentTypes['DefaultEntityMetadata']
|
||||
| ResolversParentTypes['ComponentMetadata']
|
||||
| ResolversParentTypes['TemplateMetadata'];
|
||||
String: Partial<Scalars['String']>;
|
||||
Int: Partial<Scalars['Int']>;
|
||||
DefaultEntityMetadata: Partial<DefaultEntityMetadata>;
|
||||
ComponentMetadata: Partial<ComponentMetadata>;
|
||||
TemplateMetadata: Partial<TemplateMetadata>;
|
||||
TemplateEntitySpec: Partial<TemplateEntitySpec>;
|
||||
ComponentEntitySpec: Partial<ComponentEntitySpec>;
|
||||
LocationEntitySpec: Partial<LocationEntitySpec>;
|
||||
DefaultEntitySpec: Partial<DefaultEntitySpec>;
|
||||
EntitySpec: Partial<
|
||||
| ResolversParentTypes['DefaultEntitySpec']
|
||||
| ResolversParentTypes['TemplateEntitySpec']
|
||||
| ResolversParentTypes['LocationEntitySpec']
|
||||
| ResolversParentTypes['ComponentEntitySpec']
|
||||
>;
|
||||
CatalogEntity: Partial<
|
||||
Omit<CatalogEntity, 'spec'> & { spec: ResolversParentTypes['EntitySpec'] }
|
||||
>;
|
||||
CatalogQuery: Partial<CatalogQuery>;
|
||||
Query: {};
|
||||
Boolean: Partial<Scalars['Boolean']>;
|
||||
}>;
|
||||
|
||||
export interface JsonScalarConfig
|
||||
extends GraphQLScalarTypeConfig<ResolversTypes['JSON'], any> {
|
||||
name: 'JSON';
|
||||
}
|
||||
|
||||
export interface JsonObjectScalarConfig
|
||||
extends GraphQLScalarTypeConfig<ResolversTypes['JSONObject'], any> {
|
||||
name: 'JSONObject';
|
||||
}
|
||||
|
||||
export type EntityMetadataResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['EntityMetadata']
|
||||
> = ResolversObject<{
|
||||
__resolveType: TypeResolveFn<
|
||||
'DefaultEntityMetadata' | 'ComponentMetadata' | 'TemplateMetadata',
|
||||
ParentType,
|
||||
ContextType
|
||||
>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
annotations?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
annotation?: Resolver<
|
||||
Maybe<ResolversTypes['JSON']>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<EntityMetadataAnnotationArgs, 'name'>
|
||||
>;
|
||||
labels?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
label?: Resolver<
|
||||
Maybe<ResolversTypes['JSON']>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<EntityMetadataLabelArgs, 'name'>
|
||||
>;
|
||||
uid?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
etag?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
generation?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
}>;
|
||||
|
||||
export type DefaultEntityMetadataResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['DefaultEntityMetadata']
|
||||
> = ResolversObject<{
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
annotations?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
annotation?: Resolver<
|
||||
Maybe<ResolversTypes['JSON']>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<DefaultEntityMetadataAnnotationArgs, 'name'>
|
||||
>;
|
||||
labels?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
label?: Resolver<
|
||||
Maybe<ResolversTypes['JSON']>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<DefaultEntityMetadataLabelArgs, 'name'>
|
||||
>;
|
||||
uid?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
etag?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
generation?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type ComponentMetadataResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['ComponentMetadata']
|
||||
> = ResolversObject<{
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
annotations?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
annotation?: Resolver<
|
||||
Maybe<ResolversTypes['JSON']>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<ComponentMetadataAnnotationArgs, 'name'>
|
||||
>;
|
||||
labels?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
label?: Resolver<
|
||||
Maybe<ResolversTypes['JSON']>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<ComponentMetadataLabelArgs, 'name'>
|
||||
>;
|
||||
uid?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
etag?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
generation?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
relationships?: Resolver<
|
||||
Maybe<ResolversTypes['String']>,
|
||||
ParentType,
|
||||
ContextType
|
||||
>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type TemplateMetadataResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['TemplateMetadata']
|
||||
> = ResolversObject<{
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
annotations?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
annotation?: Resolver<
|
||||
Maybe<ResolversTypes['JSON']>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<TemplateMetadataAnnotationArgs, 'name'>
|
||||
>;
|
||||
labels?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
label?: Resolver<
|
||||
Maybe<ResolversTypes['JSON']>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<TemplateMetadataLabelArgs, 'name'>
|
||||
>;
|
||||
uid?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
etag?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
generation?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
updatedBy?: Resolver<
|
||||
Maybe<ResolversTypes['String']>,
|
||||
ParentType,
|
||||
ContextType
|
||||
>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type TemplateEntitySpecResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['TemplateEntitySpec']
|
||||
> = ResolversObject<{
|
||||
type?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
path?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
schema?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
templater?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type ComponentEntitySpecResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['ComponentEntitySpec']
|
||||
> = ResolversObject<{
|
||||
title?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
lifecycle?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
owner?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type LocationEntitySpecResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['LocationEntitySpec']
|
||||
> = ResolversObject<{
|
||||
type?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
target?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
targets?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type DefaultEntitySpecResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['DefaultEntitySpec']
|
||||
> = ResolversObject<{
|
||||
raw?: Resolver<Maybe<ResolversTypes['JSONObject']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type EntitySpecResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['EntitySpec']
|
||||
> = ResolversObject<{
|
||||
__resolveType: TypeResolveFn<
|
||||
| 'DefaultEntitySpec'
|
||||
| 'TemplateEntitySpec'
|
||||
| 'LocationEntitySpec'
|
||||
| 'ComponentEntitySpec',
|
||||
ParentType,
|
||||
ContextType
|
||||
>;
|
||||
}>;
|
||||
|
||||
export type CatalogEntityResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['CatalogEntity']
|
||||
> = ResolversObject<{
|
||||
apiVersion?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
kind?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
metadata?: Resolver<
|
||||
Maybe<ResolversTypes['EntityMetadata']>,
|
||||
ParentType,
|
||||
ContextType
|
||||
>;
|
||||
spec?: Resolver<ResolversTypes['EntitySpec'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type CatalogQueryResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['CatalogQuery']
|
||||
> = ResolversObject<{
|
||||
list?: Resolver<
|
||||
Array<ResolversTypes['CatalogEntity']>,
|
||||
ParentType,
|
||||
ContextType
|
||||
>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
|
||||
}>;
|
||||
|
||||
export type QueryResolvers<
|
||||
ContextType = ModuleContext,
|
||||
ParentType = ResolversParentTypes['Query']
|
||||
> = ResolversObject<{
|
||||
catalog?: Resolver<ResolversTypes['CatalogQuery'], ParentType, ContextType>;
|
||||
}>;
|
||||
|
||||
export type Resolvers<ContextType = ModuleContext> = ResolversObject<{
|
||||
JSON?: GraphQLScalarType;
|
||||
JSONObject?: GraphQLScalarType;
|
||||
EntityMetadata?: EntityMetadataResolvers<ContextType>;
|
||||
DefaultEntityMetadata?: DefaultEntityMetadataResolvers<ContextType>;
|
||||
ComponentMetadata?: ComponentMetadataResolvers<ContextType>;
|
||||
TemplateMetadata?: TemplateMetadataResolvers<ContextType>;
|
||||
TemplateEntitySpec?: TemplateEntitySpecResolvers<ContextType>;
|
||||
ComponentEntitySpec?: ComponentEntitySpecResolvers<ContextType>;
|
||||
LocationEntitySpec?: LocationEntitySpecResolvers<ContextType>;
|
||||
DefaultEntitySpec?: DefaultEntitySpecResolvers<ContextType>;
|
||||
EntitySpec?: EntitySpecResolvers<ContextType>;
|
||||
CatalogEntity?: CatalogEntityResolvers<ContextType>;
|
||||
CatalogQuery?: CatalogQueryResolvers<ContextType>;
|
||||
Query?: QueryResolvers<ContextType>;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Use "Resolvers" root object instead. If you wish to get "IResolvers", add "typesPrefix: I" to your config.
|
||||
*/
|
||||
export type IResolvers<ContextType = ModuleContext> = Resolvers<ContextType>;
|
||||
@@ -14,20 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getRootLogger } from '@backstage/backend-common';
|
||||
import yn from 'yn';
|
||||
import { startStandaloneServer } from './service/standaloneServer';
|
||||
|
||||
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000;
|
||||
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
|
||||
const logger = getRootLogger();
|
||||
|
||||
startStandaloneServer({ port, enableCors, logger }).catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('CTRL+C pressed; exiting.');
|
||||
process.exit(0);
|
||||
});
|
||||
export * from './graphql/module';
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const metadataFields = /* GraphQL */ `
|
||||
name: String!
|
||||
annotations: JSONObject!
|
||||
annotation(name: String!): JSON
|
||||
labels: JSONObject!
|
||||
label(name: String!): JSON
|
||||
uid: String!
|
||||
etag: String!
|
||||
generation: Int!
|
||||
`;
|
||||
|
||||
const schema = /* GraphQL */ `
|
||||
scalar JSON
|
||||
scalar JSONObject
|
||||
|
||||
interface EntityMetadata {
|
||||
${metadataFields}
|
||||
}
|
||||
|
||||
type DefaultEntityMetadata implements EntityMetadata {
|
||||
${metadataFields}
|
||||
}
|
||||
|
||||
type ComponentMetadata implements EntityMetadata {
|
||||
${metadataFields}
|
||||
# mock field to prove extensions working
|
||||
relationships: String
|
||||
}
|
||||
|
||||
type TemplateMetadata implements EntityMetadata {
|
||||
${metadataFields}
|
||||
# mock field to prove extensions working
|
||||
updatedBy: String
|
||||
}
|
||||
|
||||
# TODO: move this definition into plugin-scaffolder-graphql
|
||||
type TemplateEntitySpec {
|
||||
type: String!
|
||||
path: String
|
||||
schema: JSONObject!
|
||||
templater: String!
|
||||
}
|
||||
|
||||
type ComponentEntitySpec {
|
||||
type: String!
|
||||
lifecycle: String!
|
||||
owner: String!
|
||||
}
|
||||
|
||||
type DefaultEntitySpec {
|
||||
raw: JSONObject
|
||||
}
|
||||
|
||||
union EntitySpec = DefaultEntitySpec | TemplateEntitySpec | ComponentEntitySpec
|
||||
|
||||
type CatalogEntity {
|
||||
apiVersion: String!
|
||||
kind: String!
|
||||
metadata: EntityMetadata
|
||||
spec: EntitySpec!
|
||||
}
|
||||
|
||||
type CatalogQuery {
|
||||
list: [CatalogEntity!]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
catalog: CatalogQuery!
|
||||
}
|
||||
`;
|
||||
|
||||
export default schema;
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { CatalogClient } from './client';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
|
||||
describe('Catalog GraphQL Module', () => {
|
||||
const worker = setupServer();
|
||||
|
||||
beforeAll(() => worker.listen());
|
||||
afterAll(() => worker.close());
|
||||
|
||||
afterEach(() => worker.resetHandlers());
|
||||
|
||||
const baseUrl = 'http://localhost:1234';
|
||||
|
||||
it('will return the entities', async () => {
|
||||
const expectedResponse = [{ id: 'something' }];
|
||||
|
||||
worker.use(
|
||||
rest.get(`${baseUrl}/catalog/entities`, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(expectedResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
const client = new CatalogClient(baseUrl);
|
||||
|
||||
const response = await client.list();
|
||||
|
||||
expect(response).toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('throws an error with the text', async () => {
|
||||
const expectedResponse = 'something broke';
|
||||
|
||||
worker.use(
|
||||
rest.get(`${baseUrl}/catalog/entities`, (_, res, ctx) =>
|
||||
res(ctx.status(500), ctx.text(expectedResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
const client = new CatalogClient(baseUrl);
|
||||
|
||||
await expect(() => client.list()).rejects.toThrow(expectedResponse);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { Entity, EntityMeta } from '@backstage/catalog-model';
|
||||
import fetch from 'node-fetch';
|
||||
import { JsonObject } from '@backstage/config';
|
||||
|
||||
export interface ReaderEntityMeta extends EntityMeta {
|
||||
uid: string;
|
||||
etag: string;
|
||||
generation: number;
|
||||
namespace: string;
|
||||
annotations: Record<string, string>;
|
||||
labels: Record<string, string>;
|
||||
}
|
||||
export interface ReaderEntity extends Entity {
|
||||
metadata: JsonObject & ReaderEntityMeta;
|
||||
}
|
||||
export class CatalogClient {
|
||||
constructor(private baseUrl: string) {}
|
||||
async list(): Promise<ReaderEntity[]> {
|
||||
const res = await fetch(`${this.baseUrl}/catalog/entities`);
|
||||
if (!res.ok) {
|
||||
// todo(blam): need some better way to handle errors here
|
||||
// experiment with throwing the input errors etc and having graphql versions of that
|
||||
throw new Error(await res.text());
|
||||
}
|
||||
|
||||
const entities: ReaderEntity[] = await res.json();
|
||||
return entities;
|
||||
}
|
||||
}
|
||||
@@ -13,21 +13,4 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getRootLogger } from '@backstage/backend-common';
|
||||
import yn from 'yn';
|
||||
import { startStandaloneServer } from './service/standaloneServer';
|
||||
|
||||
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000;
|
||||
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
|
||||
const logger = getRootLogger();
|
||||
|
||||
startStandaloneServer({ port, enableCors, logger }).catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('CTRL+C pressed; exiting.');
|
||||
process.exit(0);
|
||||
});
|
||||
export {};
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Getting Started
|
||||
|
||||
This backend plugin can be started in a standalone mode from directly in this package
|
||||
with `yarn start`. However, it will have limited functionality and that process is
|
||||
most convenient when developing the plugin itself.
|
||||
This is the GraphQL Backend plugin.
|
||||
|
||||
It is responsible for merging different `graphql-plugins` together to provide the end schema.
|
||||
|
||||
To run it within the backend do:
|
||||
|
||||
|
||||
@@ -19,14 +19,19 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/config": "^0.1.1-alpha.22",
|
||||
"@backstage/plugin-catalog-graphql": "^0.1.1-alpha.22",
|
||||
"@graphql-modules/core": "^0.7.17",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.22",
|
||||
"@types/express": "^4.17.6",
|
||||
"apollo-server": "^2.16.0",
|
||||
"apollo-server-express": "^2.16.0",
|
||||
"apollo-server": "^2.16.1",
|
||||
"apollo-server-express": "^2.16.1",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"graphql": "^15.3.0",
|
||||
"whatwg-fetch": "^3.4.0",
|
||||
"helmet": "^4.0.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"winston": "^3.2.1",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
type CatalogEntity {
|
||||
id: String
|
||||
}
|
||||
|
||||
type CatalogQuery {
|
||||
list: [CatalogEntity!]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
catalog: CatalogQuery!
|
||||
}
|
||||
|
||||
@@ -14,6 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
require('whatwg-fetch');
|
||||
|
||||
export * from './service/router';
|
||||
|
||||
@@ -13,10 +13,25 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import './router';
|
||||
import { createRouter } from './router';
|
||||
import supertest from 'supertest';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { createLogger } from 'winston';
|
||||
import express from 'express';
|
||||
|
||||
describe('Router', () => {
|
||||
it('should pass the test', () => {
|
||||
expect(true).toBeTruthy();
|
||||
describe('/health', () => {
|
||||
it('should return ok', async () => {
|
||||
const config = ConfigReader.fromConfigs([
|
||||
{ data: { backend: { baseUrl: 'lol' } }, context: 'something' },
|
||||
]);
|
||||
|
||||
const router = await createRouter({ config, logger: createLogger() });
|
||||
const app = express().use(router);
|
||||
|
||||
const { body } = await supertest(app).get('/health');
|
||||
|
||||
expect(body).toEqual({ status: 'ok' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,11 @@ import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import fs from 'fs';
|
||||
import { GraphQLModule } from '@graphql-modules/core';
|
||||
import { ApolloServer } from 'apollo-server-express';
|
||||
import { createModule as createCatalogModule } from '@backstage/plugin-catalog-graphql';
|
||||
import { Config } from '@backstage/config';
|
||||
import helmet from 'helmet';
|
||||
|
||||
const schemaPath = resolvePackagePath(
|
||||
'@backstage/plugin-graphql-backend',
|
||||
@@ -28,6 +32,7 @@ const schemaPath = resolvePackagePath(
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
export async function createRouter(
|
||||
@@ -35,16 +40,39 @@ export async function createRouter(
|
||||
): Promise<express.Router> {
|
||||
const typeDefs = await fs.promises.readFile(schemaPath, 'utf-8');
|
||||
|
||||
const server = new ApolloServer({ typeDefs, logger: options.logger });
|
||||
const router = Router();
|
||||
const catalogModule = await createCatalogModule(options);
|
||||
|
||||
const apolloMiddleware = server.getMiddleware({ path: '/' });
|
||||
router.use(apolloMiddleware);
|
||||
const { schema } = new GraphQLModule({
|
||||
imports: [catalogModule],
|
||||
typeDefs,
|
||||
});
|
||||
|
||||
const server = new ApolloServer({
|
||||
schema,
|
||||
logger: options.logger,
|
||||
introspection: true,
|
||||
playground: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/health', (_, response) => {
|
||||
response.send({ status: 'ok' });
|
||||
});
|
||||
|
||||
const apolloMiddleware = server.getMiddleware({ path: '/' });
|
||||
|
||||
if (process.env.NODE_ENV === 'development')
|
||||
router.use(
|
||||
helmet.contentSecurityPolicy({
|
||||
directives: {
|
||||
defaultSrc: ["'self'", "'unsafe-inline'", 'http://*'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
router.use(apolloMiddleware);
|
||||
|
||||
router.use(errorHandler());
|
||||
|
||||
return router;
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { createServiceBuilder } from '@backstage/backend-common';
|
||||
import { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
|
||||
export interface ServerOptions {
|
||||
port: number;
|
||||
enableCors: boolean;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export async function startStandaloneServer(
|
||||
options: ServerOptions,
|
||||
): Promise<Server> {
|
||||
const logger = options.logger.child({ service: 'graphql-backend' });
|
||||
logger.debug('Starting application server...');
|
||||
const router = await createRouter({
|
||||
logger,
|
||||
});
|
||||
|
||||
const service = createServiceBuilder(module)
|
||||
.enableCors({ origin: 'http://localhost:3000' })
|
||||
.addRouter('/graphql', router);
|
||||
|
||||
return await service.start().catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.hot?.accept();
|
||||
@@ -13,6 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
require('whatwg-fetch');
|
||||
|
||||
export {};
|
||||
|
||||
Reference in New Issue
Block a user