React-starter-kit: Mutation best practices

Created on 20 Aug 2017  Â·  10Comments  Â·  Source: kriasoft/react-starter-kit

On branch feature/apollo

Hello, I'm trying to add mutation functionality but aren't doing great. :sweat_smile:

I would like to write some documentation about adding support for mutations that connects to a local psql db.
Some of my concerns are:

  1. The changes that _needs_ to be made
  2. The best places to put mutation queries(separate folder or inside existing 'queries')
  3. How to configure the store and reducers correctly
  4. What imports I should use if I want to upload files, and generally when uploading data

Another big concern is that I'm not sure if it's optimal to use RSK for mutations, and instead use nodejs-api-starter. The drawback here is the increased cost and complexity of an interactive(CRUD) - react-starter-kit - application, that don't need third-party data.

Most helpful comment

At first, you should design some kind of security/access rights/ACL.

I'd really like to know how you've handled this part, have some code you could share? :)

I personally prefer to write graphql schemas using apollographql/graphql-tools, I find that the code is a lot easier to read/write


If you wanted to go the graphql-tools route, you could do something like this:

Create a schema.js file in the data folder, this will be the file that combines all of your sub-schemas together. Let's also add a query, a mutation and a type.


src/data/schema.js

import { merge } from 'lodash';
import { makeExecutableSchema, addErrorLoggingToSchema } from 'graphql-tools';

import {
  schema as DatabaseSchema,
  resolvers as DatabaseResolvers,
  mutations as DatabaseMutations,
  queries as DatabaseQueries
} from './types/Database/schema';

const logger = { log: (e) => console.error(e.stack) };

const RootQuery = [`
  type RootQuery {
    ${DatabaseQueries}
  }
`];

const Mutation = [`
  type Mutation {
    ${DatabaseMutations}
  }
`];

const SchemaDefinition =[`
  schema {
    query: RootQuery
    mutation: Mutation
  }
`];

// Merge all of the resolver objects together
// Put schema together into one array of schema strings
const resolvers = merge(
  DatabaseResolvers
);

const schema = [
  ...SchemaDefinition,
  ...RootQuery,
  ...DatabaseSchema,
  ...Mutation
];

export default makeExecutableSchema({
  typeDefs: schema,
  resolvers,
  logger,
});




Then create a src/data/types folder. I have my apps organized by making a folder for each "type of type" i.e. database has users, Stripe api has users etc., and each type folder gets it's own schema.js file. So for example, make a folder called Database and create a file in Database called schema.js

src/data/types/Database/schema.js will combine all Database queries/mutations/types, and this will get combined into the overall schema in src/data/schema.js. You will likely have many sub-schema files which can be added to src/data/schema.js in the same way as Database.

Create src/data/types/Database/schema.js


src/data/types/Database/schema.js

import { merge } from 'lodash';

/*** Queries ***/
import { schema as GetAllUsers, queries as GetAllUsersQueries, resolvers as GetAllUsersResolver } from './users/GetAllUsers';

/*** Mutations ***/
import { mutation as CreateUser, resolvers as CreateUserResolver } from './users/CreateUser';

// Merge all of the resolver objects together
// Put schema together into one array of schema strings
export const resolvers = merge(
  GetAllUsersResolver,
  CreateUserResolver
);

export const schema = [
  ...GetAllUsers,
];

export const mutations = [
  ...CreateUser,
]

export const queries = [
  ...GetAllUsersQueries,
]




Finally, create src/data/types/Database/users folder and create a src/data/types/Database/users/GetAllUsers.js file. This is where you define a database user type, schema and resolver


src/data/types/Database/users/GetAllUsers.js

import { User, UserClaim, UserLogin, UserProfile } from '../../../models';

export const schema = [`
  type DatabaseUser {
    id: String
    email: String
    emailConfirmed: Boolean
    logins: [DatabaseUserLogin]
    claims: [DatabaseUserClaim]
    profile: DatabaseUserProfile
    updatedAt: String
    createdAt: String
  }

  type DatabaseUserLogin {
    name: String
    key: String
    createdAt: String
    updatedAt: String
    userId: String
  }

  type DatabaseUserClaim {
    id: Int
    type: String
    value: String
    createdAt: String
    updatedAt: String
    userId: String
  }

  type DatabaseUserProfile {
    userId: String
    displayName: String
    picture: String
    gender: String
    location: String
    website: String
    createdAt: String
    updatedAt: String
  }
`];

export const queries = [`
  databaseGetAllUsers: [DatabaseUser]
  databaseGetUser(id: String!): DatabaseUser
`];

export const resolvers = {
  RootQuery: {
    databaseGetAllUsers: (parent, args) => {
      return User.findAll({
        include: [
          { model: UserLogin, as: 'logins' },
          { model: UserClaim, as: 'claims' },
          { model: UserProfile, as: 'profile' },
        ],
      }).then((users) => {
        //console.log(JSON.stringify(users, null, 4));

        return users.map(user => {
          return user;
        });
      }).catch((err) => {
        throw err;
      })
    },
    databaseGetUser: (parent, args) => {
      return User.findOne({
        where: {id: args.id},
        include: [
          { model: UserLogin, as: 'logins' },
          { model: UserClaim, as: 'claims' },
          { model: UserProfile, as: 'profile' },
        ],
      }).then((user) => {
        //console.log(JSON.stringify(user, null, 4));

        return user;
      }).catch((err) => {
        throw err;
      })
    },
  },
};




You can add mutations in a very similar fashion to queries.




src/data/types/Database/users/CreateUser.js

import { User } from '../../../models';

export const mutation = [`
  databaseCreateUser(
    name: String!
  ): DatabaseUser
`];

export const resolvers = {
  Mutation: {
    databaseCreateUser: (parent, args) => {
      return User.create({...args}).then((user) => {
        //console.log(user.dataValues);

        return user.dataValues;
      }).catch((err) => {
        //console.log(JSON.stringify(err, null, 2));

        throw err.name + ": " + err.parent.code;
      })
    },
  },
};




Welp... this post turned into something really long. I hope that helps. I could flesh this out better in a code recipe if there is demand :P

All 10 comments

The changes that needs to be made

At first, you should design some kind of security/access rights/ACL.

The best places to put mutation queries(separate folder or inside existing 'queries')

I have separate directory src/data/mutations, GraphQL follows GraphQL Relay Specification in almost all.

How to configure the store and reducers correctly

You do not need this with Apollo, but of course, there are special cases where using Redux with Apollo together have nice side effects :-)
In Apollo just return type which you mutated. If you have proper global identifier, everything should work like a charm :-)

What imports I should use if I want to upload files, and generally when uploading data

Always depends… One possible solution and most of the others :smiling_imp:

At first, you should design some kind of security/access rights/ACL.

I'd really like to know how you've handled this part, have some code you could share? :)

I personally prefer to write graphql schemas using apollographql/graphql-tools, I find that the code is a lot easier to read/write


If you wanted to go the graphql-tools route, you could do something like this:

Create a schema.js file in the data folder, this will be the file that combines all of your sub-schemas together. Let's also add a query, a mutation and a type.


src/data/schema.js

import { merge } from 'lodash';
import { makeExecutableSchema, addErrorLoggingToSchema } from 'graphql-tools';

import {
  schema as DatabaseSchema,
  resolvers as DatabaseResolvers,
  mutations as DatabaseMutations,
  queries as DatabaseQueries
} from './types/Database/schema';

const logger = { log: (e) => console.error(e.stack) };

const RootQuery = [`
  type RootQuery {
    ${DatabaseQueries}
  }
`];

const Mutation = [`
  type Mutation {
    ${DatabaseMutations}
  }
`];

const SchemaDefinition =[`
  schema {
    query: RootQuery
    mutation: Mutation
  }
`];

// Merge all of the resolver objects together
// Put schema together into one array of schema strings
const resolvers = merge(
  DatabaseResolvers
);

const schema = [
  ...SchemaDefinition,
  ...RootQuery,
  ...DatabaseSchema,
  ...Mutation
];

export default makeExecutableSchema({
  typeDefs: schema,
  resolvers,
  logger,
});




Then create a src/data/types folder. I have my apps organized by making a folder for each "type of type" i.e. database has users, Stripe api has users etc., and each type folder gets it's own schema.js file. So for example, make a folder called Database and create a file in Database called schema.js

src/data/types/Database/schema.js will combine all Database queries/mutations/types, and this will get combined into the overall schema in src/data/schema.js. You will likely have many sub-schema files which can be added to src/data/schema.js in the same way as Database.

Create src/data/types/Database/schema.js


src/data/types/Database/schema.js

import { merge } from 'lodash';

/*** Queries ***/
import { schema as GetAllUsers, queries as GetAllUsersQueries, resolvers as GetAllUsersResolver } from './users/GetAllUsers';

/*** Mutations ***/
import { mutation as CreateUser, resolvers as CreateUserResolver } from './users/CreateUser';

// Merge all of the resolver objects together
// Put schema together into one array of schema strings
export const resolvers = merge(
  GetAllUsersResolver,
  CreateUserResolver
);

export const schema = [
  ...GetAllUsers,
];

export const mutations = [
  ...CreateUser,
]

export const queries = [
  ...GetAllUsersQueries,
]




Finally, create src/data/types/Database/users folder and create a src/data/types/Database/users/GetAllUsers.js file. This is where you define a database user type, schema and resolver


src/data/types/Database/users/GetAllUsers.js

import { User, UserClaim, UserLogin, UserProfile } from '../../../models';

