Skip to content

Commit f36e276

Browse files
committed
feat: added new function composeMongoose which generates TypeComposer without resolvers. Now Resolvers can be generated on-demand in such way PostTC.generateResolver.findMany() (before was PostTC.getResolver('findMany')).
related #263
1 parent 7e3de3d commit f36e276

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+330
-134
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { schemaComposer, graphql } from 'graphql-compose';
2+
import { composeMongoose } from '../../index';
3+
import { mongoose } from '../../__mocks__/mongooseCommon';
4+
import { Document } from 'mongoose';
5+
6+
const UserSchema = new mongoose.Schema({
7+
name: { type: String, required: true },
8+
});
9+
10+
interface IUser extends Document {
11+
name: string;
12+
}
13+
14+
const PostSchema = new mongoose.Schema({
15+
title: { type: String, required: true },
16+
authorId: { type: mongoose.Types.ObjectId },
17+
reviewerIds: { type: [mongoose.Types.ObjectId] },
18+
});
19+
20+
interface IPost extends Document {
21+
title: string;
22+
authorId?: mongoose.Types.ObjectId;
23+
reviewerIds?: [mongoose.Types.ObjectId];
24+
}
25+
26+
const UserModel = mongoose.model<IUser>('User', UserSchema);
27+
const PostModel = mongoose.model<IPost>('Post', PostSchema);
28+
29+
const UserTC = composeMongoose(UserModel);
30+
const PostTC = composeMongoose(PostModel);
31+
32+
PostTC.addRelation('author', {
33+
resolver: UserTC.generateResolver.dataLoaderLean(),
34+
prepareArgs: {
35+
_id: (s) => s.authorId,
36+
},
37+
projection: { authorId: true },
38+
});
39+
40+
PostTC.addRelation('reviewers', {
41+
resolver: UserTC.generateResolver.dataLoaderManyLean(),
42+
prepareArgs: {
43+
_ids: (s) => s.reviewerIds,
44+
},
45+
projection: { reviewerIds: true },
46+
});
47+
48+
schemaComposer.Query.addFields({
49+
posts: PostTC.generateResolver.findMany(),
50+
});
51+
const schema = schemaComposer.buildSchema();
52+
53+
// console.log(schemaComposer.toSDL());
54+
55+
beforeAll(async () => {
56+
await UserModel.base.createConnection();
57+
const User1 = await UserModel.create({ name: 'User1' });
58+
const User2 = await UserModel.create({ name: 'User2' });
59+
const User3 = await UserModel.create({ name: 'User3' });
60+
61+
await PostModel.create({ title: 'Post1', authorId: User1._id });
62+
await PostModel.create({ title: 'Post2', authorId: User1._id, reviewerIds: [] });
63+
await PostModel.create({ title: 'Post3', authorId: User1._id, reviewerIds: [User2._id] });
64+
await PostModel.create({ title: 'Post4', authorId: User2._id, reviewerIds: [User1._id] });
65+
await PostModel.create({ title: 'Post5', authorId: User2._id, reviewerIds: [User1._id] });
66+
await PostModel.create({
67+
title: 'Post6',
68+
authorId: User3._id,
69+
reviewerIds: [User1._id, User2._id],
70+
});
71+
});
72+
afterAll(() => UserModel.base.disconnect());
73+
74+
describe('issue #263 - new resolvers which works via DataLoader', () => {
75+
it('check response', async () => {
76+
// 👀 uncomment next line if you want to see real mongoose queries
77+
// mongoose.set('debug', true);
78+
79+
expect(
80+
await graphql.graphql({
81+
schema,
82+
source: `query {
83+
posts {
84+
title
85+
author { name }
86+
reviewers { name }
87+
}
88+
}`,
89+
contextValue: {},
90+
})
91+
).toEqual({
92+
data: {
93+
posts: [
94+
{ title: 'Post1', author: { name: 'User1' }, reviewers: [] },
95+
{ title: 'Post2', author: { name: 'User1' }, reviewers: [] },
96+
{ title: 'Post3', author: { name: 'User1' }, reviewers: [{ name: 'User2' }] },
97+
{ title: 'Post4', author: { name: 'User2' }, reviewers: [{ name: 'User1' }] },
98+
{ title: 'Post5', author: { name: 'User2' }, reviewers: [{ name: 'User1' }] },
99+
{
100+
title: 'Post6',
101+
author: { name: 'User3' },
102+
reviewers: [{ name: 'User1' }, { name: 'User2' }],
103+
},
104+
],
105+
},
106+
});
107+
});
108+
});

src/composeMongoose.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { ObjectTypeComposer, SchemaComposer, Resolver } from 'graphql-compose';
2+
import { schemaComposer as globalSchemaComposer } from 'graphql-compose';
3+
import type { Model, Document } from 'mongoose';
4+
import { convertModelToGraphQL } from './fieldsConverter';
5+
import { allResolvers } from './resolvers';
6+
import MongoID from './types/MongoID';
7+
import { ArgsMap } from './resolvers/helpers';
8+
import {
9+
prepareFields,
10+
createInputType,
11+
TransformRecordIdFn,
12+
TypeConverterInputTypeOpts,
13+
} from './composeWithMongoose';
14+
15+
export type ComposeMongooseOpts<TContext> = {
16+
schemaComposer?: SchemaComposer<TContext>;
17+
name?: string;
18+
description?: string;
19+
fields?: {
20+
only?: string[];
21+
// rename?: { [oldName: string]: string },
22+
remove?: string[];
23+
};
24+
inputType?: TypeConverterInputTypeOpts;
25+
/** You may customize `recordId` field in mutation payloads */
26+
transformRecordId?: TransformRecordIdFn<TContext>;
27+
};
28+
29+
export type GenerateResolverType<TDoc extends Document, TContext = any> = {
30+
// get all available resolver generators, then leave only 3rd arg – opts
31+
// because first two args will be attached via bind() method at runtime:
32+
// count = count.bind(undefined, model, tc);
33+
[resolver in keyof typeof allResolvers]: <TSource = any>(
34+
opts?: Parameters<typeof allResolvers[resolver]>[2]
35+
) => Resolver<TSource, TContext, ArgsMap, TDoc>;
36+
};
37+
38+
export function composeMongoose<TDoc extends Document, TContext = any>(
39+
model: Model<TDoc>,
40+
opts: ComposeMongooseOpts<TContext> = {}
41+
): ObjectTypeComposer<TDoc, TContext> & {
42+
generateResolver: GenerateResolverType<TDoc, TContext>;
43+
} {
44+
const m: Model<any> = model;
45+
const name: string = (opts && opts.name) || m.modelName;
46+
47+
const sc = opts.schemaComposer || globalSchemaComposer;
48+
sc.add(MongoID);
49+
50+
if (sc.has(name)) {
51+
throw new Error(
52+
`You try to generate GraphQL Type with name ${name} from mongoose model but this type already exists in SchemaComposer. Please choose another type name "composeWithMongoose(model, { name: 'NewTypeName' })", or reuse existed type "schemaComposer.getOTC('TypeName')", or remove type from SchemaComposer before calling composeWithMongoose method "schemaComposer.delete('TypeName')".`
53+
);
54+
}
55+
if (sc.has(m.schema)) {
56+
// looks like you want to generate new TypeComposer from model
57+
// so remove cached model (which is used for cross-reference types)
58+
sc.delete(m.schema);
59+
}
60+
61+
const tc = convertModelToGraphQL(m, name, sc);
62+
63+
if (opts.description) {
64+
tc.setDescription(opts.description);
65+
}
66+
67+
if (opts.fields) {
68+
prepareFields(tc, opts.fields);
69+
}
70+
71+
if (opts.inputType) {
72+
// generate input type only it was customized
73+
createInputType(tc, opts.inputType);
74+
}
75+
76+
tc.makeFieldNonNull('_id');
77+
78+
const generateResolver = {} as any;
79+
Object.keys(allResolvers).forEach((name) => {
80+
generateResolver[name] = (allResolvers as any)[name].bind(undefined, model, tc);
81+
});
82+
(tc as any).generateResolver = generateResolver;
83+
84+
return tc as any;
85+
}

src/composeWithMongoose.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { ObjectTypeComposer, InputTypeComposer, SchemaComposer } from 'grap
44
import { schemaComposer as globalSchemaComposer } from 'graphql-compose';
55
import type { Model, Document } from 'mongoose';
66
import { convertModelToGraphQL } from './fieldsConverter';
7-
import * as resolvers from './resolvers';
7+
import { allResolvers, AllResolversOpts } from './resolvers';
88
import MongoID from './types/MongoID';
99
import { GraphQLResolveInfo } from 'graphql';
1010

