-
-
Notifications
You must be signed in to change notification settings - Fork 96
Mongoose discriminantors, composeWithMongooseDiscriminators #107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mongoose discriminantors, composeWithMongooseDiscriminators #107
Conversation
|
Wow, thanks! 👍 |
|
Well I am using the hack already, found many issues. Well few explanations too. I would be happy if we could make the hack better, the minor implementation is making my work really easy. 🤓🤓 |
|
Made some eslint and flowtype fixes to your branch. |
| }; | ||
|
|
||
| // Enum MongooseComposeResolvers | ||
| export const EMCResolvers = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should avoid using export const EMCResolvers = { here.
If you want to create such mapping, you need to do it in https://github.com/graphql-compose/graphql-compose-mongoose/blob/master/src/resolvers/index.js
I'll refactor connection and pagination resolvers in upcoming days, so all resolvers will be under /src/resolvers folder.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mernxl Moved connection and pagination resolvers to the /src/resolvers folder.
yarn install is required! Was updated graphql-compose-pagination and graphql-compose-connection packages.
| // Add a Generic Field, else we have generic Errors when resolving interface | ||
| // this is somehow i don't understand, but we don't get any type if we never query it | ||
| // I guess under the hud, graphql-compose shakes it off. | ||
| this.GQC.Query.addFields({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's very bad to automatically add fields to Query type. Developers should to it manually in their code. In my schema I'm using resolvers under my, admin, public fields in the Query (they have different permissions and a different set of resolvers (eg. I do not use pagination, remove* resolvers under Query.public)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will work on removing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nodkz, i have this issue with the discriminators. When you don't add a particular discriminator type to query, mutation or subscription, when resolving the Interface, Graphql throws an Error if it references that type which is not in schema.
I believe this explains it more...
In SchemaComposer.js
buildSchema(): GraphQLSchema {
const roots = {};
if (this.has('Query')) {
const tc = this.getTC('Query');
this.removeEmptyTypes(tc, new Set());
roots.query = tc.getType();
}
if (this.has('Mutation')) {
const tc = this.getTC('Mutation');
this.removeEmptyTypes(tc, new Set());
roots.mutation = tc.getType();
}
if (this.has('Subscription')) {
const tc = this.getTC('Subscription');
this.removeEmptyTypes(tc, new Set());
roots.subscription = tc.getType();
}
if (Object.keys(roots).length === 0) {
throw new Error(
'Can not build schema. Must be initialized at least one ' +
'of the following types: Query, Mutation, Subscription.'
);
}
return new GraphQLSchema(roots);
}While seeking you opinion on how to solve the issue, i am proposing to add a field called _disTypes that contains all types that are either Discrimator or Child of Discriminator to rootQuery. It only host the Types, cannot be Queried, nor Mutated.
In composeWithMongooseDiscriminators.js
if (!this.GQC.rootQuery().hasField('_disTypes')) {
this.GQC.rootQuery().addFields({
_disTypes: TypeComposer.create({
name: '_disTypes',
description: 'Hoisting for Discriminator Types',
}),
});
}
this.GQC.rootQuery()
.getFieldTC('_disTypes')
.addFields({
[this.getTypeName()]: this,
});There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a broken test case without _distTypes logic? I can solve this problem today after several hours. I'm very dislike any pollution to the users' schemas.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the test case, there are three test cases,
in src/tests/_disTypes-test.js
- Test that doesn't include _distTypes but manually i manually added resolvers that return all the Types that implement DInterface
- Test that includes _disTypes by setting discriminator
test_disTypesoption totrue. This case executes without errors - Test that turns
test_disTypesoption tofalse, the case returns GraphqlError that iasserted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your tests help me very well to figure out the problem.
I found a solution. But it required to wait for some time while I make proper changes in graphql-compose package.
Just added InterfaceTypeComposer. It still in progress in a separate branch.
After that, it is necessary to teach TypeComposer to work with InterfaceTypeComposer.
And then make small changes in SchemaComposer. Midnight is already, try to do it tomorrow.
@mernxl while you wait for me, you may suggest some new methods to graphql-compose which it would be good to have, based on the experience of writing discriminators. If there are any ;)
Also, you may add some info about how discriminators work to README.md.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nodkz, great work on InterfaceTypeComposer. Don't know of those new methods as of now, but if they do pop up i will do a PR.
-
Will be adding the info on how discriminators work to README.md.
-
While working on discriminators, i wrote a small function that merges two
TypeConverterOptions, that is takes the first option and merges it with the second. The first opts only replaces the undefined opts in the second arg. I think it will be nice to expose this function so someone could create base type converter options and only merge them with extras when he needs them.
const baseCustomOptions: TypeConverterOpts = {
fields: baseFields,
inputType: {
name: 'BaseInput',
description: 'Hello Base',
fields: baseInputTypeFields,
},
resolvers: {
findMany: {
limit: { defaultValue: 20 },
// sort: false,
skip: false,
filter: {
isRequired: true,
removeFields: ['id', 'dob'],
operators: {
one: ['gt', 'gte', 'lt'],
two: ['gt', 'gte', 'lt', 'in[]', 'nin[]'],
},
},
},
findById: false,
},
};
const childCustomOptions: TypeConverterOpts = {
fields: childFields,
inputType: {
name: 'ChildInputs',
description: 'Hello Child',
fields: childInputTypeFields,
},
resolvers: {
findMany: {
limit: { defaultValue: 50 },
sort: false,
// skip: false,
filter: {
removeFields: ['gender', 'dob', 'age'],
operators: {
one: ['gt', 'lte', 'ne', 'in[]', 'nin[]'],
two: ['gt', 'gte', 'lt', 'lte', 'ne'],
three: ['gte', 'lt'],
},
},
},
updateById: {
input: {
removeFields: ['one', 'two', 'five'],
requiredFields: ['eight', 'two', 'five'],
},
},
},
};
const expected: TypeConverterOpts = {
fields: expectedFields,
inputType: {
name: 'ChildInputs',
description: 'Hello Child',
fields: expectedInputTypes,
},
resolvers: {
findMany: {
limit: { defaultValue: 50 },
sort: false,
skip: false,
filter: {
isRequired: true,
removeFields: ['id', 'dob', 'gender', 'age'],
operators: {
one: ['gt', 'gte', 'lt', 'lte', 'ne', 'in[]', 'nin[]'],
two: ['gt', 'gte', 'lt', 'in[]', 'nin[]', 'lte', 'ne'],
three: ['gte', 'lt'],
},
},
},
findById: false,
updateById: {
input: {
removeFields: ['one', 'two', 'five'],
requiredFields: ['eight', 'two', 'five'],
},
},
},
};
it('should merge customisation Options', () => {
expect(mergeCustomizationOptions(baseCustomOptions, childCustomOptions)).toEqual(expected);
});There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, lets expose mergeCustomizationOptions method in this package.
|
|
||
| super(childImplements(dTC, childTC).gqType); | ||
|
|
||
| // !ORDER MATTERS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why order matters?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, the recomposeChildResolvers references the DiscriminatorTC, so I felt putting recomposeChildResolvers before setting the child DiscriminatorTC reference might yield errors.
But I have found a way to avoid all those issues.
| // added a query that returns this type on rootQuery. | ||
| // this is somehow i don't understand, but we don't get any type if we never query it | ||
| // I guess under the hud, graphql-compose shakes it off. | ||
| dTC.getGQC().Query.addFields({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
graphql-compose-mongoose generates tons of types and resolvers. Developers pick only what they need in their schemas and manually add to Query type in their code or relate with other types. We should not pollute their Query, Mutation types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, will look for a means of hoisting those types. I noticed something, when the developer doesn't add those types, they tend to error out when referenced by the DInterface.
| } | ||
| } | ||
|
|
||
| export class ChildDiscriminatorTypeComposer extends TypeComposer { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need this ChildDiscriminatorTypeComposer class? I think that standard TypeComposer will be enough for child models.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will refactor the childDTC, to TypeCompose.
| @@ -0,0 +1,97 @@ | |||
| /* @flow */ | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
recomposeBaseResolvers.js is a not a good name.
Maybe prepareResolversForDiscriminator.js or something shorter. But from files tree, it should be clear what logic put inside.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or maybe better to create a /discriminators folder and put all files into it. So all logic and tests will be in one place.
…(Required graphql-compose-pagination
…red graphql-compose-connection 3.2.0 or above)
…rate folder Change recompose[...Resolver] to prepare[...Resolver]
…ongooseSchema) Removed old logic, where generated schemas stored in the mongoose object `model.schema._gqcTypeComposer`. Related: graphql-compose#26 (comment)
Options on baseModel are copied to child, if child is undefined
Fixed in 4.3.0 version graphql-compose@34e756d
…r adding hidden types under Interfaces. Related graphql-compose/graphql-compose-mongoose#107
| }); | ||
| } | ||
|
|
||
| export class DiscriminatorTypeComposer extends TypeComposer { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move DiscriminatorTypeComposer class to its own file src/discriminators/DiscriminatorTypeComposer.js
|
@mernxl I think now we have a working solution. 🎉 Ok, it's time to clean up and polish the code/tests. Remove old unhelpful comments. Add an example to README.md Also try to remove one-time used functions (like I did with replacing call of When you do it, please ping me. After that, I'll review code again, make last corrections and merge PR. |
| super.setField(fieldName, fieldConfig); | ||
|
|
||
| for (const childTC of this.childTCs) { | ||
| childTC.setField(fieldName, fieldConfig); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is an error.
You completely replace all fields in childTC, but it may have it own additional types. Code should be fixed in such way:
- childTC.setField(fieldName, fieldConfig);
+ // childTC may have additional fields, so replace only Base fields
+ childTC.removeField(super.getFieldNames());
+ childTC.addFields(fields);Also need tests for your methods.
|
@mernxl I refactor some things. For now, need to go. Tomorrow will continue. Current problems:
Please fix them. Thanks! |
|
Hey @nodkz, will work on those points. What about generating Typescript types? I mostly use typescript. |
|
We add typescript defs manually. And then keep them in sync, when introduce new features or make changes for existed methods. If will be automated method will be good. In graphql-compose type defs were added in this PR graphql-compose/graphql-compose#83 Feel free to add .d.ts files to this package. |
|
ok, will work on that on a new PR when done here. |
…ex.js Other Utils -> mergeCustomisationsOptions, DiscriminatorTypeComposer class
|
|
||
| it('should copy all baseFields from BaseDTC to ChildTCs', () => { | ||
| expect(DroidTC.getFieldNames()).toEqual(expect.arrayContaining(CharacterDTC.getFieldNames())); | ||
| expect(PersonTC.getFieldNames()).toEqual(expect.arrayContaining(CharacterDTC.getFieldNames())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting some flow errors, you might want to check please.
From lines 22, 23 ...
Error:(22, 68) Cannot call expect.arrayContaining with CharacterDTC.getFieldNames() bound to value because string [1] is incompatible with mixed [2] in array element.
Converting a regular model to the discriminator model should be easy and without annoying config opts changes
|
🎉 This PR is included in version 4.4.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
|
:) thank you too @nodkz for being a great leader, and for creating this framework. I am happy i could contribute. |
Was able to implement the interface concept minimally, check this issue out #78
Guess we can start the polishing from here.