Skip to content

Commit

Permalink
[deploy_website] enhance(schema): add some options to improve schema …
Browse files Browse the repository at this point in the history
…creation performance (#2280)

* enhance(schema): add some options to improve schema creation performance

* Fix lint and build issues
  • Loading branch information
ardatan committed Nov 26, 2020
1 parent 9050764 commit 4f5a4ef
Show file tree
Hide file tree
Showing 14 changed files with 139 additions and 102 deletions.
7 changes: 7 additions & 0 deletions .changeset/warm-candles-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@graphql-tools/schema': minor
'@graphql-tools/stitch': minor
'@graphql-tools/utils': minor
---

enhance(schema): add some options to improve schema creation performance
3 changes: 2 additions & 1 deletion packages/load/src/load-typedefs/load-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export async function loadFile(pointer: string, options: LoadTypedefsOptions): P
const canLoad = await loader.canLoad(pointer, options);

if (canLoad) {
return await loader.load(pointer, options);
const loadedValue = await loader.load(pointer, options);
return loadedValue;
}
} catch (error) {
debugLog(`Failed to find any GraphQL type definitions in: ${pointer} - ${error.message}`);
Expand Down
21 changes: 11 additions & 10 deletions packages/schema/src/buildSchemaFromTypeDefinitions.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
import { extendSchema, buildASTSchema, GraphQLSchema, DocumentNode, ASTNode } from 'graphql';
import { extendSchema, buildASTSchema, GraphQLSchema, DocumentNode } from 'graphql';

import { ITypeDefinitions, GraphQLParseOptions, parseGraphQLSDL } from '@graphql-tools/utils';
import { ITypeDefinitions, GraphQLParseOptions, parseGraphQLSDL, isDocumentNode } from '@graphql-tools/utils';

import { extractExtensionDefinitions, filterExtensionDefinitions } from './extensionDefinitions';
import { filterAndExtractExtensionDefinitions } from './extensionDefinitions';
import { concatenateTypeDefs } from './concatenateTypeDefs';

export function buildSchemaFromTypeDefinitions(
typeDefinitions: ITypeDefinitions,
parseOptions?: GraphQLParseOptions
parseOptions?: GraphQLParseOptions,
noExtensionExtraction?: boolean
): GraphQLSchema {
const document = buildDocumentFromTypeDefinitions(typeDefinitions, parseOptions);
const typesAst = filterExtensionDefinitions(document);

if (noExtensionExtraction) {
return buildASTSchema(document);
}

const { typesAst, extensionsAst } = filterAndExtractExtensionDefinitions(document);

const backcompatOptions = { commentDescriptions: true };
let schema: GraphQLSchema = buildASTSchema(typesAst, backcompatOptions);

const extensionsAst = extractExtensionDefinitions(document);
if (extensionsAst.definitions.length > 0) {
schema = extendSchema(schema, extensionsAst, backcompatOptions);
}

return schema;
}

export function isDocumentNode(typeDefinitions: ITypeDefinitions): typeDefinitions is DocumentNode {
return (typeDefinitions as ASTNode).kind !== undefined;
}

export function buildDocumentFromTypeDefinitions(
typeDefinitions: ITypeDefinitions,
parseOptions?: GraphQLParseOptions
Expand Down
27 changes: 11 additions & 16 deletions packages/schema/src/concatenateTypeDefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,25 @@ import { print, ASTNode } from 'graphql';

import { ITypedef } from '@graphql-tools/utils';

export function concatenateTypeDefs(typeDefinitionsAry: Array<ITypedef>, calledFunctionRefs = [] as any): string {
let resolvedTypeDefinitions: Array<string> = [];
export function concatenateTypeDefs(
typeDefinitionsAry: Array<ITypedef>,
calledFunctionRefs = new Set<ITypedef>()
): string {
const resolvedTypeDefinitions = new Set<string>();
typeDefinitionsAry.forEach((typeDef: ITypedef) => {
if (typeof typeDef === 'function') {
if (calledFunctionRefs.indexOf(typeDef) === -1) {
calledFunctionRefs.push(typeDef);
resolvedTypeDefinitions = resolvedTypeDefinitions.concat(concatenateTypeDefs(typeDef(), calledFunctionRefs));
if (!calledFunctionRefs.has(typeDef)) {
calledFunctionRefs.add(typeDef);
resolvedTypeDefinitions.add(concatenateTypeDefs(typeDef(), calledFunctionRefs));
}
} else if (typeof typeDef === 'string') {
resolvedTypeDefinitions.push(typeDef.trim());
resolvedTypeDefinitions.add(typeDef.trim());
} else if ((typeDef as ASTNode).kind !== undefined) {
resolvedTypeDefinitions.push(print(typeDef).trim());
resolvedTypeDefinitions.add(print(typeDef).trim());
} else {
const type = typeof typeDef;
throw new Error(`typeDef array must contain only strings, documents, or functions, got ${type}`);
}
});
return uniq(resolvedTypeDefinitions.map(x => x.trim())).join('\n');
}

function uniq(array: Array<any>): Array<any> {
return array.reduce(
(accumulator, currentValue) =>
accumulator.indexOf(currentValue) === -1 ? [...accumulator, currentValue] : accumulator,
[]
);
return [...resolvedTypeDefinitions].join('\n');
}
60 changes: 33 additions & 27 deletions packages/schema/src/extensionDefinitions.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@
import { DocumentNode, DefinitionNode, Kind } from 'graphql';

export function extractExtensionDefinitions(ast: DocumentNode) {
const extensionDefs = ast.definitions.filter(
(def: DefinitionNode) =>
def.kind === Kind.OBJECT_TYPE_EXTENSION ||
def.kind === Kind.INTERFACE_TYPE_EXTENSION ||
def.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION ||
def.kind === Kind.UNION_TYPE_EXTENSION ||
def.kind === Kind.ENUM_TYPE_EXTENSION ||
def.kind === Kind.SCALAR_TYPE_EXTENSION ||
def.kind === Kind.SCHEMA_EXTENSION
);
const isExtensionNode = (def: DefinitionNode) =>
def.kind === Kind.OBJECT_TYPE_EXTENSION ||
def.kind === Kind.INTERFACE_TYPE_EXTENSION ||
def.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION ||
def.kind === Kind.UNION_TYPE_EXTENSION ||
def.kind === Kind.ENUM_TYPE_EXTENSION ||
def.kind === Kind.SCALAR_TYPE_EXTENSION ||
def.kind === Kind.SCHEMA_EXTENSION;

export function filterAndExtractExtensionDefinitions(ast: DocumentNode) {
const extensionDefs: DefinitionNode[] = [];
const typesDefs: DefinitionNode[] = [];
ast.definitions.forEach(def => {
if (isExtensionNode(def)) {
extensionDefs.push(def);
} else {
typesDefs.push(def);
}
});

return {
...ast,
definitions: extensionDefs,
typesAst: {
...ast,
definitions: typesDefs,
},
extensionsAst: {
...ast,
definitions: extensionDefs,
},
};
}

export function filterExtensionDefinitions(ast: DocumentNode) {
const extensionDefs = ast.definitions.filter(
(def: DefinitionNode) =>
def.kind !== Kind.OBJECT_TYPE_EXTENSION &&
def.kind !== Kind.INTERFACE_TYPE_EXTENSION &&
def.kind !== Kind.INPUT_OBJECT_TYPE_EXTENSION &&
def.kind !== Kind.UNION_TYPE_EXTENSION &&
def.kind !== Kind.ENUM_TYPE_EXTENSION &&
def.kind !== Kind.SCALAR_TYPE_EXTENSION &&
def.kind !== Kind.SCHEMA_EXTENSION
);
const { typesAst } = filterAndExtractExtensionDefinitions(ast);
return typesAst;
}

return {
...ast,
definitions: extensionDefs,
};
export function extractExtensionDefinitions(ast: DocumentNode) {
const { extensionsAst } = filterAndExtractExtensionDefinitions(ast);
return extensionsAst;
}
2 changes: 1 addition & 1 deletion packages/schema/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export { buildSchemaFromTypeDefinitions, buildDocumentFromTypeDefinitions } from
export { chainResolvers } from './chainResolvers';
export { concatenateTypeDefs } from './concatenateTypeDefs';
export { decorateWithLogger } from './decorateWithLogger';
export { extractExtensionDefinitions, filterExtensionDefinitions } from './extensionDefinitions';
export * from './extensionDefinitions';
export { addResolversToSchema } from './addResolversToSchema';
export { checkForResolveTypeResolver } from './checkForResolveTypeResolver';
export { extendResolversFromInterfaces } from './extendResolversFromInterfaces';
Expand Down
73 changes: 46 additions & 27 deletions packages/schema/src/makeExecutableSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { addSchemaLevelResolver } from './addSchemaLevelResolver';
import { buildSchemaFromTypeDefinitions } from './buildSchemaFromTypeDefinitions';
import { addErrorLoggingToSchema } from './addErrorLoggingToSchema';
import { addCatchUndefinedToSchema } from './addCatchUndefinedToSchema';
import { IExecutableSchemaDefinition } from './types';
import { ExecutableSchemaTransformation, IExecutableSchemaDefinition } from './types';

/**
* Builds a schema from the provided type definitions and resolvers.
Expand Down Expand Up @@ -63,10 +63,12 @@ export function makeExecutableSchema<TContext = any>({
resolverValidationOptions = {},
directiveResolvers,
schemaDirectives,
schemaTransforms = [],
schemaTransforms: userProvidedSchemaTransforms,
parseOptions = {},
inheritResolversFromInterfaces = false,
pruningOptions,
updateResolversInPlace = false,
noExtensionExtraction = false,
}: IExecutableSchemaDefinition<TContext>) {
// Validate and clean up arguments
if (typeof resolverValidationOptions !== 'object') {
Expand All @@ -77,51 +79,68 @@ export function makeExecutableSchema<TContext = any>({
throw new Error('Must provide typeDefs');
}

// We allow passing in an array of resolver maps, in which case we merge them
const resolverMap: any = Array.isArray(resolvers) ? resolvers.reduce(mergeDeep, {}) : resolvers;

// Arguments are now validated and cleaned up

let schema = buildSchemaFromTypeDefinitions(typeDefs, parseOptions);

schema = addResolversToSchema({
schema,
resolvers: resolverMap,
resolverValidationOptions,
inheritResolversFromInterfaces,
});

if (Object.keys(resolverValidationOptions).length > 0) {
assertResolversPresent(schema, resolverValidationOptions);
}
const schemaTransforms: ExecutableSchemaTransformation[] = [
schema => {
// We allow passing in an array of resolver maps, in which case we merge them
const resolverMap: any = Array.isArray(resolvers) ? resolvers.reduce(mergeDeep, {}) : resolvers;

const schemaWithResolvers = addResolversToSchema({
schema,
resolvers: resolverMap,
resolverValidationOptions,
inheritResolversFromInterfaces,
updateResolversInPlace,
});

if (Object.keys(resolverValidationOptions).length > 0) {
assertResolversPresent(schemaWithResolvers, resolverValidationOptions);
}

return schemaWithResolvers;
},
];

if (!allowUndefinedInResolve) {
schema = addCatchUndefinedToSchema(schema);
schemaTransforms.push(addCatchUndefinedToSchema);
}

if (logger != null) {
schema = addErrorLoggingToSchema(schema, logger);
schemaTransforms.push(schema => addErrorLoggingToSchema(schema, logger));
}

if (typeof resolvers['__schema'] === 'function') {
// TODO a bit of a hack now, better rewrite generateSchema to attach it there.
// not doing that now, because I'd have to rewrite a lot of tests.
schema = addSchemaLevelResolver(schema, resolvers['__schema'] as GraphQLFieldResolver<any, any>);
schemaTransforms.push(schema =>
addSchemaLevelResolver(schema, resolvers['__schema'] as GraphQLFieldResolver<any, any>)
);
}

schemaTransforms.forEach(schemaTransform => {
schema = schemaTransform(schema);
});
if (userProvidedSchemaTransforms) {
schemaTransforms.push(schema =>
userProvidedSchemaTransforms.reduce((s, schemaTransform) => schemaTransform(s), schema)
);
}

// directive resolvers are implemented using SchemaDirectiveVisitor.visitSchemaDirectives
// schema visiting modifies the schema in place
if (directiveResolvers != null) {
schema = attachDirectiveResolvers(schema, directiveResolvers);
schemaTransforms.push(schema => attachDirectiveResolvers(schema, directiveResolvers));
}

if (schemaDirectives != null) {
SchemaDirectiveVisitor.visitSchemaDirectives(schema, schemaDirectives);
schemaTransforms.push(schema => {
SchemaDirectiveVisitor.visitSchemaDirectives(schema, schemaDirectives);
return schema;
});
}

if (pruningOptions) {
schemaTransforms.push(pruneSchema);
}

return pruningOptions ? pruneSchema(schema, pruningOptions) : schema;
const schemaFromTypeDefs = buildSchemaFromTypeDefinitions(typeDefs, parseOptions, noExtensionExtraction);

return schemaTransforms.reduce((schema, schemaTransform) => schemaTransform(schema), schemaFromTypeDefs);
}
12 changes: 11 additions & 1 deletion packages/schema/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export interface IExecutableSchemaDefinition<TContext = any> {
/**
* An array of schema transformation functions
*/
schemaTransforms?: Array<(originalWrappingSchema: GraphQLSchema) => GraphQLSchema>;
schemaTransforms?: ExecutableSchemaTransformation[];
/**
* Additional options for parsing the type definitions if they are provided
* as a string
Expand All @@ -67,4 +67,14 @@ export interface IExecutableSchemaDefinition<TContext = any> {
* Additional options for removing unused types from the schema
*/
pruningOptions?: PruneSchemaOptions;
/**
* Do not create a schema again and use the one from `buildASTSchema`
*/
updateResolversInPlace?: boolean;
/**
* Do not extract and apply extensions seperately and leave it to `buildASTSchema`
*/
noExtensionExtraction?: boolean;
}

export type ExecutableSchemaTransformation = (originalWrappingSchema: GraphQLSchema) => GraphQLSchema;
5 changes: 0 additions & 5 deletions packages/stitch/src/stitchSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
GraphQLDirective,
specifiedDirectives,
extendSchema,
ASTNode,
} from 'graphql';

import { SchemaDirectiveVisitor, mergeDeep, IResolvers, pruneSchema } from '@graphql-tools/utils';
Expand Down Expand Up @@ -210,7 +209,3 @@ function applySubschemaConfigTransforms(

return transformedSubschemas;
}

export function isDocumentNode(object: any): object is DocumentNode {
return (object as ASTNode).kind !== undefined;
}
6 changes: 6 additions & 0 deletions packages/utils/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ export interface GraphQLParseOptions {
allowLegacySDLEmptyFields?: boolean;
allowLegacySDLImplementsInterfaces?: boolean;
experimentalFragmentVariables?: boolean;
/**
* Set to `true` in order to convert all GraphQL comments (marked with # sign) to descriptions (""")
* GraphQL has built-in support for transforming descriptions to comments (with `print`), but not while
* parsing. Turning the flag on will support the other way as well (`parse`)
*/
commentDescriptions?: boolean;
}

// graphql-tools typings
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ export * from './visitResult';
export * from './getArgumentValues';
export * from './valueMatchesCriteria';
export * from './isAsyncIterable';
export * from './isDocumentNode';
5 changes: 5 additions & 0 deletions packages/utils/src/isDocumentNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ASTNode, DocumentNode } from 'graphql';

export function isDocumentNode(object: any): object is DocumentNode {
return (object as ASTNode).kind !== undefined;
}
4 changes: 2 additions & 2 deletions packages/utils/src/loaders.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DocumentNode, GraphQLSchema, BuildSchemaOptions } from 'graphql';
import { GraphQLSchemaValidationOptions } from 'graphql/type/schema';
import { ExtendedParseOptions } from './parse-graphql-sdl';
import { GraphQLParseOptions } from './Interfaces';

export interface Source {
document?: DocumentNode;
Expand All @@ -9,7 +9,7 @@ export interface Source {
location?: string;
}

export type SingleFileOptions = ExtendedParseOptions &
export type SingleFileOptions = GraphQLParseOptions &
GraphQLSchemaValidationOptions &
BuildSchemaOptions & {
cwd?: string;
Expand Down

0 comments on commit 4f5a4ef

Please sign in to comment.