@@ -18,7 +18,7 @@ export type ComposeWithMongooseOpts<TContext> = {
1818
remove?: string[];
1919
};
2020
inputType?: TypeConverterInputTypeOpts;
21-
resolvers?: false | resolvers.AllResolversOpts;
21+
resolvers?: false | AllResolversOpts;
2222
/** You may customize document id */
2323
transformRecordId?: TransformRecordIdFn<TContext>;
2424
};
@@ -145,11 +145,11 @@ export function createInputType(
145145
export function createResolvers(
146146
model: Model<any>,
147147
tc: ObjectTypeComposer<any, any>,
148-
opts: resolvers.AllResolversOpts
148+
opts: AllResolversOpts
149149
): void {
150-
resolvers.availableResolverNames.forEach((resolverName) => {
151-
if (!{}.hasOwnProperty.call(opts, resolverName) || opts[resolverName] !== false) {
152-
const createResolverFn = resolvers[resolverName] as any;
150+
(Object.keys(allResolvers) as any).forEach((resolverName: keyof typeof allResolvers) => {
151+
if (!opts.hasOwnProperty(resolverName) || opts[resolverName] !== false) {
152+
const createResolverFn = allResolvers[resolverName] as any;
153153
if (typeof createResolverFn === 'function') {
154154
const resolver = createResolverFn(model, tc, opts[resolverName] || {});
155155
if (resolver) {

src/discriminators/prepareBaseResolvers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Resolver } from 'graphql-compose';
2-
import { availableResolverNames } from '../resolvers';
2+
import { allResolvers } from '../resolvers';
33
import { DiscriminatorTypeComposer } from './DiscriminatorTypeComposer';
44

55
// change type on DKey generated by composeWithMongoose
@@ -28,7 +28,7 @@ function setDKeyEnumOnITCArgs(resolver: Resolver, baseTC: DiscriminatorTypeCompo
2828
// Also sets up DKey enum as type for DKey field on composers with filter and/or record args
2929
// composeWithMongoose composers
3030
export function prepareBaseResolvers(baseTC: DiscriminatorTypeComposer<any, any>): void {
31-
availableResolverNames.forEach((resolverName) => {
31+
Object.keys(allResolvers).forEach((resolverName) => {
3232
if (baseTC.hasResolver(resolverName)) {
3333
const resolver = baseTC.getResolver(resolverName);
3434

src/discriminators/prepareChildResolvers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
ComposeWithMongooseDiscriminatorsOpts,
44
DiscriminatorTypeComposer,
55
} from './DiscriminatorTypeComposer';
6-
import { availableResolverNames } from '../resolvers';
6+
import { allResolvers } from '../resolvers';
77

88
// set the DKey as a query on filter, also project it
99
// Also look at it like setting for filters, makes sure to limit
@@ -136,7 +136,7 @@ export function prepareChildResolvers<TSource, TContext>(
136136
childTC: ObjectTypeComposer<TSource, TContext>,
137137
opts: ComposeWithMongooseDiscriminatorsOpts<TContext>
138138
): void {
139-
availableResolverNames.forEach((resolverName) => {
139+
Object.keys(allResolvers).forEach((resolverName) => {
140140
if (childTC.hasResolver(resolverName)) {
141141
const resolver = childTC.getResolver(resolverName);
142142

src/fieldsConverter.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ export function convertModelToGraphQL<TDoc extends Document, TContext>(
146146
return sc.getOTC(model.schema);
147147
}
148148

149+
// add type to registry before fields creation
150+
// it helps to avoid circuit dependencies, eg. `User { friends: [User] }`
149151
const typeComposer = sc.getOrCreateOTC(typeName);
150152
sc.set(model.schema, typeComposer);
151153
sc.set(typeName, typeComposer);
@@ -167,7 +169,7 @@ export function convertModelToGraphQL<TDoc extends Document, TContext>(
167169

168170
if (
169171
mongooseField.isRequired &&
170-
// conditional required field in mongoose cannot be NonNulable in GraphQL
172+
// conditional required field in mongoose cannot be NonNullable in GraphQL
171173
typeof mongooseField?.originalRequiredValue !== 'function'
172174
) {
173175
requiredFields.push(fieldName);

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import GraphQLBSONDecimal from './types/BSONDecimal';
55
export default composeWithMongoose;
66

77
export * from './composeWithMongoose';
8+
export * from './composeMongoose';
89
export * from './composeWithMongooseDiscriminators';
910
export * from './fieldsConverter';
1011
export * from './resolvers';

src/resolvers/__tests__/connection-test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Resolver, schemaComposer, ObjectTypeComposer } from 'graphql-compose';
22
import { Query } from 'mongoose';
33
import { UserModel, IUser } from '../../__mocks__/userModel';
4-
import connection, { prepareCursorQuery } from '../connection';
5-
import findMany from '../findMany';
6-
import count from '../count';
4+
import { connection, prepareCursorQuery } from '../connection';
5+
import { findMany } from '../findMany';
6+
import { count } from '../count';
77
import { convertModelToGraphQL } from '../../fieldsConverter';
88
import { ExtendedResolveParams } from '..';
99

src/resolvers/__tests__/count-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Resolver, schemaComposer, ObjectTypeComposer } from 'graphql-compose';
22
import { GraphQLInt } from 'graphql-compose/lib/graphql';
33
import { UserModel } from '../../__mocks__/userModel';
4-
import count from '../count';
4+
import { count } from '../count';
55
import { convertModelToGraphQL } from '../../fieldsConverter';
66
import { ExtendedResolveParams } from '..';
77

src/resolvers/__tests__/createMany-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { GraphQLInt, GraphQLList, GraphQLNonNull } from 'graphql-compose/lib/gra
55
import { mongoose } from '../../__mocks__/mongooseCommon';
66
import { UserModel, IUser } from '../../__mocks__/userModel';
77
import { convertModelToGraphQL } from '../../fieldsConverter';
8-
import createMany from '../createMany';
8+
import { createMany } from '../createMany';
99
import { ExtendedResolveParams } from '..';
1010

1111
beforeAll(() => UserModel.base.createConnection());

0 commit comments

Comments
 (0)