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

Add federation support #455

Merged
merged 33 commits into from
Feb 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c38c853
chore(federation): add deps
marcus-sa Jun 22, 2019
75df349
build: import tslib helpers
marcus-sa Jun 22, 2019
f2d5950
feat(federation): ResolveReference decorator
marcus-sa Jun 22, 2019
2c83518
feat(federation): make getTypesFromPaths method public
marcus-sa Jun 22, 2019
7e235f9
feat(federation): interfaces for gateway module options
marcus-sa Jun 22, 2019
7a0da24
refactor(federation): barrels
marcus-sa Jun 22, 2019
b090702
feat(federation): extract metadata reference resolver
marcus-sa Jun 22, 2019
ded92ce
feat(federation): graphql gateway module
marcus-sa Jun 22, 2019
d98b480
feat(federation): graphql federation factory
marcus-sa Jun 22, 2019
5948d67
feat(federation): graphql federation module
marcus-sa Jun 22, 2019
63fa587
fix(federation): filter resolvers predication
marcus-sa Jun 22, 2019
5940608
fix(federation): use resolver reference instead of resolver name
marcus-sa Jun 22, 2019
3a54832
style: formatting with prettier
rickdgeerling Oct 29, 2019
073a701
test: add integration test for federation module
rickdgeerling Oct 29, 2019
bef82ab
test: add integration test for gateway module
rickdgeerling Oct 29, 2019
ee39632
test: asynchronous federation module initialisation
rickdgeerling Oct 29, 2019
aba664f
feat: add async module support to graphql gateway
rickdgeerling Oct 29, 2019
3b9175f
test: add tests for generating interfaces for federation
rickdgeerling Oct 29, 2019
ee32547
feat: add fastify support to federation and gateway modules
rickdgeerling Oct 29, 2019
c85b139
test: add test for injecting custom buildservice into gateway
rickdgeerling Oct 30, 2019
aecc008
build: update apollo federation and gateway
rickdgeerling Oct 30, 2019
1d3ef17
fix: change GatewayModuleOptions interface
rickdgeerling Oct 30, 2019
5689b21
style: cleanup unused imports
rickdgeerling Oct 30, 2019
cf87ea7
fix: export BuildService token
rickdgeerling Oct 30, 2019
5c6e168
fix: add all options for ApolloServer to Gateway
rickdgeerling Oct 31, 2019
a5bf10b
fix: support Apollo Graph Manager
Nov 5, 2019
dfa7332
fix: scalar resolvers
Nov 6, 2019
32f395b
fix: support for external resolvers
Nov 6, 2019
77ae270
fix(federation): use type merger from 'merge-graphql-schemas'
Nov 13, 2019
bc4be48
fix(federation): allow apollo engine config in gateway
Nov 13, 2019
ef2e409
feat: add code-first support for grahql federation
rickdgeerling Nov 17, 2019
c8ba590
fix: invert logic for providing your own schema
Dec 5, 2019
48ff69c
chore(deps): remove deprecated @types/graphql dependency
Jan 20, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"printWidth": 100,
"trailingComma": "all",
"singleQuote": true
}
1 change: 1 addition & 0 deletions lib/decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './mutation.decorator';
export * from './parent.decorator';
export * from './query.decorator';
export * from './resolve-property.decorator';
export * from './resolve-reference.decorator';
export * from './resolver.decorator';
export * from './root.decorator';
export * from './scalar.decorator';
Expand Down
8 changes: 8 additions & 0 deletions lib/decorators/resolve-reference.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { RESOLVER_REFERENCE_METADATA } from '../graphql.constants';
import { SetMetadata } from '@nestjs/common';

export function ResolveReference(): MethodDecorator {
return (target: Function | Object, key?: string | symbol, descriptor?: any) => {
SetMetadata(RESOLVER_REFERENCE_METADATA, true)(target, key, descriptor);
};
}
17 changes: 7 additions & 10 deletions lib/external/type-graphql.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Type } from '@nestjs/common';
import { GraphQLScalarType } from 'graphql';
import { GraphQLDirective, GraphQLScalarType } from 'graphql';

/**
* Some external types have to be included in order to provide types safety
Expand All @@ -8,12 +8,7 @@ import { GraphQLScalarType } from 'graphql';
* see: https://github.com/19majkel94/type-graphql
* 0.16.0
*/
export type TypeValue =
| Type<any>
| GraphQLScalarType
| Function
| object
| symbol;
export type TypeValue = Type<any> | GraphQLScalarType | Function | object | symbol;
export type ReturnTypeFuncValue = TypeValue | [TypeValue];
export type ReturnTypeFunc = (returns?: void) => ReturnTypeFuncValue;
export type NullableListOptions = 'items' | 'itemsAndList';
Expand All @@ -38,12 +33,14 @@ export interface ResolverClassOptions {
}
export type ClassTypeResolver = (of?: void) => Type<any>;
export type BasicOptions = DecoratorTypeOptions & DescriptionOptions;
export type AdvancedOptions = BasicOptions &
DepreciationOptions &
SchemaNameOptions;
export type AdvancedOptions = BasicOptions & DepreciationOptions & SchemaNameOptions;

export interface BuildSchemaOptions {
dateScalarMode?: DateScalarMode;
scalarsMap?: ScalarsTypeMap[];
/** Any types that are not directly referenced or returned by resolvers */
orphanedTypes?: Function[];
directives?: GraphQLDirective[];
}
export type DateScalarMode = 'isoDate' | 'timestamp';
export interface ScalarsTypeMap {
Expand Down
60 changes: 53 additions & 7 deletions lib/graphql-definitions.factory.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { isEmpty } from '@nestjs/common/utils/shared.utils';
import { loadPackage } from '@nestjs/common/utils/load-package.util';
import { gql } from 'apollo-server-core';
import { makeExecutableSchema } from 'graphql-tools';
import * as chokidar from 'chokidar';
import { printSchema } from 'graphql';
import { GraphQLAstExplorer } from './graphql-ast.explorer';
import { GraphQLTypesLoader } from './graphql-types.loader';
import { removeTempField } from './utils/remove-temp.util';
import { removeTempField, extend } from './utils';

export class GraphQLDefinitionsFactory {
private readonly gqlAstExplorer = new GraphQLAstExplorer();
Expand All @@ -17,17 +18,16 @@ export class GraphQLDefinitionsFactory {
outputAs?: 'class' | 'interface';
watch?: boolean;
debug?: boolean;
federation?: boolean;
}) {
const isDebugEnabled = !(options && options.debug === false);
const typePathsExists = options.typePaths && !isEmpty(options.typePaths);
const isFederation = options && options.federation;
if (!typePathsExists) {
throw new Error(`"typePaths" property cannot be empty.`);
}
if (options.watch) {
this.printMessage(
'GraphQL factory is watching your files...',
isDebugEnabled,
);
this.printMessage('GraphQL factory is watching your files...', isDebugEnabled);
const watcher = chokidar.watch(options.typePaths);
watcher.on('change', async file => {
this.printMessage(
Expand All @@ -38,6 +38,7 @@ export class GraphQLDefinitionsFactory {
options.typePaths,
options.path,
options.outputAs,
isFederation,
isDebugEnabled,
);
});
Expand All @@ -46,6 +47,7 @@ export class GraphQLDefinitionsFactory {
options.typePaths,
options.path,
options.outputAs,
isFederation,
isDebugEnabled,
);
}
Expand All @@ -54,11 +56,55 @@ export class GraphQLDefinitionsFactory {
typePaths: string[],
path: string,
outputAs: 'class' | 'interface',
isFederation: boolean,
isDebugEnabled: boolean,
) {
const typeDefs = await this.gqlTypesLoader.mergeTypesByPaths(
typePaths || [],
if (isFederation) {
return this.exploreAndEmitFederation(typePaths, path, outputAs, isDebugEnabled);
}
return this.exploreAndEmitRegular(typePaths, path, outputAs, isDebugEnabled);
}

private async exploreAndEmitFederation(
typePaths: string[],
path: string,
outputAs: 'class' | 'interface',
isDebugEnabled: boolean,
) {
const typeDefs = await this.gqlTypesLoader.mergeTypesByPaths(typePaths);

const { buildFederatedSchema } = loadPackage('@apollo/federation', 'ApolloFederation');
const { printSchema } = loadPackage('@apollo/federation', 'ApolloFederation');

const schema = buildFederatedSchema([
{
typeDefs: gql`
${typeDefs}
`,
resolvers: {},
},
]);
const tsFile = await this.gqlAstExplorer.explore(
gql`
${printSchema(schema)}
`,
path,
outputAs,
);
await tsFile.save();
this.printMessage(
`[${new Date().toLocaleTimeString()}] The definitions have been updated.`,
isDebugEnabled,
);
}

private async exploreAndEmitRegular(
typePaths: string[],
path: string,
outputAs: 'class' | 'interface',
isDebugEnabled: boolean,
) {
const typeDefs = await this.gqlTypesLoader.mergeTypesByPaths(typePaths || []);
if (!typeDefs) {
throw new Error(`"typeDefs" property cannot be null.`);
}
Expand Down
100 changes: 100 additions & 0 deletions lib/graphql-federation.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Injectable } from '@nestjs/common';
import { gql } from 'apollo-server-core';
import { loadPackage } from '@nestjs/common/utils/load-package.util';
import { extend } from './utils';
import { isEmpty, forEach } from 'lodash';
import {
ScalarsExplorerService,
DelegatesExplorerService,
ResolversExplorerService,
} from './services';
import { mergeSchemas } from 'graphql-tools';
import { GqlModuleOptions } from './interfaces';
import { GraphQLSchemaBuilder } from './graphql-schema-builder';
import { GraphQLFactory } from './graphql.factory';
import { GraphQLSchema, GraphQLSchemaConfig } from 'graphql';
import { GraphQLObjectType } from 'graphql';

@Injectable()
export class GraphQLFederationFactory {
constructor(
private readonly resolversExplorerService: ResolversExplorerService,
private readonly delegatesExplorerService: DelegatesExplorerService,
private readonly scalarsExplorerService: ScalarsExplorerService,
private readonly gqlSchemaBuilder: GraphQLSchemaBuilder,
private readonly graphqlFactory: GraphQLFactory,
) {}

async mergeOptions(options: GqlModuleOptions = {}): Promise<GqlModuleOptions> {
const transformSchema = async s => (options.transformSchema ? options.transformSchema(s) : s);

let schema: GraphQLSchema;
if (options.autoSchemaFile) {
// Enable support when Directive support in type-graphql goes stable
throw new Error('Code-first not supported yet');
schema = await this.generateSchema(options);
} else if (isEmpty(options.typeDefs)) {
schema = options.schema;
} else {
schema = this.buildSchemaFromTypeDefs(options);
}

return {
...options,
schema: await transformSchema(schema),
typeDefs: undefined,
};
}

private buildSchemaFromTypeDefs(options: GqlModuleOptions) {
const { buildFederatedSchema } = loadPackage('@apollo/federation', 'ApolloFederation');

return buildFederatedSchema([
{
typeDefs: gql`
${options.typeDefs}
`,
resolvers: this.getResolvers(options.resolvers),
},
]);
}

private async generateSchema(options: GqlModuleOptions): Promise<GraphQLSchema> {
const { buildFederatedSchema, printSchema } = loadPackage(
'@apollo/federation',
'ApolloFederation',
);

const autoGeneratedSchema: GraphQLSchema = await this.gqlSchemaBuilder.buildFederatedSchema(
options.autoSchemaFile,
options.buildSchemaOptions,
this.resolversExplorerService.getAllCtors(),
);
const executableSchema = buildFederatedSchema({
typeDefs: gql(printSchema(autoGeneratedSchema)),
resolvers: this.getResolvers(options.resolvers),
});

const schema = options.schema
? mergeSchemas({
schemas: [options.schema, executableSchema],
})
: executableSchema;

return schema;
}

private getResolvers(optionResolvers) {
optionResolvers = Array.isArray(optionResolvers) ? optionResolvers : [optionResolvers];
return this.extendResolvers([
this.resolversExplorerService.explore(),
this.delegatesExplorerService.explore(),
...this.scalarsExplorerService.explore(),
...optionResolvers,
]);
}

private extendResolvers(resolvers: any[]) {
return resolvers.reduce((prev, curr) => extend(prev, curr), {});
}
}
Loading