export const schema = [`
  type DatabaseUser {
    id: String
    email: String
    emailConfirmed: Boolean
    logins: [DatabaseUserLogin]
    claims: [DatabaseUserClaim]
    profile: DatabaseUserProfile
    updatedAt: String
    createdAt: String
  }

  type DatabaseUserLogin {
    name: String
    key: String
    createdAt: String
    updatedAt: String
    userId: String
  }

  type DatabaseUserClaim {
    id: Int
    type: String
    value: String
    createdAt: String
    updatedAt: String
    userId: String
  }

  type DatabaseUserProfile {
    userId: String
    displayName: String
    picture: String
    gender: String
    location: String
    website: String
    createdAt: String
    updatedAt: String
  }
`];

export const queries = [`
  databaseGetAllUsers: [DatabaseUser]
  databaseGetUser(id: String!): DatabaseUser
`];

export const resolvers = {
  RootQuery: {
    databaseGetAllUsers: (parent, args) => {
      return User.findAll({
        include: [
          { model: UserLogin, as: 'logins' },
          { model: UserClaim, as: 'claims' },
          { model: UserProfile, as: 'profile' },
        ],
      }).then((users) => {
        //console.log(JSON.stringify(users, null, 4));

        return users.map(user => {
          return user;
        });
      }).catch((err) => {
        throw err;
      })
    },
    databaseGetUser: (parent, args) => {
      return User.findOne({
        where: {id: args.id},
        include: [
          { model: UserLogin, as: 'logins' },
          { model: UserClaim, as: 'claims' },
          { model: UserProfile, as: 'profile' },
        ],
      }).then((user) => {
        //console.log(JSON.stringify(user, null, 4));

        return user;
      }).catch((err) => {
        throw err;
      })
    },
  },
};




You can add mutations in a very similar fashion to queries.




src/data/types/Database/users/CreateUser.js

import { User } from '../../../models';

export const mutation = [`
  databaseCreateUser(
    name: String!
  ): DatabaseUser
`];

export const resolvers = {
  Mutation: {
    databaseCreateUser: (parent, args) => {
      return User.create({...args}).then((user) => {
        //console.log(user.dataValues);

        return user.dataValues;
      }).catch((err) => {
        //console.log(JSON.stringify(err, null, 2));

        throw err.name + ": " + err.parent.code;
      })
    },
  },
};




Welp... this post turned into something really long. I hope that helps. I could flesh this out better in a code recipe if there is demand :P

@tim-soft Wow very powerful writing. Not for GH I believe. This should be at Wiki. Can you?

Most definitely, I've had a lot of fun with your apollo branch :)

Maybe the starter schema in the apollo branch should be done this way? A lot easier imo

@tim-soft I'm in phase searching you, wish write private mail :-) I really need someone who can help me :smiling_imp:

My branches are really minimal in needs — but I hope helping a lot..

Wow, nice replies! I greatly appreciate it.

I'm sorry but I forgot to write that I'm trying to do this on the feature/apollo branch, I don't know if that makes any difference to your suggestions though.

So far I've written a poorly edited guide to setting up a secure PSQL DB, with a user that has limited permissions. I can share it somewhere if you can't wait to look at it. :slightly_smiling_face:
I'm still filling it out and am trying to make a complete guide 'from cloning RSK, to adding data to the table'

The changes I think has to happen (given my mutation is called "createTest"):

Assuming you have PSQL running and set up locally:

Add the following inside the /src folder:

  • TestType in /data/types
  • Folder in /data called mutations

    • test.js file in folder

  • Test model to /models
  • Import and export Test in /data/models/index
  • Import and Define createTest mutation in /data/schema

P.S I've not been able to make a mutation yet, but I'm able to see it on the /graphql endpoint.

Am I missing anything or on the completely wrong track? Again, thank you for the great replies!

I'm working on a guide for the wiki for doing what I mentioned earlier. Nuke your queries folder and give this a try ;)

I got it to work with your code, even with a test mutation and everything! Really nice. I only encountered one problem: when importing the schemas in /data/database/schema.js; I got some weird errors until I figured I had to use [... DatabaseSchema, ...TestSchema] to merge the schemes properly. Let me know if I can help with anything and thanks for the great examples. :)

I'm soon done with the first iteration of a small guide on how to connect to PostgreSQL and make PostgreSQL and Docker talk.

The only 'problem' I've thought about but not done anything, is the manual change of the URI if you want to run the app in dev mode outside of Docker.

@GideonFlynn very nice small guide :+1: :-)

The only 'problem' I've thought about but not done anything, is the manual change of the URI if you want to run the app in dev mode outside of Docker.

You can pass any string via environment variables. You can pass environment vars into docker too.. then just read them from process.env

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rochadt picture rochadt  Â·  3Comments

fchienvuhoang picture fchienvuhoang  Â·  3Comments

nguyenbathanh picture nguyenbathanh  Â·  4Comments

Bogdaan picture Bogdaan  Â·  3Comments

cbravo picture cbravo  Â·  3Comments