├── core └── .gitkeep ├── seeders ├── .gitkeep ├── 20160704120245-appellation.js └── 20160704134958-wine.js ├── .gitignore ├── graphql ├── index.js ├── Wine │ ├── index.js │ ├── mutation.js │ ├── input.js │ └── type.js └── Appellation │ ├── index.js │ ├── input.js │ ├── type.js │ └── mutation.js ├── models ├── Appellation.js ├── Wine.js └── index.js ├── migrations ├── 20160604102819-Appellation.js └── 20160624110904-Wine.js ├── config └── config.sample.json ├── README.md ├── package.json └── app.js /core/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /seeders/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | config/config.json 3 | .DS_Store -------------------------------------------------------------------------------- /graphql/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | Wine: require('./Wine'), 5 | Appellation: require('./Appellation') 6 | } -------------------------------------------------------------------------------- /graphql/Wine/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const input = require('./input'); 4 | const type = require('./type'); 5 | const mutation = require('./mutation'); 6 | 7 | module.exports = { 8 | input: input, 9 | type: type, 10 | mutation: mutation, 11 | } -------------------------------------------------------------------------------- /graphql/Appellation/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const input = require('./input'); 4 | const type = require('./type'); 5 | const mutation = require('./mutation'); 6 | 7 | module.exports = { 8 | input: input, 9 | type: type, 10 | mutation: mutation, 11 | } -------------------------------------------------------------------------------- /graphql/Appellation/input.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const graphql = require('graphql'); 4 | 5 | module.exports = new graphql.GraphQLInputObjectType({ 6 | name: 'AppellationInput', 7 | fields: () => ({ 8 | id: { 9 | type: new graphql.GraphQLNonNull(graphql.GraphQLInt), 10 | description: 'appellation id', 11 | }, 12 | name: { 13 | type: graphql.GraphQLString, 14 | }, 15 | }) 16 | }); -------------------------------------------------------------------------------- /models/Appellation.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | 3 | module.exports = function (sequelize, Sequelize) { 4 | const Appellation = sequelize.define('Appellation', { 5 | name: Sequelize.STRING, 6 | }, { 7 | classMethods: function (models) { 8 | Appellation.hasMany(models.Wine, { 9 | foreignKey: 'appellation_id', 10 | }); 11 | }, 12 | }); 13 | 14 | return Appellation; 15 | }; 16 | -------------------------------------------------------------------------------- /graphql/Appellation/type.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const graphql = require('graphql'); 4 | 5 | module.exports = new graphql.GraphQLObjectType({ 6 | name: 'Appellation', 7 | description: 'the appellation', 8 | fields: () => ({ 9 | id: { 10 | type: new graphql.GraphQLNonNull(graphql.GraphQLInt), 11 | description: 'the id of the appellation', 12 | }, 13 | name: { 14 | type: graphql.GraphQLString, 15 | description: 'Name of the appellation' 16 | }, 17 | }) 18 | }); -------------------------------------------------------------------------------- /migrations/20160604102819-Appellation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | return queryInterface.createTable('Appellations', { 6 | id: { 7 | type: Sequelize.INTEGER, 8 | primaryKey: true, 9 | autoIncrement: true 10 | }, 11 | name: Sequelize.STRING 12 | }); 13 | }, 14 | 15 | down: function (queryInterface, Sequelize) { 16 | return queryInterface.dropTable('Appellation'); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /config/config.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "username": "baloran", 4 | "password": null, 5 | "database": "graphql", 6 | "host": "127.0.0.1", 7 | "dialect": "postgres" 8 | }, 9 | "test": { 10 | "username": "root", 11 | "password": null, 12 | "database": "database_test", 13 | "host": "127.0.0.1", 14 | "dialect": "mysql" 15 | }, 16 | "production": { 17 | "username": "root", 18 | "password": null, 19 | "database": "database_production", 20 | "host": "127.0.0.1", 21 | "dialect": "mysql" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /models/Wine.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Sequelize = require('sequelize'); 4 | 5 | module.exports = function (sequelize, Sequelize) { 6 | const Wine = sequelize.define('Wine', { 7 | name: Sequelize.STRING, 8 | price: Sequelize.DECIMAL, 9 | flavors: Sequelize.TEXT, 10 | bio: Sequelize.BOOLEAN, 11 | color: Sequelize.STRING, 12 | appellation_id: { 13 | type: Sequelize.INTEGER, 14 | references: 'Appellation', 15 | referencesKey: 'id', 16 | }, 17 | }, { 18 | classMethods: function (models) { 19 | Wine.belongsTo(models.Appellation, { 20 | foreignKey: 'id', 21 | }); 22 | }, 23 | }); 24 | 25 | return Wine; 26 | }; 27 | -------------------------------------------------------------------------------- /graphql/Appellation/mutation.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const graphql = require('graphql'); 4 | const path = require('path'); 5 | 6 | // Model 7 | const db = require(path.join(__dirname, '../../models')); 8 | 9 | // GraphQL | Sequelize 10 | const graphSequel = require('graphql-sequelize'); 11 | 12 | module.exports = new graphql.GraphQLObjectType({ 13 | name: 'appellationMutation', 14 | fields: () => ({ 15 | getByName: { 16 | type: require('../type'), 17 | description: 'Get appellation By Name', 18 | args: { 19 | name: { type: new graphql.GraphQLNonNull(graphql.GraphQLInt) } 20 | }, 21 | resolve: graphSequel.resolver(db.Wine, { 22 | include: false 23 | }) 24 | } 25 | }) 26 | }); -------------------------------------------------------------------------------- /seeders/20160704120245-appellation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | return queryInterface.bulkInsert('Appellations', [{ 6 | id: 0, 7 | name: 'Bourgogne', 8 | }, { 9 | id: 1, 10 | name: 'Corbières', 11 | }, { 12 | id: 2, 13 | name: 'Côtes de Gascogne', 14 | }, { 15 | id: 3, 16 | name: 'Pouilly-fumé', 17 | },]); 18 | }, 19 | 20 | down: function (queryInterface, Sequelize) { 21 | /* 22 | Add reverting commands here. 23 | Return a promise to correctly handle asynchronicity. 24 | 25 | Example: 26 | return queryInterface.bulkDelete('Person', null, {}); 27 | */ 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KoaJS - GraphQL - Sequelize 2 | 3 | Developed for a test for [Wino](https://wino.fr). 4 | 5 | ## Stack 6 | 7 | - [x] NodeJS 8 | - [x] KoaJS 9 | - [x] Sequelize 10 | - [x] GraphQL 11 | 12 | ## Requirement 13 | 14 | - [ ] NodeJS 15 | - [ ] Posgresql 16 | 17 | ## Install 18 | 19 | ``` 20 | npm i 21 | 22 | cp config.sample.json config.json 23 | ``` 24 | 25 | Edit the ```config.json``` with your configuration of the personal postgresql. 26 | 27 | ``` 28 | sequelize db:migrate 29 | sequelize db:seed:all 30 | ``` 31 | 32 | You can start the server 33 | 34 | ``` 35 | node app.js 36 | ``` 37 | 38 | Visit [https://localhost:3000/graphql](https://localhost:3000) for graphql test. 39 | 40 | ## Contributing 41 | 42 | Pull Request are welcome 🍷❤️ -------------------------------------------------------------------------------- /migrations/20160624110904-Wine.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | return queryInterface.createTable('Wines', { 6 | id: { 7 | type: Sequelize.INTEGER, 8 | primaryKey: true, 9 | autoIncrement: true 10 | }, 11 | name: Sequelize.STRING, 12 | price: Sequelize.DECIMAL, 13 | flavors: Sequelize.TEXT, 14 | bio: Sequelize.BOOLEAN, 15 | color: Sequelize.STRING, 16 | appellation_id: { 17 | type: Sequelize.INTEGER, 18 | references: 'Appellations', 19 | referencesKey: 'id', 20 | } 21 | }); 22 | }, 23 | 24 | down: function (queryInterface, Sequelize) { 25 | return queryInterface.dropTable('Wine'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /graphql/Wine/mutation.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const graphql = require('graphql'); 4 | const path = require('path'); 5 | 6 | // Model 7 | const db = require(path.join(__dirname, '../../models')); 8 | 9 | // GraphQL | Sequelize 10 | const graphSequel = require('graphql-sequelize'); 11 | 12 | module.exports = new graphql.GraphQLObjectType({ 13 | name: 'wineMutation', 14 | fields: () => ({ 15 | getAll: { 16 | type: new graphql.GraphQLList(require(path.join(__dirname, '../../graphql/Wine/type'))), 17 | description: 'get all wines', 18 | args: { 19 | name: { 20 | type: graphql.GraphQLString 21 | }, 22 | limit: { 23 | type: graphql.GraphQLInt 24 | }, 25 | order: { 26 | type: graphql.GraphQLString 27 | } 28 | }, 29 | resolve: graphSequel.resolver(db.Wine) 30 | } 31 | }) 32 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-sequelize-graphql", 3 | "version": "1.0.0", 4 | "description": "Koa graphql Sequelize", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "node", 11 | "sequelize", 12 | "koa", 13 | "graphql" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/baloran/koa-sequelize-graphql" 18 | }, 19 | "author": "Baloran aka Arnaud Allouis ", 20 | "license": "ISC", 21 | "dependencies": { 22 | "co": "^4.6.0", 23 | "co-body": "^4.2.0", 24 | "graphql": "^0.6.0", 25 | "graphql-relay": "^0.4.2", 26 | "graphql-sequelize": "^2.1.1", 27 | "kcors": "^1.2.1", 28 | "koa": "^1.2.0", 29 | "koa-graphql": "^0.5.4", 30 | "koa-logger": "^1.3.0", 31 | "koa-mount": "^1.3.0", 32 | "koa-router": "^5.4.0", 33 | "pg": "^6.0.0", 34 | "sequelize": "^3.23.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var Sequelize = require('sequelize'); 6 | var basename = path.basename(module.filename); 7 | var env = process.env.NODE_ENV || 'development'; 8 | var config = require(__dirname + '/../config/config.json')[env]; 9 | var db = {}; 10 | 11 | if (config.use_env_variable) { 12 | var sequelize = new Sequelize(process.env[config.use_env_variable]); 13 | } else { 14 | var sequelize = new Sequelize(config.database, config.username, config.password, config); 15 | } 16 | 17 | fs 18 | .readdirSync(__dirname) 19 | .filter(function(file) { 20 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); 21 | }) 22 | .forEach(function(file) { 23 | var model = sequelize['import'](path.join(__dirname, file)); 24 | db[model.name] = model; 25 | }); 26 | 27 | Object.keys(db).forEach(function(modelName) { 28 | if (db[modelName].associate) { 29 | db[modelName].associate(db); 30 | } 31 | }); 32 | 33 | db.sequelize = sequelize; 34 | db.Sequelize = Sequelize; 35 | 36 | module.exports = db; 37 | -------------------------------------------------------------------------------- /graphql/Wine/input.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const graphql = require('graphql'); 4 | const path = require('path'); 5 | const graphSequel = require('graphql-sequelize'); 6 | 7 | // Appellation 8 | const appellation = require(path.join(__dirname, '../Appellation')); 9 | 10 | // Model 11 | const model = require(path.join(__dirname, '../../models')); 12 | 13 | 14 | module.exports = new graphql.GraphQLInputObjectType({ 15 | name: 'WineInput', 16 | fields: () => ({ 17 | id: { 18 | type: new graphql.GraphQLNonNull(graphql.GraphQLInt), 19 | description: 'vin id', 20 | }, 21 | name: { 22 | type: graphql.GraphQLString, 23 | description: 'name of the wine.', 24 | }, 25 | price: { 26 | type: graphql.GraphQLFloat, 27 | description: 'price of the wine.', 28 | }, 29 | flavors: { 30 | type: graphql.GraphQLString, 31 | description: 'flavors of the wine.', 32 | }, 33 | bio: { 34 | type: graphql.GraphQLText, 35 | description: 'Wine is bio ?', 36 | }, 37 | color: { 38 | type: graphql.GraphQLString, 39 | description: 'color of the wine.', 40 | }, 41 | appellation_id: { 42 | type: new graphql.GraphQLList(appellation.type), 43 | resolve: graphSequel.resolver(model.Appellation, { 44 | separate: true // load seperately, disables auto including - default: false 45 | }) 46 | } 47 | }) 48 | }); -------------------------------------------------------------------------------- /seeders/20160704134958-wine.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | return queryInterface.bulkInsert('Wines', [{ 6 | 'name': 'Pommard 1er Cru Bertins', 7 | 'price': 55, 8 | 'flavors': 'Puissance et velouté.', 9 | 'bio': true, 10 | 'color': 'red', 11 | 'appellation_id': 0, // Bourgogne 12 | },{ 13 | 'name': 'Les Ollieux Romanis', 14 | 'price': 7, 15 | 'flavors': 'La terre des Ollieux a connu les premières traces de vie humaine avec l’arrivée des romains.', 16 | 'bio': true, 17 | 'color': 'red', 18 | 'appellation_id': 1, // Corbières 19 | },{ 20 | 'name': 'L\'Eté Gascon', 21 | 'price': 6.90, 22 | 'flavors': 'Vin fruité et doux. Très bien pour l\'apéritif.', 23 | 'bio': true, 24 | 'color': 'white', 25 | 'appellation_id': 2, // Côtes de Gascogne 26 | },{ 27 | 'name': 'Silex Dagueneau', 28 | 'price': 101.5, 29 | 'flavors': 'Grand Sauvignon.', 30 | 'bio': true, 31 | 'color': 'white', 32 | 'appellation_id': 3, // Pouilly-fumé 33 | },]); 34 | }, 35 | 36 | down: function (queryInterface, Sequelize) { 37 | /* 38 | Add reverting commands here. 39 | Return a promise to correctly handle asynchronicity. 40 | 41 | Example: 42 | return queryInterface.bulkDelete('Person', null, {}); 43 | */ 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /graphql/Wine/type.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const graphql = require('graphql'); 4 | const path = require('path'); 5 | const graphSequel = require('graphql-sequelize'); 6 | 7 | // Appellation 8 | const appellation = require(path.join(__dirname, '../Appellation')); 9 | 10 | // Model 11 | const model = require(path.join(__dirname, '../../models')); 12 | 13 | module.exports = new graphql.GraphQLObjectType({ 14 | name: 'Wine', 15 | description: 'A wine', 16 | fields: () => ({ 17 | id: { 18 | type: new graphql.GraphQLNonNull(graphql.GraphQLInt), 19 | description: 'the id of the wine', 20 | }, 21 | name: { 22 | type: graphql.GraphQLString, 23 | description: 'name of the wine.', 24 | }, 25 | price: { 26 | type: graphql.GraphQLFloat, 27 | description: 'price of the wine.', 28 | }, 29 | flavors: { 30 | type: graphql.GraphQLString, 31 | description: 'flavors of the wine.', 32 | }, 33 | bio: { 34 | type: graphql.GraphQLBoolean, 35 | description: 'Wine is bio ?', 36 | }, 37 | color: { 38 | type: graphql.GraphQLString, 39 | description: 'color of the wine.', 40 | }, 41 | appellation_id: { 42 | type: new graphql.GraphQLList(appellation.type), 43 | resolve: graphSequel.resolver(model.Appellation, { 44 | separate: true // load seperately, disables auto including - default: false 45 | }) 46 | } 47 | }) 48 | }); -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const koa = require('koa'); 4 | const co = require('co'); 5 | const mount = require('koa-mount'); 6 | 7 | const db = require('./models'); 8 | 9 | // CORS 10 | const cors = require('kcors'); 11 | 12 | // GraphQL Needed 13 | const graphqlHTTP = require('koa-graphql'); 14 | const graphql = require('graphql'); 15 | const graphSequel = require('graphql-sequelize'); 16 | const graphqlModel = require('./graphql'); 17 | 18 | // Koa APP 19 | const app = koa(); 20 | 21 | /** 22 | * Schema Database 23 | * TODO: 24 | * - Split this part of code 25 | */ 26 | let schema = new graphql.GraphQLSchema({ 27 | query: new graphql.GraphQLObjectType({ 28 | name: 'RootQueryType', 29 | fields: { 30 | wines: { 31 | type: new graphql.GraphQLList(graphqlModel.Wine.type), 32 | // args will automatically be mapped to `where` 33 | args: { 34 | id: { 35 | type: graphql.GraphQLInt 36 | }, 37 | name: { 38 | type: graphql.GraphQLString 39 | }, 40 | limit: { 41 | type: graphql.GraphQLInt 42 | }, 43 | order: { 44 | type: graphql.GraphQLString 45 | } 46 | }, 47 | resolve: graphSequel.resolver(db.Wine, { 48 | include: false // disable auto including of associations based on AST - default: true 49 | }) 50 | }, 51 | appellation: { 52 | type: graphqlModel.Appellation.type, 53 | args: { 54 | id: { 55 | description: 'id of the appellation', 56 | type: graphql.GraphQLInt 57 | }, 58 | name: { 59 | description: 'name of the appellation', 60 | type: graphql.GraphQLString 61 | } 62 | }, 63 | resolve: graphSequel.resolver(db.Appellation, { 64 | include: false 65 | }) 66 | } 67 | }, 68 | }), 69 | mutation: graphqlModel.Wine.mutation 70 | }); 71 | 72 | /** 73 | * CORS SUPPORT 74 | * origin: * 75 | * allowMethods: GET,HEAD,PUT,POST,DELETE 76 | */ 77 | app.use(cors()); 78 | 79 | /** 80 | * Mount graphQL route with schema 81 | */ 82 | app.use(mount('/graphql', graphqlHTTP({ 83 | schema: schema, 84 | graphiql: true 85 | }))); 86 | 87 | /** 88 | * Start the application 89 | * Test if DB is synchronized and Listen on port 3000 90 | */ 91 | co(function *(){ 92 | const connection = yield db.sequelize.sync(); 93 | return connection 94 | }).then(function (connection) { 95 | if(connection){ 96 | app.listen(3000); 97 | console.log('connected to database and listening on port 3000'); 98 | } 99 | }, function (err) { 100 | console.error(err.stack); 101 | }); 102 | --------------------------------------------------------------------------------