Skip to content

Commit

Permalink
feat(federation): need a custom graphql factory
Browse files Browse the repository at this point in the history
  • Loading branch information
Davide-Gheri committed Feb 15, 2021
1 parent 5b740c4 commit 989aace
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 35 deletions.
263 changes: 230 additions & 33 deletions lib/graphql.factory.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,139 @@
import { Injectable } from '@nestjs/common';
import gql from 'graphql-tag';
import {
GraphQLField,
GraphQLObjectType,
GraphQLSchema,
GraphQLSchemaConfig,
lexicographicSortSchema,
printSchema,
} from 'graphql';
import { forEach, isEmpty, merge } from 'lodash';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { mergeSchemas } from '@graphql-tools/merge';
import {
DefinitionsGeneratorOptions,
GraphQLAstExplorer,
GraphQLFactory as NestGraphQLFactory,
GraphQLSchemaHost,
} from '@nestjs/graphql';
import { SchemaDirectiveVisitor } from '@graphql-tools/utils';
import {
PluginsExplorerService,
ResolversExplorerService,
ScalarsExplorerService,
} from '@nestjs/graphql/dist/services';
import { GraphQLSchemaBuilder } from '@nestjs/graphql/dist/graphql-schema.builder';
import { extend } from '@nestjs/graphql/dist/utils';
import { extend, removeTempField } from '@nestjs/graphql/dist/utils';
import { MercuriusModuleOptions, ValidationRules } from './interfaces';
import {
LoadersExplorerService,
ValidationRuleExplorerService,
} from './services';
import { existsSync, lstatSync, readFileSync } from 'fs';

@Injectable()
export class GraphQLFactory extends NestGraphQLFactory {
export class GraphQLFactory {
constructor(
resolversExplorerService: ResolversExplorerService,
scalarsExplorerService: ScalarsExplorerService,
// FIXME this should be removed since Plugins are not supported by Mercurius
pluginsExplorerService: PluginsExplorerService,
graphqlAstExplorer: GraphQLAstExplorer,
gqlSchemaBuilder: GraphQLSchemaBuilder,
gqlSchemaHost: GraphQLSchemaHost,
private readonly resolversExplorerService: ResolversExplorerService,
private readonly scalarsExplorerService: ScalarsExplorerService,
private readonly graphqlAstExplorer: GraphQLAstExplorer,
private readonly gqlSchemaBuilder: GraphQLSchemaBuilder,
private readonly gqlSchemaHost: GraphQLSchemaHost,
private readonly loaderExplorerService: LoadersExplorerService,
private readonly validationRuleExplorerService: ValidationRuleExplorerService,
) {
super(
resolversExplorerService,
scalarsExplorerService,
pluginsExplorerService,
graphqlAstExplorer,
gqlSchemaBuilder,
gqlSchemaHost,
) {}

async mergeOptions(
options: MercuriusModuleOptions = { typeDefs: [] },
): Promise<MercuriusModuleOptions> {
const resolvers = this.resolversExplorerService.explore();
const typesResolvers = extend(
this.scalarsExplorerService.explore(),
resolvers,
);
}

async mergeOptions(options?: any): Promise<any> {
const parentOptions = ((await super.mergeOptions(
options as any,
)) as unknown) as MercuriusModuleOptions;
delete (parentOptions as any).plugins;
const transformSchema = async (schema: GraphQLSchema) =>
options.transformSchema ? await options.transformSchema(schema) : schema;

parentOptions.loaders = extend(
parentOptions.loaders || {},
this.loaderExplorerService.explore(),
);
if (options.autoSchemaFile) {
const autoGeneratedSchema: GraphQLSchema = await this.gqlSchemaBuilder.build(
options.autoSchemaFile,
options as any,
this.resolversExplorerService.getAllCtors(),
);
const executableSchema = makeExecutableSchema({
resolvers: extend(typesResolvers, options.resolvers),
typeDefs: gql`
${printSchema(autoGeneratedSchema)}
`,
resolverValidationOptions: {
...(options.resolverValidationOptions || {}),
requireResolversForResolveType: false,
},
});
let schema = options.schema
? mergeSchemas({
schemas: [options.schema, executableSchema],
})
: executableSchema;

parentOptions.validationRules = this.mergeValidationRules(
parentOptions.validationRules,
);
const autoGeneratedSchemaConfig = autoGeneratedSchema.toConfig();
const schemaConfig = this.overrideOrExtendResolvers(
schema.toConfig(),
autoGeneratedSchemaConfig,
);

schema = new GraphQLSchema(schemaConfig);
if (options.schemaDirectives) {
SchemaDirectiveVisitor.visitSchemaDirectives(
schema,
options.schemaDirectives,
);
}

return parentOptions;
schema = await transformSchema(schema);
schema = options.sortSchema ? lexicographicSortSchema(schema) : schema;
this.gqlSchemaHost.schema = schema;

return {
...options,
typeDefs: undefined,
schema,
};
}
if (isEmpty(options.typeDefs)) {
const schema = await transformSchema(options.schema);
this.gqlSchemaHost.schema = schema;
return {
...options,
typeDefs: undefined,
schema,
};
}
const executableSchema = makeExecutableSchema({
resolvers: extend(typesResolvers, options.resolvers),
directiveResolvers: options.directiveResolvers,
schemaDirectives: options.schemaDirectives as any,
typeDefs: gql`
${options.typeDefs}
`,
resolverValidationOptions: options.resolverValidationOptions,
});
let schema = options.schema
? mergeSchemas({
schemas: [options.schema, executableSchema],
})
: executableSchema;

removeTempField(schema);
schema = await transformSchema(schema);
schema = options.sortSchema ? lexicographicSortSchema(schema) : schema;
this.gqlSchemaHost.schema = schema;

return {
...options,
typeDefs: undefined,
schema,
};
}

mergeValidationRules(existingValidationRules?: ValidationRules) {
Expand All @@ -65,4 +143,123 @@ export class GraphQLFactory extends NestGraphQLFactory {
...rules.map((rule) => (context) => rule.validate(params, context)),
];
}

overrideOrExtendResolvers(
executableSchemaConfig: GraphQLSchemaConfig,
autoGeneratedSchemaConfig: GraphQLSchemaConfig,
): GraphQLSchemaConfig {
const schemaConfig = autoGeneratedSchemaConfig;
const rootResolverKeys: ('mutation' | 'query' | 'subscription')[] = [
'mutation',
'query',
'subscription',
];

rootResolverKeys
.filter((key) => executableSchemaConfig[key] && schemaConfig[key])
.forEach((key) => {
const executableSchemaFields = executableSchemaConfig[key].getFields();
const schemaFields = schemaConfig[key].getFields();

forEach(executableSchemaFields, (value, resolverName) => {
if (schemaFields[resolverName]) {
schemaFields[resolverName].resolve =
executableSchemaFields[resolverName].resolve;
schemaFields[resolverName].subscribe =
executableSchemaFields[resolverName].subscribe;
} else {
schemaFields[resolverName] = executableSchemaFields[resolverName];
}
});
});

const getAutoGeneratedTypeByName = (name: string): GraphQLObjectType =>
schemaConfig.types.find(
(type) => type.name === name,
) as GraphQLObjectType;

executableSchemaConfig.types
.filter((type) => type instanceof GraphQLObjectType)
.forEach((type: GraphQLObjectType) => {
const fields = type.getFields();
const autoGeneratedType = getAutoGeneratedTypeByName(type.name);
if (!autoGeneratedType) {
return;
}

/**
* Inherit "resolve()" functions from auto-generated interfaces
*/
const implementedInterfaces = autoGeneratedType.getInterfaces() || [];
if (implementedInterfaces.length > 0) {
implementedInterfaces.forEach((interfaceRef) => {
const interfaceInExecutableSchema = executableSchemaConfig.types.find(
(type) => type.name === interfaceRef.name,
) as GraphQLObjectType;

forEach(
interfaceRef.getFields(),
(value: GraphQLField<unknown, unknown>, key: string) => {
const fieldInExecutableSchema = interfaceInExecutableSchema.getFields()[
key
];
if (!fieldInExecutableSchema) {
return;
}
if (!fieldInExecutableSchema.resolve) {
return;
}
const baseClassField = autoGeneratedType.getFields()[key];
baseClassField &&
(baseClassField.resolve = fieldInExecutableSchema.resolve);
},
);
});
}

forEach(
fields,
(value: GraphQLField<unknown, unknown>, key: string) => {
if (!value.resolve) {
return;
}
const field = autoGeneratedType.getFields()[key];
field && (field.resolve = value.resolve);
},
);
});

return schemaConfig;
}

async generateDefinitions(
typeDefs: string | string[],
options: MercuriusModuleOptions,
) {
if (isEmpty(typeDefs) || !options.definitions) {
return;
}
const definitionsGeneratorOptions: DefinitionsGeneratorOptions = {
emitTypenameField: options.definitions.emitTypenameField,
skipResolverArgs: options.definitions.skipResolverArgs,
defaultScalarType: options.definitions.defaultScalarType,
customScalarTypeMapping: options.definitions.customScalarTypeMapping,
additionalHeader: options.definitions.additionalHeader,
};
const tsFile = await this.graphqlAstExplorer.explore(
gql`
${typeDefs}
`,
options.definitions.path,
options.definitions.outputAs,
definitionsGeneratorOptions,
);
if (
!existsSync(options.definitions.path) ||
!lstatSync(options.definitions.path).isFile() ||
readFileSync(options.definitions.path, 'utf8') !== tsFile.getFullText()
) {
await tsFile.save();
}
}
}
6 changes: 6 additions & 0 deletions tests/example/resolvers/user.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Subscription,
ID,
Directive,
ResolveReference,
} from '@nestjs/graphql';
import { UserType } from '../types/user.type';
import {
Expand Down Expand Up @@ -147,4 +148,9 @@ export class UserResolver {
) {
return toAsyncIterator(ctx.pubsub.subscribe('USER_ADDED'));
}

@ResolveReference()
resolveReference(ref: { __typename: string; id: number }) {
return this.userService.find(ref.id);
}
}
3 changes: 2 additions & 1 deletion tests/example/types/post.type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';

@Directive('@key(fields: "id")')
@ObjectType()
export class PostType {
@Field(() => ID)
Expand Down
3 changes: 2 additions & 1 deletion tests/example/types/user.type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
import { HashScalar } from '../scalars/hash.scalar';

@Directive('@key(fields: "id")')
@ObjectType('User')
export class UserType {
@Field(() => ID)
Expand Down

0 comments on commit 989aace

Please sign in to comment.