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