graffiti-mongoose
graffiti-mongoose copied to clipboard
Extending grafitti schema
getSchema returns a GraphQLSchema object based on MongooseModels, how do I go about adding my own types to this schema?
Consider the use case:
I have a Post object with getLikedFriends method, which looks up all Post.likes references and queries Users collection to check if any of the User.friends references for the loggedIn user match with Post.likes
So instead of creating a new Mongoose model and re-implementing the logic inside my pre-existing getLikedFriends method, I'd like to just add this as a new type to the GraphQL schema and let the resolve function call Post.getLikedFriends(ctx.loggedInUser)
This is something I can easily do using vanilla GraphQL but not sure how to augment the schema returned by graffiti :/
In short I'm trying to figure out how to add types that resolve over arbitrary code over mongoose models, given the biggest selling point of GraphQL is the ability to query over arbitrary code, there should be a canonical way to do this with graffiti-mongoose
@zuhair-naqvi Do you have a preferred API for getSchema to add this feature?
I think producing the schema directly from models is too restrictive, instead the top-level item in the getSchema array could be a higher-order object that specifies the model and any arbitrary queries to be added to the model's field definition. Graffiti could then pass the instance of the model as the first parameter to the resolve method of each query, allowing for something like the following pseudo-code:
let Post = mongoose.model('Post');
let User = mongoose.model('User');
PostType = {
model: Post,
queries: {
getLikedFriends: {
type: new GraphQLList(UserType),
args: { id: { type: GraphQLID } },
resolve: (_instance, args) => _instance.getLikedFriends(agrs.id, ctx.loggedInUser)
},
customSearch: {
type: new GraphQLList(Post),
args: {
keywords: { type: String },
isByFriend: { type: Boolean }
},
resolve: (_instance, args) => _instance.performCustomSearch(args.keywords, args.isByFriend)
}
}
}
UserType = {
model: User,
queries: {
getPosts: {
type: new GraphQLList(PostType),
resolve: (_instance) => _instance.getRecentPosts()
}
}
}
const schema = getSchema([PostType, UserType]);
I'm quite new to GraphQL so let me know if this is making sense!
+1 for providing a way to extend the default set of queries (and hopefully mutations too).
I also like the idea to add a new config step so that not everything is exposed by default. This is the most common use case anyway: I believe most people would want to expose only a subset of their queries and mutations. I know that it's possible to hide fields or restrict access with hooks, but it's more complicated. Having a dedicated step for that seems to be the cleanest approach.
Perhaps add a fields property to config which allows you to choose which default bindings you wish to expose so the resulting object might look like:
let User = mongoose.model('User')
UserConfig = {
model: User,
fields: [ ...User.schema.paths ],
queries: {
q1: { type: new GraphQLObjectType, resolve: (_instance) => /** your code **/ },
q2: { type: new GraphQLListType, agrs: { x: {type: Integer} }, resolve: (_instance, args) => /** your code **/ },
}
}
This would be necessary for anyone with existing application code to be able to use graffiti-mongoose for real use cases.
Feels like we're re-inventing the wheel here. You're wrapping what looks suspiciously like the graffiti model around your mongoose model. Maybe we could just give the user the opportunity to pass in the graffiti model themselves and/or merge it with our generated model?
@burkhardr you're right. There needs to be a way to augment graffiti models, to pass in the array of exposed fields as well as attach callbacks to the graffiti model that receive the model instance, args and AST for querying - haven't given mutation much thought yet but this should generally work for both.
The other question this brings up is how you attach additional types say I want to fetch some data from mongo but other from a RESTful backend, which is again a key selling point of GraphQL.
The more I think about this the more it's becoming apparent that graffiti should delegate control of the underlying GraphQL schema / RootQuery to users rather than building the entire schema for them. This will allow users to gradually phase out the ORM as ORMs only really make sense in the legacy REST world.
I think the most sensible use-case for graffiti mongoose will be to help transition existing application code away from mongoose over time.
Any further thoughts?
@zuhair-naqvi I am open to changes! First, we definitely need to split up this project into 2. Migrate out the non Mongoose specific parts into an independent library, that would make creating new adapters easier and we could take advantage of the graffiti model too. I will be really busy in the next few weeks though (finishing school), I am happy to see PRs and following suggestions!
I agree that we definitely need a way to extend the current schema, preferably with easy access to the existing schema.
I think there's two things we need to be able to do:
- Add queries and mutators to existing
mongoose.Schemas/mongoose.models - Add arbitrary extensions and resolves to the schema
For #1, we could add queries and mutators to the mongoose.Schema directly:
const PostSchema = new mongoose.Schema({
name: String
// ... etc
})
PostSchema.queries.getLikedFriends = ...
PostSchema.queries.customSearch = ...
For #2, we could insert another hook in RootQuery like this:
const RootQuery = new GraphQLObjectType({
name: 'RootQuery',
fields: {
viewer: viewerField,
node: {
name: 'node',
description: 'Fetches an object given its ID',
type: nodeInterface,
args: {
id: {
type: new GraphQLNonNull(GraphQLID),
description: 'The ID of an object'
}
},
resolve: addHooks(getIdFetcher(graffitiModels), singular)
},
...(addQueryHooks(queries)) // <-- here
}
});
... and similarly for RootMutation.
I've been looking more into this and I just wanted to leave a tip for anyone else trying to extend the graffiti-mongoose schema.
Just a word of warning, this uses private variables so it's a bit of a hack until an official way to extend the schema is available.
Basically the idea is that instead of using getSchema we'll use getFields and 1. extract the RootQuery 2. modify it 3. use your modified RootQuery to build the schema. The reason we're able to rebuild the RootQuery is because GraphQLObjectType stores the GraphQLObjectTypeConfig in the instance variable _typeConfig.
Here's the (hacky) solution:
const graffitiModels = getModels([Post, User]);
var graffitiFields = getFields(graffitiModels);
var testRootType = new GraphQLObjectType({
name: 'testRoot',
fields: {
hello: {
type: GraphQLString,
resolve() {
return 'world';
}
}
}
});
var originalRootQuery = graffitiFields.query._typeConfig;
originalRootQuery.fields.test = {
type: testRootType,
resolve(obj) { return obj; }
}
var fields = {
query: new GraphQLObjectType(originalRootQuery),
}
const schema = new GraphQLSchema(fields);
Adding customQueries and customMutations to getSchema: https://github.com/wellth-app/graffiti-mongoose/commit/404395f4f801e069f855ee014ea969f2b6cba83c
@tothandras may review it and import
@nodkz Is there any documentation for customMutations?
For example, how to use existing types generated/defined by graffiti-mongoose for say, the output fields of mutationWithClientMutationId
Using the types generated by getTypes result in a Schema must contain unique named types... error
@zopf could you provide a simple example of customQuery and customMutations?
@sibeliusseraphini please see sample unit tests below taken from https://github.com/wellth-app/graffiti-mongoose/commit/b2b841e7a6d2ff44eb977b2fa1466799c587f037#diff-8b7cf0fa5fd81301d632e0a6f8fb2af6
and to @Secretmapper's point, I ended up working around that by creating a type cache (disabled by default) in the type module: https://github.com/wellth-app/graffiti-mongoose/commit/4f59703af1e929325fa7fa9d31cccebdb56caef5 and https://github.com/wellth-app/graffiti-mongoose/commit/4f59703af1e929325fa7fa9d31cccebdb56caef5
it('should return a GraphQL schema with custom queries', () => {
const graphQLType = types.TestQuery;
const customQueries = {
testQuery: {
type: graphQLType,
args: {
id: {
type: new GraphQLNonNull(GraphQLID)
}
}
}
};
const schema = getSchema({}, {customQueries});
expect(schema).instanceOf(GraphQLSchema);
expect(schema._queryType.name).to.be.equal('RootQuery');
expect(schema._mutationType.name).to.be.equal('RootMutation');
expect(schema._queryType._fields.testQuery.name).to.be.equal('testQuery');
expect(schema._queryType._fields.testQuery.type._fields.fetchCount.resolve()).to.be.equal('42');
});
it('should return a GraphQL schema with custom mutations', () => {
const graphQLType = types.TestQuery;
const customMutations = {
testQuery: {
type: graphQLType,
args: {
id: {
type: new GraphQLNonNull(GraphQLID)
}
}
}
};
const schema = getSchema({}, {customMutations});
expect(schema).instanceOf(GraphQLSchema);
expect(schema._queryType.name).to.be.equal('RootQuery');
expect(schema._mutationType.name).to.be.equal('RootMutation');
expect(schema._mutationType._fields.testQuery.name).to.be.equal('testQuery');
expect(schema._mutationType._fields.testQuery.type._fields.fetchCount.resolve()).to.be.equal('42');
});
this custom query is only available on the root node? can i add a custom query inside a Model?
I only added it on the root node. Haven't really thought about it inside of models...
Inside of models is like a virtual field in fact.
And it would fix some issues: https://github.com/RisingStack/graffiti-mongoose/issues/102, https://github.com/RisingStack/graffiti-mongoose/issues/64, https://github.com/RisingStack/graffiti-mongoose/issues/63, https://github.com/RisingStack/graffiti-mongoose/issues/25
Here's a proposal for these "virtual" fields:
Just add your own fields to the object returned by type.getTypes(models).YourModel.getFields().
Here's an example I just tested and seems to work on my fork ( https://github.com/wellth-app/graffiti-mongoose ). Please note that I'm using my fork's rebuildCache=false param as I make the call to getSchema, which allows me to use the cached types generated by my first call to getTypes when I make the later call to getSchema (which calls getTypes internally).
const UserSchema = new mongoose.Schema({
nameFirst: String
});
const User = mongoose.model('User', UserSchema);
const models = [
User
];
const graphQLTypes = getTypes(models);
const userFields = graphQLTypes.User.getFields();
userFields.myCustomField = {
name: 'myCustomField',
description: undefined,
type: graphQLTypes.User,
resolve: async function resolveCustomField(value, context, info) {
// here's where you'd do some kind of fancy filtering or what have you
return await User.findOne({ _id: value._id }).exec();
},
args: []
};
// proves that this custom field stays on the type after it has been set
console.log("graphQLTypes.User.getFields().myCustomField: ", graphQLTypes.User.getFields().myCustomField);
// my code builds a set of custom queries and mutations here...
const customQueries = {};
const customMutations = {};
// Overall Schema
return getSchema(
models,
{
hooks,
allowMongoIDMutation: true,
rebuildCache: false, // use cached types from previous getTypes call
customQueries,
customMutations
}
);
Booting up that code (in the context of some other stuff, admittedly) allows me to make the following query:
query justTesting {
users {
_id
nameFirst
myCustomField {
_id
nameFirst
}
}
}
... and receive the following result:
{
"data": {
"users": [
{
"_id": "56cf2e8f2c69da9a7293662f",
"nameFirst": "Alec",
"myCustomField": {
"_id": "56cf2e8f2c69da9a7293662f",
"nameFirst": "Alec"
}
}
]
}
}
What do you think? I can't tell if it's hacky or reasonable to be modifying the object returned from getFields()... but it certainly is giving us the internal object, not a clone. And replacing it does seem to work. Feedback from those more experienced than myself in GraphQL would be great 👍
thanks for the great example @zopf I will take a look tm
any updates on this issue?
@tothandras what solution should we use for now?? At the moment I'm implementing the hacky solution of @jashmenn
@sibelius I dont think that graffiti will work well together with relay if you want to customize every type of data. I am setting up a specification for a ORM where you define the table, and graphql/relay data in once place. There may be other solutions but i think this is the best so far.. https://github.com/stoffern/graphorm-spec Please come with ideas :wink:
@stoffern take a look on https://github.com/nodkz/graphql-compose And it's examples (server) http://graphql-compose.herokuapp.com/ Relay live example (client) https://nodkz.github.io/relay-northwind/
Please come with ideas 😉
Sorry, is there any solution for that which will work on current version?
@lyxsus graphql-compose have everything 😉
Just adding my $0.02: Yes, I think that the library should be more extendable and flexible. I've just tried it and thought about integrating it in my new application, because the generated schema is simply awesome. However... I was very disappointed to see that:
- Mongoose hooks aren't called, and the alternative (root-level hooks) is not very sexy
- I can't add validation easily
- Can't manage permissions with enough granularity (except adding hooks everywhere on fields)
- Can't add my own mutations/queries or remove auto-generated ones
- Etc
Also, it could be interesting to access the Mongoose model instance in hooks, so we can benefit from the Mongoose model's methods and statics.
Y'all did an awesome work on this lib and I've never created an API so easily. But right now, it's unusable for general-purpose use in most real-life applications.
We built CreateGraphQL that generate code from mongoose models.
Code generation is the best way to be extensible and very customizable.
Check it out our post: https://medium.com/entria/announcing-create-graphql-17bdd81b9f96#.6ez6y751o
We would like to be very extensible and support multiples databases and templates https://github.com/lucasbento/create-graphql/issues/59
This was my solution:
const { GraphQLObjectType, GraphQLSchema } = require('graphql');
const { getModels } = require('@risingstack/graffiti-mongoose/lib/model');
const { getFields } = require('@risingstack/graffiti-mongoose/lib/schema');
const { contactMutation } = require('./mutations');
const { contactQuery } = require('./queries');
const models = require('../models');
const graffitiModels = getModels(models);
const graffitiFields = getFields(graffitiModels);
const rootQuery = graffitiFields.query._typeConfig;
const rootMutation = graffitiFields.mutation._typeConfig;
Object.assign(rootQuery.fields, {
contact: contactQuery.user,
contacts: contactQuery.users,
});
Object.assign(rootMutation.fields, {
createContact: contactMutation.createContact,
});
module.exports = new GraphQLSchema({
query: new GraphQLObjectType(rootQuery),
mutation: new GraphQLObjectType(rootMutation),
});
I hope this can help you :).