diff --git a/.eslintrc.js b/.eslintrc.js index 10a2b9d1..e625aaf7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,6 +9,7 @@ module.exports = { useJSXTextNode: true, project: [path.resolve(__dirname, 'tsconfig.json')], }, + root: true, rules: { 'no-underscore-dangle': 0, 'arrow-body-style': 0, 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/__mocks__/mongooseCommon.ts b/src/__mocks__/mongooseCommon.ts index 6c8e53f2..d33589fd 100644 --- a/src/__mocks__/mongooseCommon.ts +++ b/src/__mocks__/mongooseCommon.ts @@ -10,7 +10,7 @@ mongoose.Promise = Promise; const originalConnect = mongoose.connect; mongoose.createConnection = (async () => { const mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); + const mongoUri = await mongoServer.getUri(); // originalConnect.bind(mongoose)(mongoUri, { useMongoClient: true }); // mongoose 4 originalConnect.bind(mongoose)(mongoUri, { 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 456f0458..7aef6c0b 100644 --- a/src/composeMongoose.ts +++ b/src/composeMongoose.ts @@ -1,6 +1,12 @@ -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'; import { convertModelToGraphQL } from './fieldsConverter'; import { resolverFactory } from './resolvers'; import MongoID from './types/MongoID'; @@ -71,6 +77,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 +116,7 @@ export type GenerateResolverType = { export function composeMongoose( model: Model, opts: ComposeMongooseOpts = {} -): ObjectTypeComposer & { +): (ObjectTypeComposer | EDiscriminatorTypeComposer) & { mongooseResolvers: GenerateResolverType; } { const m: Model = model; @@ -121,7 +136,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); @@ -152,7 +167,7 @@ export function composeMongoose( } function makeFieldsNonNullWithDefaultValues( - tc: ObjectTypeComposer, + tc: ObjectTypeComposer | InterfaceTypeComposer, alreadyWorked = new Set() ): void { if (alreadyWorked.has(tc)) return; @@ -188,7 +203,7 @@ function makeFieldsNonNullWithDefaultValues( } export function prepareFields( - tc: ObjectTypeComposer, + tc: ObjectTypeComposer | InterfaceTypeComposer, opts: ComposeMongooseOpts = {} ): void { const onlyFields = opts?.onlyFields || opts?.fields?.only; @@ -202,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/discriminators/__mocks__/characterModels.ts b/src/discriminators/__mocks__/characterModels.ts index c6875154..fd1efeca 100644 --- a/src/discriminators/__mocks__/characterModels.ts +++ b/src/discriminators/__mocks__/characterModels.ts @@ -33,7 +33,9 @@ export const CharacterObject = { const CharacterSchema = new Schema(CharacterObject); const ACharacterSchema = new Schema({ ...CharacterObject }); -export function getCharacterModels(DKey: string): { +export function getCharacterModels( + DKey: string +): { CharacterModel: Model; PersonModel: Model; DroidModel: Model; 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: } 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..13a2a4a2 --- /dev/null +++ b/src/enhancedDiscriminators/__tests__/eDiscriminatorTypeComposer-test.ts @@ -0,0 +1,240 @@ +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 mongoose, { 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 eDTC: EDiscriminatorTypeComposer; +let DInputObject: ObjectTypeComposer; + +beforeEach(() => { + eDTC = composeMongoose(CharacterModel); + DInputObject = eDTC.getDInputObject(); +}); + +afterEach(() => { + eDTC.schemaComposer.clear(); +}); + +describe('EDiscriminatorTypeComposer', () => { + it('throws an error when default (__t) discriminator key is set', () => { + eDTC.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 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); + expect(noDiscrim instanceof EDiscriminatorTypeComposer).toBeFalsy(); + }); + + describe('Custom Getters', () => { + test('getting discriminator key', () => { + expect(eDTC.getDKey()).toEqual(defaultDKey); + }); + + test('getting discriminator input object TC', () => { + expect(eDTC.getDInputObject()).toBe(DInputObject); + }); + }); + + 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(fakeModel, '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('eDTC', () => { + it('shares field names with base model used to compose it', () => { + expect(eDTC.getFieldNames()).toEqual( + expect.arrayContaining( + Object.keys(CharacterModel.schema.paths).filter((x) => !x.startsWith('__')) + ) + ); + }); + + it('should be an instance of InterfaceTypeComposer', () => { + expect(eDTC instanceof InterfaceTypeComposer).toBeTruthy(); + }); + + it('should have the input TC from DInputObject as input TC', () => { + expect(eDTC.getInputTypeComposer()).toEqual(eDTC.getDInputObject().getInputTypeComposer()); + }); + }); + + describe('Get Discriminator TCs', () => { + it('returns discrimTCs with mongooseResolvers present', () => { + Object.values(eDTC.getDiscriminatorTCs()).forEach((discimTC) => { + expect(discimTC).toHaveProperty('mongooseResolvers'); + }); + }); + it('returns empty object with mongooseResolvers missing', () => { + (eDTC.discrimTCs[Object.keys(eDTC.discrimTCs)[0]] as any).mongooseResolvers = undefined; + Object.values(eDTC.getDiscriminatorTCs()).forEach((discimTC) => { + expect(discimTC).toHaveProperty('mongooseResolvers'); + }); + }); + }); + + describe('Overridden eDTC Class Methods', () => { + describe('Set Field', () => { + it('updates field on all child TCs', () => { + eDTC.setField('newField', 'String'); + expect(eDTC.hasField('newField')).toBeTruthy(); + expect(DInputObject.hasField('newField')).toBeTruthy(); + Object.values(eDTC.discrimTCs).forEach((dTC) => { + expect(dTC.hasField('newField')).toBeTruthy(); + }); + }); + + it('updates input TC when field is added', () => { + eDTC.setField('inputField', 'String'); + expect(schemaComposer.getIFTC(eDTC.getTypeName()).getInputTypeComposer()).toEqual( + DInputObject.getInputTypeComposer() + ); + }); + }); + + describe('Set Extensions', () => { + it('updates field on all child TCs', () => { + eDTC.setExtensions({ newField: 'testExtension' }); + expect(eDTC.hasExtension('newField')).toBeTruthy(); + expect(DInputObject.hasExtension('newField')).toBeTruthy(); + Object.values(eDTC.discrimTCs).forEach((dTC) => { + expect(dTC.hasExtension('newField')).toBeTruthy(); + }); + }); + }); + + describe('Remove Field', () => { + it('deletes field on all child TCs', () => { + eDTC.addFields({ toDelete: 'String' }); + // check field added + expect(eDTC.hasField('toDelete')).toBeTruthy(); + expect(DInputObject.hasField('toDelete')).toBeTruthy(); + Object.values(eDTC.discrimTCs).forEach((dTC) => { + expect(dTC.hasField('toDelete')).toBeTruthy(); + }); + + eDTC.removeField('toDelete'); + expect(eDTC.hasField('toDelete')).toBeFalsy(); + expect(DInputObject.hasField('toDelete')).toBeFalsy(); + Object.values(eDTC.discrimTCs).forEach((dTC) => { + expect(dTC.hasField('toDelete')).toBeFalsy(); + }); + }); + }); + + describe('Remove Other Fields', () => { + it('removes all other fields from base TC from all child TCs', () => { + eDTC.addFields({ toKeep: 'String' }); + + const otherFields = eDTC.getFieldNames().filter((field) => field !== 'toKeep'); + eDTC.removeOtherFields('toKeep'); + + otherFields.forEach((removedField) => { + expect(eDTC.hasField(removedField)).toBeFalsy(); + expect(DInputObject.hasField(removedField)).toBeFalsy(); + Object.values(eDTC.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(''); + + eDTC.reorderFields(fieldOrder); + + expect(eDTC.getFieldNames().join('').startsWith(fieldOrderString)).toBeTruthy(); + expect(DInputObject.getFieldNames().join('').startsWith(fieldOrderString)).toBeTruthy(); + Object.values(eDTC.discrimTCs).forEach((dTC) => { + expect(dTC.getFieldNames().join('').startsWith(fieldOrderString)).toBeTruthy(); + }); + }); + }); + + describe('Make Fields Nullable/Non-null', () => { + it('makes a nullable field non-null', () => { + eDTC.addFields({ initialNullable: 'String' }); + expect(eDTC.isFieldNonNull('initialNullable')).toBeFalsy(); + + eDTC.makeFieldNonNull('initialNullable'); + + expect(eDTC.isFieldNonNull('initialNullable')).toBeTruthy(); + expect(DInputObject.isFieldNonNull('initialNullable')).toBeTruthy(); + Object.values(eDTC.discrimTCs).forEach((dTC) => { + expect(dTC.isFieldNonNull('initialNullable')).toBeTruthy(); + }); + }); + + it('makes a non-null field nullable', () => { + eDTC.addFields({ initialNonNull: 'String!' }); + expect(eDTC.isFieldNonNull('initialNonNull')).toBeTruthy(); + + eDTC.makeFieldNullable('initialNonNull'); + + expect(eDTC.isFieldNonNull('initialNonNull')).toBeFalsy(); + expect(DInputObject.isFieldNonNull('initialNonNull')).toBeFalsy(); + Object.values(eDTC.discrimTCs).forEach((dTC) => { + expect(dTC.isFieldNonNull('initialNonNull')).toBeFalsy(); + }); + }); + }); + }); +}); diff --git a/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts new file mode 100644 index 00000000..6bc910f9 --- /dev/null +++ b/src/enhancedDiscriminators/eDiscriminatorTypeComposer.ts @@ -0,0 +1,323 @@ +import { GraphQLInterfaceType } from 'graphql'; +import { + EnumTypeComposer, + Extensions, + InterfaceTypeComposer, + ObjectTypeComposer, + ObjectTypeComposerFieldConfig, + ObjectTypeComposerFieldConfigDefinition, + schemaComposer, + SchemaComposer, +} from 'graphql-compose'; +import { Document, Model, Schema } from 'mongoose'; +import { convertModelToGraphQL, MongoosePseudoModelT } from '../fieldsConverter'; +import { composeMongoose, ComposeMongooseOpts, GenerateResolverType } from '../composeMongoose'; + +export function convertModelToGraphQLWithDiscriminators( + model: Model | MongoosePseudoModelT, + typeName: string, + schemaComposer: SchemaComposer, + opts: ComposeMongooseOpts = {} +): ObjectTypeComposer | InterfaceTypeComposer { + 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 as Model).discriminators || model.schema.discriminators)) { + return convertModelToGraphQL(model, typeName, sc, opts); + } else { + return EDiscriminatorTypeComposer.createFromModel(model as Model, typeName, sc, opts); + } +} + +export interface ComposeMongooseDiscriminatorsOpts extends ComposeMongooseOpts { + reorderFields?: boolean | string[]; // true order: _id, DKey, DInterfaceFields, DiscriminatorFields +} + +export class EDiscriminatorTypeComposer extends InterfaceTypeComposer< + TSource, + TContext +> { + discriminatorKey: string = ''; + discrimTCs: { + [key: string]: + | (ObjectTypeComposer & { + mongooseResolvers: GenerateResolverType; + }) + | ObjectTypeComposer; + } = {}; + + DInputObject: ObjectTypeComposer; + opts: ComposeMongooseDiscriminatorsOpts = {}; + DKeyETC?: EnumTypeComposer; + + constructor(gqType: GraphQLInterfaceType, schemaComposer: SchemaComposer) { + super(gqType, schemaComposer); + + this.DInputObject = schemaComposer.getOrCreateOTC(`${gqType.name}Input`); + return this; + } + + static createFromModel( + model: Model, + typeName: string, + schemaComposer: SchemaComposer, + opts: ComposeMongooseDiscriminatorsOpts + ): EDiscriminatorTypeComposer { + if (!((model as Model).discriminators || 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}BaseOTC`, + schemaComposer, + opts + ) as ObjectTypeComposer; + + // create new enhanced discriminator TC + const eDTC = new EDiscriminatorTypeComposer( + new GraphQLInterfaceType({ + name: typeName, + fields: () => ({}), + }), + schemaComposer + ); + + 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' + ); + } + + eDTC._buildDiscriminatedInterface(model, baseTC); + + // 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); + + // cleanup baseTC + schemaComposer.delete(baseTC.getTypeName()); + return eDTC as any; + } + + _buildDiscriminatedInterface( + model: Model, + baseTC: ObjectTypeComposer + ): EDiscriminatorTypeComposer { + this.setFields(baseTC.getFields()); // should be overriden at internal setField call? + this.DInputObject.setFields(baseTC.getFields()); + + const discriminators = model.discriminators || model.schema.discriminators; + + if (!discriminators) { + throw Error('Discriminators should be present to use this function'); + } + + 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]; + + // 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 + // 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]; + 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)) + ); + + this.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 this; + } + + _addFieldToInputOTC( + fieldName: string, + field: ObjectTypeComposerFieldConfig + ): void { + // if another discrimTC has already defined the field (not from original TC) + if (this.DInputObject.hasField(fieldName) && !this.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; + } + + // getDKeyETC(): EnumTypeComposer { + // return this.DKeyETC as any; + // } + + getDInputObject(): ObjectTypeComposer { + return this.DInputObject as any; + } + + getDiscriminatorTCs(): { + [key: string]: ObjectTypeComposer & { + mongooseResolvers: GenerateResolverType; + }; + } { + // 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 { + return {}; + } + } + + // OVERRIDES + + setField( + fieldName: string, + fieldConfig: ObjectTypeComposerFieldConfigDefinition + ): this { + super.setField(fieldName, fieldConfig); + this.getDInputObject().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); + + for (const discrimTC in this.discrimTCs) { + this.discrimTCs[discrimTC].setExtensions(extensions); + } + + return this; + } + + setDescription(description: string): this { + super.setDescription(description); + this.getDInputObject().setDescription(description); + + return this; + } + + removeField(fieldNameOrArray: string | Array): this { + super.removeField(fieldNameOrArray); + this.getDInputObject().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.getFieldNames().filter( + (field) => !keepFieldNames.includes(field) + ); + + super.removeField(fieldNamesToDelete); + this.getDInputObject().removeField(fieldNamesToDelete); + + for (const discrimTC in this.discrimTCs) { + this.discrimTCs[discrimTC].removeField(fieldNamesToDelete); + } + + return this; + } + + reorderFields(names: string[]): this { + super.reorderFields(names); + this.getDInputObject().reorderFields(names); + + for (const discrimTC in this.discrimTCs) { + this.discrimTCs[discrimTC].reorderFields(names); + } + + return this; + } + + makeFieldNonNull(fieldNameOrArray: string | Array): this { + super.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.getDInputObject().makeFieldNullable(fieldNameOrArray); + + for (const discrimTC in this.discrimTCs) { + this.discrimTCs[discrimTC].makeFieldNullable(fieldNameOrArray); + } + + 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 a2e07240..606b2ae4 100644 --- a/src/fieldsConverter.ts +++ b/src/fieldsConverter.ts @@ -2,17 +2,20 @@ import mongoose, { Document } from 'mongoose'; import type { Schema, Model } from 'mongoose'; -import type { +import { SchemaComposer, ObjectTypeComposer, 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 } from './enhancedDiscriminators'; +import { ComposeMongooseOpts } from './composeMongoose'; type MongooseFieldT = { path?: string; @@ -134,8 +137,9 @@ export function getFieldsFromModel(model: Model | MongoosePseudoModelT): Mo export function convertModelToGraphQL( model: Model | MongoosePseudoModelT, typeName: string, - schemaComposer: SchemaComposer -): ObjectTypeComposer { + schemaComposer: SchemaComposer, + opts: ComposeMongooseOpts = {} +): InterfaceTypeComposer | ObjectTypeComposer { const sc = schemaComposer; if (!typeName) { @@ -147,8 +151,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 +182,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` @@ -209,8 +215,9 @@ export function convertModelToGraphQL( export function convertSchemaToGraphQL( schema: Schema, typeName: string, - schemaComposer: SchemaComposer -): ObjectTypeComposer { + schemaComposer: SchemaComposer, + opts: ComposeMongooseOpts = {} +): ObjectTypeComposer | InterfaceTypeComposer { const sc = schemaComposer; if (!typeName) { @@ -221,7 +228,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(); @@ -232,7 +239,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); @@ -243,15 +251,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: @@ -322,7 +330,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( @@ -333,15 +342,18 @@ 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 -): ObjectTypeComposer { + schemaComposer: SchemaComposer, + opts: ComposeMongooseOpts = {} +): ObjectTypeComposer | InterfaceTypeComposer { const fieldName = _getFieldName(field); const fieldType = _getFieldType(field); @@ -357,8 +369,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( @@ -392,8 +405,9 @@ export function enumToGraphQL( export function documentArrayToGraphQL( field: MongooseFieldT, prefix: string = '', - schemaComposer: SchemaComposer -): [ObjectTypeComposer] { + schemaComposer: SchemaComposer, + opts: ComposeMongooseOpts = {} +): [ObjectTypeComposer | InterfaceTypeComposer] { if (!(field instanceof mongoose.Schema.Types.DocumentArray) && !field?.schema?.paths) { throw new Error( 'You provide incorrect mongoose field to `documentArrayToGraphQL()`. ' + @@ -403,7 +417,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]; } 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 }; 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/connection.ts b/src/resolvers/connection.ts index 8191c2da..04380839 100644 --- a/src/resolvers/connection.ts +++ b/src/resolvers/connection.ts @@ -1,3 +1,4 @@ +import { ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; import type { Document, Model } from 'mongoose'; import { prepareConnectionResolver, @@ -5,7 +6,7 @@ import { ConnectionSortMapOpts as _ConnectionSortMapOpts, ConnectionTArgs, } from 'graphql-compose-connection'; -import type { Resolver, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; +import type { Resolver } 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 82fd539a..9e1d4962 100644 --- a/src/resolvers/count.ts +++ b/src/resolvers/count.ts @@ -1,4 +1,5 @@ -import type { Resolver, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; +import { ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; +import type { Resolver } from 'graphql-compose'; import type { Model, Document } from 'mongoose'; import { filterHelper, @@ -29,7 +30,7 @@ export function count { diff --git a/src/resolvers/createOne.ts b/src/resolvers/createOne.ts index 28f63888..7c87c9a6 100644 --- a/src/resolvers/createOne.ts +++ b/src/resolvers/createOne.ts @@ -1,4 +1,5 @@ -import { Resolver, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; +import { ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; +import type { Resolver } from 'graphql-compose'; import type { Model, Document } from 'mongoose'; import { recordHelperArgs, RecordHelperArgsOpts } from './helpers'; import type { ExtendedResolveParams } from './index'; @@ -30,7 +31,7 @@ export function createOne({ type: tc, name: 'findById', diff --git a/src/resolvers/findByIds.ts b/src/resolvers/findByIds.ts index ff052c8c..7ec19b7a 100644 --- a/src/resolvers/findByIds.ts +++ b/src/resolvers/findByIds.ts @@ -1,5 +1,5 @@ -import { toInputType } from 'graphql-compose'; -import type { Resolver, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; +import { toInputType, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; +import type { Resolver } from 'graphql-compose'; import type { Model, Document } from 'mongoose'; import { limitHelper, @@ -48,7 +48,7 @@ export function findByIds', () => { beforeEach(() => { schemaComposer.clear(); - UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer); + UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer) as ObjectTypeComposer; }); describe('filterHelperArgs()', () => { diff --git a/src/resolvers/helpers/__tests__/limit-test.ts b/src/resolvers/helpers/__tests__/limit-test.ts index 213c8cfd..b1b4bfa7 100644 --- a/src/resolvers/helpers/__tests__/limit-test.ts +++ b/src/resolvers/helpers/__tests__/limit-test.ts @@ -9,11 +9,9 @@ describe('Resolver helper `limit` ->', () => { it('should process `opts.defaultValue` arg', () => { expect((limitHelperArgs() as any).limit.defaultValue).toBe(100); expect( - ( - limitHelperArgs({ - defaultValue: 333, - }) as any - ).limit.defaultValue + (limitHelperArgs({ + defaultValue: 333, + }) as any).limit.defaultValue ).toBe(333); }); }); 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) { diff --git a/src/resolvers/helpers/record.ts b/src/resolvers/helpers/record.ts index 0271aec9..17c41f6c 100644 --- a/src/resolvers/helpers/record.ts +++ b/src/resolvers/helpers/record.ts @@ -46,7 +46,7 @@ export function recordHelperArgs( tc: ObjectTypeComposer | InterfaceTypeComposer, opts?: RecordHelperArgsOpts ): ObjectTypeComposerArgumentConfigMapDefinition<{ record: any }> { - if (!tc || tc.constructor.name !== 'ObjectTypeComposer') { + if (!tc || !(tc instanceof ObjectTypeComposer || tc instanceof InterfaceTypeComposer)) { 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 31259786..e46f8188 100644 --- a/src/resolvers/helpers/sort.ts +++ b/src/resolvers/helpers/sort.ts @@ -29,7 +29,7 @@ export function sortHelperArgs( model: Model, opts?: SortHelperArgsOpts ): ObjectTypeComposerArgumentConfigMapDefinition<{ sort: any }> { - if (!tc || tc.constructor.name !== 'ObjectTypeComposer') { + if (!tc || !(tc instanceof ObjectTypeComposer || tc instanceof InterfaceTypeComposer)) { 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 a72fad88..bb7a7b98 100644 --- a/src/resolvers/pagination.ts +++ b/src/resolvers/pagination.ts @@ -1,4 +1,5 @@ -import type { Resolver, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; +import { ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; +import type { Resolver } from 'graphql-compose'; import type { Model, Document } from 'mongoose'; import { preparePaginationResolver, diff --git a/src/resolvers/removeById.ts b/src/resolvers/removeById.ts index 99efe0df..d0be7d75 100644 --- a/src/resolvers/removeById.ts +++ b/src/resolvers/removeById.ts @@ -1,5 +1,5 @@ -import { toInputType } from 'graphql-compose'; -import type { Resolver, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; +import { toInputType, ObjectTypeComposer, InterfaceTypeComposer } from 'graphql-compose'; +import type { Resolver } from 'graphql-compose'; import type { Model, Document } from 'mongoose'; import { findById } from './findById'; import type { ExtendedResolveParams } from './index'; @@ -28,7 +28,7 @@ export function removeById