New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

koa-restql

Package Overview
Dependencies
Maintainers
4
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

koa-restql - npm Package Compare versions

Comparing version 0.0.1 to 0.1.0

lib/loaders.js

300

lib/common.js
'use strict'
const qs = require('qs');
const debug = require('debug')('koa-restql:common');
const defaultLimit = 20;
function switchByType (param, callbacks) {
module.exports.getAssociationName = (association) => {
let isSingular = association.isSingleAssociation
, name = association.options.name;
callbacks = callbacks || {};
return isSingular ? name.singular : name.plural;
}
const {
object, array, string, bool, number, defaults
} = callbacks;
const parseAttributes = (_attributes) => {
let attrs;
let callback;
if (Array.isArray(_attributes)) {
attrs = _attributes;
} else if (typeof _attributes === 'string') {
attrs = _attributes.split(/(,|\ )/);
}
switch (typeof param) {
case 'object':
if (Array.isArray(param)) {
callback = array;
} else {
callback = object;
}
break;
case 'string':
callback = string;
break;
case 'boolean':
callback = bool;
break;
case 'number':
callback = number;
break;
default:
callback = defaults;
break;
}
return attrs;
if (callback !== undefined) {
if ('function' === typeof callback) {
return callback(param);
} else {
return callback;
}
}
}
const unionAttributes = (_attributes, attributes) => {
function shouldIgnoreAssociation (method, options) {
if (typeof _attributes === 'object') {
if (_attributes.include) {
/**
* @FIXME
*
* This is a bug in Sequelize
* include doesn't work at all
*/
_attributes.include = parseAttributes(_attributes.include);
}
if (_attributes.exclude) {
_attributes.exclude = parseAttributes(_attributes.exclude);
}
} else {
return parseAttributes(_attributes);
}
options = options || {}
return _attributes;
let ignore = options.ignore;
return switchByType(ignore, {
array : () =>
ignore.find(ignoreMethod => ignoreMethod.toLowerCase() === method),
bool : () => ignore
})
}
const unionWhere = (_where, attributes) => {
if (!_where || typeof _where !== 'object')
return;
function parseAttributes (_attributes, attributes) {
let attrs;
let where;
if (_attributes)
Object.keys(_where).forEach(key => {
if (attributes[key]) {
where = where || {};
where[key] = _where[key];
}
return switchByType(_attributes, {
array : () => (_attributes.filter(attr => attributes[attr])),
string : () => (_attributes.split(/,/).filter(attr => attributes[attr]))
})
return where;
}
const unionOrder = (_order, attributes) => {
if (!_order)
return;
function unionWhere (_where) {
if (Array.isArray(_order)) {
if (!_order.length)
return;
return switchByType(_where, {
object : () => {
let where;
return _order.filter(item => {
if (!item)
return false;
let name;
if (Array.isArray(item)) {
name = item[0];
} else if (typeof item === 'string'){
name = item.split(' ')[0];
}
return name && !!attributes[name];
}).map(item => {
if (typeof item === 'string') {
return item.split(' ');
}
return item;
})
} else if (typeof _order === 'string') {
let order = _order.split(' ');
if (attributes[order[0]]) {
return [order];
}
}
Object.keys(_where).forEach(key => {
if (!/^_/.test(key)) {
where = where || {};
where[key] = _where[key];
}
})
return where;
}
})
}
const shouldIgnoreAssociation = (method, options) => {
function parseInclude (_include, associations, method) {
options = options || {}
return switchByType(_include, {
string : () => {
let ignore = options.ignore;
let association = associations[_include];
switch (typeof ignore) {
case 'object':
if (Array.isArray(ignore)) {
if (ignore.indexOf(method.name) !== -1)
return true;
}
break;
case 'boolean':
if (ignore) return true;
break;
default:
break;
}
if (!association)
return;
return false;
}
if (shouldIgnoreAssociation(method, association.options.restql))
return;
const parseInclude = (_include, associations, method) => {
return association;
},
let include, association, where, attributes, through, required;
object : () => {
if ('string' === typeof _include) {
let include = []
, where = _include.where
, attributes = _include.attributes
, required = !!+_include.required
, through = _include.through
, association = associations[_include.association];
association = associations[_include];
if (!association)
return;
if (!association)
return;
if (shouldIgnoreAssociation(method, association.options.restql))
return;
let options = association.options;
} else if ('object' === typeof _include) {
if (shouldIgnoreAssociation(method,options.restql))
return;
where = _include.where;
attributes = _include.attributes;
required = _include.required;
through = _include.through;
if (_include.include) {
include = unionInclude(_include.include, association.target.associations);
}
association = associations[_include.association];
return {
where, attributes, through, association, required, include
}
if (!association)
return;
}
let options = association.options
if (shouldIgnoreAssociation(method, association.options.restql))
return;
})
if (_include.include) {
include = unionInclude(_include.include, association.target.associations);
}
}
return {
where, attributes, through, association, required,
include: include || []
}
}
const unionInclude = (_include, associations, method) => {
if (!_include)
return;
function unionInclude (_include, associations, method) {
if (Array.isArray(_include)) {
return _include.map(item => {
return parseInclude(item, associations, method);
}).filter(item => item);
} else {
let include = parseInclude(_include, associations, method);
return include ? [ include ] : [];
}
return switchByType(_include, {
array: () => {
return _include
.map(item => parseInclude(item, associations, method))
.filter(item => item);
},
object: () => {
let include = parseInclude(_include, associations, method);
return include || [];
},
string: () => {
let include = parseInclude(_include, associations, method);
return include || [];
},
defaults: () => ([])
})
}
module.exports.parseQuerystring = (querystring, model, method) => {
function parseQuery (query, model, method, options) {
let attributes = model.attributes
, query = qs.parse(querystring, { allowDots: true });
const {
_attributes, _order, _through, _include, _offset, _limit
} = query
debug(query);
let where = unionWhere(query)
, include = unionInclude(_include, model.associations, method)
, attributes = _attributes
, order = _order
, through = _through
, offset = +_offset || 0
, limit = +_limit || options.query._limit
let _where = unionWhere(query, attributes)
, _attributes = unionAttributes(query._attributes, attributes)
, _order = unionOrder(query._order, attributes)
, _through = query._through
, _include = unionInclude(query._include, model.associations, method)
, _offset = query._offset ? +query._offset : 0
, _limit = query._limit ? +query._limit : defaultLimit
, _ignoreDuplicates = query._ignoreDuplicates;
debug(where)
debug(attributes)
debug(order)
debug(through)
debug(include)
debug(offset)
debug(limit)
debug(_where);
debug(_attributes);
debug(_order);
debug(_through);
debug(_include);
debug(_offset);
debug(_limit);
debug(_ignoreDuplicates);
return {
offset, limit, order, where, through, include, attributes
}
return {
_offset : _offset,
_limit : _limit,
_order : _order,
_where : _where,
_through : _through,
_include : _include || [],
_attributes : _attributes || Object.keys(model.attributes),
_ignoreDuplicates : _ignoreDuplicates
};
}
module.exports.shouldIgnoreAssociation = shouldIgnoreAssociation;
module.exports.parseQuery = parseQuery
module.exports.switchByType = switchByType
module.exports.shouldIgnoreAssociation = shouldIgnoreAssociation

@@ -5,18 +5,4 @@ 'use strict'

/**
* when isSingular === undefined
* method can be mounted on either singular or plural path
*
* when isSingular === true
* method can just be mounted on a singular path
*
* when isSingular === false
* method can just be mounted on a plural path
*/
module.exports = [
{ name : 'get' },
{ name : 'post', isSingular : false },
{ name : 'put' , isSingular : true },
{ name : 'del' , isSingular : true }
]
'get', 'post', 'put', 'del'
];
'use strict'
const co = require('co');
const parse = require('co-body');
const debug = require('debug')('koa-restql:middlewares');
const co = require('co')
const qs = require('qs')
const parse = require('co-body')
const debug = require('debug')('koa-restql:middlewares')
const common = require('./common');
const getAssociationName = common.getAssociationName;
const parseQuerystring = common.parseQuerystring;
const common = require('./common')
const capitalizeFirstLetter = (string) => {
return string.charAt(0).toUpperCase() + string.slice(1);
const switchByType = common.switchByType
function _getIndexes (model) {
const {
primaryKeys, options: { indexes, uniqueKeys }
} = model
const idxes = []
if (primaryKeys) {
const keys = Object.keys(primaryKeys)
if (keys.length) {
idxes.push({
name : 'PRIMARY',
unique : true,
primary : true,
fields : keys
})
}
}
indexes.forEach(index => {
idxes.push({
unique : index.unique,
name : index.name,
fields : index.fields
})
})
Object.keys(uniqueKeys).forEach(key => {
let uniqueKey = uniqueKeys[key]
idxes.push({
unique : true,
name : uniqueKey.name,
fields : uniqueKey.fields
})
})
return idxes
}
const get = (method, model, association) => {
return function * (next) {
let params = this.params
, id = params.id
, qs = this.request.querystring
, query, data;
function _getUniqueIndexes (model) {
return _getIndexes(model).filter(index => index.unique)
let debugInfo = `get ${this.request.url}, using model: ${model.name}`;
}
if (association) {
debugInfo += `, with association:
${common.getAssociationName(association)}`;
function _getInstanceValidIndexes (indexes, data) {
if (!data)
return []
return indexes.filter(index =>
index.fields.every(field => data[field] !== undefined))
}
function _getInstanceValidIndexFields (indexes, data) {
if (!data || !indexes)
return
const validIndexes = _getInstanceValidIndexes(indexes, data)
if (!validIndexes.length)
return
const index = validIndexes[0]
const fields = {}
index.fields.forEach(field => {
fields[field] = data[field]
})
return fields
}
function * _upsert (model, data) {
const uniqueIndexes = _getUniqueIndexes(model)
const where = _getInstanceValidIndexFields(uniqueIndexes, data)
if (!where) {
this.throw('RestQL: unique index fields cannot be found', 400)
}
let created
try {
created =
yield model.upsert(data)
} catch (error) {
if (error.name !== 'SequelizeUniqueConstraintError') {
throw new Error(error)
}
const message = `RestQL: ${model.name} unique constraint error`
this.throw(message, 409)
debug(debugInfo);
}
if (!association) {
query = parseQuerystring(qs, model, method);
data =
yield model.find({
where,
paranoid: false
})
if (id === undefined) {
data = yield model.findAndCount({
attributes : query._attributes,
where : query._where,
order : query._order,
include : query._include,
limit : query._limit,
offset : query._offset
})
yield data.restore()
this.response.set('X-Range',
`objects ${query._offset}-${query._offset + data.rows.length}/${data.count}`);
data = data.rows;
} else {
data = yield model.findOne({
attributes : query._attributes,
where : { id },
include : query._include
})
return { created, data }
if (!data) {
this.throw(`${model.name} ${id} is not found`, 404);
}
}
} else {
let associationId = params.associationId
, associationModel = association.target
, order;
}
query = parseQuerystring(qs, associationModel, method);
function * _bulkUpsert (model, data) {
if (!associationId) {
let name = getAssociationName(association)
, isPlural = association.isMultiAssociation
, getter = `get${capitalizeFirstLetter(name)}`
, counter = `count${capitalizeFirstLetter(name)}`;
if (!data.length)
return []
data = yield model.findOne({
where : { id }
})
/**
* updateOnDuplicate fields should be consistent
*/
let isValid = true
if (data.length) {
let match = JSON.stringify(Object.keys(data[0]).sort())
isValid = data.map(row =>
JSON.stringify(Object.keys(row).sort())).every(item => item === match)
}
if (!data) {
this.throw(`${model.name} ${id} is not found`, 404);
}
if (!isValid) {
this.throw('RestQL: array elements have different attributes', 400)
}
let promises = [];
const $or = []
const uniqueIndexes = _getUniqueIndexes(model)
promises.push(data[getter]({
attributes : query._attributes,
where : query._where,
order : query._order,
include : query._include,
through : query._through,
limit : query._limit,
offset : query._offset
}));
data.forEach(row => {
if (isPlural) {
promises.push(data[counter]({
where: query._where
}))
}
const where = _getInstanceValidIndexFields(uniqueIndexes, row)
data = yield promises;
if (!where) {
this.throw('RestQL: unique index fields cannot be found', 400)
}
if (isPlural) {
this.response.set('X-Range',
`objects ${query._offset}-${query._offset + data[0].length}/${data[1]}`);
}
$or.push(where)
})
data = data[0];
} else {
data = yield associationModel.findOne({
attributes : query._attributes,
where : { id: associationId },
include : query._include
})
/**
* ignoreDuplicates only work in mysql
*/
if (!data) {
this.throw(`${model.name} ${id}
association ${associationId} is not found`, 404);
}
}
try {
let updatedFields = Object.keys(data[0]).filter(key =>
['id'].indexOf(key) === -1)
yield model.bulkCreate(data, {
updateOnDuplicate: updatedFields
})
} catch (error) {
if (error.name !== 'SequelizeUniqueConstraintError') {
throw new Error(error)
}
const message = `RestQL: ${model.name} unique constraint error`
this.throw(message, 409)
this.body = data;
this.status = 200;
}
yield next;
data =
yield model.findAll({
where: { $or },
order: [['id', 'ASC']]
})
return data
}
function _getUniqueConstraintErrorFields (model, error) {
const attributes = model.attributes
const fields = error.fields
if (!fields)
return
let fieldsIsValid = Object.keys(fields).every(key => attributes[key])
if (fieldsIsValid)
return fields
const uniqueIndexes = _getUniqueIndexes(model)
const uniqueIndex = uniqueIndexes.find(index => fields[index.name])
if (uniqueIndex && Array.isArray(uniqueIndex.fields)) {
let value = fields[uniqueIndex.name]
value = common.switchByType(value, {
'number' : value => [ value ],
'string' : value => value.split('-')
})
if (!value || !value.length)
return
const ret = {}
uniqueIndex.fields.forEach((field, index) => {
ret[field] = value[index]
})
return ret
}
}
const isSameDeletedAt = (a, b) => {
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime();
} else {
return a === b;
function * _handleUniqueConstraintError (model, error) {
const message = `RestQL: ${model.name} unique constraint error`
const status = 409
const fields = _getUniqueConstraintErrorFields(model, error)
const attributes = model.attributes
const paranoid = model.options.paranoid
const deletedAtCol = model.options.deletedAt
if (!deletedAtCol || !paranoid)
this.throw(message, status)
let row =
yield model.find({
paranoid: false,
where: fields
})
if (!fields || !row) {
this.throw('RestQL: Sequelize goes wrong', 500)
}
let deletedAtVal = attributes[deletedAtCol].defaultValue
if (deletedAtVal === undefined) {
deletedAtVal = null
}
function isSameDeletedAt (a, b) {
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime()
} else {
return a === b
}
}
if (isSameDeletedAt(row[deletedAtCol], deletedAtVal)) {
this.throw(message, status)
}
for (let key in attributes) {
let defaultValue = attributes[key].defaultValue
if (defaultValue !== undefined) {
row.setDataValue(key, defaultValue)
}
}
return { row, fields }
}
const getUniqueConstraintErrorFields = (model, e) => {
function * _create (model, data, options) {
let indexes = model.options.indexes
, attributes = model.attributes
, fields = {}
, uniqueIndex, fieldsIsValid;
const id = data.id
if (!e.fields)
return;
try {
fieldsIsValid = Object.keys(e.fields).every(key => attributes[e.fields[key]]);
if (id) {
delete data.id
}
if (fieldsIsValid)
return e.fields;
data =
yield model.create(data, options)
let index = indexes.find(index => e.fields[index.name] && index.unique);
return data
if (index && Array.isArray(index.fields)) {
let value = e.fields[index.name];
} catch (error) {
switch (typeof value) {
case 'number':
value = [ value ];
case 'string':
value = value.split('-');
break;
if (error.name !== 'SequelizeUniqueConstraintError') {
throw new Error(error)
}
index.fields.forEach((field, index) => {
fields[field] = value[index]
})
const conflict =
yield* _handleUniqueConstraintError.call(this, model, error)
const {
row, fields
} = conflict
data =
yield* _update.call(this, model,
Object.assign({}, row.dataValues, data), { where: fields })
data = data[0]
return data
}
return fields;
}
const create = (model, row, options) => {
return function * () {
let id = row.id
, message = `${model.name} unique constraint error`
, status = 409
, data;
function * _bulkCreate (model, data) {
options = options || {}
const $or = []
const conflicts = []
const uniqueIndexes = _getUniqueIndexes(model)
let {
ignoreDuplicates = false
} = options;
data = data.slice()
data.forEach(row => {
const where = _getInstanceValidIndexFields(uniqueIndexes, row)
if (!where) {
this.throw('RestQL: unique index fields cannot be found', 400)
}
$or.push(where)
})
while (true) {
try {
if (id) {
delete row.id;
yield model.bulkCreate(data)
break
} catch (error) {
if (error.name !== 'SequelizeUniqueConstraintError') {
throw new Error(error)
}
data =
yield model.create(row, options);
const conflict =
yield* _handleUniqueConstraintError.call(this, model, error)
} catch (e) {
if (e.name === 'SequelizeUniqueConstraintError') {
const {
row, fields
} = conflict
let where = getUniqueConstraintErrorFields(model, e)
, attributes = model.attributes
, deletedAtCol = model.options.deletedAt;
const index = data.findIndex(row =>
Object.keys(fields).every(key => fields[key] == row[key]))
if (!deletedAtCol)
this.throw(message, status);
if (index !== -1) {
conflict.row = Object.assign({}, row.dataValues, data[index])
conflicts.push(conflict)
data.splice(index, 1)
} else {
this.throw('RestQL: bulkCreate unique index field error', 500)
}
data = yield model.find({
paranoid: false,
where
})
}
if (!data) {
this.throw('Sequelize goes wrong', 500);
}
}
let deletedAtVal = attributes[deletedAtCol].defaultValue;
if (deletedAtVal === undefined) {
deletedAtVal = null;
}
if (conflicts.length) {
const rows = conflicts.map(conflicts => conflicts.row)
const upsert = (row) => {
return function * () {
yield model.upsert(row);
let data = yield model.find({
where
});
return data;
}
}
try {
if (!isSameDeletedAt(data[deletedAtCol], deletedAtVal)) {
yield data.restore();
data = yield upsert(row);
} else {
if (!ignoreDuplicates) {
this.throw(message, status);
}
data = yield upsert(row);
}
} else {
throw new Error(e);
yield model.bulkCreate(rows, {
updateOnDuplicate: Object.keys(model.attributes)
})
} catch (error) {
if (error.name !== 'SequelizeUniqueConstraintError') {
throw new Error(error)
}
const message = `RestQL: ${model.name} unique constraint error`
this.throw(message, 409)
}
}
return data;
}
data =
yield model.findAll({
where: { $or },
order: [['id', 'ASC']]
})
return data
}
const post = (method, model, association) => {
return function * (next) {
let body = this.request.body || (yield parse(this))
, params = this.params
, qs = this.request.querystring
, data;
function * _update (model, data, options) {
let debugInfo = `post ${this.request.url}, using model: ${model.name}`;
try {
if (association) {
debugInfo += `, with association:
${common.getAssociationName(association)}`;
if (data.id) {
delete data.id
}
debug(debugInfo);
data =
yield model.update(data, options)
const _create = (model) => {
return function * () {
let query = parseQuerystring(qs, model, method);
if (Array.isArray(body)) {
let promises = body.map(row => {
let gen = create(model, row, {
include : query._include,
ignoreDuplicates : query._ignoreDuplicates
})
return co(gen);
})
data =
yield model.findAll(options)
let data = yield promises;
return data;
} else {
return yield create(model, body, {
include : query._include,
ignoreDuplicates : query._ignoreDuplicates
});
}
}
return data
} catch (error) {
if (error.name !== 'SequelizeUniqueConstraintError') {
throw new Error(error)
}
if (!association) {
data = yield _create(model);
} else {
let id = params.id
, name = association.options.name.singular
, add = `add${capitalizeFirstLetter(name)}`;
const conflict =
yield* _handleUniqueConstraintError.call(this, model, error)
data = yield model.findOne({
where: { id }
});
const { row } = conflict
if (!data) {
this.throw(`${model.name} ${id} is not found`, 404);
/**
* @FIXME
* restql should delete the conflict with paranoid = false
* and update again, now return 409 directly
* for conflict happens rarely
*/
const message = `RestQL: ${model.name} unique constraint error`
this.throw(message, 409)
}
}
function * _findExistingRows (model, data) {
const $or = []
const uniqueIndexes = _getUniqueIndexes(model)
function getOr (uniqueIndexes, data) {
let fields = _getInstanceValidIndexFields(uniqueIndexes, data)
, row = data
return { fields, row }
}
common.switchByType(data, {
object : (data) => $or.push(getOr(uniqueIndexes, data)),
array : (data) => data.forEach(row => $or.push(getOr(uniqueIndexes, row)))
})
data =
yield model.findAll({
where: { $or : $or.map(or => or.fields) }
})
let existingRows = []
let newRows = []
if (data.length === $or.length) {
existingRows = data
} else {
/*
* find existing rows
*/
$or.forEach(or => {
let index = data.findIndex(row =>
Object.keys(or.fields).every(key => row[key] === or.row[key]))
if (index !== -1) {
existingRows.push(data[index])
data.splice(index, 1)
} else {
newRows.push(or.row)
}
let associationData = yield _create(association.target);
yield data[add](associationData);
})
data = yield association.target.findById(associationData.id);
}
return { existingRows, newRows }
}
function before () {
return function * (next) {
debug(`RestQL: ${this.request.method} ${this.url}`)
this.restql = this.restql || {}
this.restql.params = this.restql.params || {}
this.restql.request = this.restql.request || {}
this.restql.response = this.restql.response || {}
yield* next
}
}
function after () {
return function * (next) {
const {
response
} = this.restql
this.response.status = response.status || 200
this.response.body = response.body
const headers = response.headers || {}
for (let key in headers) {
this.response.set(key, response.headers[key])
}
this.body = data;
this.status = 201;
debug(`RestQL: Succeed and Goodbye`)
yield next;
yield* next
}
}
const put = (method, model, association) => {
function parseQuery (model, options) {
return function * (next) {
let body = this.request.body || (yield parse(this))
, params = this.params
, id = params.id
, include = association ? [association] : []
, data, status;
let debugInfo = `put ${this.request.url}, using model: ${model.name}`;
const {
method, querystring
} = this.request
if (association) {
debugInfo += `, with association:
${common.getAssociationName(association)}`;
const query = this.restql.query || qs.parse(querystring, options.qs || {})
this.restql.query =
common.parseQuery(query, model, method.toLowerCase(), options)
yield* next
}
}
function findById (model, query) {
return function * (next) {
const q = query || this.restql.query || {}
const id = this.params.id
if (!id) {
return yield* next
}
const data =
yield model.findById(id, {
attributes : q.attributes,
include : q.include
})
if (!data) {
this.throw(`RestQL: ${model.name} ${id} cannot be found`, 404)
}
debug(debugInfo);
this.restql.params.id = data
this.restql.response.body = data
data = yield model.findOne({
where: { id },
include
})
yield* next
}
}
function findOne (model, query) {
return function * (next) {
const {
request, response
} = this.restql
const q = query || this.restql.query || {}
const data =
yield model.findOne({
attributes : q.attributes,
include : q.include,
where : q.where,
})
if (!data) {
this.throw(`${model.name} ${id} is not found`, 404);
this.throw(`RestQL: ${model.name} cannot be found`, 404)
}
if (!association) {
yield data.update(body);
response.body = data
yield* next
}
}
function pagination (model) {
return function * (next) {
const {
response, params, query
} = this.restql
const {
count, rows
} = response.body
const {
offset, limit
} = query
let status = 200
const xRangeHeader = `objects ${offset}-${offset + rows.length}/${count}`
if (count > limit)
status = 206
response.headers = response.headers || {}
response.headers['X-Range'] = xRangeHeader
response.body = rows
response.status = status
yield* next
}
}
function upsert (model) {
return function * (next) {
const {
request, response
} = this.restql
let status = 200
if (Array.isArray(request.body)) {
return yield* next
}
const result =
yield* _upsert.call(this, model, request.body)
const created = result.created
const data = result.data
if (created)
status = 201
response.body = data
response.status = status
yield* next
}
}
function findOrUpsert (model) {
return function * (next) {
const {
request, response
} = this.restql
let status = 200
if (Array.isArray(request.body)) {
return yield* next
}
const {
existingRows, newRows
} = yield* _findExistingRows.call(this, model, [ request.body ])
let data
if (newRows.length){
status = 201
let ret =
yield* _upsert.call(this, model, newRows[0])
if (ret.created)
status = 201
data = ret.data
} else {
let associationId = params.associationId;
data = existingRows[0]
}
response.body = data
response.status = status
if (associationId) {
/*
* plural assocation
*/
data = yield association.target.findOne({
where: {
id: associationId
}
});
yield* next
if (!data) {
this.throw(`${model.name} ${id}
association ${associationId} is not found`, 404);
}
}
}
delete body.id;
yield data.update(body);
} else {
/*
* singular association
*/
let name = association.options.name.singular
, setter = `set${capitalizeFirstLetter(name)}`;
let associationData = data[name];
function bulkUpsert (model) {
return function * (next) {
delete body.id;
if (!associationData) {
associationData = yield create(association.target, body);
status = 201;
yield data[setter](associationData);
} else {
yield associationData.update(body);
const {
request, response
} = this.restql
const body = request.body
const status = 200
if (!Array.isArray(body)) {
return yield* next
}
const data =
yield* _bulkUpsert.call(this, model, body)
response.body = data
response.status = status
yield* next
}
}
function bulkFindOrUpsert (model) {
return function * (next) {
const {
request, response
} = this.restql
const status = 200
if (!Array.isArray(request.body)) {
return yield* next
}
const {
existingRows, newRows
} = yield* _findExistingRows.call(this, model, request.body)
let data = []
if (newRows.length){
data =
yield* _bulkUpsert.call(this, model, newRows)
}
data.forEach(row => existingRows.push(row))
response.body = existingRows
response.status = status
yield* next
}
}
function create (model) {
return function * (next) {
const {
request, response
} = this.restql
const body = request.body
const status = 201
if (Array.isArray(body)) {
return yield* next
}
const include = []
const associations = model.associations
const associationList = Object.keys(associations)
for (let key in body) {
let value = body[key]
if ('object' === typeof value) {
if (associationList.indexOf(key) !== -1) {
include.push(associations[key])
}
data = associationData;
}
}
const data =
yield* _create.call(this, model, body, {
include
})
this.body = data;
this.status = status || 200;
response.body = data
response.status = status
yield next;
return yield* next
}
}
const del = (method, model, association) => {
function bulkCreate (model) {
return function * (next) {
let params = this.params
, id = params.id
, include = association ? [association] : []
, data = null;
const {
request, response
} = this.restql
let debugInfo = `delete ${this.request.url}, using model: ${model.name}`;
const body = request.body
const status = 201
if (association) {
debugInfo += `, with association:
${common.getAssociationName(association)}`;
if (!Array.isArray(body)) {
return yield* next
}
debug(debugInfo);
const data =
yield* _bulkCreate.call(this, model, body)
data = yield model.findOne({
where: { id },
include
})
response.body = data
response.status = status
if (!data) {
this.throw(`${model.name} ${id} is not found`, 404);
}
yield* next
}
}
if (!association) {
yield model.destroy({
where: {
id: data.id
}
});
} else {
let associationId = params.associationId
, name = association.options.name.singular
, remove = `remove${capitalizeFirstLetter(name)}`
, associationModel = association.target
, associationData = null;
function parseRequestBody (allowedTypes) {
return function * (next) {
if (associationId) {
/*
* plural assocation
*/
associationData = yield associationModel.findOne({
where: {
id: associationId
}
});
const body = this.request.body
|| this.restql.request.body
|| (yield parse(this))
if (!data) {
this.throw(`${model.name} ${id}
association ${associationId} is not found`, 404);
}
this.restql.request.body = this.request.body = body
let associationType = association.associationType;
if (associationType === 'HasMany') {
yield associationModel.destroy({
where: {
id: associationId
}
})
} else {
yield data[remove](associationData);
}
} else {
/*
* singular association
*/
if (!allowedTypes) {
return yield* next
}
associationData = data[name];
const validators = {}
allowedTypes.forEach(type => {
validators[type] = true
})
if (associationData) {
yield associationModel.destroy({
where: {
id: associationData.id
}
});
}
}
validators.defaults = () => {
this.throw(`RestQL: ${allowedTypes.join()} body are supported`, 400)
}
this.data = {};
this.status = 204;
common.switchByType(body, validators)
yield next;
yield* next
}
}
module.exports.handlers = {
get, post, put, del
};
function destroy (model) {
return function * (next) {
const query = this.restql.query || {}
const where = query.where || {}
const status = 204
yield model.destroy({
where
})
this.restql.response.status = status
yield* next
}
}
module.exports.before = before
module.exports.after = after
module.exports.pagination = pagination
module.exports.parseRequestBody = parseRequestBody
module.exports.parseQuery = parseQuery
module.exports.upsert = upsert
module.exports.bulkUpsert = bulkUpsert
module.exports.findOrUpsert = findOrUpsert
module.exports.bulkFindOrUpsert = bulkFindOrUpsert
module.exports.create = create
module.exports.bulkCreate = bulkCreate
module.exports.destroy = destroy
module.exports.findOne = findOne
module.exports.findById = findById

@@ -18,10 +18,20 @@ 'use strict';

function RestQL (models, opts) {
function RestQL (models, options) {
if (!(this instanceof RestQL)) {
return new RestQL(models, opts);
return new RestQL(models, options);
}
this.options = opts || {};
const defaultOptions = {
query: {
_limit: 20
},
qs: {
allowDots : true,
strictNullHandling : true
}
}
this.options = options || defaultOptions
if (!models) {

@@ -31,8 +41,8 @@ throw new Error('paramter models does not exist')

this.models = models;
this.router = router.load(models, this.options);
this.models = models
this.router = router.load(models, this.options)
this.routes = () => {
return this.router.routes();
return this.router.routes()
}
}
'use strict';
const debug = require('debug')('koa-restql:router');
const Router = require('koa-router');
const Router = require('koa-router');
const debug = require('debug')('koa-restql:router');
const common = require('./common');
const methods = require('./methods');
const middlewares = require('./middlewares');
const common = require('./common');
const methods = require('./methods');
const loaders = require('./loaders');
const handlers = middlewares.handlers;
const global = {};
const methodShouldMount = (path, method, options) => {
function loadModelRoutes (router, method, model, name, options) {
options = options || {};
let base = `/${name}`
, associations = model.associations
, schema = model.options.schema;
let ignore = options.ignore;
if (schema) {
base = `/${schema}${base}`;
}
if (common.shouldIgnoreAssociation(method, options))
return false;
if (method.isSingular !== undefined && method.isSingular !== path.isSingular)
return false;
return true;
}
const createModelRoutes = (path, model, association, options) => {
let models = global.models || {}
, router = global.router || {}
, paths = [ path ];
/**
* if association === undefined and path is plural,
* mount /path
* and /path/:id to router
*
* else if there is a association
* and if this path is singular
* mount /path
*
* and if this path is plural
* mount /path
* and /path/:associationId
*/
if (!association || !path.isSingular) {
let id = !association ? ':id' : ':associationId';
paths.push({ name: `${path.name}/${id}`, isSingular: true });
let loader = loaders.model[method];
if (loader) {
loader(router, base, model, options)
}
paths.forEach(path => {
methods.forEach(method => {
if (methodShouldMount(path, method, options)) {
let args = [method, model]
, name = method.name;
Object.keys(associations).forEach(key => {
if (association)
args.push(association);
let association = associations[key]
, isSingular = association.isSingleAssociation
, associationType = association.associationType
, loaderPath = loaders.model.association
, loader
debug(path.name);
router[name](path.name, handlers[name].apply(this, args));
}
})
})
/***
* to camel case
*/
associationType =
associationType.replace(/^(.)/, $1 => $1.toLowerCase())
if (association || !model.associations)
return;
loaderPath = isSingular ? loaderPath.singular : loaderPath.plural
loader = (loaderPath[associationType] &&
loaderPath[associationType][method]) || loaderPath[method]
Object.keys(model.associations).forEach(key => {
let association = model.associations[key]
, options = association.options
, isSingular = !! association.isSingleAssociation
, pathName = paths[1].name.slice();
pathName += `/${common.getAssociationName(association)}`;
createModelRoutes({ name: pathName, isSingular }, model, association, options.restql);
if (loader) {
loader(router, `${base}/:id/${key}`, model, association, options)
}
})
}
module.exports.load = (models, opts) => {
function load (models, options) {
let router = new Router();
global.models = models;
global.router = router;
Object.keys(models).forEach(key => {
let model = models[key]
, schema = model.options.schema
, path = `/${key}`;
let model = models[key];
if (schema) {
path = `/${schema}${path}`;
}
methods.forEach(method => {
loadModelRoutes(router, method.toLowerCase(), model, key, options);
})
})
createModelRoutes({ name: path, isSingular: false }, model);
})
return router;
}
module.exports.methodShouldMount = methodShouldMount;
module.exports.load = load;
{
"name": "koa-restql",
"version": "0.0.1",
"version": "0.1.0",
"description": "Koa RESTful API middleware based on Sequlizejs",

@@ -26,3 +26,3 @@ "main": "lib/RestQL.js",

"node-uuid": "^1.4.7",
"sequelize": "^3.23.2",
"sequelize": "^3.23.6",
"glob": "^7.0.3",

@@ -29,0 +29,0 @@ "mocha": "^2.3.4",

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc