graphql-compose-mongoose

by graphql-compose

graphql-compose /graphql-compose-mongoose

Mongoose model converter to GraphQL types with resolvers for graphql-compose https://github.com/nodk...

482 Stars 74 Forks Last release: 28 days ago (v8.0.2) MIT License 436 Commits 120 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

graphql-compose-mongoose

travis build codecov coverage npm trends Commitizen friendly Backers on Open Collective Sponsors on Open Collective

This is a plugin for graphql-compose, which derives GraphQLType from your mongoose model. Also derives bunch of internal GraphQL Types. Provide all CRUD resolvers, including

graphql connection
, also provided basic search via operators ($lt, $gt and so on).

Installation

npm install graphql graphql-compose mongoose graphql-compose-mongoose --save

Modules

graphql
,
graphql-compose
,
mongoose
are in
peerDependencies
, so should be installed explicitly in your app. They have global objects and should not have ability to be installed as submodule.

If you want to add additional resolvers

connection
and/or
pagination
- just install following packages and

graphql-compose-mongoose
will add them automatically.
npm install graphql-compose-connection graphql-compose-pagination --save

Intro video

Viktor Kjartansson created a quite solid intro for graphql-compose-mongoose in comparison with graphql-tools:

#2 Mongoose - add GraphQL with graphql-compose

https://www.youtube.com/watch?v=RXcY-OoGnQ8 (23 mins)

Example

Live demo: https://graphql-compose.herokuapp.com/

Source code: https://github.com/graphql-compose/graphql-compose-examples

Small explanation for variables naming:

  • UserSchema
    - this is a mongoose schema
  • User
    - this is a mongoose model
  • UserTC
    - this is a
    ObjectTypeComposer
    instance for User.
    ObjectTypeComposer
    has
    GraphQLObjectType
    inside, available via method
    UserTC.getType()
    .
  • Here and in all other places of code variables suffix
    ...TC
    means that this is
    ObjectTypeComposer
    instance,
    ...ITC
    -
    InputTypeComposer
    ,
    ...ETC
    -
    EnumTypeComposer
    .
import mongoose from 'mongoose';
import { composeWithMongoose } from 'graphql-compose-mongoose';
import { schemaComposer } from 'graphql-compose';

// STEP 1: DEFINE MONGOOSE SCHEMA AND MODEL const LanguagesSchema = new mongoose.Schema({ language: String, skill: { type: String, enum: [ 'basic', 'fluent', 'native' ], }, });

const UserSchema = new mongoose.Schema({ name: String, // standard types age: { type: Number, index: true, }, ln: { type: [LanguagesSchema], // you may include other schemas (here included as array of embedded documents) default: [], alias: 'languages', // in schema ln will be named as languages }, contacts: { // another mongoose way for providing embedded documents email: String, phones: [String], // array of strings }, gender: { // enum field with values type: String, enum: ['male', 'female'], }, someMixed: { type: mongoose.Schema.Types.Mixed, description: 'Can be any mixed type, that will be treated as JSON GraphQL Scalar Type', }, }); const User = mongoose.model('User', UserSchema);

// STEP 2: CONVERT MONGOOSE MODEL TO GraphQL PIECES const customizationOptions = {}; // left it empty for simplicity, described below const UserTC = composeWithMongoose(User, customizationOptions);

// STEP 3: Add needed CRUD User operations to the GraphQL Schema // via graphql-compose it will be much much easier, with less typing schemaComposer.Query.addFields({ userById: UserTC.getResolver('findById'), userByIds: UserTC.getResolver('findByIds'), userOne: UserTC.getResolver('findOne'), userMany: UserTC.getResolver('findMany'), userCount: UserTC.getResolver('count'), userConnection: UserTC.getResolver('connection'), userPagination: UserTC.getResolver('pagination'), });

schemaComposer.Mutation.addFields({ userCreateOne: UserTC.getResolver('createOne'), userCreateMany: UserTC.getResolver('createMany'), userUpdateById: UserTC.getResolver('updateById'), userUpdateOne: UserTC.getResolver('updateOne'), userUpdateMany: UserTC.getResolver('updateMany'), userRemoveById: UserTC.getResolver('removeById'), userRemoveOne: UserTC.getResolver('removeOne'), userRemoveMany: UserTC.getResolver('removeMany'), });

const graphqlSchema = schemaComposer.buildSchema(); export default graphqlSchema;

That's all! You think that is to much code? I don't think so, because by default internally was created about 55 graphql types (for input, sorting, filtering). So you will need much much more lines of code to implement all these CRUD operations by hands.

Working with Mongoose Collection Level Discriminators

Variable Namings

  • ...DTC
    - Suffix for a
    DiscriminatorTypeComposer
    instance, which is also an instance of
    ObjectTypeComposer
    . All fields and Relations manipulations on this instance affects all registered discriminators and the Discriminator Interface.
  import mongoose from 'mongoose';
  import { schemaComposer } from 'graphql-compose';
  import { composeWithMongooseDiscriminators } from 'graphql-compose-mongoose';

// pick a discriminatorKey const DKey = 'type';

const enumCharacterType = { PERSON: 'Person', DROID: 'Droid', };

// DEFINE BASE SCHEMA const CharacterSchema = new mongoose.Schema({ // _id: field... type: { type: String, required: true, enum: (Object.keys(enumCharacterType): Array), description: 'Character type Droid or Person', },

name: String,
height: Number,
mass: Number,
films: [String],

});

// DEFINE DISCRIMINATOR SCHEMAS const DroidSchema = new mongoose.Schema({ makeDate: String, primaryFunction: [String], });

const PersonSchema = new mongoose.Schema({ gender: String, hairColor: String, starships: [String], });

// set discriminator Key CharacterSchema.set('discriminatorKey', DKey);

// create base Model const CharacterModel = mongoose.model('Character', CharacterSchema);

// create mongoose discriminator models const DroidModel = CharacterModel.discriminator(enumCharacterType.DROID, DroidSchema); const PersonModel = CharacterModel.discriminator(enumCharacterType.PERSON, PersonSchema);

// create DiscriminatorTypeComposer const baseOptions = { // regular TypeConverterOptions, passed to composeWithMongoose fields: { remove: ['friends'], } } const CharacterDTC = composeWithMongooseDiscriminators(CharacterModel, baseOptions);

// create Discriminator Types const droidTypeConverterOptions = { // this options will be merged with baseOptions -> customizationsOptions fields: { remove: ['makeDate'], } }; const DroidTC = CharacterDTC.discriminator(DroidModel, droidTypeConverterOptions); const PersonTC = CharacterDTC.discriminator(PersonModel); // baseOptions -> customizationsOptions applied

// You may now use CharacterDTC to add fields to all Discriminators // Use DroidTC, `PersonTC as any other ObjectTypeComposer. schemaComposer.Mutation.addFields({ droidCreate: DroidTC.getResolver('createOne'), personCreate: PersonTC.getResolver('createOne'), });

const schema = schemaComposer.buildSchema();

describe('createOne', () => { it('should create child document without specifying DKey', async () => { const res = await graphql.graphql( schema, `mutation CreateCharacters { droidCreate(record: {name: "Queue XL", modelNumber: 360 }) { record { __typename type name modelNumber } }

      personCreate(record: {name: "mernxl", dob: 57275272}) {
        record {
          __typename
          type
          name
          dob
        }
      }
    }`
  );

  expect(res).toEqual({
    data: {
      droidCreate: {
        record: { __typename: 'Droid', type: 'Droid', name: 'Queue XL', modelNumber: 360 },
      },
      personCreate: {
        record: { __typename: 'Person', type: 'Person', name: 'mernxl', dob: 57275272 },
      },
    },
  });
});

});

FAQ

Can I get generated vanilla GraphQL types?

const UserTC = composeWithMongoose(User);
UserTC.getType(); // returns GraphQLObjectType
UserTC.getInputType(); // returns GraphQLInputObjectType, eg. for args
UserTC.get('languages').getType(); // get GraphQLObjectType for nested field
UserTC.get('fieldWithNesting.subNesting').getType(); // get GraphQL type of deep nested field

How to add custom fields?

UserTC.addFields({
  lonLat: ObjectTypeComposer.create('type LonLat { lon: Float, lat: Float }'),
  notice: 'String', // shorthand definition
  noticeList: { // extended
    type: '[String]', // String, Int, Float, Boolean, ID, Json
    description: 'Array of notices',
    resolve: (source, args, context, info) => 'some value',
  },
  bio: {
    type: GraphQLString,
    description: 'Providing vanilla GraphQL type'
  }
})

How to build nesting/relations?

Suppose you

User
model has
friendsIds
field with array of user ids. So let build some relations:
UserTC.addRelation(
  'friends',
  {
    resolver: () => UserTC.getResolver('findByIds'),
    prepareArgs: { // resolver `findByIds` has `_ids` arg, let provide value to it
      _ids: (source) => source.friendsIds,
    },
    projection: { friendsIds: 1 }, // point fields in source object, which should be fetched from DB
  }
);
UserTC.addRelation(
  'adultFriendsWithSameGender',
  {
    resolver: () => UserTC.get('$findMany'), // shorthand for `UserTC.getResolver('findMany')`
    prepareArgs: { // resolver `findMany` has `filter` arg, we may provide mongoose query to it
      filter: (source) => ({
        _operators : { // Applying criteria on fields which have
                       // operators enabled for them (by default, indexed fields only)
          _id : { in: source.friendsIds },
          age: { gt: 21 }
        },
        gender: source.gender,
      }),
      limit: 10,
    },
    projection: { friendsIds: 1, gender: 1 }, // required fields from source object
  }
);

Reusing the same mongoose Schema in embedded object fields

Suppose you have a common structure you use as embedded object in multiple Schemas. Also suppose you want the structure to have the same GraphQL type across all parent types. (For instance, to allow reuse of fragments for this type) Here are Schemas to demonstrate:

import { Schema } from 'mongoose';

const ImageDataStructure = Schema({ url: String, dimensions : { width: Number, height: Number } }, { _id: false });

const UserProfile = Schema({ fullName: String, personalImage: ImageDataStructure });

const Article = Schema({ title: String, heroImage: ImageDataStructure });

If you want the

ImageDataStructure
to use the same GraphQL type in both
Article
and
UserProfile
you will need create it as a mongoose schema (not a standard javascript object) and to explicitly tell
graphql-compose-mongoose
the name you want it to have. Otherwise, without the name, it would generate the name according to the first parent this type was embedded in.

Do the following:

import { schemaComposer } from 'graphql-compose'; // get the default schemaComposer or your created schemaComposer
import { convertSchemaToGraphQL } from 'graphql-compose-mongoose';

convertSchemaToGraphQL(ImageDataStructure, 'EmbeddedImage', schemaComposer); // Force this type on this mongoose schema

Before continuing to convert your models to TypeComposers:

import mongoose from 'mongoose';
import { composeWithMongoose } from 'graphql-compose-mongoose';

const UserProfile = mongoose.model('UserProfile', UserProfile); const Article = mongoose.model('Article', Article);

const UserProfileTC = composeWithMongoose(UserProfile); const ArticleTC = composeWithMongoose(Article);

Then, you can use queries like this:

query {
  topUser {
    fullName
    personalImage {
      ...fullImageData
    }
  }
  topArticle {
    title
    heroImage {
      ...fullImageData
    }
  }
}
fragment fullImageData on EmbeddedImage {
  url
  dimensions {
    width height
  }
}

Access and modify mongoose doc before save

This library provides some amount of ready resolvers for fetch and update data which was mentioned above. And you can create your own resolver of course. However you can find that add some actions or light modifications of mongoose document directly before save at existing resolvers appears more simple than create new resolver. Some of resolvers accepts before save hook which can be provided in resolver params as param named

beforeRecordMutate
. This hook allows to have access and modify mongoose document before save. The resolvers which supports this hook are:
  • createOne
  • createMany
  • removeById
  • removeOne
  • updateById
  • updateOne

The prototype of before save hook:

(doc: mixed, rp: ResolverResolveParams) => Promise,

The typical implementation may be like this:

// extend resolve params with hook
rp.beforeRecordMutate = async function(doc, rp) {
  doc.userTouchedAt = new Date();

const canMakeUpdate = await performAsyncTask( ...provide data from doc... ) if (!canMakeUpdate) { throw new Error('Forbidden!'); }

return doc; }

