graphql-compose-mongoose icon indicating copy to clipboard operation
graphql-compose-mongoose copied to clipboard

Create multiple Objects upon the create of Master Object?

Open johnnykesler opened this issue 7 years ago • 10 comments

First off, I'm going to apologize from the get go, I'm more of a front end guy than a DBA. That said I've taken it upon myself to convert our startup's data API from MongoDB to MongoDB with GraphQL API.

Secondly and most importantly, I have to applaud you all on an amazingly slick tool. After just a few days of learning/ playing with it, our basic schema has been converted and I'm now venturing into more advanced territories. However, I've run aground on a certain approach and need some help.

PROBLEM:

When a user is created we would like to create/mutate 3 other objects and then tie them to the user object. They will basically be rendered with default options.

(i.e. Mailbox defaults with an empty inbox, sent and deleted schema as well as a dynamic folder for later use. )

These 3 Objects will be persistent to every user in our app and we could certainly use code to create them and then create the relationships using the addRelationship function but I think it might be better to do it all with GraphQL-Compose upon the creation of the user, it just seems more clean. The objects we would like to create are:

  1. A Mailbox Object, to store messages
  2. Media Library Object, to store media
  3. A Profile Object to store user info outside of the user object.

We would then like to take those Objects and create a relationship to the User Object. So finally, here are the questions/problems I'm battling:

1st: Should we do this with GraphQL-Compose? 2nd: How do we create the Objects once the User has been created? 3rd: How do we remove the objects if the User is deleted?

My initial thought was to create them by calling the createOne resolver once the user is successfully created, then upon their creation, addRelation to the User Object and the newly created Objects.

Id this on the right track? If so or if not, could anyone drop me an example?

Any help on this would be tremendous and greatly appreciated!

Thanks,

John Kesler

johnnykesler avatar Feb 06 '18 18:02 johnnykesler

You should create a custom resolver with your logic.

In our app we have tons of mutations with complex logic. And in such case we do not use standard resolvers at all.

Create custom mutation

UserTC.addResolver({
  kind: 'mutation',
  name: 'createWithObjects', /// <-- name for your custom resolver, will be used in mutation type
  args: {
    email: 'String!',
    // ... other args if needed
  },
  type: UserTC.getResolver('updateById').getType(), // make output type the same like in updateById
  resolve: async ({ args }) => {
    if (checkIfEmailExists(args.email)) {
      throw new Error('');
    }

    const user = await User.create({ email: email, ...otherData });

    await createMailbox({ userId: user._id, ...other args });

    await createMediaLibrary({ userId: user._id, ...other args });

    await createProfile({ userId: user._id, ...other args });

    await sendMail({
       template: 'new_user',
       email: args.email,
    });

    return { // return the same shape of data which describe type UserTC.getResolver('updateById').getType()
      record: cabinet,
      recordId: cabinet._id,
    };
  },
});

And then add this resolver to your mutation type.

import { UserTC } from './user/userTC';

export const MutationTC = GQC.rootMutation();
MutationTC.addFields({
   createNewUser: UserTC.getResolver('createWithObjects'),
});

How we get created User data with related data Mailbox, MediaLibrary, Profile

We need to create the proper relations between UsertTC and other TCs and graphql will resolve all that we need according to mutation query for us. Let call our mutation with all needed data, eg:

mutation CreateNewUser {
  createNewUser(email: "[email protected]") {
    record { # here will be User type
      _id
      email
      mailbox {
        # mailbox fields
      }
      mediaLibrary {
        # mediaLibrary fields
      }
      profile {
        # profile fields
      }
    }
  }
}

Where mailbox, mediaLibrary and profile are fields (relations) which retrieve data for current User.

Such relations we create in following manner:

// add field with name `mailbox` via helperMethod `addRelation`
UserTC.addRelation('mailbox', { 
  // use standart resolver `findOne` from MailboxTC
  resolver: () => MailboxTC.getResolver('findOne'), 
  // prepare args for our `findOne` resolver, assume that `filter` has `userId` argument
  prepareArgs: { 
    // make arg `filter` equal to `{ userId: _currentUserId_ }`
    // `source` contains current user data, so let take its `_id`
    filter: source => ({ userId: `${source._id}` }), 
  },
  // ensure that field `_id` requested from mongodb for current user and will be present in `source` variable
  projection: { _id: true }, 
});

UserTC.addRelation('mediaLibrary', {
  resolver: () => MediaLibraryTC.getResolver('findOne'),
  prepareArgs: {
    filter: source => ({ userId: `${source._id}` }),
  },
  projection: { _id: true },
});

UserTC.addRelation('profile', {
  resolver: () => ProfileTC.getResolver('findOne'),
  prepareArgs: {
    filter: source => ({ userId: `${source._id}` }),
  },
  projection: { _id: true },
});

That's all. The added relations for User now will be available everywhere where you use this type, even in mutation payloads.

nodkz avatar Feb 08 '18 12:02 nodkz

@nodkz Thanks - this would help a lot of users :)

My two cents about this part:

    const user = await User.create({ email: email, ...otherData });

    await createMailbox();

    await createMediaLibrary();

    await createProfile();

Since @nodkz created relations by looking for the "userId" on those entities, it is probably reasonable to pass the user id of the newly created user over to the "create*" methods he describes. so:

    const user = await User.create({ email: email, ...otherData });

    await createMailbox({ userId: user._id, ...other args });

    await createMediaLibrary({ userId: user._id, ...other args });

    await createProfile({ userId: user._id, ...other args });

Assuming you want the relationship to be stored in the DB as mailbox -> user and not user.mailboxes[].

yoadsn avatar Feb 08 '18 14:02 yoadsn

@yoadsn thanks for additions, updated my example with providing userId property for creation sub-models.

nodkz avatar Feb 08 '18 14:02 nodkz

Wow, @nodkz and @yoadsn, not only did you two answer my questions, you guys went above and beyond. Thank you beyond words! This helped me accomplish everything I was hoping it would and more.

Last question for you @nodkz, in this post: #10 , you used mongoose to check the password via UserSchema.methods.checkPassword = function (password) { return bcrypt.compareSync(password, this.hashedPassword); };

How do you call that out in a query or resolver for login purposes? I'm guessing you would do it via a custom findOne resolver, but where and how do I pull UserSchema.method.checkPassword from the Mongoose schema? I was messing around with this type of .get method but can't seem to figure out where to put the args to pull the bcrypt.compareSync from the schema.

UserTC.get('$findOne.@filter').addFields({ password: 'String'!}), // some way to query mongoose schema and reeturn encrypted veriosn to check against };

I'm sure after looking over your example above more thoroughly, I'll probably be able to derive the solution, but if you have a minute more I'd love an example to work from.

Again thank you both for the amazing responses. If I could star this project twice I would :).

Best,

John Kesler

johnnykesler avatar Feb 08 '18 20:02 johnnykesler

In #10 discussed how to create and update users via mutations. Authentification and authorization a little bit another task.

TL;DR GraphQL does not provide any auth mechanism. Do it outside graphql and use context for providing any helper methods for your resolvers.

In our internal app I'm using JWT tokens. Basic scenario:

  1. User somehow obtain JWT token (email/pass, oauth, sms, etc..)
  2. Add this token in HTTP header for every GraphQL request on client side
  3. Check tokens on express layer and put info about authorized user to req.user or admin to req.admin
  4. Provide req to graphql context also may provide helper methods
import graphqlHTTP from 'express-graphql';
const graphQLMiddleware = graphqlHTTP(async (req) => ({
    schema: GraphQLSchema,
    graphiql: true,
    context: { // <--------- proper way to pass REQUEST specific data, or HELPERS methods for resolvers
       req,
       isAdmin: () => {
          return req.admin && !!req.admin.getEmail();
       },
       userData: async () => {
          const userAuthData = req.getSomehowData();
          return await UserModel.findAndAuthUser(userAuthData));
       },
       // any data, which will be avaliable across all resolvers in your graphql schema
    },
  }));
  1. In the schema on top-level fields for Query/Mutations in their resolvers check permission from context:
import { GQC } from 'graphql-compose';
export const QueryTC = GQC.rootQuery();

QueryTC.addFields({
  viewer: ViewerTC.getResolver('load'),
  adm: AdmTC.getResolver('onlyForAdmins'),
  ...
});

ViewerTC.addResolver({
  name: 'load',
  type: ViewerTC,
  resolve: () => ({}), // without any checks
});

AdmTC.addResolver({
  name: 'onlyForAdmins',
  type: AdmTC,
  resolve: ({ context }) => {
    if (!context.isAdmin()) {
      throw new Error('You should be admin, to have access to this area.');
      // or if you don't want to send error, you may silently do not provide data
      // return null; 
    }
    return {}; // IMPORTANT to return empty object; on return null/undefined graphql will not traverse and execute nested fields
  },
});

nodkz avatar Feb 09 '18 05:02 nodkz

Thanks again @nodkz! As I'm implementing a few more features I realized I had placed the addResolve function in the wrong file. I added it in the UserModel.js file so I can call the User object, but now it's not allowing me to call createMailbox,, createMediaLib, createProfile saying it's undefined. How do I call that into the addResolver. Should import it at the top of the file from schema? Should the UserSchema have fields for each of the relations? It seems that this approach creates a "virtual" relation between the two collections. I'm I wrong? See my user page below. I have refactored the schema page right now so it' ugly as sin but I'll include that following the UserModel.js page.

## UserModel.js data/user/UserModel.js

import mongoose from 'mongoose';
import composeWithMongoose from 'graphql-compose-mongoose';
import composeWithRelay from 'graphql-compose-relay';
import crypto from 'crypto';
import bcrypt from 'bcrypt';

import { ProfileTC } from './ProfileModel';
import { OrgTC } from '../orgs/OrgModel';
import { MemberTC } from '../orgs/OrgMemberModel';
import { MediaLibTC } from '../general/MediaLibModel';
import { MailBoxTC } from '../communication/MailBoxModel';

export const UserSchema = new mongoose.Schema(
  {
    email: { type: String, unique:true, set: v => v.toLowerCase().trim() },
    username: { type: String, unique:true, set: v => v.toLowerCase().trim() },
    hashedPassword: { type: String, required: true, },
    userStatus: { type: Number, default: 1 },
    verified: {type: Boolean, default: false},
  },
  {
    timestamps: true,
  },
);
UserSchema.index({ email: 1, username: 1 });

UserSchema.virtual('password')
  .set(function (password) {
    this._plainPassword = password;
    this.hashedPassword = this.encryptPassword(password);
  })
  .get(function () {
    return this._plainPassword;
  });
