diff --git a/src/decorators/Metadata.ts b/src/decorators/Metadata.ts new file mode 100644 index 000000000..c819aa0a6 --- /dev/null +++ b/src/decorators/Metadata.ts @@ -0,0 +1,27 @@ +import { getMetadataStorage } from "../metadata/getMetadataStorage"; +import { MethodAndPropDecorator } from "./types"; + +export interface Metadata { + [index: string]: any; +} + +export function Metadata(data: Metadata): ClassDecorator | MethodAndPropDecorator | any { + return ( + targetOrPrototype: Function | Object, + propertyKey?: string | symbol, + descriptor?: any, + ): void => { + if (typeof targetOrPrototype === "function") { + getMetadataStorage().collectAdditionalObjectTypeMetadata({ + target: targetOrPrototype, + data, + }); + } else if (propertyKey !== undefined) { + getMetadataStorage().collectAdditionalFieldMetadata({ + name: propertyKey, + target: targetOrPrototype.constructor, + data, + }); + } + }; +} diff --git a/src/metadata/definitions/class-metadata.ts b/src/metadata/definitions/class-metadata.ts index fe42f793a..98fc52a09 100644 --- a/src/metadata/definitions/class-metadata.ts +++ b/src/metadata/definitions/class-metadata.ts @@ -6,4 +6,5 @@ export interface ClassMetadata { fields?: FieldMetadata[]; description?: string; interfaceClasses?: Function[]; + metadata?: any; } diff --git a/src/metadata/definitions/field-metadata.ts b/src/metadata/definitions/field-metadata.ts index 7d3560f85..d36dc5153 100644 --- a/src/metadata/definitions/field-metadata.ts +++ b/src/metadata/definitions/field-metadata.ts @@ -15,4 +15,5 @@ export interface FieldMetadata { params?: ParamMetadata[]; roles?: any[]; middlewares?: Array>; + metadata?: any; } diff --git a/src/metadata/definitions/index.ts b/src/metadata/definitions/index.ts index 15aa7d652..a1c3a0c7a 100644 --- a/src/metadata/definitions/index.ts +++ b/src/metadata/definitions/index.ts @@ -6,3 +6,4 @@ export * from "./middleware-metadata"; export * from "./param-metadata"; export * from "./resolver-metadata"; export * from "./union-metadata"; +export * from "./metadata"; diff --git a/src/metadata/definitions/metadata.ts b/src/metadata/definitions/metadata.ts new file mode 100644 index 000000000..39599afe6 --- /dev/null +++ b/src/metadata/definitions/metadata.ts @@ -0,0 +1,5 @@ +export interface Metadata { + data: any; + target: Function; + name?: string | symbol; +} diff --git a/src/metadata/metadata-storage.ts b/src/metadata/metadata-storage.ts index a5de7f335..36b284e3b 100644 --- a/src/metadata/metadata-storage.ts +++ b/src/metadata/metadata-storage.ts @@ -1,25 +1,26 @@ +import { NoExplicitTypeError } from "../errors"; +import { ClassType } from "../interfaces"; import { - ResolverMetadata, - ClassMetadata, - FieldMetadata, - ParamMetadata, - FieldResolverMetadata, AuthorizedMetadata, BaseResolverMetadata, + ClassMetadata, EnumMetadata, - UnionMetadata, - UnionMetadataWithSymbol, + FieldMetadata, + FieldResolverMetadata, + Metadata, + MiddlewareMetadata, + ParamMetadata, ResolverClassMetadata, + ResolverMetadata, SubscriptionResolverMetadata, - MiddlewareMetadata, + UnionMetadata, + UnionMetadataWithSymbol, } from "./definitions"; -import { ClassType } from "../interfaces"; -import { NoExplicitTypeError } from "../errors"; import { - mapSuperResolverHandlers, + ensureReflectMetadataExists, mapMiddlewareMetadataToArray, mapSuperFieldResolverHandlers, - ensureReflectMetadataExists, + mapSuperResolverHandlers, } from "./utils"; export class MetadataStorage { @@ -28,6 +29,8 @@ export class MetadataStorage { subscriptions: SubscriptionResolverMetadata[] = []; fieldResolvers: FieldResolverMetadata[] = []; objectTypes: ClassMetadata[] = []; + additionalObjectTypeMetadata: Metadata[] = []; + additionalFieldMetadata: Array = []; inputTypes: ClassMetadata[] = []; argumentTypes: ClassMetadata[] = []; interfaceTypes: ClassMetadata[] = []; @@ -47,33 +50,43 @@ export class MetadataStorage { collectQueryHandlerMetadata(definition: ResolverMetadata) { this.queries.push(definition); } + collectMutationHandlerMetadata(definition: ResolverMetadata) { this.mutations.push(definition); } + collectSubscriptionHandlerMetadata(definition: SubscriptionResolverMetadata) { this.subscriptions.push(definition); } + collectFieldResolverMetadata(definition: FieldResolverMetadata) { this.fieldResolvers.push(definition); } + collectObjectMetadata(definition: ClassMetadata) { this.objectTypes.push(definition); } + collectInputMetadata(definition: ClassMetadata) { this.inputTypes.push(definition); } + collectArgsMetadata(definition: ClassMetadata) { this.argumentTypes.push(definition); } + collectInterfaceMetadata(definition: ClassMetadata) { this.interfaceTypes.push(definition); } + collectAuthorizedFieldMetadata(definition: AuthorizedMetadata) { this.authorizedFields.push(definition); } + collectEnumMetadata(definition: EnumMetadata) { this.enums.push(definition); } + collectUnionMetadata(definition: UnionMetadata) { const unionSymbol = Symbol(definition.name); this.unions.push({ @@ -82,6 +95,7 @@ export class MetadataStorage { }); return unionSymbol; } + collectMiddlewareMetadata(definition: MiddlewareMetadata) { this.middlewares.push(definition); } @@ -89,9 +103,19 @@ export class MetadataStorage { collectResolverClassMetadata(definition: ResolverClassMetadata) { this.resolverClasses.push(definition); } + collectClassFieldMetadata(definition: FieldMetadata) { this.fields.push(definition); } + + collectAdditionalFieldMetadata(definition: Metadata & { name: string | symbol }) { + this.additionalFieldMetadata.push(definition); + } + + collectAdditionalObjectTypeMetadata(definition: Metadata) { + this.additionalObjectTypeMetadata.push(definition); + } + collectHandlerParamMetadata(definition: ParamMetadata) { this.params.push(definition); } @@ -145,7 +169,9 @@ export class MetadataStorage { middleware => middleware.target === field.target && middleware.fieldName === field.name, ), ); + field.metadata = this.findAdditionalFieldMetadata(field.target, field.name); }); + def.metadata = this.findAdditionalObjectTypeMetadata(def.target); def.fields = fields; }); } @@ -251,4 +277,25 @@ export class MetadataStorage { } return authorizedField.roles; } + + private findAdditionalFieldMetadata( + target: Function, + fieldName: string, + ): Metadata & { name: string | symbol } | undefined { + const metadata = this.additionalFieldMetadata.find( + val => val.target === target && val.name === fieldName, + ); + if (!metadata) { + return; + } + return metadata.data; + } + + private findAdditionalObjectTypeMetadata(target: Function): Metadata | undefined { + const metadata = this.additionalObjectTypeMetadata.find(val => val.target === target); + if (!metadata) { + return; + } + return metadata.data; + } } diff --git a/src/schema/schema-generator.ts b/src/schema/schema-generator.ts index 1e441c1c7..22a58cac0 100644 --- a/src/schema/schema-generator.ts +++ b/src/schema/schema-generator.ts @@ -1,65 +1,70 @@ import { - GraphQLSchema, - GraphQLObjectType, - GraphQLNamedType, + graphql, + GraphQLEnumType, + GraphQLEnumValueConfigMap, + GraphQLFieldConfigArgumentMap, GraphQLFieldConfigMap, - GraphQLOutputType, + GraphQLInputFieldConfigMap, GraphQLInputObjectType, - GraphQLFieldConfigArgumentMap, GraphQLInputType, - GraphQLInputFieldConfigMap, GraphQLInterfaceType, - graphql, - introspectionQuery, - GraphQLEnumType, - GraphQLEnumValueConfigMap, + GraphQLNamedType, + GraphQLObjectType, + GraphQLOutputType, + GraphQLSchema, GraphQLUnionType, + introspectionQuery, } from "graphql"; -import { withFilter, ResolverFn } from "graphql-subscriptions"; - -import { getMetadataStorage } from "../metadata/getMetadataStorage"; +import { ResolverFn, withFilter } from "graphql-subscriptions"; +import { TypeOptions, TypeValue } from "../decorators/types"; +import { + GeneratingSchemaError, + MissingSubscriptionTopicsError, + UnionResolveTypeError, +} from "../errors"; +import { convertTypeIfScalar, getEnumValuesMap, wrapWithTypeOptions } from "../helpers/types"; +import { ResolverFilterData, ResolverTopicData } from "../interfaces"; import { - ResolverMetadata, - ParamMetadata, ClassMetadata, + ParamMetadata, + ResolverMetadata, SubscriptionResolverMetadata, } from "../metadata/definitions"; -import { TypeOptions, TypeValue } from "../decorators/types"; -import { wrapWithTypeOptions, convertTypeIfScalar, getEnumValuesMap } from "../helpers/types"; + +import { getMetadataStorage } from "../metadata/getMetadataStorage"; import { - createHandlerResolver, createAdvancedFieldResolver, + createHandlerResolver, createSimpleFieldResolver, } from "../resolvers/create"; import { BuildContext, BuildContextOptions } from "./build-context"; -import { - UnionResolveTypeError, - GeneratingSchemaError, - MissingSubscriptionTopicsError, -} from "../errors"; -import { ResolverFilterData, ResolverTopicData } from "../interfaces"; import { getFieldMetadataFromInputType, getFieldMetadataFromObjectType } from "./utils"; interface ObjectTypeInfo { target: Function; type: GraphQLObjectType; } + interface InputObjectTypeInfo { target: Function; type: GraphQLInputObjectType; } + interface InterfaceTypeInfo { target: Function; type: GraphQLInterfaceType; } + interface EnumTypeInfo { enumObj: object; type: GraphQLEnumType; } + interface UnionTypeInfo { unionSymbol: symbol; type: GraphQLUnionType; } + // tslint:disable-next-line:no-empty-interface export interface SchemaGeneratorOptions extends BuildContextOptions {} @@ -236,6 +241,7 @@ export abstract class SchemaGenerator { : createSimpleFieldResolver(field), description: field.description, deprecationReason: field.deprecationReason, + ...field.metadata, }; return fieldsMap; }, @@ -264,6 +270,7 @@ export abstract class SchemaGenerator { } return fields; }, + ...objectType.metadata, }), }; }); diff --git a/tests/functional/fields.ts b/tests/functional/fields.ts index f55c1315a..036baf7b1 100644 --- a/tests/functional/fields.ts +++ b/tests/functional/fields.ts @@ -1,16 +1,17 @@ -import "reflect-metadata"; import { - IntrospectionSchema, - IntrospectionObjectType, - IntrospectionNonNullTypeRef, - IntrospectionNamedTypeRef, IntrospectionListTypeRef, + IntrospectionNamedTypeRef, + IntrospectionNonNullTypeRef, + IntrospectionObjectType, + IntrospectionSchema, TypeKind, } from "graphql"; +import "reflect-metadata"; +import { Field, ObjectType, Query, Resolver } from "../../src"; +import { Metadata } from "../../src/decorators/Metadata"; import { getMetadataStorage } from "../../src/metadata/getMetadataStorage"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; -import { ObjectType, Field, Query, Resolver } from "../../src"; describe("Fields - schema", () => { let schemaIntrospection: IntrospectionSchema; @@ -27,6 +28,7 @@ describe("Fields - schema", () => { stringField: string; } + @Metadata({ sqlName: "overwritten_name", uniqueKey: "implicitStringField" }) @ObjectType() class SampleObject { @Field() @@ -53,6 +55,7 @@ describe("Fields - schema", () => { @Field(type => [SampleNestedObject], { nullable: true }) nullableObjectArrayField: SampleNestedObject[] | null; + @Metadata({ sqlName: "overwritten_name" }) @Field({ name: "overwrittenName", nullable: true }) overwrittenStringField: string; @@ -68,7 +71,7 @@ describe("Fields - schema", () => { } } - // get builded schema info from retrospection + // get built schema info from retrospection const schemaInfo = await getSchemaInfo({ resolvers: [SampleResolver], }); @@ -98,6 +101,39 @@ describe("Fields - schema", () => { expect(complexField.complexity).toBe(10); }); + it("it should register metadata for field", async () => { + const metadataStorage = getMetadataStorage(); + const sampleObj = metadataStorage.objectTypes.find(it => it.name === "SampleObject")!; + const field = sampleObj.fields!.find(it => it.name === "overwrittenStringField")!; + + expect(field.metadata).toEqual({ sqlName: "overwritten_name" }); + }); + + it("it should not register metadata for field without metadata decorator", async () => { + const metadataStorage = getMetadataStorage(); + const sampleObj = metadataStorage.objectTypes.find(it => it.name === "SampleObject")!; + const field = sampleObj.fields!.find(it => it.name === "complexField")!; + + expect(field.metadata).toBeUndefined(); + }); + + it("it should register metadata for object type", async () => { + const metadataStorage = getMetadataStorage(); + const sampleObj = metadataStorage.objectTypes.find(it => it.name === "SampleObject")!; + + expect(sampleObj.metadata).toEqual({ + sqlName: "overwritten_name", + uniqueKey: "implicitStringField", + }); + }); + + it("it should not register metadata for object type without metadata decorator", async () => { + const metadataStorage = getMetadataStorage(); + const sampleObj = metadataStorage.objectTypes.find(it => it.name === "SampleNestedObject")!; + + expect(sampleObj.metadata).toBeUndefined(); + }); + it("should throw error when field type not provided", async () => { expect.assertions(3); getMetadataStorage().clear();