Skip to content
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

Extending grafitti schema #64

Open
zuhair-naqvi opened this issue Nov 23, 2015 · 29 comments
Open

Extending grafitti schema #64

zuhair-naqvi opened this issue Nov 23, 2015 · 29 comments

Comments

@zuhair-naqvi
Copy link

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 :/

@zuhair-naqvi zuhair-naqvi changed the title Extending schema Extending grafitti schema Nov 23, 2015
@zuhair-naqvi
Copy link
Author

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

@tothandras
Copy link
Contributor

@zuhair-naqvi Do you have a preferred API for getSchema to add this feature?

@zuhair-naqvi
Copy link
Author

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!

@macrostart
Copy link

+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.

@zuhair-naqvi
Copy link
Author

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.

@ghost
Copy link

ghost commented Nov 23, 2015

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?

@zuhair-naqvi
Copy link
Author

@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.

@zuhair-naqvi
Copy link
Author

Any further thoughts?

@tothandras
Copy link
Contributor

@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!

@jashmenn
Copy link
Collaborator

jashmenn commented Dec 5, 2015

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:

  1. Add queries and mutators to existing mongoose.Schemas/mongoose.models
  2. 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.

@jashmenn
Copy link
Collaborator

jashmenn commented Dec 6, 2015

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);

@nodkz
Copy link
Contributor

nodkz commented Mar 19, 2016

Adding customQueries and customMutations to getSchema:
https://github.com/wellth-app/graffiti-mongoose/commit/404395f4f801e069f855ee014ea969f2b6cba83c

@tothandras may review it and import

@Secretmapper
Copy link

Secretmapper commented Apr 30, 2016

@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

@sibelius
Copy link
Contributor

sibelius commented May 4, 2016

@zopf could you provide a simple example of customQuery and customMutations?

@zopf
Copy link
Contributor

zopf commented May 4, 2016

@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');
    });

@sibelius
Copy link
Contributor

sibelius commented May 4, 2016

this custom query is only available on the root node? can i add a custom query inside a Model?

@zopf
Copy link
Contributor

zopf commented May 4, 2016

I only added it on the root node. Haven't really thought about it inside of models...

@sibelius
Copy link
Contributor

sibelius commented May 4, 2016

Inside of models is like a virtual field in fact.

And it would fix some issues: #102, #64, #63, #25

@zopf
Copy link
Contributor

zopf commented May 4, 2016

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 👍

@sibelius
Copy link
Contributor

sibelius commented May 4, 2016

thanks for the great example @zopf I will take a look tm

@flipace
Copy link

flipace commented Aug 4, 2016

any updates on this issue?

@ajagesser
Copy link

@tothandras what solution should we use for now??
At the moment I'm implementing the hacky solution of @jashmenn

@st0ffern
Copy link

st0ffern commented Sep 8, 2016

@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 😉

@nodkz
Copy link
Contributor

nodkz commented Sep 8, 2016

@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 😉

@lyxsus
Copy link

lyxsus commented Sep 26, 2016

Sorry, is there any solution for that which will work on current version?

@st0ffern
Copy link

@lyxsus graphql-compose have everything 😉

@toverux
Copy link

toverux commented Feb 8, 2017

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.

@sibelius
Copy link
Contributor

sibelius commented Feb 8, 2017

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 aerogear/create-graphql#59

@JohnProg
Copy link

JohnProg commented Apr 6, 2017

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 :).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests