GithubHelp home page GithubHelp logo

graphql-compose / graphql-compose-mongoose Goto Github PK

View Code? Open in Web Editor NEW
707.0 20.0 95.0 3.36 MB

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

License: MIT License

JavaScript 0.34% TypeScript 99.66%
graphql-compose-plugin graphql mongoose schema-builder graphql-types graphql-compose hacktoberfest

graphql-compose-mongoose's Introduction

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

Release Notes for v9.0.0 contains a lot of improvements. It's strongly recommended for reading before upgrading from v8.

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.

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 { composeMongoose } 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 = composeMongoose(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.mongooseResolvers.findById(),
  userByIds: UserTC.mongooseResolvers.findByIds(),
  userOne: UserTC.mongooseResolvers.findOne(),
  userMany: UserTC.mongooseResolvers.findMany(),
  userDataLoader: UserTC.mongooseResolvers.dataLoader(),
  userDataLoaderMany: UserTC.mongooseResolvers.dataLoaderMany(),
  userByIdLean: UserTC.mongooseResolvers.findById({ lean: true }),
  userByIdsLean: UserTC.mongooseResolvers.findByIds({ lean: true }),
  userOneLean: UserTC.mongooseResolvers.findOne({ lean: true }),
  userManyLean: UserTC.mongooseResolvers.findMany({ lean: true }),
  userDataLoaderLean: UserTC.mongooseResolvers.dataLoader({ lean: true }),
  userDataLoaderManyLean: UserTC.mongooseResolvers.dataLoaderMany({ lean: true }),
  userCount: UserTC.mongooseResolvers.count(),
  userConnection: UserTC.mongooseResolvers.connection(),
  userPagination: UserTC.mongooseResolvers.pagination(),
});

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

// STEP 4: BUILD GraphQL SCHEMA OBJECT
const schema = schemaComposer.buildSchema();
export default schema;

// STEP 5: DEMO USE OF GraphQL SCHEMA OBJECT
// Just a demo, normally you'd pass schema object to server such as Apollo server.
import { graphql } from 'graphql';

(async () => {
  await mongoose.connect('mongodb://localhost:27017/test');
  await mongoose.connection.dropDatabase();

  await User.create({ name: 'alice', age: 29, gender: 'female' });
  await User.create({ name: 'maria', age: 31, gender: 'female' });
  const bob = await User.create({ name: 'bob', age: 30, gender: 'male' });

  const response1 = await graphql({
    schema,
    source: 'query { userMany { _id name } }',
  });
  console.dir(response1, { depth: 5 });

  const response2 = await graphql({
    schema,
    source: 'query($id: MongoID!) { userById(_id: $id) { _id name } }',
    variableValues: { id: bob._id },
  });
  console.dir(response2, { depth: 5 });

  const response3 = await graphql({
    schema,
    source: 'mutation($id: MongoID!, $name: String) { userUpdateOne(filter: {_id: $id}, record: { name: $name }) { record { _id name } } }',
    variableValues: { id: bob._id, name: 'bill' },
  });
  console.dir(response3, { depth: 5 });

  mongoose.disconnect();
})();

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 { composeMongooseDiscriminators } 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<string>),
      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 composeMongoose
    fields: {
      remove: ['friends'],
    }
  }
  const CharacterDTC = composeMongooseDiscriminators(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,
        source: `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 },
          },
        },
      });
    });
  });

Customization options

composeMongoose customization options

When you converting mongoose model const UserTC = composeMongoose(User, opts: ComposeMongooseOpts); you may tune every piece of future derived types – setup name and description for the main type, remove fields or leave only desired fields.

type ComposeMongooseOpts = {
  /**
   * Which type registry use for generated types.
   * By default is used global default registry.
   */
  schemaComposer?: SchemaComposer<TContext>;
  /**
   * What should be base type name for generated type from mongoose model.
   */
  name?: string;
  /**
   * Provide arbitrary description for generated type.
   */
  description?: string;
  /**
   * You can leave only whitelisted fields in type via this option.
   * Any other fields will be removed.
   */
  onlyFields?: string[];
  /**
   * You can remove some fields from type via this option.
   */
  removeFields?: string[];
  /**
   * You may configure generated InputType
   */
  inputType?: TypeConverterInputTypeOpts;
  /**
   * You can make fields as NonNull if they have default value in mongoose model.
   */
  defaultsAsNonNull?: boolean;
};

This is opts.inputType options for default InputTypeObject which will be provided to all resolvers for filter and input args.

type TypeConverterInputTypeOpts = {
  /**
   * What should be input type name.
   * By default: baseTypeName + 'Input'
   */
  name?: string;
  /**
   * Provide arbitrary description for generated type.
   */
  description?: string;
  /**
   * You can leave only whitelisted fields in type via this option.
   * Any other fields will be removed.
   */
  onlyFields?: string[];
  /**
   * You can remove some fields from type via this option.
   */
  removeFields?: string[];
  /**
   * This option makes provided fieldNames as required
   */
  requiredFields?: string[];
};

Resolvers' customization options

When you are creating resolvers from mongooseResolvers factory, you may provide customizationOptions to it:

UserTC.mongooseResolvers.findMany(opts);

connection(opts?: ConnectionResolverOpts)

type ConnectionResolverOpts<TContext = any> = {
  /** See below **/
  sort?: ConnectionSortMapOpts;
  name?: string;
  defaultLimit?: number | undefined;
  edgeTypeName?: string;
  edgeFields?: ObjectTypeComposerFieldConfigMap<any, TContext>;
  /** See below **/
  countOpts?: CountResolverOpts;
  /** See below **/
  findManyOpts?: FindManyResolverOpts;
}

The countOpts and findManyOpts props would be used to customize the internally created findMany and count resolver factories used by the connection resolver. If not provided the default configuration for each of the resolver factories is assumed.

The sort prop is optional. When provided it is used to customize the sorting behaviour of the connection. When not provided, the sorting configuration is derived from the existing indexes on the model.

Please refer to the documentation of the graphql-compose-connection plugin for more details on the sorting customization parameter.

count(opts?: CountResolverOpts)

interface CountResolverOpts {
  /** If you want to generate different resolvers you may avoid Type name collision by adding a suffix to type names */
  suffix?: string;
  /** Customize input-type for `filter` argument. If `false` then arg will be removed. */
  filter?: FilterHelperArgsOpts | false;
}

createMany(opts?: CreateManyResolverOpts)

interface CreateManyResolverOpts {
  /** If you want to generate different resolvers you may avoid Type name collision by adding a suffix to type names */
  suffix?: string;
  /** Customize input-type for `records` argument. */
  records?: RecordHelperArgsOpts;
  /** Customize payload.recordIds field. If false, then this field will be removed. */
  recordIds?: PayloadRecordIdsHelperOpts | false;
  /** Customize payload.error field. If true, then this field will be removed. */
  disableErrorField?: boolean;
}

createOne(opts?: CreateOneResolverOpts)

interface CreateOneResolverOpts {
  /** If you want to generate different resolvers you may avoid Type name collision by adding a suffix to type names */
  suffix?: string;
  /** Customize input-type for `record` argument */
  record?: RecordHelperArgsOpts;
  /** Customize payload.recordId field. If false, then this field will be removed. */
  recordId?: PayloadRecordIdHelperOpts | false;
  /** Customize payload.error field. If true, then this field will be removed. */
  disableErrorField?: boolean;
}

dataLoader(opts?: DataLoaderResolverOpts)

interface DataLoaderResolverOpts {
  /**
   * Enabling the lean option tells Mongoose to skip instantiating
   * a full Mongoose document and just give you the plain JavaScript objects.
   * Documents are much heavier than vanilla JavaScript objects,
   * because they have a lot of internal state for change tracking.
   * The downside of enabling lean is that lean docs don't have:
   *   Default values
   *   Getters and setters
   *   Virtuals
   * Read more about `lean`: https://mongoosejs.com/docs/tutorials/lean.html
   */
  lean?: boolean;
}

dataLoaderMany(opts?: DataLoaderManyResolverOpts)

interface DataLoaderManyResolverOpts {
  /**
   * Enabling the lean option tells Mongoose to skip instantiating
   * a full Mongoose document and just give you the plain JavaScript objects.
   * Documents are much heavier than vanilla JavaScript objects,
   * because they have a lot of internal state for change tracking.
   * The downside of enabling lean is that lean docs don't have:
   *   Default values
   *   Getters and setters
   *   Virtuals
   * Read more about `lean`: https://mongoosejs.com/docs/tutorials/lean.html
   */
  lean?: boolean;
}

findById(opts?: FindByIdResolverOpts)

interface FindByIdResolverOpts {
  /**
   * Enabling the lean option tells Mongoose to skip instantiating
   * a full Mongoose document and just give you the plain JavaScript objects.
   * Documents are much heavier than vanilla JavaScript objects,
   * because they have a lot of internal state for change tracking.
   * The downside of enabling lean is that lean docs don't have:
   *   Default values
   *   Getters and setters
   *   Virtuals
   * Read more about `lean`: https://mongoosejs.com/docs/tutorials/lean.html
   */
  lean?: boolean;
}

findByIds(opts?: FindByIdsResolverOpts)

interface FindByIdsResolverOpts {
  /**
   * Enabling the lean option tells Mongoose to skip instantiating
   * a full Mongoose document and just give you the plain JavaScript objects.
   * Documents are much heavier than vanilla JavaScript objects,
   * because they have a lot of internal state for change tracking.
   * The downside of enabling lean is that lean docs don't have:
   *   Default values
   *   Getters and setters
   *   Virtuals
   * Read more about `lean`: https://mongoosejs.com/docs/tutorials/lean.html
   */
  lean?: boolean;
  limit?: LimitHelperArgsOpts | false;
  sort?: SortHelperArgsOpts | false;
}

findMany(opts?: FindManyResolverOpts)

interface FindManyResolverOpts {
  /**
   * Enabling the lean option tells Mongoose to skip instantiating
   * a full Mongoose document and just give you the plain JavaScript objects.
   * Documents are much heavier than vanilla JavaScript objects,
   * because they have a lot of internal state for change tracking.
   * The downside of enabling lean is that lean docs don't have:
   *   Default values
   *   Getters and setters
   *   Virtuals
   * Read more about `lean`: https://mongoosejs.com/docs/tutorials/lean.html
   */
  lean?: boolean;
  /** If you want to generate different resolvers you may avoid Type name collision by adding a suffix to type names */
  suffix?: string;
  /** Customize input-type for `filter` argument. If `false` then arg will be removed. */
  filter?: FilterHelperArgsOpts | false;
  sort?: SortHelperArgsOpts | false;
  limit?: LimitHelperArgsOpts | false;
  skip?: false;
}

findOne(opts?: FindOneResolverOpts)

interface FindOneResolverOpts {
  /**
   * Enabling the lean option tells Mongoose to skip instantiating
   * a full Mongoose document and just give you the plain JavaScript objects.
   * Documents are much heavier than vanilla JavaScript objects,
   * because they have a lot of internal state for change tracking.
   * The downside of enabling lean is that lean docs don't have:
   *   Default values
   *   Getters and setters
   *   Virtuals
   * Read more about `lean`: https://mongoosejs.com/docs/tutorials/lean.html
   */
  lean?: boolean;
  /** If you want to generate different resolvers you may avoid Type name collision by adding a suffix to type names */
  suffix?: string;
  /** Customize input-type for `filter` argument. If `false` then arg will be removed. */
  filter?: FilterHelperArgsOpts | false;
  sort?: SortHelperArgsOpts | false;
  skip?: false;
}

pagination(opts?: PaginationResolverOpts)

interface PaginationResolverOpts {
  name?: string;
  perPage?: number;
  countOpts?: CountResolverOpts;
  findManyOpts?: FindManyResolverOpts;
}

removeById(opts?: RemoveByIdResolverOpts)

interface RemoveByIdResolverOpts {
  /** If you want to generate different resolvers you may avoid Type name collision by adding a suffix to type names */
  suffix?: string;
  /** Customize payload.recordId field. If false, then this field will be removed. */
  recordId?: PayloadRecordIdHelperOpts | false;
  /** Customize payload.error field. If true, then this field will be removed. */
  disableErrorField?: boolean;
}

removeMany(opts?: RemoveManyResolverOpts)

interface RemoveManyResolverOpts {
  /** If you want to generate different resolvers you may avoid Type name collision by adding a suffix to type names */
  suffix?: string;
  /** Customize input-type for `filter` argument. If `false` then arg will be removed. */
  filter?: FilterHelperArgsOpts | false;
  limit?: LimitHelperArgsOpts | false;
  /** Customize payload.error field. If true, then this field will be removed. */
  disableErrorField?: boolean;
}

removeOne(opts?: RemoveOneResolverOpts)

interface RemoveOneResolverOpts {
  /** If you want to generate different resolvers you may avoid Type name collision by adding a suffix to type names */
  suffix?: string;
  /** Customize input-type for `filter` argument. If `false` then arg will be removed. */
  filter?: FilterHelperArgsOpts | false;
  sort?: SortHelperArgsOpts | false;
  /** Customize payload.recordId field. If false, then this field will be removed. */
  recordId?: PayloadRecordIdHelperOpts | false;
  /** Customize payload.error field. If true, then this field will be removed. */
  disableErrorField?: boolean;
}

updateById(opts?: UpdateByIdResolverOpts)

interface UpdateByIdResolverOpts {
  /** If you want to generate different resolvers you may avoid Type name collision by adding a suffix to type names */
  suffix?: string;
  /** Customize input-type for `record` argument. */
  record?: RecordHelperArgsOpts;
  /** Customize payload.recordId field. If false, then this field will be removed. */
  recordId?: PayloadRecordIdHelperOpts | false;
  /** Customize payload.error field. If true, then this field will be removed. */
  disableErrorField?: boolean;
}

updateMany(opts?: UpdateManyResolverOpts)

interface UpdateManyResolverOpts {
  /** If you want to generate different resolvers you may avoid Type name collision by adding a suffix to type names */
  suffix?: string;
  /** Customize input-type for `record` argument. */
  record?: RecordHelperArgsOpts;
  /** Customize input-type for `filter` argument. If `false` then arg will be removed. */
  filter?: FilterHelperArgsOpts | false;
  sort?: SortHelperArgsOpts | false;
  limit?: LimitHelperArgsOpts | false;
  skip?: false;
  /** Customize payload.error field. If true, then this field will be removed. */
  disableErrorField?: boolean;
}

updateOne(opts?: UpdateOneResolverOpts)

interface UpdateOneResolverOpts {
  /** If you want to generate different resolvers you may avoid Type name collision by adding a suffix to type names */
  suffix?: string;
  /** Customize input-type for `record` argument. */
  record?: RecordHelperArgsOpts;
  /** Customize input-type for `filter` argument. If `false` then arg will be removed. */
  filter?: FilterHelperArgsOpts | false;
  sort?: SortHelperArgsOpts | false;
  skip?: false;
  /** Customize payload.recordId field. If false, then this field will be removed. */
  recordId?: PayloadRecordIdHelperOpts | false;
  /** Customize payload.error field. If true, then this field will be removed. */
  disableErrorField?: boolean;
}

Description of common resolvers' options

FilterHelperArgsOpts

type FilterHelperArgsOpts = {
  /**
   * Add to filter arg only that fields which are indexed.
   * If false then all fields will be available for filtering.
   * By default: true
   */
  onlyIndexed?: boolean;
  /**
   * You can remove some fields from type via this option.
   */
  removeFields?: string | string[];
  /**
   * This option makes provided fieldNames as required
   */
  requiredFields?: string | string[];
  /**
   * Customize operators filtering or disable it at all.
   * By default, for performance reason, `graphql-compose-mongoose` generates operators
   * *only for indexed* fields.
   *
   * BUT you may enable operators for all fields when creating resolver in the following way:
   *   // enables all operators for all fields
   *   operators: true,
   * OR provide a more granular `operators` configuration to suit your needs:
   *   operators: {
   *     // for `age` field add just 3 operators
   *     age: ['in', 'gt', 'lt'],
   *     // for non-indexed `amount` field add all operators
   *     amount: true,
   *     // don't add this field to operators
   *     indexedField: false,
   *   }
   *
   * Available logic operators: AND, OR
   * Available field operators: gt, gte, lt, lte, ne, in, nin, regex, exists
   */
  operators?: FieldsOperatorsConfig | false;
  /**
   * Make arg `filter` as required if this option is true.
   */
  isRequired?: boolean;
  /**
   * Base type name for generated filter argument.
   */
  baseTypeName?: string;
  /**
   * Provide custom prefix for Type name
   */
  prefix?: string;
  /**
   * Provide custom suffix for Type name
   */
  suffix?: string;
};

SortHelperArgsOpts

type SortHelperArgsOpts = {
  /**
   * Allow sort arg to be an array of enum values. Example [AGE_DESC, NAME_ASC, _ID_ASC].
   * Note enum values will only ever be generated for *indexed fields*.
   */
  multi?: boolean;
  /**
   * This option set custom type name for generated sort argument.
   */
  sortTypeName?: string;
};

RecordHelperArgsOpts

type RecordHelperArgsOpts = {
  /**
   * You can remove some fields from type via this option.
   */
  removeFields?: string[];
  /**
   * This option makes provided fieldNames as required
   */
  requiredFields?: string[];
  /**
   * This option makes all fields nullable by default.
   * May be overridden by `requiredFields` property
   */
  allFieldsNullable?: boolean;
  /**
   * Provide custom prefix for Type name
   */
  prefix?: string;
  /**
   * Provide custom suffix for Type name
   */
  suffix?: string;
  /**
   * Make arg `record` as required if this option is true.
   */
  isRequired?: boolean;
};

LimitHelperArgsOpts

type LimitHelperArgsOpts = {
  /**
   * Set limit for default number of returned records
   * if it does not provided in query.
   * By default: 100
   */
  defaultValue?: number;
};

FAQ

Can I get generated vanilla GraphQL types?

const UserTC = composeMongoose(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.mongooseResolvers.dataLoaderMany(),
    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.mongooseResolvers.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 { composeMongoose } from 'graphql-compose-mongoose';

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

const UserProfileTC = composeMongoose(UserProfile);
const ArticleTC = composeMongoose(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.mongooseResolvers.createOne(),
  createResources: ResourceTC.mongooseResolvers.createMany(),
  ...adminAccess({
    updateResource: ResourceTC.mongooseResolvers.updateById(),
    removeResource: ResourceTC.mongooseResolvers.removeById(),
  }),
});

How can I restrict access to certain fields or documents?

This library allows modifying the query before it is executed using the beforeQuery hook. This lets us prevent certain fields or documents from being read. Here's an example of restricting access to specific fields:

schemaComposer.Query.addFields({
  userOne: UserTC.mongooseResolvers.findOne().wrapResolve((next) => (rp) => {
    const { role } = rp.context;

    rp.beforeQuery = (query: Query<unknown, unknown>) => {
      if (role === 'admin') {
        // Don't change the projection and still allow all fields to be read
      } else if (role === 'moderator') {
        // Only allow the name, age, and gender fields to be read
        query.projection({ name: 1, age: 1, gender: 1 });
      } else if (role === 'public') {
        // Only allow the name field to be read
        query.projection({ name: 1 });
      }
    };

    return next(rp);
  }),
});

Note that fields that are sometimes restricted should not be marked as required in the mongoose schema. Otherwise, when you query them you will get a "Cannot return null for non-nullable field" error because the database query didn't return a value for the field.

You can also use beforeQuery to hide certain documents from the query. Here's an example:

schemaComposer.Query.addFields({
  postMany: PostTC.mongooseResolvers.findMany().wrapResolve((next) => (rp) => {
    const { userId } = rp.context;

    rp.beforeQuery = (query: Query<unknown, unknown>) => {
      // Only allow users to see their own posts
      query.where('authorId', userId);
    };

    return next(rp);
  }),
});

Both of these examples require putting extra data in the resolver context. Here's how to attach context data in Apollo Server:

const server = new ApolloServer({
  schema: schemaComposer.buildSchema(),
  context() {
    // This role should actually come from a JWT or something
    return { role: 'admin' };
  },
});

Other GraphQL servers are likely similar.

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

schemaComposer.Mutation.addFields({
  userPushToArray: {
    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
    }
  }
})

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 composeMongoose uses global schemaComposer for generated types. If you need to create different GraphQL schemas you need create own schemaComposers and provide them to customizationOptions:

import { SchemaComposer } from 'graphql-compose';

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

const UserTCForSchema1 = composeMongoose(User, { schemaComposer: schema1 });
const UserTCForSchema2 = composeMongoose(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, // <-- disable id addition in mongoose
    address: { type: String },
    verified: Boolean
  }]
});

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

Backers

Thank you to all our backers! 🙏 [Become a backer]

Sponsors

Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor]

License

MIT

graphql-compose-mongoose's People

Contributors

antoniopresto avatar benny1hk avatar berrymat avatar canac avatar corydeppen avatar danez avatar dependabot[bot] avatar exneval avatar francois-spectre avatar greenkeeper[bot] avatar greenkeeperio-bot avatar israelglar avatar janfabian avatar lihail-melio avatar mattslight avatar meabed avatar mernxl avatar michaelbeaumont avatar natac13 avatar neverbehave avatar nodkz avatar oklas avatar oluwatemilorun avatar petemac88 avatar robertlowe avatar sgpinkus avatar toverux avatar unkleho avatar yoadsn avatar yossisp avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

graphql-compose-mongoose's Issues

Features: createMany, mutation for an existing array

hello~

Do we have plan to add createMany, and recordIds returns the array of ObjectIDs, records returns the updated objects.

//createMany
var array = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }];
Movies.insertMany(array, function(error, docs) {});

Could we support mutation for updating an array field if I pass an ObjectId that adding to the existing array instead of overwrite, the overwrite only happens if I pass an array of ObjectId?

The first mutation for phones and returns two values for phones being "111-222-333-444", "444-555-666-777".

mutation {
  userCreate(record: {
    contacts: {
      phones: [
        "111-222-333-444",
        "444-555-666-777"
      ]
    },
  }) {
    recordId
    record {
      contacts {
        email
        phones
      }
    }
  }
}

{
  "data": {
    "userCreate": {
      "recordId": "593ca1149f15dd003778ba1f",
      "record": {
        "contacts": {
          "email": null,
          "phones": [
            "111-222-333-444",
            "444-555-666-777"
          ]
        }
      }
    }
  }
}

The second mutation for phones, the expected returns are three values for phones being "111-222-333-444", "444-555-666-777", "444-555-666-888" instead of "444-555-666-888".

mutation {
  userCreate(record: {
    contacts: {
      phones: "444-555-666-888"
    },
  }) {
    recordId
    record {
      contacts {
        email
        phones
      }
    }
  }
}

{
  "data": {
    "userCreate": {
      "recordId": "593ca1149f15dd003778ba1f",
      "record": {
        "contacts": {
          "email": null,
          "phones": [
            "111-222-333-444",
            "444-555-666-777",
            "444-555-666-888"
          ]
        }
      }
    }
  }
}

thanks,
anderson

how to add mongodb $near to filter

Hello

how to add filter to use $near
my addressBook schema is

import { Schema } from 'mongoose';

export const AddressBook = new Schema({
  title: {
    type: String,
    description: "Title of this address",
  },
  country: {
      type: String,
      description: "Counry of the account",
      indexed: true
  },
  region: {
      type: String,
      description: "Region of the account",
      indexed: true
  },
  city: {
      type: String,
      description: "City of the account",
      indexed: true
  },
  postalcode: {
    type: String,
    description: "City of the account",
    indexed: true
  },
  street: {
      type: String,
      description: "Phisical Address of the account"
  },
  latitude: {
      type: Number,
      description: "latitude",
      indexed: true
  },
  longitude: {
      type: Number,
      description: "longitude",
      indexed: true
  }
}, {
  collection: 'addressbook'
});

and my User Schame

import mongoose, { Schema } from 'mongoose';
import composeWithMongoose from 'graphql-compose-mongoose';
import composeWithRelay from 'graphql-compose-relay';

import { AddressBook } from './addressBook';
export const UserSchema = new Schema({
    name: String,
    email: {
        type: String,
        set: v => v.toLowerCase().trim(),
        unique: true,
        indexed: true
    },
    address:{
      type:[AddressBook],
      indexed:true
    },
}, {
  collection: 'users',
});

export const User = mongoose.model('User', UserSchema);
export const UserTC = composeWithRelay(composeWithMongoose(User));

many thanks in advance

Sub documents.

Suppose I have subsets on a document like so,

friends : [ {userId: string, meta: {}},... ]

Where meta contains discriminator for the type of friend

How whould I create a relation on freinds that resolves to friends where meta.mutual = 1, and returning a list of friend Profile objects where the Profile object represents a subset user fields e.g.,
{ username, avatar, userId, ....}?

Allow integer _ids to work with findByIds?

Hi there. First up, nice work with this library - my team has been experimenting with it for our Graphql project and it has saved a lot of time.

We have just run into an issue with our primary key (_id) in some of our models. We are using an integer/number for _id rather than a MongoId. While this works with Model.getResolver('findById'), it doesn't work with Model.getResolver('findByIds).

I actually get a cast error when doing relations:

{ [CastError: Cast to number failed for value "00016cb0dd2f1e4b7df8d7bd" at path "_id"]
  message: 'Cast to number failed for value "00016cb0dd2f1e4b7df8d7bd" at path "_id"',
  name: 'CastError',
  kind: 'number',
  value: 00016cb0dd2f1e4b7df8d7bd,
  path: '_id',
  reason: undefined }

I've pinpointed the issue here:
https://github.com/nodkz/graphql-compose-mongoose/blob/master/src/resolvers/findByIds.js#L59-L60

It looks like there is a MongoId check on the id, and then it converts to a MongoId before being put into the selector query.

Is there a particular reason for this? Some flexibilty would be great - to my knowledge, Mongoose and Mongo have no issues with _id being an integer. Integers also work with findById so we assumed it would also work with findByIds.

Happy to submit a pull request, just wanted to check first :)

Many thanks
unkleho

latest release breaks the server.

/Users/ddcole/_dev/servers/jitter-party-server2/node_modules/graphql-compose-mongoose/lib/fieldsConverter.js:206

function convertFieldToGraphQL(field, prefix = '') {

SyntaxError: Unexpected token =

GraphQL 0.11 : Can only create NonNull of a Nullable GraphQLType but got: MongoID

One of the drawbacks of pre-v1 software is instability and GraphQL does not escapes that rule 😄

Starting from GraphQL v.0.11.0 and later, the app crashes when building the schema. Relavant stack trace:

/home/morgan/.../node_modules/graphql/jsutils/invariant.js:19
    throw new Error(message);
          ^
Error: Can only create NonNull of a Nullable GraphQLType but got: MongoID.
    at invariant (/home/morgan/.../node_modules/graphql/jsutils/invariant.js:19:11)
    at new GraphQLNonNull (/home/morgan/.../node_modules/graphql/type/definition.js:780:84)

I assume that a breaking change in GQL breaks graphql-compose-mongoose.

(Workaround: downgrade to [email protected])

About Connection Resolvers

May I suggest you add more information about the functionality of graphql-compose-connections. At least for me, connection reminds of relay. Since I'm not using relay, I feel I have no use for 'connection'. However, it appears "connections" resolvers are useful beyond relay particular, within the context of this package. Indeed, can you explain the use case of 'connection' resolvers?

What is the type of an array of embedded docs?

I have this schema:

const MySchema = new Schema({
   name: {type: String},
   embedded: {type: [{foo: {type: String}, bar: {type: String}]}
});

I later on want to create resolver so I can updated embedded in a mutation. In the addResolver call to make the mutation, for the args setting, what do I put as the type for embedded?

switch database :useDb()

I have been exploring various libraries for mongoose and graphql and this really looks promising.Great work guys .

I have a scenario where i need to switch database on the bases of request parameters is there any way i can replace the model object for every resolver using useDb function

I may be trying to find some thing which is already available in graphql-compose-mongoose

Setting `false` on a resolver arg in typeConverterResolversOpts doesn't disable it

With the code:

const composeWithMongoose = require('graphql-compose-mongoose').composeWithMongoose
const User = require('../../../models/User')

const opts = {
  description: 'A user of the application.',
  resolvers: {
    findMany: {
      filter: false
    }
  }
}

module.exports = composeWithMongoose(User, opts)

...filter argument is still available on the findMany resolver. This code should disable it, no? Or am I understanding it wrong?

change field value after post mutation ?

hi ,

I want to change password with md5 or other hashing method
but I did not know how to set the modification resolver for mutation ?

my schema is

export const UserSchema = new Schema({
    name: String,
    username: {
        type: String,
        required: true,
        unique: true,
        trim: true,
        indexed: true
    },
    password: {
        type: String,
        required: true
    },
    email: {
        type: String,
        trim: true,
        unique: true,
        indexed: true
    },
    createdAt: Date,
    updatedAt: {
        type: Date,
        default: Date.now,
    }
}, {
  collection: 'users',
});


export const User = mongoose.model('User', UserSchema);
export const UserTC = composeWithRelay(composeWithMongoose(User));

I want when I make userCreate or userUpdate the password automatically hashed

Removing mongoId field in a query fails

HI,

Basically I have a schema with a mongoID in a field, and I would like to set it to null.
But everytime I run the query with the variables

{
  "input": {
    "_id": "59a59d8f9ae4fd3eb6e99b3a",
    "parentId": null
  }
}

The mongoId validator will give the error:
"message": "FolderModel validation failed: parentId: Cast to ObjectID failed for value \"null\" at path \"parentId\"",

Nested SubSchema array has "Generic" type

This is similar to issue #2, but even when I use mongoose.Schema the resulting field still has "Generic" type

I can replicate this using the example from README.md

const mongoose = require('mongoose')
const { composeWithMongoose } = require('graphql-compose-mongoose')
const { GQC } = require('graphql-compose')

const LanguagesSchema = new mongoose.Schema({
  language: String
});

const UserSchema = new mongoose.Schema({
  name: String,
  languages: {
    type: [LanguagesSchema],
    default: [],
  }
});
const UserModel = mongoose.model('UserModel', UserSchema);
const UserTC = composeWithMongoose(UserModel);

UserTC.get('languages') // Generic

I'm using the following versions

    "graphql-compose": "^1.8.0",
    "graphql-compose-connection": "^2.1.2",
    "graphql-compose-mongoose": "^1.4.2",
    "mongoose": "^4.7.4",

Discussion - Names of embedded schema GQL Types

Assume I have this schema:

let imageDataStructure = Schema({
  url: String,
  creditsTo: String,
  creditsLink: String,
  dimensions : {
    aspectRatio: Number
  },
  metadata: {
    leadColor: String,
    gravityCenter: {
      x: Number,
      y: Number
    }
  }
}, { _id: false });

let homeSchema = Schema({
  address: String,
  streetImage: imageDataStructure
})

let agentSchema = Schema({
  first: String,
  last: String,
  profileImage: imageDataStructure
})

The generated GQL types (depending on order of schema parsing) would be:

homeSchema.streetImage -> HomeStreetImage
agentSchema.profileImage -> HomeStreetImage

The type is reused since it's the same mongoose schema. The name is picked by the first path that discovers this schema. All of that takes place in fieldsConverter.js around embeddedToGraphQL.

Now if I would create the image structure like so:

let imageDataStructure = {
  url: String,
  creditsTo: String,
  creditsLink: String,
  dimensions : {
    aspectRatio: Number
  },
  metadata: {
    leadColor: String,
    gravityCenter: {
      x: Number,
      y: Number
    }
  }
};

... reset is the same

the following GQL types are expected:

homeSchema.streetImage -> HomeStreetImage
agentSchema.profileImage -> AgentProfileImage

It's not the same mongoose schema - and so a new Type would be generated for each path.

Since the structure of the embedded image data is the same. I would assume it's useful to create GQL fragments for this type and use it where ever a type contains this embedded type. Something like this:

query {
  bestAgent {
    first last
    profileImage {
      ... basicImageData
    }
  }
  bestHome {
    address
    streetImage {
      ... basicImageData
    }
  }
}
fragment basicImageData on ImageData {

  url
  dimensions {
    aspectRatio
  }
}

My idea is that since when using an embedded Schema (not a plain JS object) for reuse in mongoose the name of the type is anyway going to be wrong for any other types except for the first path that discovered this type - We might want to document the best way to force the name in advance.

Calling composeWithMongoose on the image model is not solving - I guess because it's not the same schema once you create a Model from it?

Calling something like this before all other composeWithMongoose:

convertSchemaToGraphQL(imageDataStructure, 'EmbeddedImage');

Would do the trick by placing a TC on the schema object _gqcTypeComposer prop - but does it feel like a hack?

Love to hear your thought.

How to use the in[] operator in query?

How do you use the in[] operator in a query? I can't figure out how to filter a findMany resolver to search by multiple values for a single field. Thanks!

Support for Mongoose Populate

Mongoose provides a way to define relationships between schema,
I was wondering why do we have to define relationships separately as we can iterate over the mongoose model to find out 'ref' fields from the schema definition and build up the populate query as you do for projection fields

$in query in Relation

I might be missing something, but since this framework is so awesome I assume this should be somewhere in it, and I am missing where.
Given a model like this:

Model {
owners: [MongoID]
}

and


User {
  _id ....
}

I want to add a relation like this :

PlayerType.addRelation(
  // this should return all Models that have this _id in the Owner Array.
  "myData",
  () => ({
    resolver: ModelType.getResolver("findOne"), // Do I need a custom resolver that takes the id to check? 
    //***** What should be in the prepareArgs *****//
    projection: { _id: true }
  })
);

Do I need to roll my own resolver for this?

I am sorry if this is explained somewhere, but I did look and can't find anything that relates to this..

Are Subscriptions supported?

Hi,

I found your graphql-compose-* family, and consider it very useful.
Really nice not having starting from scratch.

I am wondering if your graphql-compose-mongoose plugin supports subscriptions out of the box for mongoose models.

I found the GQC.rootSubscription() method, but did not see any example how to use it.

Many thanks in advance,
best regards.

FindMany filter arg type MongoID need to be coverted to string type

I found that findMany resolver's filter arg that has MongoID type doesn't work with mongoID input.

This is what it should be?, I think it will be better if it can work without toString().

Example

....
    addRelation({
      name: 'episodesRelation',
      args: {
        filter: source => ({
          seasonId: source._id.toString(),  // this one need to be converted to String
        }),
      },
      resolver: EpisodeTC.getResolver('findMany'),
      projection: { _id: 1 },
    })

.....
    addFields({
      episodesFields: {
        type: EpisodeTC.getTypePlural(),
        args: {
          offset: { type: 'Int', defaultValue: 0 },
          limit: { type: 'Int', defaultValue: 10 },
        },
        // this directly one doesn't
        resolve: (source, { offset, limit }, { Episode }) => 
          Episode.find({ seasonId: source._id })
          .skip(offset)
          .limit(limit),
      },
    })

.... 
  addRelation({
      name: 'episodesRelation',
      argsMapper: {
        filter: source => ({
          seasonId: source._id, // this deprecated one doesn't too 😆
        }),
      },
      resolver: EpisodeTC.getResolver('findByIds'),
      projection: { _id: 1 },
    })

How to add a relation in a structured object ?

I've got a mongoose schema where the data are a bit more structured than usual :

const FixtureSchema = new Schema({
    day: { type: Number },
    idMpg: { type: String },
    home: {
        team: { type: Schema.Types.ObjectId, ref: "Team" },
        formation: { type: String },
        performances: [ { type: Schema.Types.ObjectId, ref: "Performance" }]
    },
    away: {
        team: { type: Schema.Types.ObjectId, ref: "Team" },
        formation: { type: String },
        performances: [ { type: Schema.Types.ObjectId, ref: "Performance" }]
    }
});

I tried to add a relation on home.team and away.team but I can do that as when I call the addRelation it complains about the dot. I could easily destructure the object replacing home.team by home_team and so on, but it would be a bit ugly. So what could I do to add my relation ?

Thanks,
Stéphane

Stack overflow in fieldsConverter

Given the following schema, with a recursive document array, we get a stack overflow within the lib/fieldsConverter.js file.
code:

const Schema = new mongoose.Schema({
  sometype: String
});
Schema.add({ recursiveType: [Schema] });

error:

.../node_modules/graphql-compose-mongoose/lib/fieldsConverter.js:149
  var typeComposer = new _graphqlCompose.TypeComposer(_typeStorage2.default.getOrSet(typeName, new _graphql.GraphQLObjectType({
                                                                                                           ^
RangeError: Maximum call stack size exceeded
    at convertModelToGraphQL (.../node_modules/graphql-compose-mongoose/lib/fieldsConverter.js:149:108)
    at documentArrayToGraphQL (.../node_modules/graphql-compose-mongoose/lib/fieldsConverter.js:346:22)
    at convertFieldToGraphQL (.../node_modules/graphql-compose-mongoose/lib/fieldsConverter.js:218:14)
    at .../node_modules/graphql-compose-mongoose/lib/fieldsConverter.js:162:13
    at Array.forEach (native)
    at convertModelToGraphQL (.../node_modules/graphql-compose-mongoose/lib/fieldsConverter.js:159:39)

schema for nest model

I have a user <-> company relation
so I build like that

const UserSchema = new mongoose.Schema({
  company: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Company'
  }
})

after adding relation, it works with RootQuery.

However, it failed when I try to RootMutation user with companyId.
The graphql console shows that company is "GQLReference" not MongoID or String type
till I wrote

const UserSchema = new mongoose.Schema({
  company: {
    type: String,
  }
})

to make RootQuery and RootMutation available.

Did I miss anything?

findByIds resolver issue with multiple Schemas using useDb()

Previously when there was only one schema this worked, where source.attributes is an array of MongoIDs.

AttributeGroupTC.addRelation(
'attributesData', {
    resolver: () => AttributeTC.get('$findByIds'),
    prepareArgs: {
      _ids: (source) => source.attributes,
    },
    projection: { attributes: true },
  },
);

After multiple schemas, findByIds resolves to null, only findMany works like below:

schema.js
const getSchema = (db: any) => {
  const GQC = new ComposeStorage();

  const ViewerTC = GQC.get('Viewer');
  GQC.rootQuery().addFields({
    viewer: {
      type: ViewerTC.getType(),
      description: 'Data under client context',
      resolve: () => ({}),
    },
  });
  const fields = {
    attributeGroup: AttributeGroup.getTC(db).get('$findOne'),
    attributeGroupList: AttributeGroup.getTC(db).get('$findMany'),

    attribute: Attribute.getTC(db).get('$findOne'),
    attributeList: Attribute.getTC(db).get('$findMany'),
  };

  ViewerTC.addFields(fields);

  return GQC.buildSchema();
};

export default { getSchema };
attribute.js
const AttributeSchema = () => new Schema({
  _id: Schema.Types.ObjectId,
  name: String,
  ...
},
{
  collection: 'attributes',
});
const cache = {};

const getTC = (db: any) => {
  if (cache[db.name] && cache[db.name].TC) {
    return cache[db.name].TC;
  }
  const dbModel = db.model('attribute', AttributeSchema());
  mongooseTypeStorage.clear();
  const AttributeTC = composeWithDataLoader(composeWithMongoose(dbModel), { cacheExpiration: 10000 });
  cache[db.name] = {
    ...cache[db.name],
    TC: AttributeTC,
  };
  return AttributeTC;
};

export default { getTC };
attributeGroup.js
import Attribute from './attribute';

const AttributeGroupSchema = () => new Schema({
  _id: Schema.Types.ObjectId,
  name: String,
  attributes: [Schema.Types.ObjectId],
},
{
  collection: 'attribute_groups',
});
const cache = {};

const getTC = (db: any) => {
  if (cache[db.name] && cache[db.name].TC) {
    return cache[db.name].TC;
  }

  const dbModel = db.model('AttributeGroup', AttributeGroupSchema());
  mongooseTypeStorage.clear();
  const AttributeGroupTC = composeWithDataLoader(composeWithMongoose(dbModel), { cacheExpiration: 10000 });
  AttributeGroupTC.addRelation(
    'attributesData', {
      resolver: () => Attribute.getTC(db).get('$findMany'),
      prepareArgs: {
        filter: source => ({
          _operators: {
            _id: { in: source.attributes },
          },
        }),
      },
      projection: { attributes: true },
    },
  );
  cache[db.name] = {
    ...cache[db.name],
    TC: AttributeGroupTC,
  };
  return AttributeGroupTC;
};

export default { getTC };

Here is my package.json

    "graphql-compose": "^2.2.0",
    "graphql-compose-connection": "^2.2.2",
    "graphql-compose-dataloader": "^1.1.2",
    "graphql-compose-mongoose": "^1.6.0",

Null fields in mongo return sub fields

Hey,
I am not sure where this behavior is coming from.. have been debugging both graphql-compose* and graphql-js for the past 5 hours. Any help in the right direction would be great!

Assume this mongodb document:

{
 _id: objectId("5816f273f3bb751f5021d849"),
 someField: null
}

And this mongoose schema:

let inner = {
 otherField: String
}

let MyType = Schema({
  someField: inner
})

The following gql query (assume the getOne field returns the above document):

query {
  getOne(_id: "5816f273f3bb751f5021d849") {
    someField {
      otherField
    }
  }
}

I expect to get:

{
   "data" : {
    "getOne" : {
      someField: null
    }
  }
}

This is for example what graffiti-mongoose returns..
But from graphql-compose* I get:

{
   "data" : {
    "getOne" : {
      someField: {
        otherField: null
      }
    }
  }
}

Am I expecting the wrong behavior? I found this issue graphql/graphql-js#424 that support what I expect (I think). Where is the difference?
So far I reached the conclusion that graphql-js does not recognize the value for "someField" to be null here:
https://github.com/graphql/graphql-js/blob/73513d35747cdea255156edbe30d85d9bd0c1b81/src/execution/execute.js#L774
I'm using graphql 0.7.2 although this specific code seems to remain the same in 0.8*.

Thanks!

npm package tarball still contains wrong "dependencies" of mongoose and others.

Don't know how this weird thing happened since I can see the updated package.json in Github does not have "mongoose" in the "dependencies" section.
But when doing "npm view graphql-compose-mongoose" it is clear that the package.json in the registry still has this dependencies section:

 dependencies:
   { 'babel-runtime': '^6.20.0',
     'graphql-compose': '^1.7.0',
     'graphql-compose-connection': '^2.1.2',
     mongoose: '^4.7.4',
     'object-path': '^0.11.3' },

Not sure how critical this is but it caused some trouble for me - same is with graphql-compose which has graphql in it's dependencies section.
Probably has to do with how the package was updated in npm once those dependencies were converted to peer dependencies.

Nested arrays of objects use "Generic" type

When a schema has an array of objects, the GraphQL type is GraphQLList of GraphQLObjectType, but when that array of objects is inside a nested object, the type is a "Generic" GraphQLScalarType.

> var mongoose = require('mongoose')
undefined
> var composeWithMongoose = require('graphql-compose-mongoose').default
undefined
> var model = mongoose.model('doc', new mongoose.Schema({
... arr: [{ sub: Number }]
... }))
undefined
> var tc = composeWithMongoose(model)
undefined
> tc.getField('arr')
{ type: 
   GraphQLList {
     ofType: 
      GraphQLObjectType {
        name: 'docArr',
        description: undefined,
        isTypeOf: undefined,
        _typeConfig: [Object] } },
  description: undefined }
> var model2 = mongoose.model('doc2', new mongoose.Schema({
... nested: { arr: [{ sub: Number }] }
... }))
undefined
> var tc2 = composeWithMongoose(model2)
undefined
> tc2.getByPath('nested').getField('arr')
{ type: 
   GraphQLScalarType {
     name: 'Generic',
     description: undefined,
     _scalarConfig: 
      { name: 'Generic',
        serialize: [Function: coerceDate],
        parseValue: [Function: coerceDate],
        parseLiteral: [Function: parseLiteral] } },
  description: undefined }

Is this supported? Thanks.

How to use with graphql-compose-relay

I want to use the generated types with graphql-compose-relay but I am not sure how:

let UserTC = composeWithRelay(composeWithMongoose(User));

UserTC.addResolver({
  name: 'testResolver',
  kind: 'mutation',
  ...
  args: {
    arg1: 'String'
  }
});

GQC.rootMutation.addFields({
  'createOne': UserTC.getResolver('createOne'),
  'testResolver': UserTC.getResolver('testResolver')
});

If I do the above, then createOne has RelayCreateOneUserInput type as the input argument, which I think is right for relay, but for testResolver it has arg1: String as the input argument, which I think is wrong.

I tried changing where composeWithRelay is called but it's still not right:

let UserTC = composeWithMongoose(User);

UserTC.addResolver({
  name: 'testResolver',
  kind: 'mutation',
  ...
  args: {
    arg1: 'String'
  }
});

UserTC = composeWithRelay(UserTC);

GQC.rootMutation.addFields({
  'createOne': UserTC.getResolver('createOne'),
  'testResolver': UserTC.getResolver('testResolver')
});

But the above is still not right because now testResolver has RelayTestResolverInput type, which I think is correct but now createOne has RelayCreateOneUserInput defined as:

record: CreateOneUserInput!
clientMutationId: String

So the input argument for createOne got wrapped twice I think?

Old good pagination

Hello,

I want to use pagination ({ totalCount: Number, currentPage: Number, itemsPerPage: Number, totalPages: Number }). Is it possible to add that?

Thanks.

Connection in relation

Hello, first of all thank you for the good work. The graphql-comopose libraries are amazing and make life really easier !
I just have a question about making connection on a relation field.
Imagine I have User and Post schemas. A User has afield posts with the id of its posts.
How could I have a connection from the array of ids of the field posts in a relation. I mean something like this:

UserTC.addRelation(
  'friends',
  () => ({
    resolver: postTC.getResolver('connection'), // But I would like this connection to be from the array of id 
                                                                              contained in the field posts
    args: {
      _ids: (source) => source.posts,
    },
    projection: { posts: 1 }, // point fields in source object, which should be fetched from DB
  })
);

If you could help me doing this using your libraries it would be awesome !
Thanks in advance

Enums

Whats the best way to define enums?

Relations and connections

Relations and connections



Created a Tweet model and installed a single link with User

import mongoose from 'mongoose'
import { UserTC } from './user'
import composeWithMongoose from 'graphql-compose-mongoose'
	

const TweetSchema = new mongoose.Schema({
	  text: String,
	  userId: {
	    type: mongoose.Schema.Types.ObjectId,
	    ref: 'User'
          }
}, { timestamps: true } )
	

export const Tweet = mongoose.model('Tweet', TweetSchema)
export const TweetTC = composeWithMongoose(Tweet)
	
TweetTC.addRelation(
	  'user',
	  {
	    resolver: () => UserTC.getResolver('findById'),
	    prepareArgs: {
	      _id: source => source.userId
	    },
	    projection: { userId: true },
	  }
)

Created a User model and installed a multiple link with Tweet

import mongoose from 'mongoose'
import { TweetTC } from './tweet'
import composeWithMongoose from 'graphql-compose-mongoose'
	
const UserSchema = new mongoose.Schema({
    username: {
	    type: String,
	    unique: true
	  },
	  firstName: String,
	  lastName: String,
	  avatar: String,
	  password: String,
	  email: String,
	  tweetsIds: [{
	    type: mongoose.Schema.Types.ObjectId,
	    ref: 'Tweet'
	  }],
}, { timestamps: true })

export const User = mongoose.model('User', UserSchema)
export const UserTC = composeWithMongoose(User)

UserTC.addRelation(
	  'tweets',
	  {
	    resolver: () => TweetTC.getResolver('findByIds'),
	    prepareArgs: {
	      _ids: (source) => source.tweetsIds || [],
	    },
	    projection: { tweetsIds: true },
	  }
)



I create a mutation where in the userId field I pass the users id

mutation {
  tweetCreate(record: {
    text: "Get tweet"
    userId: "5a0b3cde2c6afb534933fc5b"
  }) {
    recordId
  } 
}

As a result, the tweet was created

{
  "_id": ObjectId("5a0b47fcb4f0015a0d59b6d6"),
  "text": "Get tweet",
  "userId": ObjectId("5a0b3cde2c6afb534933fc5b"),
}

, but the array with the user's field tweetsIds is empty

 {
    "_id": "5a0b3cde2c6afb534933fc5b",
    "username": "sun",
    "tweetsIds": [],
},

The whole project lies here

Where am I mistaken?

Insert JSON in mutation from server code

I have a simple model with a single JSON field.

const UserSchema = new mongoose.Schema({
  profile: mongoose.Schema.Types.Mixed,
});
const UserModel = mongoose.model('users', UserSchema);
const UserTC = composeWithMongoose(UserModel);
...mutation methods left out 

How do I insert JSON into that field using a server-side query?

let profile = {test: "profile"}
let mutationQuery_NewUser = `
  mutation {
    userCreate(record:{
      profile: ${profile},
    }) {
      recordId
    }
  }
`
graphql.graphql(mySchema, mutationQuery_NewUser)

This does not work ([Object object] tries to get inserted), and neither does replacing with a JSON.stringified value, or anything else I could think of.

Is it possible to cast the JSON scalar to something else for input?

Avoid request to get only _id in relationship

Hello,

I have these models:

Proposal {
  offer: Offer
}
Offer {
  acceptedProposal: Proposal
}

I'd like to make a graphql request to get many Offers, and check if they have an acceptedProposal or not.
For that, I can do this:

query {
  offers {
    acceptedProposal {
      _id
    }
  }
}

But this will make a request for each offer with an acceptedProposal, to get the _id. Even though we already have the acceptedProposal's _id in Offer.
Is there an easy way to avoid these requests? For now I simply declared the relation like this:

tc.addRelation('acceptedProposal', {
    resolver: () => proposalType.getResolver('findById'),
    prepareArgs: { _id: (source: IOfferDocument) => source.acceptedProposal },
    projection: { acceptedProposal: true }
});

I know I could create a new field like acceptedProposalId which will return the id directly (or the inverse, rename the relation so that I still can get the id with acceptedProposal), but I'd like to avoid to complexify the API.
Maybe we could allow the relation field to be of composed type MongoId | Proposal in some way, so it can handle nicely this case where we don't want any Proposal field?

Thanks a lot for your awesome graphql-compose packages :)

rawQuery

I see that count, findOne and findMany can do rawQuery but I think findById does not process rawQuery. Can you please add rawQuery support to findById as well?

_id is stripped from embedded documents

This issue was driving me mad, so I followed the source code only to find that this is by design.

The _id is removed during the conversion from model to graphql (#L306 and #L351).

I want to write a query like this:

query {
  accounts {
    _id
    name
    websites {
    	_id
      domain
      description
    }
  }
}

My front-end relies on the _id field of the embedded website documents, so I'm a bit confused what the purpose of removing them is.

Here is my Account schema:

const WebsiteSchema = new Schema({
  domain: String,
  description: String
});

const AccountSchema = new Schema({
  name: String,
  createdAt: Date,
  updatedAt: Date,
  deletedAt: Date,
  ...
  websites: [WebsiteSchema]
});

graphql 0.10.0 breaking changes

This update seem to break the type resolution. I'm not sure if this is a graphql-compose issue.

GQC: can not convert field 'UserModelName.first' to InputType
      It should be GraphQLObjectType, but got 
      String
GQC: can not convert field 'UserModel.email' to InputType
      It should be GraphQLObjectType, but got 
      String

Do not call mongoose for findByIds with empty array argument

Hi,

I would assume, each call to the findByIds resolver with an empty array argument behaves just as if you pass no Array at all, it will always return the empty array []. If this assumptions holds true, we could add a new check to line 49 in the referred example, so we do not have to call mongoose.

https://github.com/nodkz/graphql-compose-mongoose/blob/4a854d2a44252d94d656b3b56ac2e2f47a44b71a/src/resolvers/findByIds.js#L46-L51

Any thoughts, objections?

Kind regards
Cedric

Add default values from request

Hello!
I use your package to compose mongoose models to GraphQL and I have a mutation that creates new Goal record:

mutation createGoal($name:String!){
  goalCreate(record:{
    name: $name
  }){
    record {
      _id
       name
     }
   }
  }

Goal schema have an userId field (an id of user that created this Goal).

I don't want to pass userId from front-end due to security concern, but want to set userId in the back-end app, and get it from req.user object.

Is it possible to accomplish this with your library?
If it is, can you give me an example of how to set record field before original resolver is called?

To be more concise:
From front-end I send:

{
  "name": "New goal"
}

In the back-end I want to transform it to:

{
  "name": "New goal",
  "userId": "592e6eaac21d362f2d86039a"
}

where userId - the id of currently logged in user (req.user.id field).
And pass this transformed object to createOne resolver.

Odd Resolution Name

I'm getting this

user(_id: MongoID!): EventPartyGuestProfile

When I am expecting this,

user(_id: MongoID!): UserAccount

Here's my sequence,

// user_account.js
export const UserAccountSchema = new mongoose.Schema(
 ...
);
export const UserAccount = mongoose.model('UserAccount', UserAccountSchema);

// user_account_type_composer.js
import { UserAccount } from '../schema/user_account';
const UserAccountTC = composeWithMongoose(UserAccount);
export default UserAccountTC;

// resolvers.js
import UserAccountTC from './user_account_type_composer';

export const queryFields = {
  user: UserAccountTC.getResolver('findById'),
  users: UserAccountTC.getResolver('findByIds'),
};

// 
import { ComposeStorage } from 'graphql-compose';
import moduleUserAccount from './user_account';

const GQC = new ComposeStorage();

GQC.rootQuery().addFields({
  ...moduleUserAccount.resolvers.queryFields
});

const schema = GQC.buildSchema();

At one point, I may have passed the wrong model name to composeWithMongoose function (
const UserAccountTC = composeWithMongoose(EventPartyGuestProfile) ), but changed the name and restarted the server several times since. I've also changed the name of the query fields with no problem. Where could this be coming from?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.