From 643bae0c334e01358893253308b40f939bee59ad Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 11 May 2021 00:31:11 +0100 Subject: [PATCH 01/14] Switch type check to use 'instanceof' instead of constructor.name for supporting extended types --- src/resolvers/connection.ts | 2 +- src/resolvers/count.ts | 4 ++-- src/resolvers/createMany.ts | 2 +- src/resolvers/createOne.ts | 2 +- src/resolvers/dataLoader.ts | 5 ++--- src/resolvers/dataLoaderMany.ts | 5 ++--- src/resolvers/findById.ts | 10 ++++++---- src/resolvers/findByIds.ts | 5 ++--- src/resolvers/findMany.ts | 6 +++--- src/resolvers/findOne.ts | 4 ++-- src/resolvers/helpers/record.ts | 2 +- src/resolvers/helpers/sort.ts | 2 +- src/resolvers/pagination.ts | 2 +- src/resolvers/removeById.ts | 5 ++--- src/resolvers/removeMany.ts | 4 ++-- src/resolvers/removeOne.ts | 4 ++-- src/resolvers/updateById.ts | 5 ++--- src/resolvers/updateMany.ts | 4 ++-- src/resolvers/updateOne.ts | 2 +- 19 files changed, 36 insertions(+), 39 deletions(-) diff --git a/src/resolvers/connection.ts b/src/resolvers/connection.ts index 0bca0bee..b3d1a238 100644 --- a/src/resolvers/connection.ts +++ b/src/resolvers/connection.ts @@ -4,7 +4,7 @@ import { ConnectionResolverOpts as _ConnectionResolverOpts, ConnectionTArgs, } from 'graphql-compose-connection'; -import type { Resolver, ObjectTypeComposer } from 'graphql-compose'; +import { Resolver, ObjectTypeComposer } from 'graphql-compose'; import { CountResolverOpts, count } from './count'; import { FindManyResolverOpts, findMany } from './findMany'; import { getUniqueIndexes, extendByReversedIndexes, IndexT } from '../utils/getIndexesFromModel'; diff --git a/src/resolvers/count.ts b/src/resolvers/count.ts index f3bd46e4..59241134 100644 --- a/src/resolvers/count.ts +++ b/src/resolvers/count.ts @@ -1,4 +1,4 @@ -import type { Resolver, ObjectTypeComposer } from 'graphql-compose'; +import { Resolver, ObjectTypeComposer } from 'graphql-compose'; import type { Model, Document } from 'mongoose'; import { filterHelper, @@ -29,7 +29,7 @@ export function count({ - type: tc, + type: tc.getTypeName(), name: 'findById', kind: 'query', args: { diff --git a/src/resolvers/findByIds.ts b/src/resolvers/findByIds.ts index 0a20ede0..df4b0334 100644 --- a/src/resolvers/findByIds.ts +++ b/src/resolvers/findByIds.ts @@ -1,5 +1,4 @@ -import { toInputType } from 'graphql-compose'; -import type { Resolver, ObjectTypeComposer } from 'graphql-compose'; +import { Resolver, ObjectTypeComposer, toInputType } from 'graphql-compose'; import type { Model, Document } from 'mongoose'; import { limitHelper, @@ -48,7 +47,7 @@ export function findByIds({ - type: tc.NonNull.List.NonNull, + type: tc.schemaComposer.getAnyTC(tc.getTypeName()).NonNull.List.NonNull, name: 'findMany', kind: 'query', args: { diff --git a/src/resolvers/findOne.ts b/src/resolvers/findOne.ts index 5d4e304b..79a7fd8b 100644 --- a/src/resolvers/findOne.ts +++ b/src/resolvers/findOne.ts @@ -1,4 +1,4 @@ -import type { Resolver, ObjectTypeComposer } from 'graphql-compose'; +import { Resolver, ObjectTypeComposer } from 'graphql-compose'; import type { Model, Document } from 'mongoose'; import { skipHelper, @@ -53,7 +53,7 @@ export function findOne( tc: ObjectTypeComposer, opts?: RecordHelperArgsOpts ): ObjectTypeComposerArgumentConfigMapDefinition<{ record: any }> { - if (!tc || tc.constructor.name !== 'ObjectTypeComposer') { + if (!tc || !(tc instanceof ObjectTypeComposer)) { throw new Error('First arg for recordHelperArgs() should be instance of ObjectTypeComposer.'); } diff --git a/src/resolvers/helpers/sort.ts b/src/resolvers/helpers/sort.ts index 51a57dd7..58c7a920 100644 --- a/src/resolvers/helpers/sort.ts +++ b/src/resolvers/helpers/sort.ts @@ -28,7 +28,7 @@ export function sortHelperArgs( model: Model, opts?: SortHelperArgsOpts ): ObjectTypeComposerArgumentConfigMapDefinition<{ sort: any }> { - if (!tc || tc.constructor.name !== 'ObjectTypeComposer') { + if (!tc || !(tc instanceof ObjectTypeComposer)) { throw new Error('First arg for sortHelperArgs() should be instance of ObjectTypeComposer.'); } diff --git a/src/resolvers/pagination.ts b/src/resolvers/pagination.ts index 54fdac95..763e18f2 100644 --- a/src/resolvers/pagination.ts +++ b/src/resolvers/pagination.ts @@ -1,4 +1,4 @@ -import type { Resolver, ObjectTypeComposer } from 'graphql-compose'; +import { Resolver, ObjectTypeComposer } from 'graphql-compose'; import type { Model, Document } from 'mongoose'; import { preparePaginationResolver, diff --git a/src/resolvers/removeById.ts b/src/resolvers/removeById.ts index 27c0bb0a..0c7851be 100644 --- a/src/resolvers/removeById.ts +++ b/src/resolvers/removeById.ts @@ -1,5 +1,4 @@ -import { toInputType } from 'graphql-compose'; -import type { Resolver, ObjectTypeComposer } from 'graphql-compose'; +import { Resolver, ObjectTypeComposer, toInputType } from 'graphql-compose'; import type { Model, Document } from 'mongoose'; import { findById } from './findById'; import type { ExtendedResolveParams } from './index'; @@ -28,7 +27,7 @@ export function removeById Date: Wed, 12 May 2021 18:45:09 +0100 Subject: [PATCH 02/14] Fix linting error in case block --- src/discriminators/prepareBaseResolvers.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/discriminators/prepareBaseResolvers.ts b/src/discriminators/prepareBaseResolvers.ts index 2aece2bb..7dcb177d 100644 --- a/src/discriminators/prepareBaseResolvers.ts +++ b/src/discriminators/prepareBaseResolvers.ts @@ -76,7 +76,7 @@ export function prepareBaseResolvers(baseTC: DiscriminatorTypeComposer }); break; - case 'connection': + case 'connection': { const edgesTC = resolver .getOTC() .getFieldOTC('edges') @@ -91,6 +91,7 @@ export function prepareBaseResolvers(baseTC: DiscriminatorTypeComposer resolver.getOTC().setField('edges', edgesTC.NonNull.List.NonNull); break; + } default: } From 7d11ae96385e154194253a7a1b84650ded876356 Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 25 May 2021 00:24:25 +0100 Subject: [PATCH 03/14] Implement optional discriminator handling inside composeMongoose function using options --- src/composeMongoose.ts | 14 +- .../eDiscriminatorTypeComposer.ts | 266 ++++++++++++++++++ src/enhancedDiscriminators/index.ts | 4 + src/fieldsConverter.ts | 59 ++-- 4 files changed, 324 insertions(+), 19 deletions(-) create mode 100644 src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts create mode 100644 src/enhancedDiscriminators/index.ts diff --git a/src/composeMongoose.ts b/src/composeMongoose.ts index 0500fa74..da009783 100644 --- a/src/composeMongoose.ts +++ b/src/composeMongoose.ts @@ -1,6 +1,7 @@ import type { SchemaComposer, Resolver, InputTypeComposer } from 'graphql-compose'; import { schemaComposer as globalSchemaComposer, ObjectTypeComposer } from 'graphql-compose'; import type { Model, Document } from 'mongoose'; +import { EDiscriminatorTypeComposer } from './enhancedDiscriminators'; import { convertModelToGraphQL } from './fieldsConverter'; import { resolverFactory } from './resolvers'; import MongoID from './types/MongoID'; @@ -71,6 +72,15 @@ export type ComposeMongooseOpts = { * You can make fields as NonNull if they have default value in mongoose model. */ defaultsAsNonNull?: boolean; + /** + * Support discriminators on base models + */ + includeBaseDiscriminators?: boolean; + /** + * EXPERIMENTAL - Support discriminated fields on nested fields + * May not work as expected on input types for mutations + */ + includeNestedDiscriminators?: boolean; /** @deprecated */ fields?: { @@ -101,7 +111,7 @@ export type GenerateResolverType = { export function composeMongoose( model: Model, opts: ComposeMongooseOpts = {} -): ObjectTypeComposer & { +): (ObjectTypeComposer | EDiscriminatorTypeComposer) & { mongooseResolvers: GenerateResolverType; } { const m: Model = model; @@ -121,7 +131,7 @@ export function composeMongoose( sc.delete(m.schema); } - const tc = convertModelToGraphQL(m, name, sc); + const tc = convertModelToGraphQL(m, name, sc, { ...opts }); if (opts.description) { tc.setDescription(opts.description); diff --git a/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts new file mode 100644 index 00000000..7d458e54 --- /dev/null +++ b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts @@ -0,0 +1,266 @@ +import { GraphQLObjectType } from 'graphql'; +import { + EnumTypeComposer, + Extensions, + InterfaceTypeComposer, + ObjectTypeComposer, + ObjectTypeComposerFieldConfigDefinition, + SchemaComposer, +} from 'graphql-compose'; +import mongoose, { Document, Model } from 'mongoose'; +import { convertModelToGraphQL, MongoosePseudoModelT } from '../fieldsConverter'; +import { composeMongoose, ComposeMongooseOpts } from '../composeMongoose'; + +export function convertModelToGraphQLWithDiscriminators( + model: Model | MongoosePseudoModelT, + typeName: string, + schemaComposer: SchemaComposer, + opts: ComposeMongooseOpts = {} +): ObjectTypeComposer { + const sc = schemaComposer; + + // workaround to avoid recursive loop on convertModel and support nested discrim fields, will be set true in nested fields + // if includeNestedDiscriminators is true to indicate that convertModel should invoke this method + opts.includeBaseDiscriminators = false; + + if (!model.schema.discriminators) { + return convertModelToGraphQL(model, typeName, sc, opts); + } else { + return EDiscriminatorTypeComposer.createFromModel(model, typeName, sc, opts); + } +} + +export interface ComposeMongooseDiscriminatorsOpts extends ComposeMongooseOpts { + reorderFields?: boolean | string[]; // true order: _id, DKey, DInterfaceFields, DiscriminatorFields +} + +export class EDiscriminatorTypeComposer extends ObjectTypeComposer< + TSource, + TContext +> { + discriminatorKey: string = ''; + discrimTCs: { [key: string]: ObjectTypeComposer } = {}; + DInputObject: ObjectTypeComposer; + DInterface: InterfaceTypeComposer; + opts: ComposeMongooseDiscriminatorsOpts = {}; + DKeyETC?: EnumTypeComposer; + + constructor(gqType: GraphQLObjectType, schemaComposer: SchemaComposer) { + super(gqType, schemaComposer); + this.DInterface = schemaComposer.getOrCreateIFTC(`${gqType.name}Interface`); + this.DInputObject = schemaComposer.getOrCreateOTC(`${gqType.name}Input`); + return this; + } + + static createFromModel( + model: Model | MongoosePseudoModelT, + typeName: string, + schemaComposer: SchemaComposer, + opts: ComposeMongooseDiscriminatorsOpts + ): EDiscriminatorTypeComposer { + if (!model.schema.discriminators) { + throw Error('Discriminators should be present to use this function'); + } + + if (!(schemaComposer instanceof SchemaComposer)) { + throw Error( + 'DiscriminatorTC.createFromModel() should receive SchemaComposer in second argument' + ); + } + + opts = { + reorderFields: true, + schemaComposer, + ...opts, + }; + + const baseTC = convertModelToGraphQL(model, typeName, schemaComposer, opts); + const baseDTC = new EDiscriminatorTypeComposer(baseTC.getType(), schemaComposer); + + // copy data from baseTC to baseDTC + baseTC.clone(baseDTC as ObjectTypeComposer); + + baseDTC.opts = { ...opts }; + baseDTC.discriminatorKey = (model as any).schema.options.discriminatorKey; + if (baseDTC.discriminatorKey === '__t') { + throw Error( + 'A custom discriminator key must be set on the model options in mongoose for discriminator behaviour to function correctly' + ); + } + baseDTC.DInterface = baseDTC._buildDiscriminatedInterface(model, baseTC); + baseDTC.DInterface.setInputTypeComposer(baseDTC.DInputObject.getInputTypeComposer()); + baseDTC.setInputTypeComposer(baseDTC.DInputObject.getInputTypeComposer()); + + baseDTC.setInterfaces([baseDTC.DInterface]); + baseDTC._gqcInputTypeComposer = baseDTC.DInputObject._gqcInputTypeComposer; + baseDTC.DInterface._gqcInputTypeComposer = baseDTC.DInputObject._gqcInputTypeComposer; + + // discriminators an object containing all discriminators with key being DNames + // import { EDiscriminatorTypeComposer } from './enhancedDiscriminators/index';baseDTC.DKeyETC = createAndSetDKeyETC(baseDTC, discriminators); + + // reorderFields(baseDTC, baseDTC.opts.reorderFields, baseDTC.discriminatorKey); + baseDTC.schemaComposer.addSchemaMustHaveType(baseDTC as any); + + // prepare Base Resolvers + // prepareBaseResolvers(baseDTC); + + return baseDTC as any; + } + + _buildDiscriminatedInterface( + model: Model | MongoosePseudoModelT, + baseTC: ObjectTypeComposer + ): InterfaceTypeComposer { + const interfaceTC = this.DInterface; + interfaceTC.removeOtherFields(''); + interfaceTC.setFields(baseTC.getFields()); + this.DInputObject.setFields(baseTC.getFields()); + + Object.keys((model as any).schema.discriminators).forEach((key) => { + const discrimType = (model as any).schema.discriminators[key]; + + // ensure valid type for calling convert + const discrimModel = + discrimType instanceof mongoose.Schema ? { schema: discrimType } : discrimType; + + const discrimTC = composeMongoose(discrimModel, { + ...this.opts, + name: this.getTypeName() + key, + }); + + // base OTC used for input schema must hold all child TC fields in the most loose way (so all types are accepted) + // more detailed type checks are done on input object by mongoose itself + // TODO - Review when Input Unions/similar are accepted into the graphQL spec and supported by graphql-js + // SEE - https://github.com/graphql/graphql-spec/blob/main/rfcs/InputUnion.md + const discrimFields = discrimTC.getFields(); + Object.keys(discrimFields).forEach((fieldName) => { + const field = discrimFields[fieldName]; + + // if another discrimTC has already defined the field (not from original TC) + if (this.DInputObject.hasField(fieldName) && !baseTC.hasField(fieldName)) { + this.DInputObject.setField(fieldName, `JSON`); + } else { + (this.DInputObject as ObjectTypeComposer).setField(fieldName, field); + } + }); + this.DInputObject.makeFieldNullable( + discrimTC.getFieldNames().filter((field) => !baseTC.hasField(field)) + ); + // there may be issues for discriminated types with overlapping fields, the last validation req will overwrite prior one + // mitigation attempted by setting type to any on filed which appears in multiple implementations + discrimTC.makeFieldNonNull('_id'); + + // also set fields on master TC so it will have all possibilities for input workaround + + const discrimVal = discrimType.discriminatorMapping.value; + interfaceTC.addTypeResolver( + discrimTC, + (obj: any) => obj[this.discriminatorKey] === discrimVal + ); + + // add TC to discrimTCs + this.discrimTCs[discrimTC.getTypeName()] = discrimTC; + }); + + return interfaceTC; + } + + getDKey(): string { + return this.discriminatorKey; + } + + // getDKeyETC(): EnumTypeComposer { + // return this.DKeyETC as any; + // } + + getDInterface(): InterfaceTypeComposer { + return this.DInterface as any; + } + + getDInputObject(): ObjectTypeComposer { + return this.DInputObject as any; + } + + // OVERRIDES + getTypeName(): string { + return this.DInterface.getTypeName(); + } + + setField( + fieldName: string, + fieldConfig: ObjectTypeComposerFieldConfigDefinition + ): this { + super.setField(fieldName, fieldConfig); + this.getDInputObject().setField(fieldName, fieldConfig); + this.getDInterface().setField(fieldName, fieldConfig); + + for (const discrimTC in this.discrimTCs) { + this.discrimTCs[discrimTC].setField(fieldName, fieldConfig); + } + + return this; + } + + setExtensions(extensions: Extensions): this { + super.setExtensions(extensions); + this.getDInputObject().setExtensions(extensions); + this.getDInterface().setExtensions(extensions); + + for (const discrimTC in this.discrimTCs) { + this.discrimTCs[discrimTC].setExtensions(extensions); + } + + return this; + } + + setDescription(description: string): this { + super.setDescription(description); + this.getDInputObject().setDescription(description); + this.getDInterface().setDescription(description); + + return this; + } + + removeField(fieldNameOrArray: string | Array): this { + super.removeField(fieldNameOrArray); + this.getDInputObject().removeField(fieldNameOrArray); + this.getDInterface().removeField(fieldNameOrArray); + + for (const discrimTC in this.discrimTCs) { + this.discrimTCs[discrimTC].removeField(fieldNameOrArray); + } + + return this; + } + + removeOtherFields(fieldNameOrArray: string | Array): this { + // get field names to delete from child TCs, so their unique fields are preserved + const keepFieldNames = + fieldNameOrArray instanceof Array ? fieldNameOrArray : [fieldNameOrArray]; + const fieldNamesToDelete = this.DInterface.getFieldNames().filter( + (field) => !keepFieldNames.includes(field) + ); + + super.removeField(fieldNamesToDelete); + this.getDInputObject().removeField(fieldNamesToDelete); + this.getDInterface().removeOtherFields(fieldNameOrArray); + + for (const discrimTC in this.discrimTCs) { + this.discrimTCs[discrimTC].removeField(fieldNamesToDelete); + } + + return this; + } + + reorderFields(names: string[]): this { + super.reorderFields(names); + this.getDInterface().reorderFields(names); + this.getDInputObject().reorderFields(names); + + for (const discrimTC in this.discrimTCs) { + this.discrimTCs[discrimTC].reorderFields(names); + } + + return this; + } +} diff --git a/src/enhancedDiscriminators/index.ts b/src/enhancedDiscriminators/index.ts new file mode 100644 index 00000000..e0fcb51e --- /dev/null +++ b/src/enhancedDiscriminators/index.ts @@ -0,0 +1,4 @@ +export { + EDiscriminatorTypeComposer, + convertModelToGraphQLWithDiscriminators, +} from './eDiscriminatorTypeComposer'; diff --git a/src/fieldsConverter.ts b/src/fieldsConverter.ts index aa01d8e2..503f140f 100644 --- a/src/fieldsConverter.ts +++ b/src/fieldsConverter.ts @@ -2,7 +2,7 @@ import mongoose, { Document } from 'mongoose'; import type { Schema, Model } from 'mongoose'; -import type { +import { SchemaComposer, ObjectTypeComposer, EnumTypeComposer, @@ -13,6 +13,11 @@ import { upperFirst } from 'graphql-compose'; import type { GraphQLScalarType } from 'graphql-compose/lib/graphql'; import GraphQLMongoID from './types/MongoID'; import GraphQLBSONDecimal from './types/BSONDecimal'; +import { + convertModelToGraphQLWithDiscriminators, + EDiscriminatorTypeComposer, +} from './enhancedDiscriminators'; +import { ComposeMongooseOpts } from './composeMongoose'; type MongooseFieldT = { path?: string; @@ -134,7 +139,8 @@ export function getFieldsFromModel(model: Model | MongoosePseudoModelT): Mo export function convertModelToGraphQL( model: Model | MongoosePseudoModelT, typeName: string, - schemaComposer: SchemaComposer + schemaComposer: SchemaComposer, + opts: ComposeMongooseOpts = {} ): ObjectTypeComposer { const sc = schemaComposer; @@ -147,8 +153,10 @@ export function convertModelToGraphQL( return sc.getOTC(model.schema); } - // add type to registry before fields creation - // it helps to avoid circuit dependencies, eg. `User { friends: [User] }` + if (opts.includeBaseDiscriminators) { + return convertModelToGraphQLWithDiscriminators(model, typeName, sc, opts); + } + const typeComposer = sc.getOrCreateOTC(typeName); sc.set(model.schema, typeComposer); sc.set(typeName, typeComposer); @@ -176,7 +184,7 @@ export function convertModelToGraphQL( requiredFields.push(fieldName); } - let type = convertFieldToGraphQL(mongooseField, typeName, sc); + let type = convertFieldToGraphQL(mongooseField, typeName, sc, opts); // in mongoose schema we use javascript `Number` object which casted to `Float` type // so in most cases _id field is `Int` @@ -184,6 +192,14 @@ export function convertModelToGraphQL( type = 'Int'; } + let trueType = type; + if (trueType instanceof Array) { + trueType = trueType[0]; + } + if (trueType instanceof EDiscriminatorTypeComposer) { + type = type instanceof Array ? [trueType.getDInterface()] : trueType.getDInterface(); + } + graphqlFields[fieldName] = { type, description: _getFieldDescription(mongooseField), @@ -203,7 +219,8 @@ export function convertModelToGraphQL( export function convertSchemaToGraphQL( schema: Schema, typeName: string, - schemaComposer: SchemaComposer + schemaComposer: SchemaComposer, + opts: ComposeMongooseOpts = {} ): ObjectTypeComposer { const sc = schemaComposer; @@ -215,7 +232,7 @@ export function convertSchemaToGraphQL( return sc.getOTC(schema); } - const tc = convertModelToGraphQL({ schema }, typeName, sc); + const tc = convertModelToGraphQL({ schema }, typeName, sc, opts); // also generate InputType tc.getInputTypeComposer(); @@ -226,7 +243,8 @@ export function convertSchemaToGraphQL( export function convertFieldToGraphQL( field: MongooseFieldT, prefix: string = '', - schemaComposer: SchemaComposer + schemaComposer: SchemaComposer, + opts: ComposeMongooseOpts = {} ): ComposeOutputTypeDefinition { if (!schemaComposer.has('MongoID')) { schemaComposer.add(GraphQLMongoID); @@ -237,15 +255,15 @@ export function convertFieldToGraphQL( case ComplexTypes.SCALAR: return scalarToGraphQL(field); case ComplexTypes.ARRAY: - return arrayToGraphQL(field, prefix, schemaComposer); + return arrayToGraphQL(field, prefix, schemaComposer, opts); case ComplexTypes.EMBEDDED: - return embeddedToGraphQL(field, prefix, schemaComposer); + return embeddedToGraphQL(field, prefix, schemaComposer, opts); case ComplexTypes.ENUM: return enumToGraphQL(field, prefix, schemaComposer); case ComplexTypes.REFERENCE: return referenceToGraphQL(field); case ComplexTypes.DOCUMENT_ARRAY: - return documentArrayToGraphQL(field, prefix, schemaComposer); + return documentArrayToGraphQL(field, prefix, schemaComposer, opts); case ComplexTypes.MIXED: return 'JSON'; case ComplexTypes.DECIMAL: @@ -316,7 +334,8 @@ export function scalarToGraphQL(field: MongooseFieldT): ComposeScalarType { export function arrayToGraphQL( field: MongooseFieldT, prefix: string = '', - schemaComposer: SchemaComposer + schemaComposer: SchemaComposer, + opts: ComposeMongooseOpts = {} ): ComposeOutputTypeDefinition { if (!field || !field.caster) { throw new Error( @@ -327,14 +346,17 @@ export function arrayToGraphQL( const unwrappedField = { ...field.caster }; - const outputType: any = convertFieldToGraphQL(unwrappedField, prefix, schemaComposer); + opts.includeBaseDiscriminators = opts.includeNestedDiscriminators; // workaround to avoid recursive loop on convertModel and support nested discrim fields + + const outputType: any = convertFieldToGraphQL(unwrappedField, prefix, schemaComposer, opts); return [outputType]; } export function embeddedToGraphQL( field: MongooseFieldT, prefix: string = '', - schemaComposer: SchemaComposer + schemaComposer: SchemaComposer, + opts: ComposeMongooseOpts = {} ): ObjectTypeComposer { const fieldName = _getFieldName(field); const fieldType = _getFieldType(field); @@ -351,8 +373,9 @@ export function embeddedToGraphQL( throw new Error(`Mongoose field '${prefix}.${fieldName}' should have 'schema' property`); } + opts.includeBaseDiscriminators = opts.includeNestedDiscriminators; // workaround to avoid recursive loop on convertModel and support nested discrim fields const typeName = `${prefix}${upperFirst(fieldName)}`; - return convertSchemaToGraphQL(fieldSchema, typeName, schemaComposer); + return convertSchemaToGraphQL(fieldSchema, typeName, schemaComposer, opts); } export function enumToGraphQL( @@ -386,7 +409,8 @@ export function enumToGraphQL( export function documentArrayToGraphQL( field: MongooseFieldT, prefix: string = '', - schemaComposer: SchemaComposer + schemaComposer: SchemaComposer, + opts: ComposeMongooseOpts = {} ): [ObjectTypeComposer] { if (!(field instanceof mongoose.Schema.Types.DocumentArray) && !field?.schema?.paths) { throw new Error( @@ -397,7 +421,8 @@ export function documentArrayToGraphQL( const typeName = `${prefix}${upperFirst(_getFieldName(field))}`; - const tc = convertModelToGraphQL(field as any, typeName, schemaComposer); + opts.includeBaseDiscriminators = opts.includeNestedDiscriminators; // workaround to avoid recursive loop on convertModel and support nested discrim fields + const tc = convertModelToGraphQL(field as any, typeName, schemaComposer, opts); return [tc]; } From 0799c4e50d670aa651d959bc34fa5eb5270fb988 Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 25 May 2021 00:24:58 +0100 Subject: [PATCH 04/14] Add unit test cases for enhanced discriminator handling --- .../__mocks__/characterDiscrimModels.ts | 97 ++++++++ .../eDiscriminatorTypeComposer-test.ts | 220 ++++++++++++++++++ 2 files changed, 317 insertions(+) create mode 100644 src/enhancedDiscriminators/__mocks__/characterDiscrimModels.ts create mode 100644 src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts diff --git a/src/enhancedDiscriminators/__mocks__/characterDiscrimModels.ts b/src/enhancedDiscriminators/__mocks__/characterDiscrimModels.ts new file mode 100644 index 00000000..20d7b35c --- /dev/null +++ b/src/enhancedDiscriminators/__mocks__/characterDiscrimModels.ts @@ -0,0 +1,97 @@ +import type { Model } from 'mongoose'; +import { mongoose, Schema, Types } from '../../__mocks__/mongooseCommon'; + +export const PersonSchema = new Schema({ + dob: Number, + primaryFunction: [String], + starShips: [String], + totalCredits: Number, +}); + +export const DroidSchema = new Schema({ + makeDate: Date, + modelNumber: Number, + primaryFunction: [String], +}); + +const MovieSchema = new Schema({ + _id: String, + + characters: { + type: [String], // redundant but i need it. + description: 'A character in the Movie, Person or Droid.', + }, + + director: { + type: String, // id of director + description: 'Directed the movie.', + }, + + imdbRatings: String, + releaseDate: String, +}); + +export const MovieModel = mongoose.model('Movie', MovieSchema); + +const enumCharacterType = { + PERSON: 'Person', + DROID: 'Droid', +}; + +export const CharacterObject = { + _id: { + type: String, + default: (): any => new Types.ObjectId(), + }, + name: String, + + type: { + type: String, + require: true, + enum: Object.keys(enumCharacterType), + }, + kind: { + type: String, + require: true, + enum: Object.keys(enumCharacterType), + }, + + friends: [String], // another Character + appearsIn: [String], // movie +}; + +export function getCharacterModels( + DKey?: string, + name: string = 'Character' +): { CharacterModel: Model; PersonModel: Model; DroidModel: Model } { + const CharacterSchema = DKey + ? new Schema(CharacterObject, { discriminatorKey: DKey }) + : new Schema(CharacterObject); + + const CharacterModel: Model = mongoose.model(name, CharacterSchema); + + const PersonModel: Model = CharacterModel.discriminator( + name + enumCharacterType.PERSON, + PersonSchema + ); + + const DroidModel: Model = CharacterModel.discriminator( + name + enumCharacterType.DROID, + DroidSchema + ); + + return { CharacterModel, PersonModel, DroidModel }; +} + +export function getCharacterModelClone(): { NoDKeyCharacterModel: Model } { + const ACharacterSchema = new Schema({ ...CharacterObject }); + const NoDKeyCharacterModel = mongoose.model('NoDKeyCharacter', ACharacterSchema); + + /* + const APersonModel = ACharacterModel.discriminator('A' + enumCharacterType.PERSON, PersonSchema.clone()); + + const ADroidModel = ACharacterModel.discriminator('A' + enumCharacterType.DROID, DroidSchema.clone()); + */ + + return { NoDKeyCharacterModel }; // APersonModel, ADroidModel }; +} diff --git a/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts b/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts new file mode 100644 index 00000000..26a421e1 --- /dev/null +++ b/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts @@ -0,0 +1,220 @@ +import { InterfaceTypeComposer, ObjectTypeComposer, schemaComposer } from 'graphql-compose'; +import { getCharacterModels } from '../__mocks__/characterDiscrimModels'; +import { + composeMongoose as composeMongooseRaw, + ComposeMongooseOpts, + GenerateResolverType, +} from '../../composeMongoose'; +import { EDiscriminatorTypeComposer } from '../eDiscriminatorTypeComposer'; +import { Document, Model, Schema } from 'mongoose'; + +const defaultDKey = 'type'; +const { CharacterModel } = getCharacterModels(defaultDKey); + +const dComposeOpts: ComposeMongooseOpts = { + includeBaseDiscriminators: true, + schemaComposer: schemaComposer, +}; + +const composeMongoose = ( + model: Model +): EDiscriminatorTypeComposer & { + mongooseResolvers: GenerateResolverType; +} => { + const generatedTC = composeMongooseRaw(model, dComposeOpts); + + if (!(generatedTC instanceof EDiscriminatorTypeComposer)) { + throw new Error('Model should include discriminated schema for testing here'); + } + + return generatedTC; +}; + +let baseDTC: EDiscriminatorTypeComposer; +let DInterface: InterfaceTypeComposer; +let DInputObject: ObjectTypeComposer; + +beforeEach(() => { + baseDTC = composeMongoose(CharacterModel); + DInterface = schemaComposer.getIFTC(baseDTC.getTypeName()); + DInputObject = baseDTC.getDInputObject(); +}); + +afterEach(() => { + baseDTC.schemaComposer.clear(); +}); + +describe('EDiscriminatorTypeComposer', () => { + it('has an interface object DInterface as the type name', () => { + expect(baseDTC.getDInterface()).toEqual(schemaComposer.getAnyTC(baseDTC.getTypeName())); + }); + + it('throws an error when default (__t) discriminator key is set', () => { + baseDTC.schemaComposer.clear(); + const { CharacterModel: noDKeyModel } = getCharacterModels(undefined, 'noDKeyCharacter'); + const errorCall = () => composeMongoose(noDKeyModel); + expect(errorCall).toThrowError( + 'A custom discriminator key must be set on the model options in mongoose for discriminator behaviour to function correctly' + ); + }); + + it('should not be used on models without discriminators', () => { + const { DroidModel } = getCharacterModels(undefined, 'noDiscrim'); + const noDiscrim = composeMongooseRaw(DroidModel, dComposeOpts); + expect(noDiscrim instanceof EDiscriminatorTypeComposer).toBeFalsy(); + }); + + describe('Custom Getters', () => { + test('getting discriminator key', () => { + expect(baseDTC.getDKey()).toEqual(defaultDKey); + }); + + test('getting discriminator interface', () => { + expect(baseDTC.getDInterface()).toBe(DInterface); + }); + + test('getting discriminator input object TC', () => { + expect(baseDTC.getDInputObject()).toBe(DInputObject); + }); + }); + + describe('createFromModel errors', () => { + it('should throw an error if it is called with no discriminators present', () => { + const errorCall = () => + EDiscriminatorTypeComposer.createFromModel( + { + schema: new Schema({ test: String, thingy: Boolean }), + }, + 'noDiscrimType', + schemaComposer, + {} + ); + + expect(errorCall).toThrowError('Discriminators should be present to use this function'); + }); + + it('should throw an error if the schema composer is not valid', () => { + const { CharacterModel: errorTestModel } = getCharacterModels('type', 'scErrorModel'); + + const errorCall = () => + EDiscriminatorTypeComposer.createFromModel(errorTestModel, 'noSCType', {} as any, {}); + expect(errorCall).toThrowError( + 'DiscriminatorTC.createFromModel() should receive SchemaComposer in second argument' + ); + }); + }); + + describe('DInterface', () => { + it('shares field names with base model used to compose it', () => { + expect(DInterface.getFieldNames()).toEqual( + expect.arrayContaining( + Object.keys(CharacterModel.schema.paths).filter((x) => !x.startsWith('__')) + ) + ); + }); + + it('should be an instance of InterfaceTypeComposer', () => { + expect(DInterface instanceof InterfaceTypeComposer).toBeTruthy(); + }); + + it('should have the input TC from DInputObject as input TC', () => { + expect(DInterface.getInputTypeComposer()).toEqual( + baseDTC.getDInputObject().getInputTypeComposer() + ); + }); + }); + + describe('Overridden eDTC Class Methods', () => { + describe('Set Field', () => { + it('updates field on all child TCs', () => { + baseDTC.setField('newField', 'String'); + expect(baseDTC.hasField('newField')).toBeTruthy(); + expect(DInterface.hasField('newField')).toBeTruthy(); + expect(DInputObject.hasField('newField')).toBeTruthy(); + Object.values(baseDTC.discrimTCs).forEach((dTC) => { + expect(dTC.hasField('newField')).toBeTruthy(); + }); + }); + + it('updates input TC when field is added', () => { + baseDTC.setField('inputField', 'String'); + expect(schemaComposer.getIFTC(baseDTC.getTypeName()).getInputTypeComposer()).toEqual( + DInputObject.getInputTypeComposer() + ); + }); + }); + + describe('Set Extensions', () => { + it('updates field on all child TCs', () => { + baseDTC.setExtensions({ newField: 'testExtension' }); + expect(baseDTC.hasExtension('newField')).toBeTruthy(); + expect(DInterface.hasExtension('newField')).toBeTruthy(); + expect(DInputObject.hasExtension('newField')).toBeTruthy(); + Object.values(baseDTC.discrimTCs).forEach((dTC) => { + expect(dTC.hasExtension('newField')).toBeTruthy(); + }); + }); + }); + + describe('Remove Field', () => { + it('deletes field on all child TCs', () => { + baseDTC.addFields({ toDelete: 'String' }); + // check field added + expect(baseDTC.hasField('toDelete')).toBeTruthy(); + expect(DInterface.hasField('toDelete')).toBeTruthy(); + expect(DInputObject.hasField('toDelete')).toBeTruthy(); + Object.values(baseDTC.discrimTCs).forEach((dTC) => { + expect(dTC.hasField('toDelete')).toBeTruthy(); + }); + + baseDTC.removeField('toDelete'); + expect(baseDTC.hasField('toDelete')).toBeFalsy(); + expect(DInterface.hasField('toDelete')).toBeFalsy(); + expect(DInputObject.hasField('toDelete')).toBeFalsy(); + Object.values(baseDTC.discrimTCs).forEach((dTC) => { + expect(dTC.hasField('toDelete')).toBeFalsy(); + }); + }); + }); + + describe('Remove Other Fields', () => { + it('removes all other fields from base TC from all child TCs', () => { + baseDTC.addFields({ toKeep: 'String' }); + + const otherFields = baseDTC + .getDInterface() + .getFieldNames() + .filter((field) => field !== 'toKeep'); + baseDTC.removeOtherFields('toKeep'); + + console.log(otherFields); + + otherFields.forEach((removedField) => { + expect(baseDTC.hasField(removedField)).toBeFalsy(); + expect(DInterface.hasField(removedField)).toBeFalsy(); + expect(DInputObject.hasField(removedField)).toBeFalsy(); + Object.values(baseDTC.discrimTCs).forEach((dTC) => { + expect(dTC.hasField(removedField)).toBeFalsy(); + }); + }); + }); + }); + + describe('Reorder Fields', () => { + it('reorders fields on all child TCs', () => { + const fieldOrder = ['_id', 'appearsIn', 'friends', 'kind', 'type']; + const fieldOrderString = fieldOrder.join(''); + console.log(fieldOrderString); + + baseDTC.reorderFields(fieldOrder); + + expect(baseDTC.getFieldNames().join('').startsWith(fieldOrderString)).toBeTruthy(); + expect(DInterface.getFieldNames().join('').startsWith(fieldOrderString)).toBeTruthy(); + expect(DInputObject.getFieldNames().join('').startsWith(fieldOrderString)).toBeTruthy(); + Object.values(baseDTC.discrimTCs).forEach((dTC) => { + expect(dTC.getFieldNames().join('').startsWith(fieldOrderString)).toBeTruthy(); + }); + }); + }); + }); +}); From fc20db77a4503b2ddcd592f3dc3ecca4729a5eb3 Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 25 May 2021 12:14:13 +0100 Subject: [PATCH 05/14] Refactor input TC field adding into class method --- .../eDiscriminatorTypeComposer.ts | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts index 7d458e54..d5961223 100644 --- a/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts +++ b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts @@ -4,6 +4,7 @@ import { Extensions, InterfaceTypeComposer, ObjectTypeComposer, + ObjectTypeComposerFieldConfig, ObjectTypeComposerFieldConfigDefinition, SchemaComposer, } from 'graphql-compose'; @@ -40,6 +41,7 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom > { discriminatorKey: string = ''; discrimTCs: { [key: string]: ObjectTypeComposer } = {}; + BaseTC: ObjectTypeComposer; DInputObject: ObjectTypeComposer; DInterface: InterfaceTypeComposer; opts: ComposeMongooseDiscriminatorsOpts = {}; @@ -49,6 +51,7 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom super(gqType, schemaComposer); this.DInterface = schemaComposer.getOrCreateIFTC(`${gqType.name}Interface`); this.DInputObject = schemaComposer.getOrCreateOTC(`${gqType.name}Input`); + this.BaseTC = schemaComposer.getOrCreateOTC(`${gqType.name}BaseTC`); return this; } @@ -87,6 +90,7 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom 'A custom discriminator key must be set on the model options in mongoose for discriminator behaviour to function correctly' ); } + baseDTC.BaseTC = baseTC; baseDTC.DInterface = baseDTC._buildDiscriminatedInterface(model, baseTC); baseDTC.DInterface.setInputTypeComposer(baseDTC.DInputObject.getInputTypeComposer()); baseDTC.setInputTypeComposer(baseDTC.DInputObject.getInputTypeComposer()); @@ -133,21 +137,16 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom // TODO - Review when Input Unions/similar are accepted into the graphQL spec and supported by graphql-js // SEE - https://github.com/graphql/graphql-spec/blob/main/rfcs/InputUnion.md const discrimFields = discrimTC.getFields(); + + // add each field to Input Object TC Object.keys(discrimFields).forEach((fieldName) => { const field = discrimFields[fieldName]; - - // if another discrimTC has already defined the field (not from original TC) - if (this.DInputObject.hasField(fieldName) && !baseTC.hasField(fieldName)) { - this.DInputObject.setField(fieldName, `JSON`); - } else { - (this.DInputObject as ObjectTypeComposer).setField(fieldName, field); - } + this._addFieldToInputOTC(fieldName, field); }); + this.DInputObject.makeFieldNullable( discrimTC.getFieldNames().filter((field) => !baseTC.hasField(field)) ); - // there may be issues for discriminated types with overlapping fields, the last validation req will overwrite prior one - // mitigation attempted by setting type to any on filed which appears in multiple implementations discrimTC.makeFieldNonNull('_id'); // also set fields on master TC so it will have all possibilities for input workaround @@ -165,6 +164,20 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom return interfaceTC; } + _addFieldToInputOTC( + fieldName: string, + field: ObjectTypeComposerFieldConfig + ): void { + // if another discrimTC has already defined the field (not from original TC) + if (this.DInputObject.hasField(fieldName) && !this.BaseTC.hasField(fieldName)) { + this.DInputObject.setField(fieldName, `JSON`); + } else { + (this.DInputObject as ObjectTypeComposer).setField(fieldName, field); + } + // there may be issues for discriminated types with overlapping fields, the last validation req will overwrite prior one + // mitigation attempted by setting type to any on filed which appears in multiple implementations + } + getDKey(): string { return this.discriminatorKey; } From e602693c3114df00442feb6f02af96d6076f6310 Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 25 May 2021 20:34:01 +0100 Subject: [PATCH 06/14] Export EDiscriminatorTypeComposer index exports and fix type error with discrimTCs --- src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts | 9 +++++++-- src/index.ts | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts index d5961223..e68b3a1f 100644 --- a/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts +++ b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts @@ -10,7 +10,7 @@ import { } from 'graphql-compose'; import mongoose, { Document, Model } from 'mongoose'; import { convertModelToGraphQL, MongoosePseudoModelT } from '../fieldsConverter'; -import { composeMongoose, ComposeMongooseOpts } from '../composeMongoose'; +import { composeMongoose, ComposeMongooseOpts, GenerateResolverType } from '../composeMongoose'; export function convertModelToGraphQLWithDiscriminators( model: Model | MongoosePseudoModelT, @@ -40,7 +40,12 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom TContext > { discriminatorKey: string = ''; - discrimTCs: { [key: string]: ObjectTypeComposer } = {}; + discrimTCs: { + [key: string]: ObjectTypeComposer & { + mongooseResolvers: GenerateResolverType; + }; + } = {}; + BaseTC: ObjectTypeComposer; DInputObject: ObjectTypeComposer; DInterface: InterfaceTypeComposer; diff --git a/src/index.ts b/src/index.ts index a2546de5..74b8bd01 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,4 +7,5 @@ export * from './composeWithMongooseDiscriminators'; export * from './fieldsConverter'; export * from './resolvers'; export * from './errors'; +export * from './enhancedDiscriminators'; export { GraphQLMongoID, GraphQLBSONDecimal }; From d586798dc80ed0bffc308ababedfe62716d6fcbb Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 25 May 2021 23:37:38 +0100 Subject: [PATCH 07/14] Ensure correct types for DTCs are referenced in resolvers using tc.getTypeName() --- src/resolvers/createOne.ts | 2 +- src/resolvers/dataLoader.ts | 2 +- src/resolvers/dataLoaderMany.ts | 2 +- src/resolvers/findByIds.ts | 2 +- src/resolvers/findOne.ts | 2 +- src/resolvers/removeById.ts | 2 +- src/resolvers/removeOne.ts | 2 +- src/resolvers/updateById.ts | 2 +- src/resolvers/updateOne.ts | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/resolvers/createOne.ts b/src/resolvers/createOne.ts index 5ea7a0e9..fdce4e43 100644 --- a/src/resolvers/createOne.ts +++ b/src/resolvers/createOne.ts @@ -52,7 +52,7 @@ export function createOne({ - type: tc, + type: tc.getTypeName(), name: 'dataLoader', kind: 'query', args: { diff --git a/src/resolvers/dataLoaderMany.ts b/src/resolvers/dataLoaderMany.ts index c50c99e5..008e01ba 100644 --- a/src/resolvers/dataLoaderMany.ts +++ b/src/resolvers/dataLoaderMany.ts @@ -51,7 +51,7 @@ export function dataLoaderMany({ - type: tc.List.NonNull, + type: tc.schemaComposer.getAnyTC(tc.getTypeName()).List.NonNull, name: 'dataLoaderMany', kind: 'query', args: { diff --git a/src/resolvers/findByIds.ts b/src/resolvers/findByIds.ts index df4b0334..d911cf26 100644 --- a/src/resolvers/findByIds.ts +++ b/src/resolvers/findByIds.ts @@ -57,7 +57,7 @@ export function findByIds({ - type: tc.NonNull.List.NonNull, + type: tc.schemaComposer.getAnyTC(tc.getTypeName()).NonNull.List.NonNull, name: 'findByIds', kind: 'query', args: { diff --git a/src/resolvers/findOne.ts b/src/resolvers/findOne.ts index 79a7fd8b..60122ce3 100644 --- a/src/resolvers/findOne.ts +++ b/src/resolvers/findOne.ts @@ -61,7 +61,7 @@ export function findOne({ - type: tc, + type: tc.getTypeName(), name: 'findOne', kind: 'query', args: { diff --git a/src/resolvers/removeById.ts b/src/resolvers/removeById.ts index 0c7851be..c2223932 100644 --- a/src/resolvers/removeById.ts +++ b/src/resolvers/removeById.ts @@ -40,7 +40,7 @@ export function removeById Date: Wed, 26 May 2021 11:51:36 +0100 Subject: [PATCH 08/14] Fix bugs with discrimTC resolvers, such as preferring model over scham dscriminators where present --- .../eDiscriminatorTypeComposer-test.ts | 16 ++-- .../eDiscriminatorTypeComposer.ts | 87 ++++++++++++------- 2 files changed, 64 insertions(+), 39 deletions(-) diff --git a/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts b/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts index 26a421e1..bdcff830 100644 --- a/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts +++ b/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts @@ -6,7 +6,7 @@ import { GenerateResolverType, } from '../../composeMongoose'; import { EDiscriminatorTypeComposer } from '../eDiscriminatorTypeComposer'; -import { Document, Model, Schema } from 'mongoose'; +import mongoose, { Document, Model, Schema } from 'mongoose'; const defaultDKey = 'type'; const { CharacterModel } = getCharacterModels(defaultDKey); @@ -80,15 +80,13 @@ describe('EDiscriminatorTypeComposer', () => { describe('createFromModel errors', () => { it('should throw an error if it is called with no discriminators present', () => { + const fakeModel = mongoose.model( + 'fakeModelEDiscrim', + new Schema({ test: String, thingy: Boolean }) + ); + const errorCall = () => - EDiscriminatorTypeComposer.createFromModel( - { - schema: new Schema({ test: String, thingy: Boolean }), - }, - 'noDiscrimType', - schemaComposer, - {} - ); + EDiscriminatorTypeComposer.createFromModel(fakeModel, 'noDiscrimType', schemaComposer, {}); expect(errorCall).toThrowError('Discriminators should be present to use this function'); }); diff --git a/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts index e68b3a1f..de743090 100644 --- a/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts +++ b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts @@ -6,9 +6,10 @@ import { ObjectTypeComposer, ObjectTypeComposerFieldConfig, ObjectTypeComposerFieldConfigDefinition, + schemaComposer, SchemaComposer, } from 'graphql-compose'; -import mongoose, { Document, Model } from 'mongoose'; +import { Document, Model, Schema } from 'mongoose'; import { convertModelToGraphQL, MongoosePseudoModelT } from '../fieldsConverter'; import { composeMongoose, ComposeMongooseOpts, GenerateResolverType } from '../composeMongoose'; @@ -24,10 +25,10 @@ export function convertModelToGraphQLWithDiscriminators).discriminators || model.schema.discriminators)) { return convertModelToGraphQL(model, typeName, sc, opts); } else { - return EDiscriminatorTypeComposer.createFromModel(model, typeName, sc, opts); + return EDiscriminatorTypeComposer.createFromModel(model as Model, typeName, sc, opts); } } @@ -41,9 +42,11 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom > { discriminatorKey: string = ''; discrimTCs: { - [key: string]: ObjectTypeComposer & { - mongooseResolvers: GenerateResolverType; - }; + [key: string]: + | (ObjectTypeComposer & { + mongooseResolvers: GenerateResolverType; + }) + | ObjectTypeComposer; } = {}; BaseTC: ObjectTypeComposer; @@ -61,12 +64,12 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom } static createFromModel( - model: Model | MongoosePseudoModelT, + model: Model, typeName: string, schemaComposer: SchemaComposer, opts: ComposeMongooseDiscriminatorsOpts ): EDiscriminatorTypeComposer { - if (!model.schema.discriminators) { + if (!((model as Model).discriminators || model.schema.discriminators)) { throw Error('Discriminators should be present to use this function'); } @@ -89,7 +92,7 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom baseTC.clone(baseDTC as ObjectTypeComposer); baseDTC.opts = { ...opts }; - baseDTC.discriminatorKey = (model as any).schema.options.discriminatorKey; + baseDTC.discriminatorKey = (model as any).schema.get('discriminatorKey') || '__t'; if (baseDTC.discriminatorKey === '__t') { throw Error( 'A custom discriminator key must be set on the model options in mongoose for discriminator behaviour to function correctly' @@ -100,13 +103,10 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom baseDTC.DInterface.setInputTypeComposer(baseDTC.DInputObject.getInputTypeComposer()); baseDTC.setInputTypeComposer(baseDTC.DInputObject.getInputTypeComposer()); - baseDTC.setInterfaces([baseDTC.DInterface]); + // baseDTC.setInterfaces([baseDTC.DInterface]); baseDTC._gqcInputTypeComposer = baseDTC.DInputObject._gqcInputTypeComposer; baseDTC.DInterface._gqcInputTypeComposer = baseDTC.DInputObject._gqcInputTypeComposer; - // discriminators an object containing all discriminators with key being DNames - // import { EDiscriminatorTypeComposer } from './enhancedDiscriminators/index';baseDTC.DKeyETC = createAndSetDKeyETC(baseDTC, discriminators); - // reorderFields(baseDTC, baseDTC.opts.reorderFields, baseDTC.discriminatorKey); baseDTC.schemaComposer.addSchemaMustHaveType(baseDTC as any); @@ -117,7 +117,7 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom } _buildDiscriminatedInterface( - model: Model | MongoosePseudoModelT, + model: Model, baseTC: ObjectTypeComposer ): InterfaceTypeComposer { const interfaceTC = this.DInterface; @@ -125,17 +125,36 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom interfaceTC.setFields(baseTC.getFields()); this.DInputObject.setFields(baseTC.getFields()); - Object.keys((model as any).schema.discriminators).forEach((key) => { - const discrimType = (model as any).schema.discriminators[key]; + const discriminators = model.discriminators || model.schema.discriminators; - // ensure valid type for calling convert - const discrimModel = - discrimType instanceof mongoose.Schema ? { schema: discrimType } : discrimType; + if (!discriminators) { + throw Error('Discriminators should be present to use this function'); + } - const discrimTC = composeMongoose(discrimModel, { - ...this.opts, - name: this.getTypeName() + key, - }); + Object.keys(discriminators).forEach((key) => { + if (!discriminators[key]) { + throw Error( + `Discriminator Model of ${model.name} missing for discriminator with key of ${key}` + ); + } + + const discrimType = discriminators[key]; + + const discrimTC = + discrimType instanceof Schema + ? convertModelToGraphQL( + { schema: discrimType }, + this.getTypeName() + key, + schemaComposer, + { + ...this.opts, + name: this.getTypeName() + key, + } + ) + : composeMongoose(discrimType as Model, { + ...this.opts, + name: this.getTypeName() + key, + }); // base OTC used for input schema must hold all child TC fields in the most loose way (so all types are accepted) // more detailed type checks are done on input object by mongoose itself @@ -152,18 +171,13 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom this.DInputObject.makeFieldNullable( discrimTC.getFieldNames().filter((field) => !baseTC.hasField(field)) ); - discrimTC.makeFieldNonNull('_id'); // also set fields on master TC so it will have all possibilities for input workaround - const discrimVal = discrimType.discriminatorMapping.value; - interfaceTC.addTypeResolver( - discrimTC, - (obj: any) => obj[this.discriminatorKey] === discrimVal - ); + interfaceTC.addTypeResolver(discrimTC, (obj: any) => obj[this.discriminatorKey] === key); // add TC to discrimTCs - this.discrimTCs[discrimTC.getTypeName()] = discrimTC; + this.discrimTCs[key] = discrimTC; }); return interfaceTC; @@ -199,6 +213,19 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom return this.DInputObject as any; } + getDiscriminatorTCs(): { + [key: string]: ObjectTypeComposer & { + mongooseResolvers: GenerateResolverType; + }; + } { + // check if mongooseResolvers are present + if ((this.discrimTCs[Object.keys(this.discrimTCs)[0]] as any).mongooseResolvers) { + return this.discrimTCs as any; + } else { + return {}; + } + } + // OVERRIDES getTypeName(): string { return this.DInterface.getTypeName(); From 425fbb48b18e320e594976047c98e23303afe286 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 26 May 2021 18:36:46 +0100 Subject: [PATCH 09/14] Add test case for getDiscriminatorTCs() --- .../__tests__/eDiscriminatorTypeComposer-test.ts | 14 ++++++++++++++ .../eDiscriminatorTypeComposer.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts b/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts index bdcff830..82917b37 100644 --- a/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts +++ b/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts @@ -122,6 +122,20 @@ describe('EDiscriminatorTypeComposer', () => { }); }); + describe('Get Discriminator TCs', () => { + it('returns discrimTCs with mongooseResolvers present', () => { + Object.values(baseDTC.getDiscriminatorTCs()).forEach((discimTC) => { + expect(discimTC).toHaveProperty('mongooseResolvers'); + }); + }); + it('returns empty object with mongooseResolvers missing', () => { + (baseDTC.discrimTCs[Object.keys(baseDTC.discrimTCs)[0]] as any).mongooseResolvers = undefined; + Object.values(baseDTC.getDiscriminatorTCs()).forEach((discimTC) => { + expect(discimTC).toHaveProperty('mongooseResolvers'); + }); + }); + }); + describe('Overridden eDTC Class Methods', () => { describe('Set Field', () => { it('updates field on all child TCs', () => { diff --git a/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts index de743090..758abab5 100644 --- a/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts +++ b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts @@ -218,7 +218,7 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom mongooseResolvers: GenerateResolverType; }; } { - // check if mongooseResolvers are present + // check if mongooseResolvers are present (assume on one = on all) if ((this.discrimTCs[Object.keys(this.discrimTCs)[0]] as any).mongooseResolvers) { return this.discrimTCs as any; } else { From 3cd5c339def87413a638c3265ea13d11d4728527 Mon Sep 17 00:00:00 2001 From: Jacob Date: Thu, 27 May 2021 18:16:35 +0100 Subject: [PATCH 10/14] Override makeFieldNullable/NonNull() for eDTC with tests --- .../eDiscriminatorTypeComposer-test.ts | 33 +++++++++++++++++-- .../eDiscriminatorTypeComposer.ts | 24 ++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts b/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts index 82917b37..e06e4f37 100644 --- a/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts +++ b/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts @@ -199,8 +199,6 @@ describe('EDiscriminatorTypeComposer', () => { .filter((field) => field !== 'toKeep'); baseDTC.removeOtherFields('toKeep'); - console.log(otherFields); - otherFields.forEach((removedField) => { expect(baseDTC.hasField(removedField)).toBeFalsy(); expect(DInterface.hasField(removedField)).toBeFalsy(); @@ -216,7 +214,6 @@ describe('EDiscriminatorTypeComposer', () => { it('reorders fields on all child TCs', () => { const fieldOrder = ['_id', 'appearsIn', 'friends', 'kind', 'type']; const fieldOrderString = fieldOrder.join(''); - console.log(fieldOrderString); baseDTC.reorderFields(fieldOrder); @@ -228,5 +225,35 @@ describe('EDiscriminatorTypeComposer', () => { }); }); }); + + describe('Make Fields Nullable/Non-null', () => { + it('makes a nullable field non-null', () => { + baseDTC.addFields({ initialNullable: 'String' }); + expect(baseDTC.isFieldNonNull('initialNullable')).toBeFalsy(); + + baseDTC.makeFieldNonNull('initialNullable'); + + expect(baseDTC.isFieldNonNull('initialNullable')).toBeTruthy(); + expect(DInterface.isFieldNonNull('initialNullable')).toBeTruthy(); + expect(DInputObject.isFieldNonNull('initialNullable')).toBeTruthy(); + Object.values(baseDTC.discrimTCs).forEach((dTC) => { + expect(dTC.isFieldNonNull('initialNullable')).toBeTruthy(); + }); + }); + + it('makes a non-null field nullable', () => { + baseDTC.addFields({ initialNonNull: 'String!' }); + expect(baseDTC.isFieldNonNull('initialNonNull')).toBeTruthy(); + + baseDTC.makeFieldNullable('initialNonNull'); + + expect(baseDTC.isFieldNonNull('initialNonNull')).toBeFalsy(); + expect(DInterface.isFieldNonNull('initialNonNull')).toBeFalsy(); + expect(DInputObject.isFieldNonNull('initialNonNull')).toBeFalsy(); + Object.values(baseDTC.discrimTCs).forEach((dTC) => { + expect(dTC.isFieldNonNull('initialNonNull')).toBeFalsy(); + }); + }); + }); }); }); diff --git a/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts index 758abab5..b690e268 100644 --- a/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts +++ b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts @@ -308,4 +308,28 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom return this; } + + makeFieldNonNull(fieldNameOrArray: string | Array): this { + super.makeFieldNonNull(fieldNameOrArray); + this.getDInterface().makeFieldNonNull(fieldNameOrArray); + this.getDInputObject().makeFieldNonNull(fieldNameOrArray); + + for (const discrimTC in this.discrimTCs) { + this.discrimTCs[discrimTC].makeFieldNonNull(fieldNameOrArray); + } + + return this; + } + + makeFieldNullable(fieldNameOrArray: string | Array): this { + super.makeFieldNullable(fieldNameOrArray); + this.getDInterface().makeFieldNullable(fieldNameOrArray); + this.getDInputObject().makeFieldNullable(fieldNameOrArray); + + for (const discrimTC in this.discrimTCs) { + this.discrimTCs[discrimTC].makeFieldNullable(fieldNameOrArray); + } + + return this; + } } From 152352433111ed5a6e1429f605bec4aecb8016ea Mon Sep 17 00:00:00 2001 From: Jacob Date: Thu, 10 Jun 2021 18:39:00 +0100 Subject: [PATCH 11/14] Refactor calls to schemaComposer.getAnyTC(...) to use only tc.getTypeName() in resolvers --- src/resolvers/createMany.ts | 18 ++++++++---------- src/resolvers/dataLoaderMany.ts | 2 +- src/resolvers/findByIds.ts | 2 +- src/resolvers/findMany.ts | 2 +- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/resolvers/createMany.ts b/src/resolvers/createMany.ts index b4584c6b..51d27261 100644 --- a/src/resolvers/createMany.ts +++ b/src/resolvers/createMany.ts @@ -69,16 +69,14 @@ export function createMany { diff --git a/src/resolvers/dataLoaderMany.ts b/src/resolvers/dataLoaderMany.ts index 008e01ba..b6df4b5b 100644 --- a/src/resolvers/dataLoaderMany.ts +++ b/src/resolvers/dataLoaderMany.ts @@ -51,7 +51,7 @@ export function dataLoaderMany({ - type: tc.schemaComposer.getAnyTC(tc.getTypeName()).List.NonNull, + type: '[' + tc.getTypeName() + ']!', name: 'dataLoaderMany', kind: 'query', args: { diff --git a/src/resolvers/findByIds.ts b/src/resolvers/findByIds.ts index d911cf26..ce5b1f8c 100644 --- a/src/resolvers/findByIds.ts +++ b/src/resolvers/findByIds.ts @@ -57,7 +57,7 @@ export function findByIds({ - type: tc.schemaComposer.getAnyTC(tc.getTypeName()).NonNull.List.NonNull, + type: '[' + tc.getTypeName() + '!]!', name: 'findByIds', kind: 'query', args: { diff --git a/src/resolvers/findMany.ts b/src/resolvers/findMany.ts index 9ce17f73..205bcade 100644 --- a/src/resolvers/findMany.ts +++ b/src/resolvers/findMany.ts @@ -66,7 +66,7 @@ export function findMany({ - type: tc.schemaComposer.getAnyTC(tc.getTypeName()).NonNull.List.NonNull, + type: '[' + tc.getTypeName() + '!]!', name: 'findMany', kind: 'query', args: { From 14d82de9593651ef5a3cbfcc269b18f47c525d88 Mon Sep 17 00:00:00 2001 From: Jacob Date: Thu, 29 Jul 2021 12:44:41 +0100 Subject: [PATCH 12/14] Change eDTC to extend InterfaceTypeComposer and fix various resulting type errors --- .vscode/settings.json | 2 +- package.json | 2 +- src/__tests__/github_issues/260-test.ts | 6 +- src/__tests__/github_issues/263-test.ts | 6 +- src/composeMongoose.ts | 13 +- src/composeWithMongoose.ts | 11 +- .../eDiscriminatorTypeComposer-test.ts | 119 ++++++++---------- .../eDiscriminatorTypeComposer.ts | 117 ++++++++--------- src/fieldsConverter.ts | 22 +--- src/resolvers/__tests__/connection-test.ts | 4 +- src/resolvers/__tests__/count-test.ts | 2 +- src/resolvers/__tests__/createMany-test.ts | 8 +- src/resolvers/__tests__/createOne-test.ts | 2 +- src/resolvers/__tests__/dataLoader-test.ts | 8 +- .../__tests__/dataLoaderMany-test.ts | 8 +- src/resolvers/__tests__/findById-test.ts | 8 +- src/resolvers/__tests__/findByIds-test.ts | 8 +- src/resolvers/__tests__/findMany-test.ts | 2 +- src/resolvers/__tests__/findOne-test.ts | 2 +- src/resolvers/__tests__/pagination-test.ts | 2 +- src/resolvers/__tests__/removeById-test.ts | 2 +- src/resolvers/__tests__/removeMany-test.ts | 2 +- src/resolvers/__tests__/removeOne-test.ts | 2 +- src/resolvers/__tests__/updateById-test.ts | 2 +- src/resolvers/__tests__/updateMany-test.ts | 2 +- src/resolvers/__tests__/updateOne-test.ts | 2 +- .../helpers/__tests__/filter-test.ts | 2 +- .../helpers/__tests__/record-test.ts | 2 +- src/resolvers/helpers/__tests__/sort-test.ts | 2 +- src/resolvers/helpers/filter.ts | 8 +- 30 files changed, 191 insertions(+), 187 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 149d5f93..ec28d146 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,5 @@ "typescript.format.enable": false, "editor.codeActionsOnSave": { "source.fixAll.eslint": true - }, + } } diff --git a/package.json b/package.json index bd08fd08..08898d3d 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "eslint-plugin-import": "2.23.4", "eslint-plugin-prettier": "3.4.0", "graphql": "15.5.1", - "graphql-compose": "9.0.1", + "graphql-compose": "9.0.2", "jest": "27.0.6", "mongodb-memory-server": "7.3.2", "mongoose": "5.13.3", diff --git a/src/__tests__/github_issues/260-test.ts b/src/__tests__/github_issues/260-test.ts index bedbf778..dde3abd6 100644 --- a/src/__tests__/github_issues/260-test.ts +++ b/src/__tests__/github_issues/260-test.ts @@ -1,4 +1,4 @@ -import { schemaComposer, graphql } from 'graphql-compose'; +import { schemaComposer, graphql, ObjectTypeComposer } from 'graphql-compose'; import { composeMongoose } from '../../index'; import { mongoose } from '../../__mocks__/mongooseCommon'; import { Document } from 'mongoose'; @@ -28,6 +28,10 @@ const PostModel = mongoose.model('Post', PostSchema); const UserTC = composeMongoose(UserModel); const PostTC = composeMongoose(PostModel); +if (!(UserTC instanceof ObjectTypeComposer) || !(PostTC instanceof ObjectTypeComposer)) { + throw new Error('TCs should return ObjectTypeComposers'); +} + PostTC.addRelation('author', { resolver: UserTC.mongooseResolvers.dataLoader({ lean: true, // <---- `Lean` loads record from DB without support of mongoose getters & virtuals diff --git a/src/__tests__/github_issues/263-test.ts b/src/__tests__/github_issues/263-test.ts index b43c0966..3967b224 100644 --- a/src/__tests__/github_issues/263-test.ts +++ b/src/__tests__/github_issues/263-test.ts @@ -1,4 +1,4 @@ -import { schemaComposer, graphql } from 'graphql-compose'; +import { schemaComposer, graphql, ObjectTypeComposer } from 'graphql-compose'; import { composeMongoose } from '../../index'; import { mongoose } from '../../__mocks__/mongooseCommon'; @@ -28,6 +28,10 @@ const PostModel = mongoose.model('Post', PostSchema); const UserTC = composeMongoose(UserModel); const PostTC = composeMongoose(PostModel); +if (!(UserTC instanceof ObjectTypeComposer) || !(PostTC instanceof ObjectTypeComposer)) { + throw new Error('TCs should return ObjectTypeComposers'); +} + PostTC.addRelation('author', { resolver: UserTC.mongooseResolvers.dataLoader({ lean: true }), prepareArgs: { diff --git a/src/composeMongoose.ts b/src/composeMongoose.ts index e838519a..7aef6c0b 100644 --- a/src/composeMongoose.ts +++ b/src/composeMongoose.ts @@ -1,4 +1,9 @@ -import type { SchemaComposer, Resolver, InputTypeComposer } from 'graphql-compose'; +import type { + SchemaComposer, + Resolver, + InputTypeComposer, + InterfaceTypeComposer, +} from 'graphql-compose'; import { schemaComposer as globalSchemaComposer, ObjectTypeComposer } from 'graphql-compose'; import type { Model, Document } from 'mongoose'; import { EDiscriminatorTypeComposer } from './enhancedDiscriminators'; @@ -162,7 +167,7 @@ export function composeMongoose( } function makeFieldsNonNullWithDefaultValues( - tc: ObjectTypeComposer, + tc: ObjectTypeComposer | InterfaceTypeComposer, alreadyWorked = new Set() ): void { if (alreadyWorked.has(tc)) return; @@ -198,7 +203,7 @@ function makeFieldsNonNullWithDefaultValues( } export function prepareFields( - tc: ObjectTypeComposer, + tc: ObjectTypeComposer | InterfaceTypeComposer, opts: ComposeMongooseOpts = {} ): void { const onlyFields = opts?.onlyFields || opts?.fields?.only; @@ -212,7 +217,7 @@ export function prepareFields( } export function createInputType( - tc: ObjectTypeComposer, + tc: ObjectTypeComposer | InterfaceTypeComposer, inputTypeOpts: TypeConverterInputTypeOpts = {} ): void { const inputTypeComposer = tc.getInputTypeComposer(); diff --git a/src/composeWithMongoose.ts b/src/composeWithMongoose.ts index 51579a0f..224c04f7 100644 --- a/src/composeWithMongoose.ts +++ b/src/composeWithMongoose.ts @@ -1,7 +1,7 @@ /* eslint-disable no-use-before-define, no-param-reassign, global-require */ -import type { ObjectTypeComposer, SchemaComposer } from 'graphql-compose'; -import { schemaComposer as globalSchemaComposer } from 'graphql-compose'; +import type { SchemaComposer } from 'graphql-compose'; +import { ObjectTypeComposer, schemaComposer as globalSchemaComposer } from 'graphql-compose'; import type { Model, Document } from 'mongoose'; import { convertModelToGraphQL } from './fieldsConverter'; import { resolverFactory, AllResolversOpts } from './resolvers'; @@ -53,6 +53,13 @@ export function composeWithMongoose( const tc = convertModelToGraphQL(m, name, sc); + if (!(tc instanceof ObjectTypeComposer)) { + // should be impossible I hope + throw new Error( + 'composeWithMongoose does not support discriminator mode, use composeMongoose or composeWithMongooseDiscriminators instead' + ); + } + if (opts.description) { tc.setDescription(opts.description); } diff --git a/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts b/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts index e06e4f37..13a2a4a2 100644 --- a/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts +++ b/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts @@ -30,27 +30,21 @@ const composeMongoose = ( return generatedTC; }; -let baseDTC: EDiscriminatorTypeComposer; -let DInterface: InterfaceTypeComposer; +let eDTC: EDiscriminatorTypeComposer; let DInputObject: ObjectTypeComposer; beforeEach(() => { - baseDTC = composeMongoose(CharacterModel); - DInterface = schemaComposer.getIFTC(baseDTC.getTypeName()); - DInputObject = baseDTC.getDInputObject(); + eDTC = composeMongoose(CharacterModel); + DInputObject = eDTC.getDInputObject(); }); afterEach(() => { - baseDTC.schemaComposer.clear(); + eDTC.schemaComposer.clear(); }); describe('EDiscriminatorTypeComposer', () => { - it('has an interface object DInterface as the type name', () => { - expect(baseDTC.getDInterface()).toEqual(schemaComposer.getAnyTC(baseDTC.getTypeName())); - }); - it('throws an error when default (__t) discriminator key is set', () => { - baseDTC.schemaComposer.clear(); + eDTC.schemaComposer.clear(); const { CharacterModel: noDKeyModel } = getCharacterModels(undefined, 'noDKeyCharacter'); const errorCall = () => composeMongoose(noDKeyModel); expect(errorCall).toThrowError( @@ -58,6 +52,10 @@ describe('EDiscriminatorTypeComposer', () => { ); }); + it('should have type resolvers for discriminated models', () => { + expect(eDTC.getTypeResolverNames()).toHaveLength(2); + }); + it('should not be used on models without discriminators', () => { const { DroidModel } = getCharacterModels(undefined, 'noDiscrim'); const noDiscrim = composeMongooseRaw(DroidModel, dComposeOpts); @@ -66,15 +64,11 @@ describe('EDiscriminatorTypeComposer', () => { describe('Custom Getters', () => { test('getting discriminator key', () => { - expect(baseDTC.getDKey()).toEqual(defaultDKey); - }); - - test('getting discriminator interface', () => { - expect(baseDTC.getDInterface()).toBe(DInterface); + expect(eDTC.getDKey()).toEqual(defaultDKey); }); test('getting discriminator input object TC', () => { - expect(baseDTC.getDInputObject()).toBe(DInputObject); + expect(eDTC.getDInputObject()).toBe(DInputObject); }); }); @@ -102,9 +96,9 @@ describe('EDiscriminatorTypeComposer', () => { }); }); - describe('DInterface', () => { + describe('eDTC', () => { it('shares field names with base model used to compose it', () => { - expect(DInterface.getFieldNames()).toEqual( + expect(eDTC.getFieldNames()).toEqual( expect.arrayContaining( Object.keys(CharacterModel.schema.paths).filter((x) => !x.startsWith('__')) ) @@ -112,25 +106,23 @@ describe('EDiscriminatorTypeComposer', () => { }); it('should be an instance of InterfaceTypeComposer', () => { - expect(DInterface instanceof InterfaceTypeComposer).toBeTruthy(); + expect(eDTC instanceof InterfaceTypeComposer).toBeTruthy(); }); it('should have the input TC from DInputObject as input TC', () => { - expect(DInterface.getInputTypeComposer()).toEqual( - baseDTC.getDInputObject().getInputTypeComposer() - ); + expect(eDTC.getInputTypeComposer()).toEqual(eDTC.getDInputObject().getInputTypeComposer()); }); }); describe('Get Discriminator TCs', () => { it('returns discrimTCs with mongooseResolvers present', () => { - Object.values(baseDTC.getDiscriminatorTCs()).forEach((discimTC) => { + Object.values(eDTC.getDiscriminatorTCs()).forEach((discimTC) => { expect(discimTC).toHaveProperty('mongooseResolvers'); }); }); it('returns empty object with mongooseResolvers missing', () => { - (baseDTC.discrimTCs[Object.keys(baseDTC.discrimTCs)[0]] as any).mongooseResolvers = undefined; - Object.values(baseDTC.getDiscriminatorTCs()).forEach((discimTC) => { + (eDTC.discrimTCs[Object.keys(eDTC.discrimTCs)[0]] as any).mongooseResolvers = undefined; + Object.values(eDTC.getDiscriminatorTCs()).forEach((discimTC) => { expect(discimTC).toHaveProperty('mongooseResolvers'); }); }); @@ -139,18 +131,17 @@ describe('EDiscriminatorTypeComposer', () => { describe('Overridden eDTC Class Methods', () => { describe('Set Field', () => { it('updates field on all child TCs', () => { - baseDTC.setField('newField', 'String'); - expect(baseDTC.hasField('newField')).toBeTruthy(); - expect(DInterface.hasField('newField')).toBeTruthy(); + eDTC.setField('newField', 'String'); + expect(eDTC.hasField('newField')).toBeTruthy(); expect(DInputObject.hasField('newField')).toBeTruthy(); - Object.values(baseDTC.discrimTCs).forEach((dTC) => { + Object.values(eDTC.discrimTCs).forEach((dTC) => { expect(dTC.hasField('newField')).toBeTruthy(); }); }); it('updates input TC when field is added', () => { - baseDTC.setField('inputField', 'String'); - expect(schemaComposer.getIFTC(baseDTC.getTypeName()).getInputTypeComposer()).toEqual( + eDTC.setField('inputField', 'String'); + expect(schemaComposer.getIFTC(eDTC.getTypeName()).getInputTypeComposer()).toEqual( DInputObject.getInputTypeComposer() ); }); @@ -158,11 +149,10 @@ describe('EDiscriminatorTypeComposer', () => { describe('Set Extensions', () => { it('updates field on all child TCs', () => { - baseDTC.setExtensions({ newField: 'testExtension' }); - expect(baseDTC.hasExtension('newField')).toBeTruthy(); - expect(DInterface.hasExtension('newField')).toBeTruthy(); + eDTC.setExtensions({ newField: 'testExtension' }); + expect(eDTC.hasExtension('newField')).toBeTruthy(); expect(DInputObject.hasExtension('newField')).toBeTruthy(); - Object.values(baseDTC.discrimTCs).forEach((dTC) => { + Object.values(eDTC.discrimTCs).forEach((dTC) => { expect(dTC.hasExtension('newField')).toBeTruthy(); }); }); @@ -170,20 +160,18 @@ describe('EDiscriminatorTypeComposer', () => { describe('Remove Field', () => { it('deletes field on all child TCs', () => { - baseDTC.addFields({ toDelete: 'String' }); + eDTC.addFields({ toDelete: 'String' }); // check field added - expect(baseDTC.hasField('toDelete')).toBeTruthy(); - expect(DInterface.hasField('toDelete')).toBeTruthy(); + expect(eDTC.hasField('toDelete')).toBeTruthy(); expect(DInputObject.hasField('toDelete')).toBeTruthy(); - Object.values(baseDTC.discrimTCs).forEach((dTC) => { + Object.values(eDTC.discrimTCs).forEach((dTC) => { expect(dTC.hasField('toDelete')).toBeTruthy(); }); - baseDTC.removeField('toDelete'); - expect(baseDTC.hasField('toDelete')).toBeFalsy(); - expect(DInterface.hasField('toDelete')).toBeFalsy(); + eDTC.removeField('toDelete'); + expect(eDTC.hasField('toDelete')).toBeFalsy(); expect(DInputObject.hasField('toDelete')).toBeFalsy(); - Object.values(baseDTC.discrimTCs).forEach((dTC) => { + Object.values(eDTC.discrimTCs).forEach((dTC) => { expect(dTC.hasField('toDelete')).toBeFalsy(); }); }); @@ -191,19 +179,15 @@ describe('EDiscriminatorTypeComposer', () => { describe('Remove Other Fields', () => { it('removes all other fields from base TC from all child TCs', () => { - baseDTC.addFields({ toKeep: 'String' }); + eDTC.addFields({ toKeep: 'String' }); - const otherFields = baseDTC - .getDInterface() - .getFieldNames() - .filter((field) => field !== 'toKeep'); - baseDTC.removeOtherFields('toKeep'); + const otherFields = eDTC.getFieldNames().filter((field) => field !== 'toKeep'); + eDTC.removeOtherFields('toKeep'); otherFields.forEach((removedField) => { - expect(baseDTC.hasField(removedField)).toBeFalsy(); - expect(DInterface.hasField(removedField)).toBeFalsy(); + expect(eDTC.hasField(removedField)).toBeFalsy(); expect(DInputObject.hasField(removedField)).toBeFalsy(); - Object.values(baseDTC.discrimTCs).forEach((dTC) => { + Object.values(eDTC.discrimTCs).forEach((dTC) => { expect(dTC.hasField(removedField)).toBeFalsy(); }); }); @@ -215,12 +199,11 @@ describe('EDiscriminatorTypeComposer', () => { const fieldOrder = ['_id', 'appearsIn', 'friends', 'kind', 'type']; const fieldOrderString = fieldOrder.join(''); - baseDTC.reorderFields(fieldOrder); + eDTC.reorderFields(fieldOrder); - expect(baseDTC.getFieldNames().join('').startsWith(fieldOrderString)).toBeTruthy(); - expect(DInterface.getFieldNames().join('').startsWith(fieldOrderString)).toBeTruthy(); + expect(eDTC.getFieldNames().join('').startsWith(fieldOrderString)).toBeTruthy(); expect(DInputObject.getFieldNames().join('').startsWith(fieldOrderString)).toBeTruthy(); - Object.values(baseDTC.discrimTCs).forEach((dTC) => { + Object.values(eDTC.discrimTCs).forEach((dTC) => { expect(dTC.getFieldNames().join('').startsWith(fieldOrderString)).toBeTruthy(); }); }); @@ -228,29 +211,27 @@ describe('EDiscriminatorTypeComposer', () => { describe('Make Fields Nullable/Non-null', () => { it('makes a nullable field non-null', () => { - baseDTC.addFields({ initialNullable: 'String' }); - expect(baseDTC.isFieldNonNull('initialNullable')).toBeFalsy(); + eDTC.addFields({ initialNullable: 'String' }); + expect(eDTC.isFieldNonNull('initialNullable')).toBeFalsy(); - baseDTC.makeFieldNonNull('initialNullable'); + eDTC.makeFieldNonNull('initialNullable'); - expect(baseDTC.isFieldNonNull('initialNullable')).toBeTruthy(); - expect(DInterface.isFieldNonNull('initialNullable')).toBeTruthy(); + expect(eDTC.isFieldNonNull('initialNullable')).toBeTruthy(); expect(DInputObject.isFieldNonNull('initialNullable')).toBeTruthy(); - Object.values(baseDTC.discrimTCs).forEach((dTC) => { + Object.values(eDTC.discrimTCs).forEach((dTC) => { expect(dTC.isFieldNonNull('initialNullable')).toBeTruthy(); }); }); it('makes a non-null field nullable', () => { - baseDTC.addFields({ initialNonNull: 'String!' }); - expect(baseDTC.isFieldNonNull('initialNonNull')).toBeTruthy(); + eDTC.addFields({ initialNonNull: 'String!' }); + expect(eDTC.isFieldNonNull('initialNonNull')).toBeTruthy(); - baseDTC.makeFieldNullable('initialNonNull'); + eDTC.makeFieldNullable('initialNonNull'); - expect(baseDTC.isFieldNonNull('initialNonNull')).toBeFalsy(); - expect(DInterface.isFieldNonNull('initialNonNull')).toBeFalsy(); + expect(eDTC.isFieldNonNull('initialNonNull')).toBeFalsy(); expect(DInputObject.isFieldNonNull('initialNonNull')).toBeFalsy(); - Object.values(baseDTC.discrimTCs).forEach((dTC) => { + Object.values(eDTC.discrimTCs).forEach((dTC) => { expect(dTC.isFieldNonNull('initialNonNull')).toBeFalsy(); }); }); diff --git a/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts index b690e268..ab33c17c 100644 --- a/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts +++ b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts @@ -1,4 +1,4 @@ -import { GraphQLObjectType } from 'graphql'; +import { GraphQLInterfaceType } from 'graphql'; import { EnumTypeComposer, Extensions, @@ -18,7 +18,7 @@ export function convertModelToGraphQLWithDiscriminators, opts: ComposeMongooseOpts = {} -): ObjectTypeComposer { +): ObjectTypeComposer | InterfaceTypeComposer { const sc = schemaComposer; // workaround to avoid recursive loop on convertModel and support nested discrim fields, will be set true in nested fields @@ -36,7 +36,7 @@ export interface ComposeMongooseDiscriminatorsOpts extends ComposeMong reorderFields?: boolean | string[]; // true order: _id, DKey, DInterfaceFields, DiscriminatorFields } -export class EDiscriminatorTypeComposer extends ObjectTypeComposer< +export class EDiscriminatorTypeComposer extends InterfaceTypeComposer< TSource, TContext > { @@ -49,17 +49,14 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom | ObjectTypeComposer; } = {}; - BaseTC: ObjectTypeComposer; DInputObject: ObjectTypeComposer; - DInterface: InterfaceTypeComposer; opts: ComposeMongooseDiscriminatorsOpts = {}; DKeyETC?: EnumTypeComposer; - constructor(gqType: GraphQLObjectType, schemaComposer: SchemaComposer) { + constructor(gqType: GraphQLInterfaceType, schemaComposer: SchemaComposer) { super(gqType, schemaComposer); - this.DInterface = schemaComposer.getOrCreateIFTC(`${gqType.name}Interface`); + this.DInputObject = schemaComposer.getOrCreateOTC(`${gqType.name}Input`); - this.BaseTC = schemaComposer.getOrCreateOTC(`${gqType.name}BaseTC`); return this; } @@ -85,44 +82,51 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom ...opts, }; - const baseTC = convertModelToGraphQL(model, typeName, schemaComposer, opts); - const baseDTC = new EDiscriminatorTypeComposer(baseTC.getType(), schemaComposer); - - // copy data from baseTC to baseDTC - baseTC.clone(baseDTC as ObjectTypeComposer); + const baseTC = convertModelToGraphQL( + model, + `${typeName}BaseOTC`, + schemaComposer, + opts + ) as ObjectTypeComposer; + + const eDTC = new EDiscriminatorTypeComposer( + new GraphQLInterfaceType({ + name: typeName, + fields: () => ({}), + }), + schemaComposer + ); - baseDTC.opts = { ...opts }; - baseDTC.discriminatorKey = (model as any).schema.get('discriminatorKey') || '__t'; - if (baseDTC.discriminatorKey === '__t') { + eDTC.opts = { ...opts }; + eDTC.discriminatorKey = (model as any).schema.get('discriminatorKey') || '__t'; + if (eDTC.discriminatorKey === '__t') { throw Error( 'A custom discriminator key must be set on the model options in mongoose for discriminator behaviour to function correctly' ); } - baseDTC.BaseTC = baseTC; - baseDTC.DInterface = baseDTC._buildDiscriminatedInterface(model, baseTC); - baseDTC.DInterface.setInputTypeComposer(baseDTC.DInputObject.getInputTypeComposer()); - baseDTC.setInputTypeComposer(baseDTC.DInputObject.getInputTypeComposer()); + eDTC._buildDiscriminatedInterface(model, baseTC); + eDTC.setInputTypeComposer(eDTC.DInputObject.getInputTypeComposer()); - // baseDTC.setInterfaces([baseDTC.DInterface]); - baseDTC._gqcInputTypeComposer = baseDTC.DInputObject._gqcInputTypeComposer; - baseDTC.DInterface._gqcInputTypeComposer = baseDTC.DInputObject._gqcInputTypeComposer; + // eDTC.setInterfaces([eDTC.DInterface]); + eDTC._gqcInputTypeComposer = eDTC.DInputObject._gqcInputTypeComposer; - // reorderFields(baseDTC, baseDTC.opts.reorderFields, baseDTC.discriminatorKey); - baseDTC.schemaComposer.addSchemaMustHaveType(baseDTC as any); + // reorderFields(eDTC, eDTC.opts.reorderFields, eDTC.discriminatorKey); + eDTC.schemaComposer.addSchemaMustHaveType(eDTC as any); // prepare Base Resolvers - // prepareBaseResolvers(baseDTC); + // prepareBaseResolvers(eDTC); - return baseDTC as any; + // cleanup baseTC + schemaComposer.delete(baseTC.getTypeName()); + return eDTC as any; } _buildDiscriminatedInterface( model: Model, baseTC: ObjectTypeComposer - ): InterfaceTypeComposer { - const interfaceTC = this.DInterface; - interfaceTC.removeOtherFields(''); - interfaceTC.setFields(baseTC.getFields()); + ): EDiscriminatorTypeComposer { + this.removeOtherFields(''); + this.setFields(baseTC.getFields()); this.DInputObject.setFields(baseTC.getFields()); const discriminators = model.discriminators || model.schema.discriminators; @@ -140,21 +144,21 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom const discrimType = discriminators[key]; - const discrimTC = - discrimType instanceof Schema - ? convertModelToGraphQL( - { schema: discrimType }, - this.getTypeName() + key, - schemaComposer, - { - ...this.opts, - name: this.getTypeName() + key, - } - ) - : composeMongoose(discrimType as Model, { + // help why instanceof Schema not work?? + const discrimTC = ((discrimType as any)?.__proto__?.constructor.name === 'Schema' + ? convertModelToGraphQL( + { schema: discrimType as Schema }, + this.getTypeName() + key, + schemaComposer, + { ...this.opts, name: this.getTypeName() + key, - }); + } + ) + : composeMongoose(discrimType as Model, { + ...this.opts, + name: this.getTypeName() + key, + })) as ObjectTypeComposer; // base OTC used for input schema must hold all child TC fields in the most loose way (so all types are accepted) // more detailed type checks are done on input object by mongoose itself @@ -172,15 +176,15 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom discrimTC.getFieldNames().filter((field) => !baseTC.hasField(field)) ); - // also set fields on master TC so it will have all possibilities for input workaround + this.addTypeResolver(discrimTC, (obj: any) => obj[this.discriminatorKey] === key); - interfaceTC.addTypeResolver(discrimTC, (obj: any) => obj[this.discriminatorKey] === key); + // also set fields on master TC so it will have all possibilities for input workaround // add TC to discrimTCs this.discrimTCs[key] = discrimTC; }); - return interfaceTC; + return this; } _addFieldToInputOTC( @@ -188,7 +192,7 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom field: ObjectTypeComposerFieldConfig ): void { // if another discrimTC has already defined the field (not from original TC) - if (this.DInputObject.hasField(fieldName) && !this.BaseTC.hasField(fieldName)) { + if (this.DInputObject.hasField(fieldName) && !this.hasField(fieldName)) { this.DInputObject.setField(fieldName, `JSON`); } else { (this.DInputObject as ObjectTypeComposer).setField(fieldName, field); @@ -205,10 +209,6 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom // return this.DKeyETC as any; // } - getDInterface(): InterfaceTypeComposer { - return this.DInterface as any; - } - getDInputObject(): ObjectTypeComposer { return this.DInputObject as any; } @@ -227,9 +227,6 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom } // OVERRIDES - getTypeName(): string { - return this.DInterface.getTypeName(); - } setField( fieldName: string, @@ -237,7 +234,6 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom ): this { super.setField(fieldName, fieldConfig); this.getDInputObject().setField(fieldName, fieldConfig); - this.getDInterface().setField(fieldName, fieldConfig); for (const discrimTC in this.discrimTCs) { this.discrimTCs[discrimTC].setField(fieldName, fieldConfig); @@ -249,7 +245,6 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom setExtensions(extensions: Extensions): this { super.setExtensions(extensions); this.getDInputObject().setExtensions(extensions); - this.getDInterface().setExtensions(extensions); for (const discrimTC in this.discrimTCs) { this.discrimTCs[discrimTC].setExtensions(extensions); @@ -261,7 +256,6 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom setDescription(description: string): this { super.setDescription(description); this.getDInputObject().setDescription(description); - this.getDInterface().setDescription(description); return this; } @@ -269,7 +263,6 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom removeField(fieldNameOrArray: string | Array): this { super.removeField(fieldNameOrArray); this.getDInputObject().removeField(fieldNameOrArray); - this.getDInterface().removeField(fieldNameOrArray); for (const discrimTC in this.discrimTCs) { this.discrimTCs[discrimTC].removeField(fieldNameOrArray); @@ -282,13 +275,12 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom // get field names to delete from child TCs, so their unique fields are preserved const keepFieldNames = fieldNameOrArray instanceof Array ? fieldNameOrArray : [fieldNameOrArray]; - const fieldNamesToDelete = this.DInterface.getFieldNames().filter( + const fieldNamesToDelete = this.getFieldNames().filter( (field) => !keepFieldNames.includes(field) ); super.removeField(fieldNamesToDelete); this.getDInputObject().removeField(fieldNamesToDelete); - this.getDInterface().removeOtherFields(fieldNameOrArray); for (const discrimTC in this.discrimTCs) { this.discrimTCs[discrimTC].removeField(fieldNamesToDelete); @@ -299,7 +291,6 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom reorderFields(names: string[]): this { super.reorderFields(names); - this.getDInterface().reorderFields(names); this.getDInputObject().reorderFields(names); for (const discrimTC in this.discrimTCs) { @@ -311,7 +302,6 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom makeFieldNonNull(fieldNameOrArray: string | Array): this { super.makeFieldNonNull(fieldNameOrArray); - this.getDInterface().makeFieldNonNull(fieldNameOrArray); this.getDInputObject().makeFieldNonNull(fieldNameOrArray); for (const discrimTC in this.discrimTCs) { @@ -323,7 +313,6 @@ export class EDiscriminatorTypeComposer extends ObjectTypeCom makeFieldNullable(fieldNameOrArray: string | Array): this { super.makeFieldNullable(fieldNameOrArray); - this.getDInterface().makeFieldNullable(fieldNameOrArray); this.getDInputObject().makeFieldNullable(fieldNameOrArray); for (const discrimTC in this.discrimTCs) { diff --git a/src/fieldsConverter.ts b/src/fieldsConverter.ts index 385ae1db..606b2ae4 100644 --- a/src/fieldsConverter.ts +++ b/src/fieldsConverter.ts @@ -8,15 +8,13 @@ import { EnumTypeComposer, ComposeOutputTypeDefinition, ObjectTypeComposerFieldConfigAsObjectDefinition, + InterfaceTypeComposer, } from 'graphql-compose'; import { upperFirst } from 'graphql-compose'; import type { GraphQLScalarType } from 'graphql-compose/lib/graphql'; import GraphQLMongoID from './types/MongoID'; import GraphQLBSONDecimal from './types/BSONDecimal'; -import { - convertModelToGraphQLWithDiscriminators, - EDiscriminatorTypeComposer, -} from './enhancedDiscriminators'; +import { convertModelToGraphQLWithDiscriminators } from './enhancedDiscriminators'; import { ComposeMongooseOpts } from './composeMongoose'; type MongooseFieldT = { @@ -141,7 +139,7 @@ export function convertModelToGraphQL( typeName: string, schemaComposer: SchemaComposer, opts: ComposeMongooseOpts = {} -): ObjectTypeComposer { +): InterfaceTypeComposer | ObjectTypeComposer { const sc = schemaComposer; if (!typeName) { @@ -192,14 +190,6 @@ export function convertModelToGraphQL( type = 'Int'; } - let trueType = type; - if (trueType instanceof Array) { - trueType = trueType[0]; - } - if (trueType instanceof EDiscriminatorTypeComposer) { - type = type instanceof Array ? [trueType.getDInterface()] : trueType.getDInterface(); - } - graphqlFields[fieldName] = { type, description: _getFieldDescription(mongooseField), @@ -227,7 +217,7 @@ export function convertSchemaToGraphQL( typeName: string, schemaComposer: SchemaComposer, opts: ComposeMongooseOpts = {} -): ObjectTypeComposer { +): ObjectTypeComposer | InterfaceTypeComposer { const sc = schemaComposer; if (!typeName) { @@ -363,7 +353,7 @@ export function embeddedToGraphQL( prefix: string = '', schemaComposer: SchemaComposer, opts: ComposeMongooseOpts = {} -): ObjectTypeComposer { +): ObjectTypeComposer | InterfaceTypeComposer { const fieldName = _getFieldName(field); const fieldType = _getFieldType(field); @@ -417,7 +407,7 @@ export function documentArrayToGraphQL( prefix: string = '', schemaComposer: SchemaComposer, opts: ComposeMongooseOpts = {} -): [ObjectTypeComposer] { +): [ObjectTypeComposer | InterfaceTypeComposer] { if (!(field instanceof mongoose.Schema.Types.DocumentArray) && !field?.schema?.paths) { throw new Error( 'You provide incorrect mongoose field to `documentArrayToGraphQL()`. ' + diff --git a/src/resolvers/__tests__/connection-test.ts b/src/resolvers/__tests__/connection-test.ts index a9bc03ba..02bb7d52 100644 --- a/src/resolvers/__tests__/connection-test.ts +++ b/src/resolvers/__tests__/connection-test.ts @@ -117,7 +117,7 @@ describe('connection() resolver', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; }); let user1: IUser; @@ -294,7 +294,7 @@ describe('connection() resolver', () => { it('should use internal resolver custom opts', async () => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; const mockCountOpts = { suffix: 'ABC' }; const mockFindManyOpts = { suffix: 'DEF' }; const resolver = connection(UserModel, UserTC, { diff --git a/src/resolvers/__tests__/count-test.ts b/src/resolvers/__tests__/count-test.ts index 3af6028f..29177372 100644 --- a/src/resolvers/__tests__/count-test.ts +++ b/src/resolvers/__tests__/count-test.ts @@ -13,7 +13,7 @@ describe('count() ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; }); let user1; diff --git a/src/resolvers/__tests__/createMany-test.ts b/src/resolvers/__tests__/createMany-test.ts index 1954dc84..0a98b1eb 100644 --- a/src/resolvers/__tests__/createMany-test.ts +++ b/src/resolvers/__tests__/createMany-test.ts @@ -17,7 +17,7 @@ describe('createMany() ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; UserTC.setRecordIdFn((source) => (source ? `${source._id}` : '')); }); @@ -222,7 +222,11 @@ describe('createMany() ->', () => { const ClonedUserModel = mongoose.model('UserClone', ClonedUserSchema); - const ClonedUserTC = convertModelToGraphQL(ClonedUserModel, 'UserClone', schemaComposer); + const ClonedUserTC = convertModelToGraphQL( + ClonedUserModel, + 'UserClone', + schemaComposer + ) as ObjectTypeComposer; ClonedUserTC.setRecordIdFn((source: any) => (source ? `${source._id}` : '')); const result = await createMany(ClonedUserModel, ClonedUserTC).resolve({ diff --git a/src/resolvers/__tests__/createOne-test.ts b/src/resolvers/__tests__/createOne-test.ts index 426a642b..74a608c9 100644 --- a/src/resolvers/__tests__/createOne-test.ts +++ b/src/resolvers/__tests__/createOne-test.ts @@ -16,7 +16,7 @@ describe('createOne() ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; UserTC.setRecordIdFn((source) => (source ? `${source._id}` : '')); }); diff --git a/src/resolvers/__tests__/dataLoader-test.ts b/src/resolvers/__tests__/dataLoader-test.ts index ee0358f6..b6d52a58 100644 --- a/src/resolvers/__tests__/dataLoader-test.ts +++ b/src/resolvers/__tests__/dataLoader-test.ts @@ -19,8 +19,12 @@ describe('dataLoader() ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); - PostTypeComposer = convertModelToGraphQL(PostModel, 'Post', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; + PostTypeComposer = convertModelToGraphQL( + PostModel, + 'Post', + schemaComposer + ) as ObjectTypeComposer; }); let user: IUser; diff --git a/src/resolvers/__tests__/dataLoaderMany-test.ts b/src/resolvers/__tests__/dataLoaderMany-test.ts index f2d9d824..38b6ee51 100644 --- a/src/resolvers/__tests__/dataLoaderMany-test.ts +++ b/src/resolvers/__tests__/dataLoaderMany-test.ts @@ -19,8 +19,12 @@ describe('dataLoaderMany() ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); - PostTypeComposer = convertModelToGraphQL(PostModel, 'Post', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; + PostTypeComposer = convertModelToGraphQL( + PostModel, + 'Post', + schemaComposer + ) as ObjectTypeComposer; }); let user: IUser; diff --git a/src/resolvers/__tests__/findById-test.ts b/src/resolvers/__tests__/findById-test.ts index d1eb930b..64e58cbc 100644 --- a/src/resolvers/__tests__/findById-test.ts +++ b/src/resolvers/__tests__/findById-test.ts @@ -14,8 +14,12 @@ describe('findById() ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); - PostTypeComposer = convertModelToGraphQL(PostModel, 'Post', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; + PostTypeComposer = convertModelToGraphQL( + PostModel, + 'Post', + schemaComposer + ) as ObjectTypeComposer; }); let user: IUser; diff --git a/src/resolvers/__tests__/findByIds-test.ts b/src/resolvers/__tests__/findByIds-test.ts index 5b699225..cefcd549 100644 --- a/src/resolvers/__tests__/findByIds-test.ts +++ b/src/resolvers/__tests__/findByIds-test.ts @@ -14,8 +14,12 @@ describe('findByIds() ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); - PostTypeComposer = convertModelToGraphQL(PostModel, 'Post', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; + PostTypeComposer = convertModelToGraphQL( + PostModel, + 'Post', + schemaComposer + ) as ObjectTypeComposer; }); let user1: IUser; diff --git a/src/resolvers/__tests__/findMany-test.ts b/src/resolvers/__tests__/findMany-test.ts index dfad7099..d1104916 100644 --- a/src/resolvers/__tests__/findMany-test.ts +++ b/src/resolvers/__tests__/findMany-test.ts @@ -13,7 +13,7 @@ describe('findMany() ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; }); let user1: IUser; diff --git a/src/resolvers/__tests__/findOne-test.ts b/src/resolvers/__tests__/findOne-test.ts index 6c3c02ed..907bcfc1 100644 --- a/src/resolvers/__tests__/findOne-test.ts +++ b/src/resolvers/__tests__/findOne-test.ts @@ -14,7 +14,7 @@ let user2: IUser; beforeEach(async () => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; await UserModel.deleteMany({}); diff --git a/src/resolvers/__tests__/pagination-test.ts b/src/resolvers/__tests__/pagination-test.ts index e582414b..decf34a7 100644 --- a/src/resolvers/__tests__/pagination-test.ts +++ b/src/resolvers/__tests__/pagination-test.ts @@ -14,7 +14,7 @@ describe('pagination() ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; UserTC.setResolver('findMany', findMany(UserModel, UserTC)); UserTC.setResolver('count', count(UserModel, UserTC)); }); diff --git a/src/resolvers/__tests__/removeById-test.ts b/src/resolvers/__tests__/removeById-test.ts index 433f9ec2..bf1cac30 100644 --- a/src/resolvers/__tests__/removeById-test.ts +++ b/src/resolvers/__tests__/removeById-test.ts @@ -17,7 +17,7 @@ describe('removeById() ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; UserTC.setRecordIdFn((source) => (source ? `${source._id}` : '')); }); diff --git a/src/resolvers/__tests__/removeMany-test.ts b/src/resolvers/__tests__/removeMany-test.ts index b24502e9..edda910a 100644 --- a/src/resolvers/__tests__/removeMany-test.ts +++ b/src/resolvers/__tests__/removeMany-test.ts @@ -14,7 +14,7 @@ describe('removeMany() ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; }); let user1: IUser; diff --git a/src/resolvers/__tests__/removeOne-test.ts b/src/resolvers/__tests__/removeOne-test.ts index 9256c303..07678626 100644 --- a/src/resolvers/__tests__/removeOne-test.ts +++ b/src/resolvers/__tests__/removeOne-test.ts @@ -15,7 +15,7 @@ describe('removeOne() ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; UserTC.setRecordIdFn((source) => (source ? `${source._id}` : '')); }); diff --git a/src/resolvers/__tests__/updateById-test.ts b/src/resolvers/__tests__/updateById-test.ts index a4d71ab7..01fd9fc1 100644 --- a/src/resolvers/__tests__/updateById-test.ts +++ b/src/resolvers/__tests__/updateById-test.ts @@ -17,7 +17,7 @@ describe('updateById() ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; UserTC.setRecordIdFn((source) => (source ? `${source._id}` : '')); }); diff --git a/src/resolvers/__tests__/updateMany-test.ts b/src/resolvers/__tests__/updateMany-test.ts index b1f8c351..e1e9c308 100644 --- a/src/resolvers/__tests__/updateMany-test.ts +++ b/src/resolvers/__tests__/updateMany-test.ts @@ -14,7 +14,7 @@ describe('updateMany() ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; }); let user1: IUser; diff --git a/src/resolvers/__tests__/updateOne-test.ts b/src/resolvers/__tests__/updateOne-test.ts index a279d652..646b9c31 100644 --- a/src/resolvers/__tests__/updateOne-test.ts +++ b/src/resolvers/__tests__/updateOne-test.ts @@ -16,7 +16,7 @@ describe('updateOne() ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; UserTC.setRecordIdFn((source) => (source ? `${source._id}` : '')); }); diff --git a/src/resolvers/helpers/__tests__/filter-test.ts b/src/resolvers/helpers/__tests__/filter-test.ts index b190e446..267ecba9 100644 --- a/src/resolvers/helpers/__tests__/filter-test.ts +++ b/src/resolvers/helpers/__tests__/filter-test.ts @@ -16,7 +16,7 @@ describe('Resolver helper `filter` ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; }); describe('filterHelperArgs()', () => { diff --git a/src/resolvers/helpers/__tests__/record-test.ts b/src/resolvers/helpers/__tests__/record-test.ts index 2374f350..e36bbeb8 100644 --- a/src/resolvers/helpers/__tests__/record-test.ts +++ b/src/resolvers/helpers/__tests__/record-test.ts @@ -13,7 +13,7 @@ describe('Resolver helper `record` ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; }); describe('recordHelperArgs()', () => { diff --git a/src/resolvers/helpers/__tests__/sort-test.ts b/src/resolvers/helpers/__tests__/sort-test.ts index bdfa226b..fb8ef009 100644 --- a/src/resolvers/helpers/__tests__/sort-test.ts +++ b/src/resolvers/helpers/__tests__/sort-test.ts @@ -9,7 +9,7 @@ describe('Resolver helper `sort` ->', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; }); describe('getSortTypeFromModel()', () => { diff --git a/src/resolvers/helpers/filter.ts b/src/resolvers/helpers/filter.ts index db33d2cd..75e77e4a 100644 --- a/src/resolvers/helpers/filter.ts +++ b/src/resolvers/helpers/filter.ts @@ -71,8 +71,12 @@ export function filterHelperArgs( model: Model, opts?: FilterHelperArgsOpts ): ObjectTypeComposerArgumentConfigMap<{ filter: any }> { - if (!(typeComposer instanceof ObjectTypeComposer)) { - throw new Error('First arg for filterHelperArgs() should be instance of ObjectTypeComposer.'); + if ( + !(typeComposer instanceof ObjectTypeComposer || typeComposer instanceof InterfaceTypeComposer) + ) { + throw new Error( + 'First arg for filterHelperArgs() should be instance of ObjectTypeComposer or InterfaceTypeComposer.' + ); } if (!model || !model.modelName || !model.schema) { From 7d29a856a3a5f3865239d8005f1999a62550dc13 Mon Sep 17 00:00:00 2001 From: Jacob Date: Thu, 29 Jul 2021 12:53:43 +0100 Subject: [PATCH 13/14] Tidy eDTC code --- .../eDiscriminatorTypeComposer.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts index ab33c17c..6bc910f9 100644 --- a/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts +++ b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts @@ -89,6 +89,7 @@ export class EDiscriminatorTypeComposer extends InterfaceType opts ) as ObjectTypeComposer; + // create new enhanced discriminator TC const eDTC = new EDiscriminatorTypeComposer( new GraphQLInterfaceType({ name: typeName, @@ -104,18 +105,16 @@ export class EDiscriminatorTypeComposer extends InterfaceType 'A custom discriminator key must be set on the model options in mongoose for discriminator behaviour to function correctly' ); } + eDTC._buildDiscriminatedInterface(model, baseTC); - eDTC.setInputTypeComposer(eDTC.DInputObject.getInputTypeComposer()); - // eDTC.setInterfaces([eDTC.DInterface]); + // set input to custom OTC since IFTCs are not supported in graphQL as inputs + eDTC.setInputTypeComposer(eDTC.DInputObject.getInputTypeComposer()); eDTC._gqcInputTypeComposer = eDTC.DInputObject._gqcInputTypeComposer; // reorderFields(eDTC, eDTC.opts.reorderFields, eDTC.discriminatorKey); eDTC.schemaComposer.addSchemaMustHaveType(eDTC as any); - // prepare Base Resolvers - // prepareBaseResolvers(eDTC); - // cleanup baseTC schemaComposer.delete(baseTC.getTypeName()); return eDTC as any; @@ -125,8 +124,7 @@ export class EDiscriminatorTypeComposer extends InterfaceType model: Model, baseTC: ObjectTypeComposer ): EDiscriminatorTypeComposer { - this.removeOtherFields(''); - this.setFields(baseTC.getFields()); + this.setFields(baseTC.getFields()); // should be overriden at internal setField call? this.DInputObject.setFields(baseTC.getFields()); const discriminators = model.discriminators || model.schema.discriminators; @@ -172,6 +170,7 @@ export class EDiscriminatorTypeComposer extends InterfaceType this._addFieldToInputOTC(fieldName, field); }); + // make field optional on input if not from baseTC, since we cannot specify individual union types, but rather must produce an input object type that can accept all union types at once this.DInputObject.makeFieldNullable( discrimTC.getFieldNames().filter((field) => !baseTC.hasField(field)) ); From 0167cb21c4ce2f0c1924414c54e7b7d9003d7b26 Mon Sep 17 00:00:00 2001 From: Jacob Date: Thu, 29 Jul 2021 13:01:28 +0100 Subject: [PATCH 14/14] Revert resolvers using tc.getTypeName() instead of directly passing tc for type --- src/resolvers/createOne.ts | 2 +- src/resolvers/dataLoader.ts | 2 +- src/resolvers/dataLoaderMany.ts | 2 +- src/resolvers/findById.ts | 2 +- src/resolvers/findByIds.ts | 2 +- src/resolvers/findMany.ts | 2 +- src/resolvers/findOne.ts | 2 +- src/resolvers/removeById.ts | 2 +- src/resolvers/removeOne.ts | 2 +- src/resolvers/updateById.ts | 2 +- src/resolvers/updateOne.ts | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/resolvers/createOne.ts b/src/resolvers/createOne.ts index bfa94c20..7c87c9a6 100644 --- a/src/resolvers/createOne.ts +++ b/src/resolvers/createOne.ts @@ -53,7 +53,7 @@ export function createOne({ - type: tc.getTypeName(), + type: tc, name: 'dataLoader', kind: 'query', args: { diff --git a/src/resolvers/dataLoaderMany.ts b/src/resolvers/dataLoaderMany.ts index a579ef23..6c61efae 100644 --- a/src/resolvers/dataLoaderMany.ts +++ b/src/resolvers/dataLoaderMany.ts @@ -52,7 +52,7 @@ export function dataLoaderMany({ - type: '[' + tc.getTypeName() + ']!', + type: tc.List.NonNull, name: 'dataLoaderMany', kind: 'query', args: { diff --git a/src/resolvers/findById.ts b/src/resolvers/findById.ts index 1a9e82b5..d8637470 100644 --- a/src/resolvers/findById.ts +++ b/src/resolvers/findById.ts @@ -50,7 +50,7 @@ export function findById({ - type: tc.getTypeName(), + type: tc, name: 'findById', kind: 'query', args: { diff --git a/src/resolvers/findByIds.ts b/src/resolvers/findByIds.ts index 42a89432..7ec19b7a 100644 --- a/src/resolvers/findByIds.ts +++ b/src/resolvers/findByIds.ts @@ -58,7 +58,7 @@ export function findByIds({ - type: '[' + tc.getTypeName() + '!]!', + type: tc.NonNull.List.NonNull, name: 'findByIds', kind: 'query', args: { diff --git a/src/resolvers/findMany.ts b/src/resolvers/findMany.ts index 7468baac..713a050e 100644 --- a/src/resolvers/findMany.ts +++ b/src/resolvers/findMany.ts @@ -67,7 +67,7 @@ export function findMany({ - type: '[' + tc.getTypeName() + '!]!', + type: tc.NonNull.List.NonNull, name: 'findMany', kind: 'query', args: { diff --git a/src/resolvers/findOne.ts b/src/resolvers/findOne.ts index c8fff888..f8d78c73 100644 --- a/src/resolvers/findOne.ts +++ b/src/resolvers/findOne.ts @@ -62,7 +62,7 @@ export function findOne({ - type: tc.getTypeName(), + type: tc, name: 'findOne', kind: 'query', args: { diff --git a/src/resolvers/removeById.ts b/src/resolvers/removeById.ts index 8b2f9183..d0be7d75 100644 --- a/src/resolvers/removeById.ts +++ b/src/resolvers/removeById.ts @@ -41,7 +41,7 @@ export function removeById