UserSchema.methods.encryptPassword = function (password) {
  return bcrypt.hashSync(password, 10);
};
UserSchema.methods.checkPassword = function (password) {
  return bcrypt.compareSync(password, this.hashedPassword);
};
UserSchema.methods.genPassword = function (len = 8) {
  const newPass = crypto.randomBytes(Math.ceil(len * 3 / 4)) // eslint-disable-line
        .toString('base64')   // convert to base64 format
        .slice(0, len)        // return required number of characters
        .replace(/\+/g, '0')  // replace '+' with '0'
        .replace(/\//g, '0'); // replace '/' with '0'
  this.password = newPass;
  return newPass;
};

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

UserTC.addResolver({
  kind: 'mutation',
  name: 'createWithObjects', /// <-- name for your custom resolver, will be used in mutation type
  args: {
    email: 'String!',
    username: 'String!',
    password: 'String!',
  },
  type: UserTC.getResolver('updateById').getType(), // make output type the same like in updateById
  resolve: async ({ args }) => {
    if (!args.email) {
      throw new Error('We need a valid email to create your account!');
    }
    if (!args.username) {
      throw new Error('We need a username to create your account!');
    }
    if (!args.password) {
      throw new Error('We need a valid pass to create your account!');
    }

    const user = await User.create({ email: args.email, username: args.username, password: args.password });

    await createMailBox({ userId: user._id });

    await createMediaLib({ userId: user._id });

    await createProfile({ userId: user._id });

    return { // return the same shape of data which describe type UserTC.getResolver('updateById').getType()
      recordId: user._id,
      record: {
        email: args.email,
        username: args.username,
        password: args.username,
        mailbox: {
          userId: user._id,
        },
        medialib: {
          userId: user._id,
        },
        profile:{
          user_id: user._id,
        },
      },
    };
  },
});



// UserTC.get('[email protected]').addFields({
//   password: 'String!', // Make field required for input
// });

// UserTC.get('$findOne.@filter').addFields({ password: 'String'!}),
//   // some way to query mongoose schema and reeturn encrypted veriosn to check against
//   };

// Update User and passwords here.
// This is now tied to every user update create a new resolver to handle this.
// UserTC.get('[email protected]').addFields({
//     password: 'String!',
// });

UserTC.addRelation('mailbox', {
  // use standart resolver `findOne` from MailboxTC
  resolver: () => MailBoxTC.getResolver('findOne'),
  // prepare args for our `findOne` resolver, assume that `filter` has `userId` argument
  prepareArgs: {
    // make arg `filter` equal to `{ userId: _currentUserId_ }`
    // `source` contains current user data, so let take its `_id`
    filter: source => ({ userId: `${source._id}` }),
  },
  // ensure that field `_id` requested from mongodb for current user and will be present in `source` variable
  projection: { _id: true },
});

UserTC.addRelation('mediaLib', {
  resolver: () => MediaLibTC.getResolver('findOne'),
  prepareArgs: {
    filter: source => ({ userId: `${source._id}` }),
  },
  projection: { _id: true },
});

UserTC.addRelation('profile', {
  resolver: () => ProfileTC.getResolver('findOne'),
  prepareArgs: {
    filter: source => ({ userId: `${source._id}` }),
  },
  projection: { _id: true },
});

UserTC.addRelation( 'orgs',
  {
    resolver: () => OrgTC.getResolver('findMany'),
    prepareArgs: {
      filter: source => ({ orgCreator: `${source._id}` }),
    },
    projection: { _id: true },
  }
);

UserTC.addRelation( 'memberships',
  {
    resolver: () => MemberTC.getResolver('findMany'),
    prepareArgs: {
      filter: source => ({ userId: `${source._id}` }),
    },
    projection: { _id: true },
  }
);

## schema.js data/schema.js

import { ComposeStorage } from 'graphql-compose';
import { TodoTC } from './admin/todos/TodoModel';
import { UserTC } from './users/UserModel';
import { ProfileTC } from './users/ProfileModel';
import { OrgTC } from './orgs/OrgModel';
import { MemberTC } from './orgs/OrgMemberModel';
import { MediaLibTC } from './general/MediaLibModel';
import { MediaTC } from './general/MediaModel';
import { MailBoxTC } from './communication/MailBoxModel';


const GQC = new ComposeStorage();

GQC.rootQuery().addFields({
  todo: TodoTC.getResolver('findById'),
  getManyTodos: TodoTC.getResolver('findByIds'),
  getOneTodo: TodoTC.getResolver('findOne'),
  todos: TodoTC.getResolver('findMany'),
  countTodos: TodoTC.getResolver('count'),
  todoConnection: TodoTC.getResolver('connection'),
  todoPagination: TodoTC.getResolver('pagination'),
});

GQC.rootQuery().addFields({
  getUser: UserTC.getResolver('findById'),
  getUsers: UserTC.getResolver('findByIds'),
  getOneUser: UserTC.getResolver('findOne'),
  getManyUser: UserTC.getResolver('findMany'),
  countUsers: UserTC.getResolver('count'),
  userConnection: UserTC.getResolver('connection'),
  userPagination: UserTC.getResolver('pagination'),
});

GQC.rootQuery().addFields({
  getProfile: ProfileTC.getResolver('findById'),
  getProfiles: ProfileTC.getResolver('findByIds'),
  getOneProfile: ProfileTC.getResolver('findOne'),
  getManyProfile: ProfileTC.getResolver('findMany'),
  countProfiles: ProfileTC.getResolver('count'),
  profileConnection: ProfileTC.getResolver('connection'),
  profilePagination: ProfileTC.getResolver('pagination'),
});

GQC.rootQuery().addFields({
  getOrg: OrgTC.getResolver('findById'),
  getOrgs: OrgTC.getResolver('findByIds'),
  getOneOrg: OrgTC.getResolver('findOne'),
  getManyOrg: OrgTC.getResolver('findMany'),
  countOrgs: OrgTC.getResolver('count'),
  orgConnection: OrgTC.getResolver('connection'),
  orgPagination: OrgTC.getResolver('pagination'),
});

GQC.rootQuery().addFields({
  getMember: MemberTC.getResolver('findById'),
  getMembers: MemberTC.getResolver('findByIds'),
  getOneMember: MemberTC.getResolver('findOne'),
  getManyMember: MemberTC.getResolver('findMany'),
  countMembers: MemberTC.getResolver('count'),
  memberConnection: MemberTC.getResolver('connection'),
  memberPagination: MemberTC.getResolver('pagination'),
});

GQC.rootQuery().addFields({
  getMediaLib: MediaLibTC.getResolver('findById'),
  getMediaLibs: MediaLibTC.getResolver('findByIds'),
  getOneLibMedia: MediaLibTC.getResolver('findOne'),
  getManyLibMedia: MediaLibTC.getResolver('findMany'),
  countLibMedias: MediaLibTC.getResolver('count'),
  mediaLibConnection: MediaLibTC.getResolver('connection'),
  mediaLibPagination: MediaLibTC.getResolver('pagination'),
});

GQC.rootQuery().addFields({
  getMedia: MediaTC.getResolver('findById'),
  getMedias: MediaTC.getResolver('findByIds'),
  getOneLibMedia: MediaTC.getResolver('findOne'),
  getManyLibMedia: MediaTC.getResolver('findMany'),
  countLibMedias: MediaTC.getResolver('count'),
  mediaConnection: MediaTC.getResolver('connection'),
  mediaPagination: MediaTC.getResolver('pagination'),
});

GQC.rootQuery().addFields({
  getMailBox: MailBoxTC.getResolver('findById'),
  getMailBoxes: MailBoxTC.getResolver('findByIds'),
  getOneMailBox: MailBoxTC.getResolver('findOne'),
  getManyMailBox: MailBoxTC.getResolver('findMany'),
  countMailBoxes: MailBoxTC.getResolver('count'),
  mailBoxConnection: MailBoxTC.getResolver('connection'),
  mailBoxPagination: MailBoxTC.getResolver('pagination'),
});

GQC.rootMutation().addFields({
  createTodo: TodoTC.getResolver('createOne'),
  updateTodo: TodoTC.getResolver('updateById'),
  todoUpdateOne: TodoTC.getResolver('updateOne'),
  todoUpdateMany: TodoTC.getResolver('updateMany'),
  removeTodo: TodoTC.getResolver('removeById'),
  todoRemoveOne: TodoTC.getResolver('removeOne'),
  todoRemoveMany: TodoTC.getResolver('removeMany'),
});

GQC.rootMutation().addFields({
  createUser: UserTC.getResolver('createOne'),
  updateUser: UserTC.getResolver('updateById'),
  userUpdateOne: UserTC.getResolver('updateOne'),
  userUpdateMany: UserTC.getResolver('updateMany'),
  removeUser: UserTC.getResolver('removeById'),
  userRemoveOne: UserTC.getResolver('removeOne'),
  userRemoveMany: UserTC.getResolver('removeMany'),
});

GQC.rootMutation().addFields({
  createProfile: ProfileTC.getResolver('createOne'),
  updateProfile: ProfileTC.getResolver('updateById'),
  profileUpdateOne: ProfileTC.getResolver('updateOne'),
  profileUpdateMany: ProfileTC.getResolver('updateMany'),
  removeProfile: ProfileTC.getResolver('removeById'),
  profileRemoveOne: ProfileTC.getResolver('removeOne'),
  profileRemoveMany: ProfileTC.getResolver('removeMany'),
});

GQC.rootMutation().addFields({
  createOrg: OrgTC.getResolver('createOne'),
  updateOrg: OrgTC.getResolver('updateById'),
  orgUpdateOne: OrgTC.getResolver('updateOne'),
  orgUpdateMany: OrgTC.getResolver('updateMany'),
  removeOrg: OrgTC.getResolver('removeById'),
  orgRemoveOne: OrgTC.getResolver('removeOne'),
  orgRemoveMany: OrgTC.getResolver('removeMany'),
});

GQC.rootMutation().addFields({
  createMember: MemberTC.getResolver('createOne'),
  updateMember: MemberTC.getResolver('updateById'),
  memberUpdateOne: MemberTC.getResolver('updateOne'),
  memberUpdateMany: MemberTC.getResolver('updateMany'),
  removeMember: MemberTC.getResolver('removeById'),
  memberRemoveOne: MemberTC.getResolver('removeOne'),
  memberRemoveMany: MemberTC.getResolver('removeMany'),
});

GQC.rootMutation().addFields({
  createMediaLib: MediaLibTC.getResolver('createOne'),
  updateMediaLib: MediaLibTC.getResolver('updateById'),
  mediaLibUpdateOne: MediaLibTC.getResolver('updateOne'),
  mediaLibUpdateMany: MediaLibTC.getResolver('updateMany'),
  removeMediaLib: MediaLibTC.getResolver('removeById'),
  mediaLibRemoveOne: MediaLibTC.getResolver('removeOne'),
  mediaLibRemoveMany: MediaLibTC.getResolver('removeMany'),
});

GQC.rootMutation().addFields({
  createMedia: MediaTC.getResolver('createOne'),
  updateMedia: MediaTC.getResolver('updateById'),
  mediaUpdateOne: MediaTC.getResolver('updateOne'),
  mediaUpdateMany: MediaTC.getResolver('updateMany'),
  removeMedia: MediaTC.getResolver('removeById'),
  mediaRemoveOne: MediaTC.getResolver('removeOne'),
  mediaRemoveMany: MediaTC.getResolver('removeMany'),
});

GQC.rootMutation().addFields({
  createMailBox: MailBoxTC.getResolver('createOne'),
  updateMailBox: MailBoxTC.getResolver('updateById'),
  mailBoxUpdateOne: MailBoxTC.getResolver('updateOne'),
  mailBoxUpdateMany: MailBoxTC.getResolver('updateMany'),
  removeMailBox: MailBoxTC.getResolver('removeById'),
  mailBoxRemoveOne: MailBoxTC.getResolver('removeOne'),
  mailBoxRemoveMany: MailBoxTC.getResolver('removeMany'),
});


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

export const MutationTC = GQC.rootMutation();
MutationTC.addFields({
   createNewUser: UserTC.getResolver('createWithObjects'),
});
`

## **Query** 

`mutation CreateNewUser($email: String!, $username:String!, $password:String!){
  createNewUser(email: $email, username: $username, password: $password) {
    record{
      _id
      email
      username
      mailbox{
        userId
      }
      mediaLib{
        userId
      }
      profile{
        userId
      },
    }
  }
}

The nice thing is the user is getting created with the hashed password function from inside the user model. I was having to use relay and change it up out side of the UserModel.

Sorry for all the noob questions. Thanks again for all the help.

John

johnnykesler avatar Feb 09 '18 07:02 johnnykesler

I created a simple work around and just imported the mongoose schema as well as the TC.

import {MediaLibTC, Media ) from '../general/MediaLibModel';

Then I created each of the sub collections by just using mongoose like so:

MediaLib.create({ userId: user._id });. 

Are there any side ramifications that I;m not seeing doing it this way other than it's clunky and will eventually need to be refactored?

johnnykesler avatar Feb 09 '18 16:02 johnnykesler

Your last example is not a workaround. It's correct implementation. You should import mongoose models and work with them in your custom resolver function:

  resolve: async ({ args }) => {
    if (!args.email) {
      throw new Error('We need a valid email to create your account!');
    }
    if (!args.username) {
      throw new Error('We need a username to create your account!');
    }
    if (!args.password) {
      throw new Error('We need a valid pass to create your account!');
    }

    const user = await User.create({ email: args.email, username: args.username, password: args.password });

-    await createMailBox({ userId: user._id });
+   MailBox.create({ userId: user._id });

-    await createMediaLib({ userId: user._id });
+   MediaLib.create({ userId: user._id });

-    await createProfile({ userId: user._id });
+   Profile.create({ userId: user._id });

    return { // return the same shape of data which describe type UserTC.getResolver('updateById').getType()
      recordId: user._id,
-     record: { ...
+     record: user,
    };
  },

Even more, better will be to move your resolver code to mongoose model static function. So you may need to call this method not only via graphql, but in some custom cron-tasks/scripts. So in resolver, you will have much less code, and real model logic will be placed closer to mongoose model.

UserSchema.statics.myAwesomeCreate = async function (data) {
  const user = await this.create({ email: data.email, username: data.username, password: data.password });
  await MailBox.create({ userId: user._id });
  ...
  return user;
};


UserTC.addResolver({
  kind: 'mutation',
  name: 'createWithObjects',
  args: { ... },
  type: UserTC.getResolver('updateById').getType(),
  resolve: async ({ args }) => {
     const user = await UserModel.myAwesomeCreate(args);
     return { 
       recordId: user._id,
       record: user,
    };
  }
});

You should not call in resolve methods existed mutations from the schema. I think this is anti-pattern. Of course you may do this, via info object. But I do not recommend to do it.

  resolve: async ({ args, info }) => {
     console.log(info); 
     // from info you may get access to all existed types and resolvers
     // so you may call other existed resolve method from schema
     // BUT i do not recommend do such things
  }

nodkz avatar Feb 12 '18 06:02 nodkz

GQC.rootQuery().addFields({
  todo: TodoTC.getResolver('findById'),
  getManyTodos: TodoTC.getResolver('findByIds'),
  getOneTodo: TodoTC.getResolver('findOne'),
  todos: TodoTC.getResolver('findMany'),
  countTodos: TodoTC.getResolver('count'),
  todoConnection: TodoTC.getResolver('connection'),
  todoPagination: TodoTC.getResolver('pagination'),
});

GQC.rootQuery().addFields({
  getUser: UserTC.getResolver('findById'),
  getUsers: UserTC.getResolver('findByIds'),
  getOneUser: UserTC.getResolver('findOne'),
  getManyUser: UserTC.getResolver('findMany'),
  countUsers: UserTC.getResolver('count'),
  userConnection: UserTC.getResolver('connection'),
  userPagination: UserTC.getResolver('pagination'),
});

GQC.rootQuery().addFields({
  getProfile: ProfileTC.getResolver('findById'),
  getProfiles: ProfileTC.getResolver('findByIds'),
  getOneProfile: ProfileTC.getResolver('findOne'),
  getManyProfile: ProfileTC.getResolver('findMany'),
  countProfiles: ProfileTC.getResolver('count'),
  profileConnection: ProfileTC.getResolver('connection'),
  profilePagination: ProfileTC.getResolver(........

hey @nodkz, I'm new to graphql and I have a question, Is it actually a good practice (or even safe) to add all of those resolvers like above? I imagine a good chunk of them won't be used anyway. Furthermore it is certainly cumbersome to wrap every single one to secure it with auth. I thought we should only add what we actually use.

halindraprakoso avatar Sep 24 '21 19:09 halindraprakoso

@halindraprakoso you are right. You should add only what you really want to use in your schema. graphql-compose-mongoose has the ability to generate all these resolvers, but use them or not it's your choise.

nodkz avatar Sep 28 '21 18:09 nodkz