diff --git a/package.json b/package.json index 6f44a503a..3b242ea2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "type-graphql", - "version": "2.0.0-rc.2", + "version": "2.0.0-rc.3", "private": false, "description": "Create GraphQL schema and resolvers with TypeScript, using classes and decorators!", "keywords": [ diff --git a/src/metadata/metadata-storage.ts b/src/metadata/metadata-storage.ts index 9083f7ec1..73e70a2c8 100644 --- a/src/metadata/metadata-storage.ts +++ b/src/metadata/metadata-storage.ts @@ -46,28 +46,44 @@ export class MetadataStorage { objectTypes: ObjectClassMetadata[] = []; + objectTypesCache = new Map(); + inputTypes: ClassMetadata[] = []; argumentTypes: ClassMetadata[] = []; interfaceTypes: InterfaceClassMetadata[] = []; + interfaceTypesCache = new Map(); + authorizedFields: AuthorizedMetadata[] = []; + authorizedFieldsByTargetAndFieldCache = new Map(); + authorizedResolver: AuthorizedClassMetadata[] = []; + authorizedResolverByTargetCache = new Map(); + enums: EnumMetadata[] = []; unions: UnionMetadataWithSymbol[] = []; middlewares: MiddlewareMetadata[] = []; + middlewaresByTargetAndFieldCache = new Map>(); + resolverMiddlewares: ResolverMiddlewareMetadata[] = []; + resolverMiddlewaresByTargetCache = new Map>(); + classDirectives: DirectiveClassMetadata[] = []; + classDirectivesByTargetCache = new Map(); + fieldDirectives: DirectiveFieldMetadata[] = []; + fieldDirectivesByTargetAndFieldCache = new Map(); + argumentDirectives: DirectiveArgumentMetadata[] = []; classExtensions: ExtensionsClassMetadata[] = []; @@ -76,10 +92,16 @@ export class MetadataStorage { resolverClasses: ResolverClassMetadata[] = []; + resolverClassesCache = new Map(); + fields: FieldMetadata[] = []; + fieldsCache = new Map(); + params: ParamMetadata[] = []; + paramsCache = new Map(); + collectQueryHandlerMetadata(definition: ResolverMetadata) { this.queries.push(definition); } @@ -173,6 +195,111 @@ export class MetadataStorage { this.fieldExtensions.push(definition); } + initCache() { + if (this.resolverClasses?.length) { + this.resolverClasses.forEach(resolverClass => { + if (!this.resolverClassesCache.has(resolverClass.target)) { + this.resolverClassesCache.set(resolverClass.target, resolverClass); + } + }); + } + + if (this.params?.length) { + this.params.forEach(param => { + const key = `${param.target}-${param.methodName}`; + if (!this.paramsCache.has(key)) { + this.paramsCache.set(key, []); + } + this.paramsCache.get(key)?.push(param); + }); + } + + if (this.middlewares?.length) { + this.middlewares.forEach(middleware => { + const key = `${middleware.target}-${middleware.fieldName}`; + if (!this.middlewaresByTargetAndFieldCache.has(key)) { + this.middlewaresByTargetAndFieldCache.set(key, new Set()); + } + + if (!this.middlewaresByTargetAndFieldCache.get(key)?.has(middleware)) { + this.middlewaresByTargetAndFieldCache.get(key)?.add(middleware); + } + }); + } + + if (this.resolverMiddlewares?.length) { + this.resolverMiddlewares.forEach(middleware => { + const key = middleware.target; + if (!this.resolverMiddlewaresByTargetCache.has(key)) { + this.resolverMiddlewaresByTargetCache.set(key, new Set()); + } + + if (!this.resolverMiddlewaresByTargetCache.get(key)?.has(middleware)) { + this.resolverMiddlewaresByTargetCache.get(key)?.add(middleware); + } + }); + } + + if (this.fieldDirectives?.length) { + this.fieldDirectives.forEach(directive => { + const key = `${directive.target}-${directive.fieldName}`; + if (!this.fieldDirectivesByTargetAndFieldCache.has(key)) { + this.fieldDirectivesByTargetAndFieldCache.set(key, []); + } + this.fieldDirectivesByTargetAndFieldCache.get(key)?.push(directive); + }); + } + + if (this.classDirectives?.length) { + this.classDirectives.forEach(directive => { + const key = directive.target; + if (!this.classDirectivesByTargetCache.has(key)) { + this.classDirectivesByTargetCache.set(key, []); + } + this.classDirectivesByTargetCache.get(key)?.push(directive); + }); + } + + if (this.authorizedFields?.length) { + this.authorizedFields.forEach(field => { + const key = `${field.target}-${field.fieldName}`; + if (!this.authorizedFieldsByTargetAndFieldCache.has(key)) { + this.authorizedFieldsByTargetAndFieldCache.set(key, field); + } + }); + } + + if (this.authorizedResolver?.length) { + this.authorizedResolver.forEach(resolver => { + const key = resolver.target; + if (!this.authorizedResolverByTargetCache.has(key)) { + this.authorizedResolverByTargetCache.set(key, resolver); + } + }); + } + + if (this.fields?.length) { + this.fields.forEach(field => { + if (!this.fieldsCache.has(field.target)) { + this.fieldsCache.set(field.target, []); + } + this.fieldsCache.get(field.target)?.push(field); + }); + } + + if (this.objectTypes?.length) { + this.objectTypes.forEach(objType => { + this.objectTypesCache.set(objType.target, objType); + }); + } + + if (this.interfaceTypes?.length) { + this.interfaceTypes.forEach(interfaceType => { + this.interfaceTypesCache.set(interfaceType.target, interfaceType); + }); + } + } + build(options: SchemaGeneratorOptions) { this.classDirectives.reverse(); this.fieldDirectives.reverse(); @@ -180,6 +307,8 @@ export class MetadataStorage { this.classExtensions.reverse(); this.fieldExtensions.reverse(); + this.initCache(); + this.buildClassMetadata(this.objectTypes); this.buildClassMetadata(this.inputTypes); this.buildClassMetadata(this.argumentTypes); @@ -215,6 +344,19 @@ export class MetadataStorage { this.classExtensions = []; this.fieldExtensions = []; + // clear map caches + this.fieldsCache = new Map(); + this.objectTypesCache = new Map(); + this.interfaceTypesCache = new Map(); + this.middlewaresByTargetAndFieldCache = new Map(); + this.resolverMiddlewaresByTargetCache = new Map(); + this.paramsCache = new Map(); + this.fieldDirectivesByTargetAndFieldCache = new Map(); + this.classDirectivesByTargetCache = new Map(); + this.authorizedFieldsByTargetAndFieldCache = new Map(); + this.authorizedResolverByTargetCache = new Map(); + this.resolverClassesCache = new Map(); + this.resolverClasses = []; this.fields = []; this.params = []; @@ -223,34 +365,33 @@ export class MetadataStorage { private buildClassMetadata(definitions: ClassMetadata[]) { definitions.forEach(def => { if (!def.fields) { - const fields = this.fields.filter(field => field.target === def.target); + const fields = this.fieldsCache.get(def.target) || []; fields.forEach(field => { field.roles = this.findFieldRoles(field.target, field.name); - field.params = this.params.filter( - param => param.target === field.target && field.name === param.methodName, - ); + + const paramKey = `${field.target}-${field.name}`; + field.params = this.paramsCache.get(paramKey) || []; + + const resolverMiddlewares = this.resolverMiddlewaresByTargetCache.get(field.target) || []; + const middlewaresKey = `${field.target}-${field.name}`; + const fieldMiddlewares = this.middlewaresByTargetAndFieldCache.get(middlewaresKey) || []; + field.middlewares = [ - ...mapMiddlewareMetadataToArray( - this.resolverMiddlewares.filter(middleware => middleware.target === field.target), - ), - ...mapMiddlewareMetadataToArray( - this.middlewares.filter( - middleware => - middleware.target === field.target && middleware.fieldName === field.name, - ), - ), + ...mapMiddlewareMetadataToArray(Array.from(resolverMiddlewares)), + ...mapMiddlewareMetadataToArray(Array.from(fieldMiddlewares)), ]; - field.directives = this.fieldDirectives - .filter(it => it.target === field.target && it.fieldName === field.name) - .map(it => it.directive); + + const directives = + this.fieldDirectivesByTargetAndFieldCache.get(`${field.target}-${field.name}`) || []; + field.directives = directives.map(it => it.directive); + field.extensions = this.findExtensions(field.target, field.name); }); def.fields = fields; } if (!def.directives) { - def.directives = this.classDirectives - .filter(it => it.target === def.target) - .map(it => it.directive); + const directives = this.classDirectivesByTargetCache.get(def.target) || []; + def.directives = directives.map(directive => directive.directive); } if (!def.extensions) { def.extensions = this.findExtensions(def.target); @@ -260,28 +401,21 @@ export class MetadataStorage { private buildResolversMetadata(definitions: BaseResolverMetadata[]) { definitions.forEach(def => { - const resolverClassMetadata = this.resolverClasses.find( - resolver => resolver.target === def.target, - )!; - def.resolverClassMetadata = resolverClassMetadata; - def.params = this.params.filter( - param => param.target === def.target && def.methodName === param.methodName, - ); + def.resolverClassMetadata = this.resolverClassesCache.get(def.target); + def.params = this.paramsCache.get(`${def.target}-${def.methodName}`) || []; def.roles = this.findFieldRoles(def.target, def.methodName); + + const resolverMiddlewares = this.resolverMiddlewaresByTargetCache.get(def.target) || []; + const fieldMiddlewares = + this.middlewaresByTargetAndFieldCache.get(`${def.target}-${def.methodName}`) || []; def.middlewares = [ - ...mapMiddlewareMetadataToArray( - this.resolverMiddlewares.filter(middleware => middleware.target === def.target), - ), - ...mapMiddlewareMetadataToArray( - this.middlewares.filter( - middleware => - middleware.target === def.target && def.methodName === middleware.fieldName, - ), - ), + ...mapMiddlewareMetadataToArray(Array.from(resolverMiddlewares)), + ...mapMiddlewareMetadataToArray(Array.from(fieldMiddlewares)), ]; - def.directives = this.fieldDirectives - .filter(it => it.target === def.target && it.fieldName === def.methodName) - .map(it => it.directive); + + def.directives = ( + this.fieldDirectivesByTargetAndFieldCache.get(`${def.target}-${def.methodName}`) || [] + ).map(it => it.directive); def.extensions = this.findExtensions(def.target, def.methodName); }); } @@ -293,20 +427,21 @@ export class MetadataStorage { this.buildResolversMetadata(definitions); definitions.forEach(def => { def.roles = this.findFieldRoles(def.target, def.methodName); - def.directives = this.fieldDirectives - .filter(it => it.target === def.target && it.fieldName === def.methodName) - .map(it => it.directive); + def.directives = ( + this.fieldDirectivesByTargetAndFieldCache.get(`${def.target}-${def.methodName}`) || [] + ).map(it => it.directive); def.extensions = this.findExtensions(def.target, def.methodName); def.getObjectType = def.kind === "external" - ? this.resolverClasses.find(resolver => resolver.target === def.target)!.getObjectType + ? this.resolverClassesCache.get(def.target)!.getObjectType : () => def.target as ClassType; if (def.kind === "external") { - const typeClass = this.resolverClasses.find(resolver => resolver.target === def.target)! - .getObjectType!(); + const typeClass = this.resolverClassesCache.get(def.target)!.getObjectType!(); + if (!typeClass) { + throw new Error(`Unable to find type class for external resolver ${def.target.name}`); + } const typeMetadata = - this.objectTypes.find(objTypeDef => objTypeDef.target === typeClass) || - this.interfaceTypes.find(interfaceTypeDef => interfaceTypeDef.target === typeClass); + this.objectTypesCache.get(typeClass) || this.interfaceTypesCache.get(typeClass); if (!typeMetadata) { throw new Error( `Unable to find type metadata for input type or object type named '${typeClass.name}'`, @@ -367,8 +502,7 @@ export class MetadataStorage { // copy and modify metadata of resolver from parent resolver class while (superResolver.prototype) { - // eslint-disable-next-line @typescript-eslint/no-loop-func - const superResolverMetadata = this.resolverClasses.find(it => it.target === superResolver); + const superResolverMetadata = this.resolverClassesCache.get(superResolver); if (superResolverMetadata) { this.queries = mapSuperResolverHandlers(this.queries, superResolver, def); this.mutations = mapSuperResolverHandlers(this.mutations, superResolver, def); @@ -386,9 +520,8 @@ export class MetadataStorage { private findFieldRoles(target: Function, fieldName: string): any[] | undefined { const authorizedField = - this.authorizedFields.find( - authField => authField.target === target && authField.fieldName === fieldName, - ) ?? this.authorizedResolver.find(authScope => authScope.target === target); + this.authorizedFieldsByTargetAndFieldCache.get(`${target}-${fieldName}`) || + this.authorizedResolverByTargetCache.get(target); if (!authorizedField) { return undefined; } diff --git a/src/schema/schema-generator.ts b/src/schema/schema-generator.ts index ffa11adec..f6170d15b 100644 --- a/src/schema/schema-generator.ts +++ b/src/schema/schema-generator.ts @@ -109,14 +109,22 @@ export type SchemaGeneratorOptions = { export abstract class SchemaGenerator { private static objectTypesInfo: ObjectTypeInfo[] = []; + private static objectTypesInfoMap = new Map(); + private static inputTypesInfo: InputObjectTypeInfo[] = []; + private static inputTypesInfoMap = new Map(); + private static interfaceTypesInfo: InterfaceTypeInfo[] = []; + private static interfaceTypesInfoMap = new Map(); + private static enumTypesInfo: EnumTypeInfo[] = []; private static unionTypesInfo: UnionTypeInfo[] = []; + private static unionTypesInfoMap = new Map(); + private static usedInterfaceTypes = new Set(); private static metadataStorage: MetadataStorage; @@ -124,7 +132,6 @@ export abstract class SchemaGenerator { static generateFromMetadata(options: SchemaGeneratorOptions): GraphQLSchema { this.metadataStorage = Object.assign(new MetadataStorage(), getMetadataStorage()); this.metadataStorage.build(options); - this.checkForErrors(options); BuildContext.create(options); @@ -152,7 +159,6 @@ export abstract class SchemaGenerator { throw new GeneratingSchemaError(errors); } } - return finalSchema; } @@ -203,13 +209,11 @@ export abstract class SchemaGenerator { unionObjectTypesInfo.push( ...unionMetadata .getClassTypes() - .map( - objectTypeCls => this.objectTypesInfo.find(type => type.target === objectTypeCls)!, - ), + .map(objectTypeCls => this.objectTypesInfoMap.get(objectTypeCls)!), ); return unionObjectTypesInfo.map(it => it.type); }; - return { + const unionType = { unionSymbol: unionMetadata.symbol, type: new GraphQLUnionType({ name: unionMetadata.name, @@ -236,6 +240,10 @@ export abstract class SchemaGenerator { }, }), }; + + this.unionTypesInfoMap.set(unionMetadata.symbol, unionType); + + return unionType; }); this.enumTypesInfo = this.metadataStorage.enums.map(enumMetadata => { @@ -264,12 +272,12 @@ export abstract class SchemaGenerator { const hasExtended = objectSuperClass.prototype !== undefined; const getSuperClassType = () => { const superClassTypeInfo = - this.objectTypesInfo.find(type => type.target === objectSuperClass) ?? - this.interfaceTypesInfo.find(type => type.target === objectSuperClass); + this.objectTypesInfoMap.get(objectSuperClass) ?? + this.interfaceTypesInfoMap.get(objectSuperClass); return superClassTypeInfo ? superClassTypeInfo.type : undefined; }; const interfaceClasses = objectType.interfaceClasses || []; - return { + const objectTypeInfo = { metadata: objectType, target: objectType.target, type: new GraphQLObjectType({ @@ -279,9 +287,7 @@ export abstract class SchemaGenerator { extensions: objectType.extensions, interfaces: () => { let interfaces = interfaceClasses.map(interfaceClass => { - const interfaceTypeInfo = this.interfaceTypesInfo.find( - info => info.target === interfaceClass, - ); + const interfaceTypeInfo = this.interfaceTypesInfoMap.get(interfaceClass); if (!interfaceTypeInfo) { throw new Error( `Cannot find interface type metadata for class '${interfaceClass.name}' ` + @@ -373,8 +379,9 @@ export abstract class SchemaGenerator { }, }), }; + this.objectTypesInfoMap.set(objectType.target, objectTypeInfo); + return objectTypeInfo; }); - this.interfaceTypesInfo = this.metadataStorage.interfaceTypes.map( interfaceType => { const interfaceSuperClass = Object.getPrototypeOf(interfaceType.target); @@ -398,7 +405,7 @@ export abstract class SchemaGenerator { implementingObjectTypesTargets.includes(objectTypesInfo.target), ); - return { + const interfaceTypeInfo = { metadata: interfaceType, target: interfaceType.target, type: new GraphQLInterfaceType({ @@ -407,8 +414,7 @@ export abstract class SchemaGenerator { astNode: getInterfaceTypeDefinitionNode(interfaceType.name, interfaceType.directives), interfaces: () => { let interfaces = (interfaceType.interfaceClasses || []).map( - interfaceClass => - this.interfaceTypesInfo.find(info => info.target === interfaceClass)!.type, + interfaceClass => this.interfaceTypesInfoMap.get(interfaceClass)!.type, ); // copy interfaces from super class if (hasExtended) { @@ -493,19 +499,18 @@ export abstract class SchemaGenerator { }, }), }; + this.interfaceTypesInfoMap.set(interfaceType.target, interfaceTypeInfo); + return interfaceTypeInfo; }, ); - this.inputTypesInfo = this.metadataStorage.inputTypes.map(inputType => { const objectSuperClass = Object.getPrototypeOf(inputType.target); const getSuperClassType = () => { - const superClassTypeInfo = this.inputTypesInfo.find( - type => type.target === objectSuperClass, - ); + const superClassTypeInfo = this.inputTypesInfoMap.get(objectSuperClass); return superClassTypeInfo ? superClassTypeInfo.type : undefined; }; const inputInstance = new (inputType.target as any)(); - return { + const inputTypeInfo = { target: inputType.target, type: new GraphQLInputObjectType({ name: inputType.name, @@ -551,6 +556,8 @@ export abstract class SchemaGenerator { astNode: getInputObjectTypeDefinitionNode(inputType.name, inputType.directives), }), }; + this.inputTypesInfoMap.set(inputType.target, inputTypeInfo); + return inputTypeInfo; }); } diff --git a/tests/functional/authorization.ts b/tests/functional/authorization.ts index f23e9eacd..930650ffd 100644 --- a/tests/functional/authorization.ts +++ b/tests/functional/authorization.ts @@ -21,7 +21,7 @@ describe("Authorization", () => { let schema: GraphQLSchema; let sampleResolver: any; - beforeAll(async () => { + beforeEach(async () => { getMetadataStorage().clear(); @ObjectType() @@ -608,7 +608,7 @@ describe("Authorization", () => { describe("with constant readonly array or roles", () => { let testResolver: Function; - beforeAll(() => { + beforeEach(() => { getMetadataStorage().clear(); const CONSTANT_ROLES = ["a", "b", "c"] as const; diff --git a/tests/functional/middlewares.ts b/tests/functional/middlewares.ts index 49e5477eb..b2e482882 100644 --- a/tests/functional/middlewares.ts +++ b/tests/functional/middlewares.ts @@ -282,6 +282,8 @@ describe("Middlewares", () => { // clear ResolverMiddlewareMetadata for other tests getMetadataStorage().resolverMiddlewares = []; + getMetadataStorage().resolverMiddlewaresByTargetCache = new Map(); + getMetadataStorage().middlewaresByTargetAndFieldCache = new Map(); const query = `query { middlewareOrderQuery diff --git a/tests/functional/validation.ts b/tests/functional/validation.ts index 490cb2b40..e0be46025 100644 --- a/tests/functional/validation.ts +++ b/tests/functional/validation.ts @@ -679,8 +679,13 @@ describe("Custom validation", () => { let validateResolverData: ResolverData[] = []; let sampleQueryArgs: any[] = []; - beforeAll(async () => { + beforeEach(() => { + // Reset ALL shared state getMetadataStorage().clear(); + validateArgs = []; + validateTypes = []; + validateResolverData = []; + sampleQueryArgs = []; @ArgsType() class SampleArgs { @@ -725,13 +730,6 @@ describe("Custom validation", () => { sampleResolverCls = SampleResolver; }); - beforeEach(() => { - validateArgs = []; - validateTypes = []; - validateResolverData = []; - sampleQueryArgs = []; - }); - it("should call `validateFn` function provided in option with proper params", async () => { schema = await buildSchema({ resolvers: [sampleResolverCls],