You can provide your implementation directly in type composer:

UserTC.wrapResolverResolve('updateById', next => async rp => {

// extend resolve params with hook rp.beforeRecordMutate = async (doc, resolveParams) => { ... };

return next(rp); });

or you can create wrappers for example to protect access:

function adminAccess(resolvers) {
  Object.keys(resolvers).forEach((k) => {
    resolvers[k] = resolvers[k].wrapResolve(next => async rp => {

  // extend resolve params with hook
  rp.beforeRecordMutate = async function(doc, rp) { ... }

  return next(rp)
})

}) return resolvers }

// and wrap the resolvers schemaComposer.Mutation.addFields({ createResource: ResourceTC.getResolver('createOne'), createResources: ResourceTC.getResolver('createMany'), ...adminAccess({ updateResource: ResourceTC.getResolver('updateById'), removeResource: ResourceTC.getResolver('removeById'), }), });

How can I push/pop or add/remove values to arrays?

The default resolvers, by design, will replace (overwrite) any supplied array object when using e.g.

updateById
. If you want to push or pop a value in an array you can use a custom resolver with a native MongoDB call.

For example (push):-

// Define new resolver 'pushToArray'
UserTC.addResolver({
  name: 'pushToArray',
  type: UserTC,
  args: { userId: 'MongoID!', valueToPush: 'String' },
  resolve: async ({ source, args, context, info }) => {
    const user = await User.update({ _id: args.userId }, { $push: { arrayToPushTo: args.valueToPush } })
    if (!user) return null // or gracefully return an error etc...
    return User.findOne({ _id: args.userId }) // return the record
  }
})

// Then add 'pushToArray' as a graphql field e.g. schemaComposer.Mutation.addFields({userPushToArray: UserTC.getResolver('pushToArray')})

User
is the corresponding Mongoose model. If you do not wish to allow duplicates in the array then replace
$push
with
$addToSet
. Read the graphql-compose docs on custom resolvers for more info: https://graphql-compose.github.io/docs/en/basics-resolvers.html

NB if you set

unique: true
on the array then using the
update
$push
approach will not check for duplicates, this is due to a MongoDB bug: https://jira.mongodb.org/browse/SERVER-1068. For more usage examples with
$push
and arrays see the MongoDB docs here https://docs.mongodb.com/manual/reference/operator/update/push/. Also note that
$push
will preserve order in the array (append to end of array) whereas
$addToSet
will not.

Is it possible to use several schemas?

By default

composeWithMongoose
uses global
schemaComposer
for generated types. If you need to create different GraphQL schemas you need create own
schemaComposer
s and provide them to
customizationOptions
:
import { SchemaComposer } from 'graphql-compose';

const schema1 = new SchemaComposer(); const schema2 = new SchemaComposer();

const UserTCForSchema1 = composeWithMongoose(User, { schemaComposer: schema1 }); const UserTCForSchema2 = composeWithMongoose(User, { schemaComposer: schema2 });

Embedded documents has
_id
field and you don't need it?

Just turn them off in mongoose:

const UsersSchema = new Schema({
  _id: { type: String }
  emails: [{
    _id: false, // 

Can field name in schema have different name in database?

Yes, it can. This package understands mongoose

alias
option for fields. Just provide

alias: 'country'
for field
c
and you get
country
field name in GraphQL schema and Mongoose model but
c
field in database:
const childSchema = new Schema({
  c: {
    type: String,
    alias: 'country'
  }
});

Customization options

When we convert model

const UserTC = composeWithMongoose(User, customizationOptions);
you may tune every piece of future derived types and resolvers.

Here is typed definition of this options:

The top level of customization options. Here you setup name and description for the main type, remove fields or leave only desired fields.

export type customizationOptions = {
  schemaComposer?: SchemaComposer, // will be used global schema if not provided specific instance
  name?: string,
  description?: string,
  fields?: {
    only?: string[],
    remove?: string[],
  },
  inputType?: typeConverterInputTypeOpts,
  resolvers?: false | typeConverterResolversOpts,
};

This is

opts.inputType
level of options for default InputTypeObject which will be provided to all resolvers for
filter
and
input
args.
export type typeConverterInputTypeOpts = {
  name?: string,
  description?: string,
  fields?: {
    only?: string[],
    remove?: string[],
    required?: string[]
  },
};

This is

opts.resolvers
level of options. If you set the option to
false
it will disable resolver or some of its input args. Every resolver's arg has it own options. They described below.
export type typeConverterResolversOpts = {
  findById?: false,
  findByIds?: false | {
    limit?: limitHelperArgsOpts | false,
    sort?: sortHelperArgsOpts | false,
  },
  findOne?: false | {
    filter?: filterHelperArgsOpts | false,
    sort?: sortHelperArgsOpts | false,
    skip?: false,
  },
  findMany?: false | {
    filter?: filterHelperArgsOpts | false,
    sort?: sortHelperArgsOpts | false,
    limit?: limitHelperArgsOpts | false,
    skip?: false,
  },
  updateById?: false | {
    record?: recordHelperArgsOpts | false,
  },
  updateOne?: false | {
    record?: recordHelperArgsOpts | false,
    filter?: filterHelperArgsOpts | false,
    sort?: sortHelperArgsOpts | false,
    skip?: false,
  },
  updateMany?: false | {
    record?: recordHelperArgsOpts | false,
    filter?: filterHelperArgsOpts | false,
    sort?: sortHelperArgsOpts | false,
    limit?: limitHelperArgsOpts | false,
    skip?: false,
  },
  removeById?: false,
  removeOne?: false | {
    filter?: filterHelperArgsOpts | false,
    sort?: sortHelperArgsOpts | false,
  },
  removeMany?: false | {
    filter?: filterHelperArgsOpts | false,
  },
  createOne?: false | {
    record?: recordHelperArgsOpts | false,
  },
  createMany?: false | {
    records?: recordHelperArgsOpts | false,
  },
  count?: false | {
    filter?: filterHelperArgsOpts | false,
  },
  connection?: false | {
    uniqueFields: string[],
    sortValue: mixed,
    directionFilter: ((filterArg: T, cursorData: CursorDataType, isBefore: boolean) => T),
  },
  pagination?: false | {
    perPage?: number,
  },
};

This is

opts.resolvers.[resolverName].[filter|sort|record|limit]
level of options. You may tune every resolver's args independently as you wish. Here you may setup every argument and override some fields from the default input object type, described above in
opts.inputType
.
export type filterHelperArgsOpts = {
  filterTypeName?: string, // type name for `filter`
  isRequired?: boolean, // set `filter` arg as required (wraps in GraphQLNonNull)
  onlyIndexed?: boolean, // leave only that fields, which is indexed in mongodb
  requiredFields?: string | string[], // provide fieldNames, that should be required
  operators?: filterOperatorsOpts | false, // provide filtering fields by operators, eg. $lt, $gt
                                           // if left empty - provides all operators on indexed fields
};

// supported operators names in filter arg export type filterOperatorNames = 'gt' | 'gte' | 'lt' | 'lte' | 'ne' | 'in[]' | 'nin[]'; export type filterOperatorsOpts = { [fieldName: string]: filterOperatorNames[] | false };

export type sortHelperArgsOpts = { sortTypeName?: string, // type name for sort };

export type recordHelperArgsOpts = { recordTypeName?: string, // type name for record isRequired?: boolean, // set record arg as required (wraps in GraphQLNonNull) removeFields?: string[], // provide fieldNames, that should be removed requiredFields?: string[], // provide fieldNames, that should be required };

export type limitHelperArgsOpts = { defaultValue?: number, // set your default limit, if it not provided in query (default: 1000) };

Used plugins

graphql-compose-connection

This plugin adds

connection
resolver. Build in mechanism allows sort by any unique indexes (not only by id). Also supported compound sorting (by several fields).

Besides standard connection arguments

first
,
last
,
before
and
after
, also added great arguments:

  • filter
    arg - for filtering records
  • sort
    arg - for sorting records

This plugin completely follows to Relay Cursor Connections Specification.

graphql-compose-pagination

This plugin adds

pagination
resolver.

License

MIT

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.