Skip to content

Commit d4f6cee

Browse files
committed
feat: add support for custom _id in models. Before it was only MongoID. Now it can be of any type (Int, String, Object).
closes #141
1 parent f36e276 commit d4f6cee

30 files changed

+568
-324
lines changed

package.json

+8-8
Original file line numberDiff line numberDiff line change
@@ -31,27 +31,27 @@
3131
"graphql-compose-pagination": "^7.0.0"
3232
},
3333
"peerDependencies": {
34-
"graphql-compose": "^7.1.0",
34+
"graphql-compose": "^7.21.0",
3535
"mongoose": "^5.0.0 || ^4.4.0"
3636
},
3737
"devDependencies": {
3838
"@types/graphql": "14.5.0",
39-
"@types/jest": "26.0.10",
39+
"@types/jest": "26.0.13",
4040
"@types/mongoose": "5.7.36",
41-
"@typescript-eslint/eslint-plugin": "3.10.1",
42-
"@typescript-eslint/parser": "3.10.1",
43-
"eslint": "7.7.0",
41+
"@typescript-eslint/eslint-plugin": "4.1.0",
42+
"@typescript-eslint/parser": "4.1.0",
43+
"eslint": "7.8.1",
4444
"eslint-config-airbnb-base": "14.2.0",
4545
"eslint-config-prettier": "6.11.0",
4646
"eslint-plugin-import": "2.22.0",
4747
"eslint-plugin-prettier": "3.1.4",
4848
"graphql": "15.3.0",
49-
"graphql-compose": "7.20.1",
49+
"graphql-compose": "7.21.0",
5050
"graphql-compose-connection": "^7.0.0",
5151
"graphql-compose-pagination": "^7.0.0",
5252
"jest": "26.4.2",
53-
"mongodb-memory-server": "6.6.6",
54-
"mongoose": "5.10.1",
53+
"mongodb-memory-server": "6.7.4",
54+
"mongoose": "5.10.4",
5555
"prettier": "2.1.1",
5656
"request": "2.88.2",
5757
"rimraf": "3.0.2",

src/__tests__/github_issues/136-test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ describe('issue #136 - Mongoose virtuals', () => {
5858

5959
const res = await graphql.graphql({
6060
schema,
61-
source: 'mutation { createManyComments(records: [{ links: ["a"] }]) { createCount } }',
61+
source: 'mutation { createManyComments(records: [{ links: ["a"] }]) { createdCount } }',
6262
});
6363

64-
expect(res).toEqual({ data: { createManyComments: { createCount: 1 } } });
64+
expect(res).toEqual({ data: { createManyComments: { createdCount: 1 } } });
6565
});
6666
});
+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { SchemaComposer } from 'graphql-compose';
2+
import { composeMongoose } from '../../index';
3+
import { mongoose } from '../../__mocks__/mongooseCommon';
4+
import { Document } from 'mongoose';
5+
import { testFieldConfig } from '../../utils/testHelpers';
6+
7+
const schemaComposer = new SchemaComposer<{ req: any }>();
8+
9+
const UserSchema = new mongoose.Schema({
10+
_id: { type: Number },
11+
name: { type: String, required: true },
12+
});
13+
interface IUser extends Document {
14+
_id: number;
15+
name: string;
16+
}
17+
18+
const UserModel = mongoose.model<IUser>('User', UserSchema);
19+
const UserTC = composeMongoose(UserModel, { schemaComposer });
20+
21+
schemaComposer.Query.addFields({
22+
userById: UserTC.generateResolver.findById(),
23+
userFindOne: UserTC.generateResolver.findOne(),
24+
});
25+
schemaComposer.Mutation.addFields({
26+
userCreateOne: UserTC.generateResolver.createOne(),
27+
userUpdateById: UserTC.generateResolver.updateById(),
28+
});
29+
30+
// const schema = schemaComposer.buildSchema();
31+
// console.log(schemaComposer.toSDL());
32+
33+
beforeAll(async () => {
34+
await UserModel.base.createConnection();
35+
await UserModel.create({ _id: 1, name: 'User1' });
36+
});
37+
afterAll(() => UserModel.base.disconnect());
38+
39+
describe('issue #141 - createOne with custom id (not MongoId)', () => {
40+
it('mongoose should have doc with numeric id', async () => {
41+
const user1 = await UserModel.findById(1);
42+
expect(user1?._id).toBe(1);
43+
expect(user1?.name).toBe('User1');
44+
});
45+
46+
it('UserTC should have _id with Int type', () => {
47+
expect(UserTC.getFieldTypeName('_id')).toBe('Int!');
48+
});
49+
50+
it('Resolvers *ById should have Int type for args._id', () => {
51+
expect(UserTC.generateResolver.findById().getArgTypeName('_id')).toBe('Int!');
52+
expect(UserTC.generateResolver.findByIdLean().getArgTypeName('_id')).toBe('Int!');
53+
expect(UserTC.generateResolver.removeById().getArgTypeName('_id')).toBe('Int!');
54+
expect(UserTC.generateResolver.updateById().getArgTypeName('_id')).toBe('Int!');
55+
56+
expect(UserTC.generateResolver.findByIds().getArgTypeName('_ids')).toBe('[Int!]!');
57+
expect(UserTC.generateResolver.findByIdsLean().getArgTypeName('_ids')).toBe('[Int!]!');
58+
});
59+
60+
it('Resolvers dataLoader* should have Int type for args._id', () => {
61+
expect(UserTC.generateResolver.dataLoader().getArgTypeName('_id')).toBe('Int!');
62+
expect(UserTC.generateResolver.dataLoaderLean().getArgTypeName('_id')).toBe('Int!');
63+
expect(UserTC.generateResolver.dataLoaderMany().getArgTypeName('_ids')).toBe('[Int!]!');
64+
expect(UserTC.generateResolver.dataLoaderManyLean().getArgTypeName('_ids')).toBe('[Int!]!');
65+
});
66+
67+
it('Check createOne/findOne resolvers', async () => {
68+
UserTC.generateResolver.createOne();
69+
70+
expect(
71+
await testFieldConfig({
72+
field: UserTC.generateResolver.createOne({
73+
suffix: 'WithId',
74+
record: {
75+
removeFields: [], // <-- empty array allows to override removing _id arg
76+
},
77+
}),
78+
args: {
79+
record: { _id: 15, name: 'John' },
80+
},
81+
selection: `{
82+
record {
83+
_id
84+
name
85+
}
86+
}`,
87+
})
88+
).toEqual({ record: { _id: 15, name: 'John' } });
89+
90+
expect(
91+
await testFieldConfig({
92+
field: UserTC.generateResolver.findById(),
93+
args: {
94+
_id: 15,
95+
},
96+
selection: `{
97+
_id
98+
name
99+
}`,
100+
})
101+
).toEqual({ _id: 15, name: 'John' });
102+
});
103+
104+
it('Check ComplexObject as _id', async () => {
105+
const ComplexIdSchema = new mongoose.Schema(
106+
{
107+
region: { type: String, required: true },
108+
zone: String,
109+
},
110+
{
111+
_id: false, // disable _id field in sub-schema
112+
}
113+
);
114+
const ComplexSchema = new mongoose.Schema({
115+
_id: { type: ComplexIdSchema },
116+
name: { type: String, required: true },
117+
});
118+
const ComplexModel = mongoose.model('Complex', ComplexSchema);
119+
const ComplexTC = composeMongoose(ComplexModel);
120+
121+
expect(ComplexTC.getFieldTypeName('_id')).toBe('Complex_id!');
122+
expect(ComplexTC.schemaComposer.getOTC('Complex_id').toSDL()).toMatchInlineSnapshot(`
123+
"type Complex_id {
124+
region: String!
125+
zone: String
126+
}"
127+
`);
128+
129+
expect(ComplexTC.schemaComposer.getITC('Complex_idInput').toSDL()).toMatchInlineSnapshot(`
130+
"input Complex_idInput {
131+
region: String!
132+
zone: String
133+
}"
134+
`);
135+
expect(ComplexTC.generateResolver.findById().getArgTypeName('_id')).toBe('Complex_idInput!');
136+
expect(ComplexTC.generateResolver.findByIdLean().getArgTypeName('_id')).toBe(
137+
'Complex_idInput!'
138+
);
139+
expect(ComplexTC.generateResolver.removeById().getArgTypeName('_id')).toBe('Complex_idInput!');
140+
expect(ComplexTC.generateResolver.updateById().getArgTypeName('_id')).toBe('Complex_idInput!');
141+
expect(ComplexTC.generateResolver.findByIds().getArgTypeName('_ids')).toBe(
142+
'[Complex_idInput!]!'
143+
);
144+
expect(ComplexTC.generateResolver.findByIdsLean().getArgTypeName('_ids')).toBe(
145+
'[Complex_idInput!]!'
146+
);
147+
expect(ComplexTC.generateResolver.dataLoader().getArgTypeName('_id')).toBe('Complex_idInput!');
148+
expect(ComplexTC.generateResolver.dataLoaderLean().getArgTypeName('_id')).toBe(
149+
'Complex_idInput!'
150+
);
151+
expect(ComplexTC.generateResolver.dataLoaderMany().getArgTypeName('_ids')).toBe(
152+
'[Complex_idInput!]!'
153+
);
154+
expect(ComplexTC.generateResolver.dataLoaderManyLean().getArgTypeName('_ids')).toBe(
155+
'[Complex_idInput!]!'
156+
);
157+
158+
await ComplexModel.create({
159+
_id: { region: 'us-west', zone: 'a' },
160+
name: 'Compute',
161+
});
162+
expect(
163+
await testFieldConfig({
164+
field: ComplexTC.generateResolver.findById(),
165+
args: { _id: { region: 'us-west', zone: 'a' } },
166+
selection: `{
167+
_id {
168+
region
169+
zone
170+
}
171+
name
172+
}`,
173+
})
174+
).toEqual({ _id: { region: 'us-west', zone: 'a' }, name: 'Compute' });
175+
});
176+
});

src/composeMongoose.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ export type GenerateResolverType<TDoc extends Document, TContext = any> = {
3030
// get all available resolver generators, then leave only 3rd arg – opts
3131
// because first two args will be attached via bind() method at runtime:
3232
// count = count.bind(undefined, model, tc);
33+
// TODO: explain infer
3334
[resolver in keyof typeof allResolvers]: <TSource = any>(
3435
opts?: Parameters<typeof allResolvers[resolver]>[2]
35-
) => Resolver<TSource, TContext, ArgsMap, TDoc>;
36+
) => typeof allResolvers[resolver] extends (...args: any) => Resolver<any, any, infer TArgs, any>
37+
? Resolver<TSource, TContext, TArgs, TDoc>
38+
: any;
3639
};
3740

3841
export function composeMongoose<TDoc extends Document, TContext = any>(

src/fieldsConverter.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,16 @@ export function convertModelToGraphQL<TDoc extends Document, TContext>(
175175
requiredFields.push(fieldName);
176176
}
177177

178+
let type = convertFieldToGraphQL(mongooseField, typeName, sc);
179+
180+
// in mongoose schema we use javascript `Number` object which casted to `Float` type
181+
// so in most cases _id field is `Int`
182+
if (fieldName === '_id' && type === 'Float') {
183+
type = 'Int';
184+
}
185+
178186
graphqlFields[fieldName] = {
179-
type: convertFieldToGraphQL(mongooseField, typeName, sc),
187+
type,
180188
description: _getFieldDescription(mongooseField),
181189
};
182190

src/resolvers/__tests__/createMany-test.ts

+33-19
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
/* eslint-disable no-param-reassign,func-names */
22

33
import { Resolver, schemaComposer, ObjectTypeComposer } from 'graphql-compose';
4-
import { GraphQLInt, GraphQLList, GraphQLNonNull } from 'graphql-compose/lib/graphql';
4+
import { GraphQLList, GraphQLNonNull } from 'graphql-compose/lib/graphql';
55
import { mongoose } from '../../__mocks__/mongooseCommon';
66
import { UserModel, IUser } from '../../__mocks__/userModel';
77
import { convertModelToGraphQL } from '../../fieldsConverter';
88
import { createMany } from '../createMany';
99
import { ExtendedResolveParams } from '..';
10+
import { testFieldConfig } from '../../utils/testHelpers';
1011

1112
beforeAll(() => UserModel.base.createConnection());
1213
afterAll(() => UserModel.base.disconnect());
@@ -90,11 +91,12 @@ describe('createMany() ->', () => {
9091
});
9192

9293
it('should return payload.recordIds', async () => {
93-
const result = await createMany(UserModel, UserTC).resolve({
94-
args: {
95-
records: [{ name: 'newName', contacts: { email: 'mail' } }],
96-
},
97-
projection: { error: true },
94+
const result = await testFieldConfig({
95+
field: createMany(UserModel, UserTC),
96+
args: { records: [{ name: 'newName', contacts: { email: 'mail' } }] },
97+
selection: `{
98+
recordIds
99+
}`,
98100
});
99101
expect(result.recordIds).toBeTruthy();
100102
});
@@ -109,7 +111,7 @@ describe('createMany() ->', () => {
109111
},
110112
projection: { error: true },
111113
});
112-
expect(result.createCount).toBe(2);
114+
expect(result.createdCount).toBe(2);
113115
expect(result.records[0].name).toBe('newName0');
114116
expect(result.records[1].name).toBe('newName1');
115117
});
@@ -130,28 +132,40 @@ describe('createMany() ->', () => {
130132

131133
it('should save documents to database', async () => {
132134
const checkedName = 'nameForMongoDB';
133-
const res = await createMany(UserModel, UserTC).resolve({
135+
const res = await testFieldConfig({
136+
field: createMany(UserModel, UserTC),
134137
args: {
135138
records: [
136139
{ name: checkedName, contacts: { email: 'mail' } },
137140
{ name: checkedName, contacts: { email: 'mail' } },
138141
],
139142
},
140-
projection: { error: true },
143+
selection: `{
144+
records {
145+
_id
146+
}
147+
recordIds
148+
}`,
141149
});
142150

143-
const docs = await UserModel.collection.find({ _id: { $in: res.recordIds } }).toArray();
151+
const docs = await UserModel.collection
152+
.find({ _id: { $in: res.recordIds.map((o: string) => new mongoose.Types.ObjectId(o)) } })
153+
.toArray();
144154
expect(docs.length).toBe(2);
145155
expect(docs[0].n).toBe(checkedName);
146156
expect(docs[1].n).toBe(checkedName);
147157
});
148158

149159
it('should return payload.records', async () => {
150-
const result = await createMany(UserModel, UserTC).resolve({
151-
args: {
152-
records: [{ name: 'NewUser', contacts: { email: 'mail' } }],
153-
},
154-
projection: { error: true },
160+
const result = await testFieldConfig({
161+
field: createMany(UserModel, UserTC),
162+
args: { records: [{ name: 'newName', contacts: { email: 'mail' } }] },
163+
selection: `{
164+
records {
165+
_id
166+
}
167+
recordIds
168+
}`,
155169
});
156170
expect(result.records[0]._id).toBe(result.recordIds[0]);
157171
});
@@ -254,10 +268,10 @@ describe('createMany() ->', () => {
254268
);
255269
});
256270

257-
it('should have createCount field, Int', () => {
258-
const outputType: any = createMany(UserModel, UserTC).getType();
259-
const recordField = schemaComposer.createObjectTC(outputType).getFieldConfig('createCount');
260-
expect(recordField.type).toEqual(new GraphQLNonNull(GraphQLInt));
271+
it('should have createdCount field, Int', () => {
272+
expect(createMany(UserModel, UserTC).getOTC().getFieldTypeName('createdCount')).toEqual(
273+
'Int!'
274+
);
261275
});
262276

263277
it('should reuse existed outputType', () => {

src/resolvers/__tests__/dataLoaderMany-test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('dataLoaderMany() ->', () => {
5050
describe('Resolver.args', () => {
5151
it('should have non-null `_id` arg', () => {
5252
const resolver = dataLoaderMany(UserModel, UserTC);
53-
expect(resolver.getArgTypeName('_ids')).toBe('[MongoID]!');
53+
expect(resolver.getArgTypeName('_ids')).toBe('[MongoID!]!');
5454
});
5555
});
5656

src/resolvers/__tests__/dataLoaderManyLean-test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('dataLoaderManyLean() ->', () => {
5050
describe('Resolver.args', () => {
5151
it('should have non-null `_id` arg', () => {
5252
const resolver = dataLoaderManyLean(UserModel, UserTC);
53-
expect(resolver.getArgTypeName('_ids')).toBe('[MongoID]!');
53+
expect(resolver.getArgTypeName('_ids')).toBe('[MongoID!]!');
5454
});
5555
});
5656

src/resolvers/__tests__/findByIds-test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe('findByIds() ->', () => {
5151
describe('Resolver.args', () => {
5252
it('should have non-null `_ids` arg', () => {
5353
const resolver = findByIds(UserModel, UserTC);
54-
expect(resolver.getArgTypeName('_ids')).toBe('[MongoID]!');
54+
expect(resolver.getArgTypeName('_ids')).toBe('[MongoID!]!');
5555
});
5656

5757
it('should have `limit` arg', () => {

src/resolvers/__tests__/findByIdsLean-test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe('findByIdsLean() ->', () => {
5151
describe('Resolver.args', () => {
5252
it('should have non-null `_ids` arg', () => {
5353
const resolver = findByIdsLean(UserModel, UserTC);
54-
expect(resolver.getArgTypeName('_ids')).toBe('[MongoID]!');
54+
expect(resolver.getArgTypeName('_ids')).toBe('[MongoID!]!');
5555
});
5656

5757
it('should have `limit` arg', () => {

0 commit comments

Comments
 (0)