Skip to content

Instantly share code, notes, and snippets.

@alexrjs
Last active May 5, 2019 06:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexrjs/a5ea926dc61994caa5c311ef00b7d26a to your computer and use it in GitHub Desktop.
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)
"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;
};
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"]
"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