Last active
May 5, 2019 06:18
-
-
Save alexrjs/a5ea926dc61994caa5c311ef00b7d26a to your computer and use it in GitHub Desktop.
Hack for json-server to allow get of count (/<name>/_count), ids (/<name>/_ids) and fields (/<name>/:field)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; | |
const fs = require('fs'); | |
const path = require('path'); | |
const express = require('express'); | |
const logger = require('morgan'); | |
const cors = require('cors'); | |
const compression = require('compression'); | |
const errorhandler = require('errorhandler'); | |
const objectAssign = require('object-assign'); | |
const bodyParser = require('./body-parser'); | |
module.exports = function (opts) { | |
const userDir = path.join(process.cwd(), 'public'); | |
const defaultDir = path.join(__dirname, '../../dist'); | |
const staticDir = fs.existsSync(userDir) ? userDir : defaultDir; | |
opts = objectAssign({ | |
logger: true, | |
static: staticDir | |
}, opts); | |
const arr = []; // Compress all requests | |
if (!opts.noGzip) { | |
arr.push(compression()); | |
} // Enable CORS for all the requests, including static files | |
if (!opts.noCors) { | |
arr.push(cors({ | |
origin: true, | |
credentials: true | |
})); | |
} | |
if (process.env.NODE_ENV === 'development') { | |
// only use in development | |
arr.push(errorhandler()); | |
} // Serve static files | |
express.static.mime.define({'text/plain': ['keys']}) | |
express.static.mime.define({'text/plain': ['value']}) | |
express.static.mime.define({'application/base64': ['pwd']}) | |
arr.push(express.static(opts.static)); // Logger | |
if (opts.logger) { | |
arr.push(logger('dev', { | |
skip: req => process.env.NODE_ENV === 'test' || req.path === '/favicon.ico' | |
})); | |
} // No cache for IE | |
// https://support.microsoft.com/en-us/kb/234067 | |
arr.push((req, res, next) => { | |
res.header('Cache-Control', 'no-cache'); | |
res.header('Pragma', 'no-cache'); | |
res.header('Expires', '-1'); | |
next(); | |
}); // Read-only | |
if (opts.readOnly) { | |
arr.push((req, res, next) => { | |
if (req.method === 'GET') { | |
next(); // Continue | |
} else { | |
res.sendStatus(403); // Forbidden | |
} | |
}); | |
} // Add middlewares | |
if (opts.bodyParser) { | |
arr.push(bodyParser); | |
} | |
return arr; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
FROM node:current-alpine | |
RUN npm install -g json-server | |
COPY ./plural.js /usr/local/lib/node_modules/json-server/lib/server/router/plural.js | |
COPY ./defaults.js /usr/local/lib/node_modules/json-server/lib/server/defaults.js | |
HEALTHCHECK --interval=120s --timeout=15s --start-period=30s CMD wget -S --spider http://localhost:8080/ | |
EXPOSE 80 | |
WORKDIR /data | |
VOLUME /data | |
ENTRYPOINT ["json-server"] | |
CMD ["--host", "0.0.0.0", "--port", "80", "db.json"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; | |
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; } | |
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } | |
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; | |
let _sort = req.query._sort; | |
let _order = req.query._order; | |
let _limit = req.query._limit; | |
let _embed = req.query._embed; | |
let _expand = req.query._expand; | |
let _fields = req.query._fields; | |
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; | |
delete req.query._fields; | |
// 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 (let 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 (let key in obj) { | |
const value = obj[key]; | |
if (db._.deepQuery(value, q)) { | |
return true; | |
} | |
} | |
}); | |
} | |
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]); | |
chain = chain.filter(element => { | |
return arr.map(function (value) { | |
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)$/, ''); // 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; | |
} | |
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) => 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({}, 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({}, resource, req.body); | |
} else { | |
resource = _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(); | |
} | |
function ids(req, res, next) { | |
const resource = db.get(name); | |
if (resource) { | |
let _ids = [] | |
let _values = resource.value() | |
for (var _value in _values) { | |
_ids.push(_values[_value].id) | |
} | |
res.locals.data = { 'ids': _ids, 'count': _ids.length }; | |
} | |
next(); | |
} // GET /name/_ids | |
function values(req, res, next) { | |
const fields = req.params.fields; | |
const resource = db.get(name); | |
if (resource) { | |
let _results = [] | |
let _values = resource.value() | |
let _fields | |
if (fields.indexOf('+') >= 0) | |
_fields = fields.split('+') | |
else if (fields.indexOf(',') >= 0) | |
_fields = fields.split(',') | |
else | |
next() | |
for (var _value in _values) { | |
let _result = [] | |
_fields.forEach(_field => { | |
if (_field in _values[_value]) | |
_result.push(_values[_value][_field]) | |
}) | |
if (!_results.includes(_result)) | |
_results.push(_result) | |
} | |
res.locals.data = { 'fields': _fields, 'values': _results, 'count': _results.length }; | |
} | |
next(); | |
} // GET /name/:field | |
function count(req, res, next) { | |
const resource = db.get(name); | |
if (resource) { | |
res.locals.data = { 'count': resource.value().length }; | |
} | |
next(); | |
} // POST /name/_count | |
const w = write(db); | |
router.route('/').get(list).post(create, w); | |
router.route('/_ids').get(ids); | |
router.route('/_count').get(count); | |
router.route('/_fields=:fields').get(values); | |
router.route('/:id').get(show).put(update, w).patch(update, w).delete(destroy, w); | |
return router; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment