├── README.md └── server ├── .gitignore ├── package-lock.json ├── package.json └── src ├── .babelrc ├── config ├── apollo.js ├── env.js ├── errors │ ├── authentication.js │ └── baseResolver.js ├── index.js └── middleware │ └── auth.js ├── graphql ├── index.js ├── resolvers │ ├── customScalar.js │ ├── login.js │ ├── upload.js │ └── user.js └── types │ ├── login.gql │ ├── response.gql │ ├── schema.gql │ ├── upload.gql │ └── user.gql ├── models ├── Uploads.js ├── User.js └── index.js └── utils └── upload.js /README.md: -------------------------------------------------------------------------------- 1 | # Template Graphql 2 | 3 | This template contains the **structure** and **configuration** needed for a project with **Graphql**, **Mongo**, **Express** and **Apollo-Server**. Accelerating the process of creating it. 4 | 5 | It is only the part of the **Backend**. You can feel free to add your Middlewares and additional configurations. 6 | 7 | This template is **ready to upload multiple files** with graphql 8 | 9 | What makes this template great is the **division** in **small files** of its **schema** and their respective **resolvers** of graphql, allowing more control at a production implementation level. 10 | 11 | It has a built-in **Authentication** to verify that the **token** generated by **jwt** at the time of **login** is valid in each **request**, in if the token is valid, a **refresh token** is returned for the client to **update** it. 12 | 13 | It also has a **Middleware for resolvers** configured. You have to import the middleware and **Wrap** your resolvers in the. See the example in ```server/graphql/resolvers/user.js ``` 14 | 15 | Added an authorization and authentication error handler. See the example in the folder ```server/config/errors/``` 16 | 17 | ## Optionally 18 | Open the directory ``` server/config/env.js ``` and **edit the environment variables** of connection to the database, preferably create a user from the mongo shell. For more information on how to create users visit the following Link [CreateUsers](https://docs.mongodb.com/manual/reference/method/db.createUser/) 19 | 20 | ## Steps 21 | 22 | * Enter the directory ```/server``` 23 | * Install dependencies with ``` npm install ``` 24 | * Run ``` npm start ``` 25 | * Open the Apollo server playground ```localhost:4000/graphql``` 26 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /uploads -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon ./src/config --exec babel-node .e js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "apollo-errors": "^1.9.0", 13 | "apollo-resolvers": "^1.4.1", 14 | "apollo-server-express": "^2.3.1", 15 | "babel-cli": "^6.26.0", 16 | "babel-preset-env": "^1.7.0", 17 | "babel-preset-stage-0": "^6.24.1", 18 | "bcrypt": "^3.0.2", 19 | "body-parser": "^1.18.3", 20 | "express": "^4.16.4", 21 | "graphql": "^14.0.2", 22 | "graphql-iso-date": "^3.6.1", 23 | "graphql-subscriptions": "^1.0.0", 24 | "graphql-tools": "^4.0.3", 25 | "graphql-type-json": "^0.2.1", 26 | "graphql-upload": "^8.0.4", 27 | "jsonwebtoken": "^8.4.0", 28 | "merge-graphql-schemas": "^1.5.8", 29 | "mkdirp": "^0.5.1", 30 | "mongoose": "^5.4.2", 31 | "mongoose-unique-validator": "^2.0.2", 32 | "promises-all": "^1.0.0", 33 | "shortid": "^2.2.14", 34 | "subscriptions-transport-ws": "^0.9.15" 35 | }, 36 | "devDependencies": { 37 | "nodemon": "^1.18.9" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "stage-0" 5 | ] 6 | } -------------------------------------------------------------------------------- /server/src/config/apollo.js: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from 'apollo-server-express' 2 | import { PubSub } from 'graphql-subscriptions' 3 | import { schema } from '../graphql' 4 | import { processUpload } from '../utils/upload' 5 | import models from '../models' 6 | 7 | export const pubsub = new PubSub() 8 | 9 | export const server = new ApolloServer({ 10 | schema, 11 | context({ req }) { 12 | return { 13 | models, 14 | user: { 15 | id: req.user, 16 | role: req.role 17 | }, 18 | utils: { 19 | processUpload 20 | } 21 | } 22 | }, 23 | playground: { 24 | endpoint: '/graphql', 25 | settings: { 26 | 'editor.theme': 'light' 27 | }, 28 | subscriptionEndpoint: 'ws://localhost:4000/subscriptions' 29 | } 30 | 31 | }) 32 | -------------------------------------------------------------------------------- /server/src/config/env.js: -------------------------------------------------------------------------------- 1 | process.env.PORT = process.env.PORT || 4000 // Port express 2 | 3 | process.env.EXPTOKEN = "3d" // Time live valid tokens 3 days 4 | process.env.SEED = process.env.SEED || 'No, I am your father' // Seed SECRET encrypt token 5 | 6 | // Configuration MongoDB 7 | process.env.DBNAME = process.env.DBNAME || 'myDataBase'; 8 | process.env.DBHOST = process.env.DBHOST || 'localhost'; 9 | process.env.DBUSER = process.env.DBUSER || 'user'; 10 | process.env.DBPASS = process.env.DBPASS || ''; 11 | process.env.DBPORT = process.env.DBPORT || 27017; 12 | 13 | // For development 14 | process.env.URI = `mongodb://${process.env.DBHOST}:${process.env.DBPORT}/${process.env.DBNAME}`; 15 | 16 | // For production 17 | // process.env.URI = `mongodb://${process.env.DBUSER}:${process.env.DBPASS}@${process.env.DBHOST}:${process.env.DBPORT}/${process.env.DBNAME}`; -------------------------------------------------------------------------------- /server/src/config/errors/authentication.js: -------------------------------------------------------------------------------- 1 | import { createError } from 'apollo-errors' 2 | import { baseResolver } from './baseResolver' 3 | 4 | export const ForbiddenError = createError('ForbiddenError', { 5 | message: 'You do not have permission to perform this action.' 6 | }); 7 | 8 | export const AuthenticationRequiredError = createError('AuthenticationRequiredError', { 9 | message: 'You must log in to do this' 10 | }); 11 | 12 | export const isAuth = baseResolver.createResolver( 13 | // Extract the user from the context (undefined if it does not exist) 14 | (root, args, { user: { id } }, info) => { 15 | if (!id) throw new AuthenticationRequiredError() 16 | 17 | } 18 | ); 19 | 20 | export const isAdmin = isAuth.createResolver( 21 | // Extract the user and make sure he is an administrator. 22 | (root, args, { user: { role } }, info) => { 23 | if (role !== 'ADMIN') throw new ForbiddenError() 24 | } 25 | ) -------------------------------------------------------------------------------- /server/src/config/errors/baseResolver.js: -------------------------------------------------------------------------------- 1 | import { createResolver } from 'apollo-resolvers' 2 | import { createError, isInstance } from 'apollo-errors' 3 | 4 | const UnknownError = createError('UnknownError', { 5 | message: 'An unknown error has occurred! Please try again later' 6 | }); 7 | 8 | export const baseResolver = createResolver( 9 | null, 10 | (root, args, context, error) => isInstance(error) ? error : new UnknownError() 11 | ); -------------------------------------------------------------------------------- /server/src/config/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import mongo from 'mongoose' 3 | import { checkToken } from './middleware/auth' 4 | import { server } from './apollo' 5 | import { execute, subscribe } from 'graphql' 6 | import { createServer } from 'http' 7 | import { SubscriptionServer } from 'subscriptions-transport-ws' 8 | import { schema } from '../graphql' 9 | import './env' // Environment Variables 10 | 11 | const app = express() 12 | 13 | app.use(checkToken) // Middleware for validate tokens 14 | server.applyMiddleware({ app }) 15 | 16 | // Create webSocketServer 17 | const ws = createServer(app) 18 | 19 | // Configure params for mongoConnection 20 | const options = { useNewUrlParser: true, useCreateIndex: true, useFindAndModify: false } 21 | 22 | mongo.connect(process.env.URI, options).then(() => { 23 | // If connected, then start server 24 | 25 | ws.listen(process.env.PORT, () => { 26 | console.log('Server on port', process.env.PORT) 27 | console.log('Mongo on port: ', process.env.DBPORT) 28 | 29 | // Set up the WebSocket for handling GraphQL subscriptions 30 | new SubscriptionServer({ 31 | execute, 32 | subscribe, 33 | schema 34 | }, { 35 | server: ws, 36 | path: '/subscriptions', 37 | }); 38 | }); 39 | 40 | }).catch(err => { 41 | console.log(err) 42 | }) -------------------------------------------------------------------------------- /server/src/config/middleware/auth.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | export const getToken = payload => { 4 | let token = jwt.sign(payload, process.env.SEED, { expiresIn: process.env.EXPTOKEN }); 5 | return token; 6 | } 7 | 8 | export const checkToken = (req, res, next) => { 9 | const token = req.headers["x-token"]; 10 | 11 | if (token) { 12 | try { 13 | // Verificamos que el token sea valido 14 | const { user, role } = jwt.verify(token, process.env.SEED); 15 | 16 | // Si se verifico bien, entonces creamos un nuevo token con el mismo idUser 17 | const newToken = getToken({ user, role }); 18 | 19 | // Agregamos a nuestros headers (backend) el id del usuario logeado 20 | req.user = user; 21 | req.role = role; 22 | 23 | // Enviamos a los headers del cliente el nuevo token para que lo actualice 24 | res.set("Access-Control-Expose-Headers", "x-token"); 25 | res.set("x-token", newToken); 26 | 27 | } catch (error) { 28 | // Invalid Token 29 | } 30 | } 31 | 32 | next(); 33 | } -------------------------------------------------------------------------------- /server/src/graphql/index.js: -------------------------------------------------------------------------------- 1 | import { makeExecutableSchema } from 'graphql-tools'; 2 | import { fileLoader, mergeResolvers, mergeTypes } from 'merge-graphql-schemas'; 3 | import * as path from 'path'; 4 | 5 | const resolversArray = fileLoader(path.join(__dirname, './resolvers/'), { recursive: true, extensions: ['.js'] }); 6 | const typesArray = fileLoader(path.join(__dirname, './types/'), { recursive: true, extensions: ['.gql'] }); 7 | const resolvers = mergeResolvers(resolversArray); 8 | const typeDefs = mergeTypes(typesArray, {all: true}); 9 | const schema = makeExecutableSchema({ typeDefs, resolvers }); 10 | 11 | export { schema }; 12 | -------------------------------------------------------------------------------- /server/src/graphql/resolvers/customScalar.js: -------------------------------------------------------------------------------- 1 | import GraphQLJSON from 'graphql-type-json'; 2 | import { GraphQLDate } from 'graphql-iso-date'; 3 | import { GraphQLUpload } from 'graphql-upload' 4 | 5 | export default { 6 | JSON: GraphQLJSON, 7 | Date: GraphQLDate, 8 | Upload: GraphQLUpload, 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /server/src/graphql/resolvers/login.js: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | import { getToken } from '../../config/middleware/auth'; 3 | 4 | export default { 5 | Mutation: { 6 | async login(root, { email, password }, { models: { user } }) { 7 | try { 8 | let User = await user.findOne({ email }, 'name password role'); 9 | 10 | if (User && bcrypt.compareSync(password, User.password)) { 11 | return { 12 | ok: true, 13 | message: 'Authenticated', 14 | token: getToken({ user: User.id, role: User.role }) 15 | }; 16 | 17 | } else return { 18 | ok: false, 19 | message: 'Incorrect email or password' 20 | }; 21 | 22 | } catch (err) { 23 | return { 24 | ok: false, 25 | message: err.message 26 | }; 27 | } 28 | } 29 | } 30 | }; -------------------------------------------------------------------------------- /server/src/graphql/resolvers/upload.js: -------------------------------------------------------------------------------- 1 | import promisesAll from 'promises-all' 2 | 3 | export default { 4 | Query: { 5 | async getUploads(root, {}, { models: { uploads } }) { 6 | try { return await uploads.find({}) } catch (err) { return err } 7 | } 8 | }, 9 | 10 | Mutation: { 11 | singleUpload(obj, { file }, { utils: { processUpload } }) { 12 | return processUpload(file); 13 | }, 14 | 15 | async multipleUpload(obj, { files }, { utils: { processUpload } }) { 16 | const { resolve, reject } = await promisesAll.all(files.map(processUpload)) 17 | 18 | if (reject.length) 19 | reject.forEach(({ name, message }) => console.error(`${name}: ${message}`)) 20 | 21 | return resolve 22 | } 23 | } 24 | }; -------------------------------------------------------------------------------- /server/src/graphql/resolvers/user.js: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt' 2 | import { isAuth, ForbiddenError } from '../../config/errors/authentication' 3 | import { pubsub } from '../../config/apollo' 4 | 5 | 6 | // I export my querys and mutations to join with the other resolvers 7 | export default { 8 | Subscription: { 9 | newUser: { 10 | subscribe: () => pubsub.asyncIterator('NEW_USER') 11 | } 12 | }, 13 | 14 | Query: { 15 | /* 16 | "isAuth" verifies that the user is logged in and 17 | that the token is valid, if it passes the validations 18 | then it executes the resolver that it contains inside 19 | */ 20 | getUsers: isAuth.createResolver( 21 | async (root, { since = 0, limit = 10 }, { models: { user } }) => { 22 | try { 23 | return await user.find({ status: true }).skip(since).limit(limit); 24 | } catch (err) { 25 | return { ok: false, message: err.message }; 26 | } 27 | } 28 | ), 29 | 30 | getUser: isAuth.createResolver( 31 | async (root, { id }, { models: { user } }) => { 32 | try { 33 | return await user.findOne({ status: true, _id: id }); 34 | 35 | } catch (err) { return { ok: false, message: err.message }; } 36 | } 37 | ) 38 | }, 39 | 40 | Mutation: { 41 | createUser: async (root, { input }, { models: { user } }) => { 42 | 43 | let { name, email, password, role } = input; 44 | 45 | try { 46 | let newUser = new user({ 47 | name, 48 | email, 49 | password: bcrypt.hashSync(password, 10), 50 | role 51 | }); 52 | 53 | await newUser.save() 54 | 55 | // Active eventTrigger 56 | pubsub.publish('NEW_USER', { newUser }) 57 | 58 | return { 59 | ok: true, 60 | message: 'Created correctly' 61 | }; 62 | 63 | } catch (err) { 64 | return { 65 | ok: false, 66 | message: err.message 67 | }; 68 | } 69 | }, 70 | 71 | updateUser: isAuth.createResolver( 72 | async (root, { id, input }, { models: { user } }) => { 73 | 74 | const { name, email, role } = input; 75 | 76 | let query = { _id: id, status: true }; 77 | let update = { $set: { name, email, role } }; 78 | 79 | let props = { 80 | new: true, 81 | runValidators: true, 82 | context: 'query' 83 | } 84 | 85 | try { 86 | await user.findOneAndUpdate(query, update, props); 87 | return { ok: true, message: 'Updated successfully' } 88 | 89 | } catch (err) { 90 | return { ok: false, message: err.message }; 91 | } 92 | } 93 | ), 94 | 95 | deleteUser: isAuth.createResolver( 96 | async (root, { id }, { user: { role }, models: { user } }) => { 97 | // If the user does not have the permissions to create more users, we return an error 98 | if (role !== 'ADMIN') throw new ForbiddenError(); 99 | 100 | try { 101 | let query = { _id: id, status: true }; 102 | let update = { $set: { status: false } }; 103 | 104 | let User = await user.findOneAndUpdate(query, update); 105 | 106 | if (!User) return { ok: false, message: 'It was previously removed' }; 107 | return { ok: true, message: 'Removed correctly' }; 108 | 109 | } catch (err) { 110 | return { ok: false, message: "The id does not exist" }; 111 | } 112 | } 113 | ) 114 | } 115 | 116 | } -------------------------------------------------------------------------------- /server/src/graphql/types/login.gql: -------------------------------------------------------------------------------- 1 | """ 2 | Returns the token for authentication, the name and role of the person 3 | """ 4 | type Login { 5 | ok: Boolean 6 | message: String 7 | token: String 8 | } 9 | extend type Mutation { 10 | login(email: String!, password: String!): Login! 11 | } 12 | -------------------------------------------------------------------------------- /server/src/graphql/types/response.gql: -------------------------------------------------------------------------------- 1 | type Response { 2 | ok: Boolean! 3 | message: String 4 | data: JSON 5 | } 6 | -------------------------------------------------------------------------------- /server/src/graphql/types/schema.gql: -------------------------------------------------------------------------------- 1 | scalar Upload 2 | scalar JSON 3 | scalar Date 4 | 5 | type Query { 6 | # Empty field obligatory 7 | _empty: String 8 | } 9 | 10 | type Mutation { 11 | _empty: String 12 | } 13 | 14 | type Subscription { 15 | _empty: String 16 | } 17 | -------------------------------------------------------------------------------- /server/src/graphql/types/upload.gql: -------------------------------------------------------------------------------- 1 | type File { 2 | id: ID! 3 | path: String! 4 | filename: String! 5 | mimetype: String! 6 | } 7 | 8 | type Query { 9 | getUploads: [File] 10 | } 11 | 12 | type Mutation { 13 | singleUpload(file: Upload!): File! 14 | multipleUpload(files: [Upload!]!): [File!]! 15 | } 16 | -------------------------------------------------------------------------------- /server/src/graphql/types/user.gql: -------------------------------------------------------------------------------- 1 | enum Roles { 2 | ADMIN 3 | USER 4 | } 5 | 6 | type User { 7 | id: ID 8 | name: String 9 | email: String 10 | password: String 11 | role: Roles 12 | status: Boolean 13 | } 14 | 15 | input UserInput { 16 | name: String! 17 | email: String! 18 | password: String 19 | role: Roles! 20 | } 21 | 22 | extend type Query { 23 | getUsers(since: Int, limit: Int): [User] 24 | getUser(id: ID): User 25 | } 26 | 27 | extend type Mutation { 28 | createUser(input: UserInput!): Response 29 | updateUser(id: ID!, input: UserInput!): Response 30 | deleteUser(id: ID!): Response 31 | } 32 | 33 | extend type Subscription { 34 | newUser: User! 35 | } 36 | -------------------------------------------------------------------------------- /server/src/models/Uploads.js: -------------------------------------------------------------------------------- 1 | import moongoose from 'mongoose'; 2 | 3 | let Schema = moongoose.Schema; 4 | 5 | let uploadSchema = new Schema({ 6 | filename: { 7 | type: String, 8 | required: [true, 'The filename is necessary'] 9 | }, 10 | mimetype: { 11 | type: String, 12 | required: [true, 'The mimetype is necessary'] 13 | }, 14 | path: { 15 | type: String, 16 | required: [true, 'The path is necessary'] 17 | } 18 | }); 19 | 20 | export default moongoose.model('uploads', uploadSchema); 21 | -------------------------------------------------------------------------------- /server/src/models/User.js: -------------------------------------------------------------------------------- 1 | import moongoose from 'mongoose'; 2 | import uniqueValidator from 'mongoose-unique-validator'; 3 | 4 | let roles = { 5 | values: ['ADMIN', 'USER'], 6 | message: '{VALUE} it is not a valid role' 7 | } 8 | 9 | let Schema = moongoose.Schema; 10 | 11 | let userSchema = new Schema({ 12 | name: { 13 | type: String, 14 | required: [true, 'The name is necessary'] 15 | }, 16 | email: { 17 | type: String, 18 | unique: true, 19 | required: [true, 'The email is necessary'] 20 | }, 21 | password: { 22 | type: String, 23 | required: [true, 'The password is necessary'] 24 | }, 25 | role: { 26 | type: String, 27 | default: 'USER_ROLE', 28 | enum: roles 29 | }, 30 | status: { 31 | type: Boolean, 32 | default: true 33 | } 34 | }); 35 | 36 | // Remove the password when a query is made (for security). 37 | userSchema.methods.toJSON = function () { 38 | let user = this; 39 | let userObject = user.toObject(); 40 | delete userObject.password; 41 | 42 | return userObject; 43 | } 44 | 45 | userSchema.plugin(uniqueValidator, { message: '{PATH} it must be unique' }); 46 | 47 | export default moongoose.model('User', userSchema); 48 | -------------------------------------------------------------------------------- /server/src/models/index.js: -------------------------------------------------------------------------------- 1 | import uploads from '../models/Uploads' 2 | import user from '../models/User' 3 | 4 | export default { 5 | uploads, 6 | user 7 | } -------------------------------------------------------------------------------- /server/src/utils/upload.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { createWriteStream, unlinkSync } from 'fs' 3 | import { sync } from 'mkdirp' 4 | import { generate } from 'shortid' 5 | import uploads from '../models/Uploads' 6 | 7 | const uploadDir = resolve(__dirname, '../../uploads') 8 | 9 | // Ensure upload directory exists. 10 | sync(uploadDir) 11 | 12 | const storeDB = async(file) => { 13 | const { id, filename, mimetype, path } = file; 14 | 15 | try { 16 | let file = new uploads({ id, filename, mimetype, path }); 17 | return await file.save(); 18 | 19 | } catch (err) { 20 | return err 21 | } 22 | } 23 | 24 | const storeFS = ({ stream, filename }) => { 25 | const id = generate() 26 | const path = `${uploadDir}/${id}-${filename}` 27 | 28 | return new Promise((resolve, reject) => 29 | stream.on('error', error => { 30 | if (stream.truncated) unlinkSync(path) 31 | reject(error) 32 | }) 33 | .pipe(createWriteStream(path)) 34 | .on('error', error => reject(error)) 35 | .on('finish', () => resolve({ id, path })) 36 | ) 37 | } 38 | 39 | export const processUpload = async upload => { 40 | const { createReadStream, filename, mimetype } = await upload 41 | const stream = createReadStream() 42 | const { id, path } = await storeFS({ stream, filename }) 43 | return storeDB({ id, filename, mimetype, path }) 44 | } --------------------------------------------------------------------------------