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:
Mahdi Dibaiee 2016-01-18 18:08:43 +03:30
commit bae6820e64
11 changed files with 464 additions and 0 deletions

3
.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"stage": 1
}

35
.gitignore vendored Normal file
View 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
View 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
View 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"
}
}

View File

@ -0,0 +1,4 @@
import oneToOne from './one-to-one';
import oneToMany from './one-to-many';
export { oneToOne, oneToMany };

View File

View 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);
}
})
}

View 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
View 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
View 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
View 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 };