feat(simple): simple CRUD REST API (no associations)
feat(associations): one-to-one associations feat(associations): one-to-many associations
This commit is contained in:
commit
bae6820e64
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
@ -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
|
||||||
|
|
29
Gruntfile.js
Normal file
29
Gruntfile.js
Normal file
@ -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']);
|
||||||
|
};
|
25
package.json
Normal file
25
package.json
Normal file
@ -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 <mdibaiee@aol.com> (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"
|
||||||
|
}
|
||||||
|
}
|
4
src/associations/index.js
Normal file
4
src/associations/index.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import oneToOne from './one-to-one';
|
||||||
|
import oneToMany from './one-to-many';
|
||||||
|
|
||||||
|
export { oneToOne, oneToMany };
|
0
src/associations/many-to-many.js
Normal file
0
src/associations/many-to-many.js
Normal file
73
src/associations/one-to-many.js
Normal file
73
src/associations/one-to-many.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
89
src/associations/one-to-one.js
Normal file
89
src/associations/one-to-one.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
133
src/crud.js
Normal file
133
src/crud.js
Normal file
@ -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 };
|
13
src/error.js
Normal file
13
src/error.js
Normal file
@ -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;
|
||||||
|
}
|
60
src/index.js
Normal file
60
src/index.js
Normal file
@ -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 };
|
Loading…
Reference in New Issue
Block a user