From bae6820e64fe245fe337cc210629069e746dd5c9 Mon Sep 17 00:00:00 2001 From: Mahdi Dibaiee Date: Mon, 18 Jan 2016 18:08:43 +0330 Subject: [PATCH] feat(simple): simple CRUD REST API (no associations) feat(associations): one-to-one associations feat(associations): one-to-many associations --- .babelrc | 3 + .gitignore | 35 ++++++++ Gruntfile.js | 29 +++++++ package.json | 25 ++++++ src/associations/index.js | 4 + src/associations/many-to-many.js | 0 src/associations/one-to-many.js | 73 +++++++++++++++++ src/associations/one-to-one.js | 89 +++++++++++++++++++++ src/crud.js | 133 +++++++++++++++++++++++++++++++ src/error.js | 13 +++ src/index.js | 60 ++++++++++++++ 11 files changed, 464 insertions(+) create mode 100644 .babelrc create mode 100644 .gitignore create mode 100644 Gruntfile.js create mode 100644 package.json create mode 100644 src/associations/index.js create mode 100644 src/associations/many-to-many.js create mode 100644 src/associations/one-to-many.js create mode 100644 src/associations/one-to-one.js create mode 100644 src/crud.js create mode 100644 src/error.js create mode 100644 src/index.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..3162e1a --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "stage": 1 +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88a3a35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +#### joe made this: https://goel.io/joe + +#####=== Node ===##### + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- +node_modules + +# Debug log from npm +npm-debug.log + diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..fa662ce --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,29 @@ +module.exports = function(grunt) { + grunt.initConfig({ + babel: { + scripts: { + files: [{ + expand: true, + cwd: 'src', + src: '**/*.js', + dest: 'build/' + }] + } + }, + clean: { + files: ['build/**/*.js'] + }, + watch: { + scripts: { + files: ['src/**/*.js', 'server/**/*.js'], + tasks: ['babel'] + } + } + }); + + grunt.loadNpmTasks('grunt-babel'); + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-watch'); + + grunt.registerTask('default', ['clean', 'babel']); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..7ec4169 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "hapi-sequelize-crud", + "version": "1.0.0", + "description": "Hapi plugin that automatically generates RESTful API for CRUD", + "main": "build/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "git": "https://github.com/mdibaiee/hapi-sequelize-crud" + }, + "author": "Mahdi Dibaiee (http://dibaiee.ir/)", + "license": "MIT", + "devDependencies": { + "babel": "5.8.3", + "ghooks": "1.0.3", + "grunt": "0.4.5", + "grunt-babel": "5.0.3", + "grunt-contrib-clean": "0.7.0", + "grunt-contrib-watch": "0.6.1" + }, + "dependencies": { + "joi": "7.2.1" + } +} diff --git a/src/associations/index.js b/src/associations/index.js new file mode 100644 index 0000000..518ee27 --- /dev/null +++ b/src/associations/index.js @@ -0,0 +1,4 @@ +import oneToOne from './one-to-one'; +import oneToMany from './one-to-many'; + +export { oneToOne, oneToMany }; diff --git a/src/associations/many-to-many.js b/src/associations/many-to-many.js new file mode 100644 index 0000000..e69de29 diff --git a/src/associations/one-to-many.js b/src/associations/one-to-many.js new file mode 100644 index 0000000..eb87382 --- /dev/null +++ b/src/associations/one-to-many.js @@ -0,0 +1,73 @@ +import joi from 'joi'; +import error from '../error'; + +let prefix; + +export default (server, a, b, options) => { + prefix = options.prefix; + + list(server, a, b); + destroy(server, a, b); + update(server, a, b); +} + +export const list = (server, a, b) => { + server.route({ + method: 'GET', + path: `${prefix}/${a._singular}/{aid}/${b._plural}`, + + @error + async handler(request, reply) { + let list = await request.models[b.name].findAll({ + where: { + ...request.query, + [a.name + 'Id']: request.params.aid + } + }); + + reply(list); + } + }) +} + +export const destroy = (server, a, b) => { + server.route({ + method: 'DELETE', + path: `${prefix}/${a._singular}/{aid}/${b._plural}`, + + @error + async handler(request, reply) { + let list = await request.models[b.name].findAll({ + where: { + ...request.query, + [a.name + 'Id']: request.params.aid + } + }); + + await* list.map(instance => instance.destroy()); + + reply(); + } + }) +} + +export const update = (server, a, b) => { + server.route({ + method: 'PUT', + path: `${prefix}/${a._singular}/{aid}/${b._plural}`, + + @error + async handler(request, reply) { + let list = await request.models[b.name].findOne({ + where: { + ...request.query, + [a.name + 'Id']: request.params.aid + } + }); + + await* list.map(instance => instance.update(request.payload)); + + reply(list); + } + }) +} diff --git a/src/associations/one-to-one.js b/src/associations/one-to-one.js new file mode 100644 index 0000000..dfb0497 --- /dev/null +++ b/src/associations/one-to-one.js @@ -0,0 +1,89 @@ +import joi from 'joi'; +import error from '../error'; + +let prefix; + +export default (server, a, b, options) => { + prefix = options.prefix; + + get(server, a, b); + create(server, a, b); + destroy(server, a, b); + update(server, a, b); +} + +export const get = (server, a, b) => { + server.route({ + method: 'GET', + path: `${prefix}/${a._singular}/{aid}/${b._singular}/{bid}`, + + @error + async handler(request, reply) { + let instance = await request.models[b.name].findOne({ + where: { + id: request.params.bid, + [a.name + 'Id']: request.params.aid + } + }); + + reply(instance); + } + }) +} + +export const create = (server, a, b) => { + server.route({ + method: 'POST', + path: `${prefix}/${a._singular}/{id}/${b._singular}`, + + @error + async handler(request, reply) { + request.payload[a.name + 'Id'] = request.params.id; + let instance = await request.models[b.name].create(request.payload); + + reply(instance); + } + }) +} + +export const destroy = (server, a, b) => { + server.route({ + method: 'DELETE', + path: `${prefix}/${a._singular}/{aid}/${b._singular}/{bid}`, + + @error + async handler(request, reply) { + let instance = await request.models[b.name].findOne({ + where: { + id: request.params.bid, + [a.name + 'Id']: request.params.aid + } + }); + + await instance.destroy(); + + reply(); + } + }) +} + +export const update = (server, a, b) => { + server.route({ + method: 'PUT', + path: `${prefix}/${a._singular}/{aid}/${b._singular}/{bid}`, + + @error + async handler(request, reply) { + let instance = await request.models[b.name].findOne({ + where: { + id: request.params.bid, + [a.name + 'Id']: request.params.aid + } + }); + + await instance.update(request.payload); + + reply(instance); + } + }) +} diff --git a/src/crud.js b/src/crud.js new file mode 100644 index 0000000..814d052 --- /dev/null +++ b/src/crud.js @@ -0,0 +1,133 @@ +import joi from 'joi'; +import error from './error'; + +let prefix; + +export default (server, model, options) => { + prefix = options.prefix; + + list(server, model); + get(server, model); + scope(server, model); + create(server, model); + destroy(server, model); + update(server, model); +} + +export const list = (server, model) => { + server.route({ + method: 'GET', + path: `${prefix}/${model._plural}`, + + @error + async handler(request, reply) { + console.log(request.models[model.name], request.query); + let list = await request.models[model.name].findAll({ + where: request.query + }); + + reply(list); + } + }); +} + +export const get = (server, model) => { + server.route({ + method: 'GET', + path: `${prefix}/${model._singular}/{id?}`, + + @error + async handler(request, reply) { + let where = request.params.id ? { id : request.params.id } : request.query; + + let instance = await request.models[model.name].findOne({ where }); + + reply(instance); + }, + config: { + validate: { + params: joi.object().keys({ + id: joi.number().integer() + }) + } + } + }) +} + +export const scope = (server, model) => { + let scopes = Object.keys(model.options.scopes); + + server.route({ + method: 'GET', + path: `${prefix}/${model._plural}/{scope}`, + + @error + async handler(request, reply) { + let list = await request.models[model.name].scope(request.params.scope).findAll(); + + reply(list); + }, + config: { + validate: { + params: joi.object().keys({ + scope: joi.string().valid(...scopes) + }) + } + } + }); +} + +export const create = (server, model) => { + server.route({ + method: 'POST', + path: `${prefix}/${model._singular}`, + + @error + async handler(request, reply) { + let instance = await request.models[model.name].create(request.payload); + + reply(instance); + } + }) +} + +export const destroy = (server, model) => { + server.route({ + method: 'DELETE', + path: `${prefix}/${model._singular}/{id?}`, + + @error + async handler(request, reply) { + let where = request.params.id ? { id : request.params.id } : request.query; + + let list = await request.models[model.name].findAll({ where }); + + await* list.map(instance => instance.destroy()); + + reply(); + } + }) +} + +export const update = (server, model) => { + server.route({ + method: 'PUT', + path: `/v1/${model._singular}/{id}`, + + @error + async handler(request, reply) { + let instance = await request.models[model.name].findOne({ + where: { + id: request.params.id + } + }); + + await instance.update(request.payload); + + reply(instance); + } + }) +} + +import * as associations from './associations/index'; +export { associations }; diff --git a/src/error.js b/src/error.js new file mode 100644 index 0000000..8939093 --- /dev/null +++ b/src/error.js @@ -0,0 +1,13 @@ +export default (target, key, descriptor) => { + let fn = descriptor.value; + + descriptor.value = (request, reply) => { + try { + fn(request, reply); + } catch(e) { + reply(e); + } + } + + return descriptor; +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..9bd97b6 --- /dev/null +++ b/src/index.js @@ -0,0 +1,60 @@ +import crud, { associations } from './crud'; + +const register = (server, options = {}, next) => { + options.prefix = options.prefix || ''; + + let db = server.plugins['hapi-sequelize'].db; + let models = db.sequelize.models; + + for (let modelName of Object.keys(models)) { + let model = models[modelName]; + let { plural, singular } = model.options.name; + model._plural = plural.toLowerCase(); + model._singular = singular.toLowerCase(); + + // Join tables + if (model.options.name.singular !== model.name) continue; + + crud(server, model, options); + + for (let key of Object.keys(model.associations)) { + let association = model.associations[key]; + let { associationType, source, target } = association; + + let sourceName = source.options.name; + let targetName = target.options.name; + + target._plural = targetName.plural.toLowerCase(); + target._singular = targetName.singular.toLowerCase(); + + let targetAssociations = target.associations[sourceName.plural] || target.associations[sourceName.singular]; + let sourceType = association.associationType, + targetType = (targetAssociations || {}).associationType; + + try { + if (sourceType === 'BelongsTo' && (targetType === 'BelongsTo' || !targetType)) { + associations.oneToOne(server, source, target, options); + associations.oneToOne(server, target, source, options); + } + + if (sourceType === 'BelongsTo' && (targetType === 'HasMany')) { + associations.oneToOne(server, source, target, options); + associations.oneToOne(server, target, source, options); + associations.oneToMany(server, target, source, options); + } + } catch(e) { + // There might be conflicts in case of models associated with themselves and some other + // rare cases. + } + console.log(sourceName.singular, sourceType, targetName.singular, ' & ', targetName.singular, targetType, sourceName.singular); + } + } + + next(); +} + +register.attributes = { + pkg: require('../package.json') +} + +export { register };