npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

graphql-compose-mongo

v0.0.0-7

Published

Plugin for `graphql-compose` which derive a graphql types from a mongoose model.

Downloads

32

Readme

graphql-compose-mongoose

travis build codecov coverage npm Join the chat at https://gitter.im/graphql-compose/Lobby Commitizen friendly Greenkeeper badge

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

Example

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

Source code: https://github.com/nodkz/graphql-compose-mongoose-example

import mongoose from 'mongoose';
import composeWithMongoose from 'graphql-compose-mongoose';
import { GQC } 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,
  },
  languages: {
    type: [LanguagesSchema], // you may include other schemas (here included as array of embedded documents)
    default: [],
  },
  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', 'ladyboy'],
  },
  someMixed: {
    type: mongoose.Schema.Types.Mixed,
    description: 'Can be any mixed type, that will be treated as JSON GraphQL Scalar Type',
  },
});
const UserModel = mongoose.model('UserModel', UserSchema);



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

// STEP 3: CREATE CRAZY GraphQL SCHEMA WITH ALL CRUD USER OPERATIONS
// via graphql-compose it will be much much easier, with less typing
GQC.rootQuery().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'),
});

GQC.rootMutation().addFields({
  userCreate: UserTC.getResolver('createOne'),
  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 = GQC.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.

FAQ

Can I get generated vanilla GraphQL types?

const UserTC = composeWithMongoose(UserModel);
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: TypeComposer.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 Model has friendsIds field with array of user ids. So let build some relations:

UserTC.addRelation(
  'friends',
  () => ({
    resolver: UserTC.getResolver('findByIds'),
    args: { // 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')`
    args: { // 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 { convertSchemaToGraphQL } from 'graphql-compose-mongoose';
convertSchemaToGraphQL(ImageDataStructure, 'EmbeddedImage'); // 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 UserProfileModel = mongoose.model('UserProfile', UserProfile);
const ArticleModel = mongoose.model('Article', Article);

const UserProfileTC = composeWithMongoose(UserProfileModel);
const ArticleTC = composeWithMongoose(ArticleModel);

Then, you can use queries like this:

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

Customization options

When we convert model const UserTC = composeWithMongoose(UserModel, customizationOptions); you may tune every piece of future derived types and resolvers.

Here is flow 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 typeConverterOpts = {
  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,
  },
  count?: false | {
    filter?: filterHelperArgsOpts | false,
  },
  connection?: false | {
    uniqueFields: string[],
    sortValue: mixed,
    directionFilter: (<T>(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