273 lines
9.7 KiB
JavaScript
273 lines
9.7 KiB
JavaScript
"use strict";
|
|
|
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
|
|
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
|
|
const express = require('express');
|
|
const _ = require('lodash');
|
|
const pluralize = require('pluralize');
|
|
const write = require('./write');
|
|
const getFullURL = require('./get-full-url');
|
|
const utils = require('../utils');
|
|
const delay = require('./delay');
|
|
module.exports = (db, name, opts) => {
|
|
// Create router
|
|
const router = express.Router();
|
|
router.use(delay);
|
|
|
|
// Embed function used in GET /name and GET /name/id
|
|
function embed(resource, e) {
|
|
e && [].concat(e).forEach(externalResource => {
|
|
if (db.get(externalResource).value) {
|
|
const query = {};
|
|
const singularResource = pluralize.singular(name);
|
|
query[`${singularResource}${opts.foreignKeySuffix}`] = resource.id;
|
|
resource[externalResource] = db.get(externalResource).filter(query).value();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Expand function used in GET /name and GET /name/id
|
|
function expand(resource, e) {
|
|
e && [].concat(e).forEach(innerResource => {
|
|
const plural = pluralize(innerResource);
|
|
if (db.get(plural).value()) {
|
|
const prop = `${innerResource}${opts.foreignKeySuffix}`;
|
|
resource[innerResource] = db.get(plural).getById(resource[prop]).value();
|
|
}
|
|
});
|
|
}
|
|
|
|
// GET /name
|
|
// GET /name?q=
|
|
// GET /name?attr=&attr=
|
|
// GET /name?_end=&
|
|
// GET /name?_start=&_end=&
|
|
// GET /name?_embed=&_expand=
|
|
function list(req, res, next) {
|
|
// Resource chain
|
|
let chain = db.get(name);
|
|
|
|
// Remove q, _start, _end, ... from req.query to avoid filtering using those
|
|
// parameters
|
|
let q = req.query.q;
|
|
let _start = req.query._start;
|
|
let _end = req.query._end;
|
|
let _page = req.query._page;
|
|
const _sort = req.query._sort;
|
|
const _order = req.query._order;
|
|
let _limit = req.query._limit;
|
|
const _embed = req.query._embed;
|
|
const _expand = req.query._expand;
|
|
delete req.query.q;
|
|
delete req.query._start;
|
|
delete req.query._end;
|
|
delete req.query._sort;
|
|
delete req.query._order;
|
|
delete req.query._limit;
|
|
delete req.query._embed;
|
|
delete req.query._expand;
|
|
|
|
// Automatically delete query parameters that can't be found
|
|
// in the database
|
|
Object.keys(req.query).forEach(query => {
|
|
const arr = db.get(name).value();
|
|
for (const i in arr) {
|
|
if (_.has(arr[i], query) || query === 'callback' || query === '_' || /_lte$/.test(query) || /_gte$/.test(query) || /_ne$/.test(query) || /_like$/.test(query)) return;
|
|
}
|
|
delete req.query[query];
|
|
});
|
|
if (q) {
|
|
// Full-text search
|
|
if (Array.isArray(q)) {
|
|
q = q[0];
|
|
}
|
|
q = q.toLowerCase();
|
|
chain = chain.filter(obj => {
|
|
for (const key in obj) {
|
|
const value = obj[key];
|
|
if (db._.deepQuery(value, q)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
Object.keys(req.query).forEach(key => {
|
|
// Don't take into account JSONP query parameters
|
|
// jQuery adds a '_' query parameter too
|
|
if (key !== 'callback' && key !== '_') {
|
|
// Always use an array, in case req.query is an array
|
|
const arr = [].concat(req.query[key]);
|
|
const isDifferent = /_ne$/.test(key);
|
|
const isRange = /_lte$/.test(key) || /_gte$/.test(key);
|
|
const isLike = /_like$/.test(key);
|
|
const path = key.replace(/(_lte|_gte|_ne|_like)$/, '');
|
|
chain = chain.filter(element => {
|
|
return arr.map(function (value) {
|
|
// get item value based on path
|
|
// i.e post.title -> 'foo'
|
|
const elementValue = _.get(element, path);
|
|
|
|
// Prevent toString() failing on undefined or null values
|
|
if (elementValue === undefined || elementValue === null) {
|
|
return undefined;
|
|
}
|
|
if (isRange) {
|
|
const isLowerThan = /_gte$/.test(key);
|
|
return isLowerThan ? value <= elementValue : value >= elementValue;
|
|
} else if (isDifferent) {
|
|
return value !== elementValue.toString();
|
|
} else if (isLike) {
|
|
return new RegExp(value, 'i').test(elementValue.toString());
|
|
} else {
|
|
return value === elementValue.toString();
|
|
}
|
|
}).reduce((a, b) => isDifferent ? a && b : a || b);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Sort
|
|
if (_sort) {
|
|
const _sortSet = _sort.split(',');
|
|
const _orderSet = (_order || '').split(',').map(s => s.toLowerCase());
|
|
chain = chain.orderBy(_sortSet, _orderSet);
|
|
}
|
|
|
|
// Slice result
|
|
if (_end || _limit || _page) {
|
|
res.setHeader('X-Total-Count', chain.size());
|
|
res.setHeader('Access-Control-Expose-Headers', `X-Total-Count${_page ? ', Link' : ''}`);
|
|
}
|
|
if (_page) {
|
|
_page = parseInt(_page, 10);
|
|
_page = _page >= 1 ? _page : 1;
|
|
_limit = parseInt(_limit, 10) || 10;
|
|
const page = utils.getPage(chain.value(), _page, _limit);
|
|
const links = {};
|
|
const fullURL = getFullURL(req);
|
|
if (page.first) {
|
|
links.first = fullURL.replace(`page=${page.current}`, `page=${page.first}`);
|
|
}
|
|
if (page.prev) {
|
|
links.prev = fullURL.replace(`page=${page.current}`, `page=${page.prev}`);
|
|
}
|
|
if (page.next) {
|
|
links.next = fullURL.replace(`page=${page.current}`, `page=${page.next}`);
|
|
}
|
|
if (page.last) {
|
|
links.last = fullURL.replace(`page=${page.current}`, `page=${page.last}`);
|
|
}
|
|
res.links(links);
|
|
chain = _.chain(page.items);
|
|
} else if (_end) {
|
|
_start = parseInt(_start, 10) || 0;
|
|
_end = parseInt(_end, 10);
|
|
chain = chain.slice(_start, _end);
|
|
} else if (_limit) {
|
|
_start = parseInt(_start, 10) || 0;
|
|
_limit = parseInt(_limit, 10);
|
|
chain = chain.slice(_start, _start + _limit);
|
|
}
|
|
|
|
// embed and expand
|
|
chain = chain.cloneDeep().forEach(function (element) {
|
|
embed(element, _embed);
|
|
expand(element, _expand);
|
|
});
|
|
res.locals.data = chain.value();
|
|
next();
|
|
}
|
|
|
|
// GET /name/:id
|
|
// GET /name/:id?_embed=&_expand
|
|
function show(req, res, next) {
|
|
const _embed = req.query._embed;
|
|
const _expand = req.query._expand;
|
|
const resource = db.get(name).getById(req.params.id).value();
|
|
if (resource) {
|
|
// Clone resource to avoid making changes to the underlying object
|
|
const clone = _.cloneDeep(resource);
|
|
|
|
// Embed other resources based on resource id
|
|
// /posts/1?_embed=comments
|
|
embed(clone, _embed);
|
|
|
|
// Expand inner resources based on id
|
|
// /posts/1?_expand=user
|
|
expand(clone, _expand);
|
|
res.locals.data = clone;
|
|
}
|
|
next();
|
|
}
|
|
|
|
// POST /name
|
|
function create(req, res, next) {
|
|
let resource;
|
|
if (opts._isFake) {
|
|
const id = db.get(name).createId().value();
|
|
resource = _objectSpread(_objectSpread({}, req.body), {}, {
|
|
id
|
|
});
|
|
} else {
|
|
resource = db.get(name).insert(req.body).value();
|
|
}
|
|
res.setHeader('Access-Control-Expose-Headers', 'Location');
|
|
res.location(`${getFullURL(req)}/${resource.id}`);
|
|
res.status(201);
|
|
res.locals.data = resource;
|
|
next();
|
|
}
|
|
|
|
// PUT /name/:id
|
|
// PATCH /name/:id
|
|
function update(req, res, next) {
|
|
const id = req.params.id;
|
|
let resource;
|
|
if (opts._isFake) {
|
|
resource = db.get(name).getById(id).value();
|
|
if (req.method === 'PATCH') {
|
|
resource = _objectSpread(_objectSpread({}, resource), req.body);
|
|
} else {
|
|
resource = _objectSpread(_objectSpread({}, req.body), {}, {
|
|
id: resource.id
|
|
});
|
|
}
|
|
} else {
|
|
let chain = db.get(name);
|
|
chain = req.method === 'PATCH' ? chain.updateById(id, req.body) : chain.replaceById(id, req.body);
|
|
resource = chain.value();
|
|
}
|
|
if (resource) {
|
|
res.locals.data = resource;
|
|
}
|
|
next();
|
|
}
|
|
|
|
// DELETE /name/:id
|
|
function destroy(req, res, next) {
|
|
let resource;
|
|
if (opts._isFake) {
|
|
resource = db.get(name).value();
|
|
} else {
|
|
resource = db.get(name).removeById(req.params.id).value();
|
|
|
|
// Remove dependents documents
|
|
const removable = db._.getRemovable(db.getState(), opts);
|
|
removable.forEach(item => {
|
|
db.get(item.name).removeById(item.id).value();
|
|
});
|
|
}
|
|
if (resource) {
|
|
res.locals.data = {};
|
|
}
|
|
next();
|
|
}
|
|
const w = write(db);
|
|
router.route('/').get(list).post(create, w);
|
|
router.route('/:id').get(show).put(update, w).patch(update, w).delete(destroy, w);
|
|
return router;
|
|
}; |