Skip to content

Commit 93f265d

Browse files
committed
feat: add non-nullability for mongoose fields that have a default value
closes #261
1 parent 321dded commit 93f265d

File tree

5 files changed

+153
-10
lines changed

5 files changed

+153
-10
lines changed

src/__tests__/fieldConverter-test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable no-unused-expressions, no-template-curly-in-string */
22

3-
import { EnumTypeComposer, schemaComposer, ListComposer, SchemaComposer } from 'graphql-compose';
3+
import { EnumTypeComposer, schemaComposer, SchemaComposer } from 'graphql-compose';
44
import { UserModel } from '../__mocks__/userModel';
55
import { mongoose } from '../__mocks__/mongooseCommon';
66
import {
@@ -239,9 +239,8 @@ describe('fieldConverter', () => {
239239
it('test object with field as array', () => {
240240
const someDeepTC = embeddedToGraphQL(fields.someDeep, '', schemaComposer);
241241
expect(someDeepTC.getTypeName()).toBe('SomeDeep');
242-
expect(someDeepTC.getField('periods').type).toBeInstanceOf(ListComposer);
242+
expect(someDeepTC.getFieldTypeName('periods')).toBe('[SomeDeepPeriods]');
243243
const tc = someDeepTC.getFieldOTC('periods');
244-
expect(tc.getTypeName()).toBe('SomeDeepPeriods');
245244
expect(tc.hasField('from')).toBeTruthy();
246245
expect(tc.hasField('to')).toBeTruthy();
247246
});
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { SchemaComposer, dedent } 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+
n: {
12+
type: String,
13+
alias: 'name',
14+
default: 'User',
15+
},
16+
age: { type: Number },
17+
isActive: { type: Boolean, default: false },
18+
analytics: {
19+
isEnabled: { type: Boolean, default: false },
20+
},
21+
periods: [{ from: Number, to: Number, _id: false }],
22+
});
23+
interface IUser extends Document {
24+
_id: number;
25+
name: string;
26+
age?: number;
27+
isActive: boolean;
28+
analytics: {
29+
isEnabled: boolean;
30+
};
31+
periods: Array<{ from: number; to: number }>;
32+
}
33+
34+
const UserModel = mongoose.model<IUser>('User', UserSchema);
35+
const UserTC = composeMongoose(UserModel, { schemaComposer });
36+
37+
schemaComposer.Query.addFields({
38+
userById: UserTC.mongooseResolvers.findById(),
39+
});
40+
41+
// const schema = schemaComposer.buildSchema();
42+
// console.log(schemaComposer.toSDL());
43+
44+
beforeAll(async () => {
45+
await UserModel.base.createConnection();
46+
await UserModel.create({ _id: 1 } as any);
47+
});
48+
afterAll(() => UserModel.base.disconnect());
49+
50+
describe('issue #261 - Non-nullability for mongoose fields that have a default value', () => {
51+
it('mongoose should hydrate doc with default values', async () => {
52+
const user1 = await UserModel.findById(1);
53+
expect(user1?.toObject({ virtuals: true })).toEqual(
54+
expect.objectContaining({
55+
_id: 1,
56+
name: 'User',
57+
isActive: false,
58+
analytics: { isEnabled: false },
59+
periods: [],
60+
})
61+
);
62+
});
63+
64+
it('UserTC should have non-null fields if default value is provided', () => {
65+
expect(UserTC.toSDL({ deep: true, omitScalars: true })).toBe(dedent`
66+
type User {
67+
_id: Int!
68+
name: String!
69+
age: Float
70+
isActive: Boolean!
71+
analytics: UserAnalytics
72+
periods: [UserPeriods]!
73+
}
74+
75+
type UserAnalytics {
76+
isEnabled: Boolean!
77+
}
78+
79+
type UserPeriods {
80+
from: Float
81+
to: Float
82+
}
83+
`);
84+
});
85+
86+
it('check that graphql gets all default values', async () => {
87+
expect(
88+
await testFieldConfig({
89+
field: UserTC.mongooseResolvers.findById(),
90+
args: { _id: 1 },
91+
selection: `{
92+
_id
93+
name
94+
age
95+
isActive
96+
analytics {
97+
isEnabled
98+
}
99+
periods {
100+
from
101+
to
102+
}
103+
}`,
104+
})
105+
).toEqual({
106+
_id: 1,
107+
age: null,
108+
analytics: { isEnabled: false },
109+
isActive: false,
110+
name: 'User',
111+
periods: [],
112+
});
113+
});
114+
});

src/__tests__/github_issues/268-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe('issue #268 - Allow to provide `suffix` option for resolvers configs',
5555
expect(createOne2.getArgITC('record').toSDL()).toMatchInlineSnapshot(`
5656
"\\"\\"\\"\\"\\"\\"
5757
input CreateOneUserShortInput {
58-
_id: Int!
58+
_id: Int
5959
name: String!
6060
}"
6161
`);

src/composeMongoose.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { ObjectTypeComposer, SchemaComposer, Resolver } from 'graphql-compose';
2-
import { schemaComposer as globalSchemaComposer } from 'graphql-compose';
1+
import type { SchemaComposer, Resolver } from 'graphql-compose';
2+
import { schemaComposer as globalSchemaComposer, ObjectTypeComposer } from 'graphql-compose';
33
import type { Model, Document } from 'mongoose';
44
import { convertModelToGraphQL } from './fieldsConverter';
55
import { allResolvers } from './resolvers';
@@ -75,10 +75,14 @@ export function composeMongoose<TDoc extends Document, TContext = any>(
7575
prepareFields(tc, opts.fields);
7676
}
7777

78-
if (opts.inputType) {
79-
// generate input type only it was customized
80-
createInputType(tc, opts.inputType);
81-
}
78+
// generate InputObjectType with required fields,
79+
// before we made fields with default values required too
80+
createInputType(tc, opts.inputType);
81+
// making fields with default values required
82+
// but do it AFTER input object type generation
83+
// NonNull fields !== Required field
84+
// default values should not affect on that input fields became required
85+
makeFieldsNonNullWithDefaultValues(tc);
8286

8387
tc.makeFieldNonNull('_id');
8488

@@ -90,3 +94,23 @@ export function composeMongoose<TDoc extends Document, TContext = any>(
9094

9195
return tc as any;
9296
}
97+
98+
function makeFieldsNonNullWithDefaultValues(
99+
tc: ObjectTypeComposer,
100+
alreadyWorked = new Set()
101+
): void {
102+
if (alreadyWorked.has(tc)) return;
103+
alreadyWorked.add(tc);
104+
105+
tc.getFieldNames().forEach((fieldName) => {
106+
const fc = tc.getField(fieldName);
107+
// traverse nested Objects
108+
if (fc.type instanceof ObjectTypeComposer) {
109+
makeFieldsNonNullWithDefaultValues(fc.type);
110+
}
111+
const defaultValue = fc?.extensions?.defaultValue;
112+
if (defaultValue !== null && defaultValue !== undefined) {
113+
tc.makeFieldNonNull(fieldName);
114+
}
115+
});
116+
}

src/fieldsConverter.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type MongooseFieldT = {
2424
};
2525
originalRequiredValue?: string | (() => any);
2626
isRequired?: boolean;
27+
defaultValue?: any;
2728
enumValues?: string[];
2829
schema?: Schema;
2930
_index?: { [optionName: string]: any };
@@ -188,6 +189,11 @@ export function convertModelToGraphQL<TDoc extends Document, TContext>(
188189
description: _getFieldDescription(mongooseField),
189190
};
190191

192+
if (mongooseField?.defaultValue !== null && mongooseField?.defaultValue !== undefined) {
193+
if (!graphqlFields[fieldName].extensions) graphqlFields[fieldName].extensions = {};
194+
(graphqlFields as any)[fieldName].extensions.defaultValue = mongooseField?.defaultValue;
195+
}
196+
191197
if (deriveComplexType(mongooseField) === ComplexTypes.EMBEDDED) {
192198
// https://github.com/nodkz/graphql-compose-mongoose/issues/7
193199
graphqlFields[fieldName].resolve = (source) => {

0 commit comments

Comments
 (0)