diff --git a/lib/database.js b/lib/database.js index f17b7d45..98a873d1 100644 --- a/lib/database.js +++ b/lib/database.js @@ -1,6 +1,6 @@ 'use strict'; -const JSONStream = require('JSONStream'); +const { parse: createJsonParseStream } = require('./jsonstream'); const Promise = require('bluebird'); const fs = require('graceful-fs'); const Model = require('./model'); @@ -134,7 +134,7 @@ class Database { }; // data event arg0 wrap key/value pair. - const parseStream = JSONStream.parse('models.$*'); + const parseStream = createJsonParseStream('models.$*'); parseStream.once('header', getMetaCallBack); parseStream.once('footer', getMetaCallBack); diff --git a/lib/jsonstream/index.js b/lib/jsonstream/index.js new file mode 100644 index 00000000..7a6d26bd --- /dev/null +++ b/lib/jsonstream/index.js @@ -0,0 +1,202 @@ +'use strict'; + +const through2 = require('through2'); +const Parser = require('jsonparse'); + +/** + * Check whether a x and y are equal, or x matches y, or x(y) is truthy. + * @param {boolean | string | RegExp | (args: any[]) => boolean} x + * @param {*} y + * @returns {boolean} + */ +const check = (x, y) => { + if (typeof x === 'string') { + return y === x; + } + + if (x && typeof x.exec === 'function') { + return x.exec(y); + } + + if (typeof x === 'boolean' || typeof x === 'object') { + return x; + } + + if (typeof x === 'function') { + return x(y); + } + + return false; +}; + +module.exports.parse = function(path, map) { + let header, footer; + + const parser = new Parser(); + const stream = through2.obj( + (chunk, enc, cb) => { + if (typeof chunk === 'string') { + chunk = Buffer.from(chunk); + } + parser.write(chunk); + cb(); + }, + cb => { + if (header) { + stream.emit('header', header); + } + if (footer) { + stream.emit('footer', footer); + } + + if (parser.tState !== Parser.C.START || parser.stack.length > 0) { + cb(new Error('Incomplete JSON')); + return; + } + + cb(); + } + ); + + if (typeof path === 'string') { + path = path.split('.').map(e => { + if (e === '$*') { + return { emitKey: true }; + } + if (e === '*') { + return true; + } + if (e === '') { + // '..'.split('.') returns an empty string + return { recurse: true }; + } + return e; + }); + } + + + if (!path || !path.length) { + path = null; + } + + parser.onValue = function(value) { + if (!this.root) { stream.root = value; } + + if (!path) return; + + let i = 0; // iterates on path + let j = 0; // iterates on stack + let emitKey = false; + let emitPath = false; + while (i < path.length) { + const key = path[i]; + let c; + j++; + + if (key && !key.recurse) { + c = j === this.stack.length ? this : this.stack[j]; + if (!c) return; + if (!check(key, c.key)) { + setHeaderFooter(c.key, value); + return; + } + emitKey = !!key.emitKey; + emitPath = !!key.emitPath; + i++; + } else { + i++; + const nextKey = path[i]; + if (!nextKey) return; + + // eslint-disable-next-line no-constant-condition + while (true) { + c = j === this.stack.length ? this : this.stack[j]; + if (!c) return; + if (check(nextKey, c.key)) { + i++; + if (!Object.isFrozen(this.stack[j])) { + this.stack[j].value = null; + } + break; + } else { + setHeaderFooter(c.key, value); + } + j++; + } + } + + } + + // emit header + if (header) { + stream.emit('header', header); + header = false; + } + if (j !== this.stack.length) return; + + const actualPath = this.stack.slice(1).map(element => element.key); + actualPath.push(this.key); + + let data = this.value[this.key]; + if (data != null) { + if ((data = map ? map(data, actualPath) : data) != null) { + if (emitKey || emitPath) { + data = { + value: data + }; + if (emitKey) { + data.key = this.key; + } + if (emitPath) { + data.path = actualPath; + } + } + + stream.push(data); + } + } + + delete this.value[this.key]; + + for (const k in this.stack) { + if (!Object.isFrozen(this.stack[k])) { + this.stack[k].value = null; + } + } + }; + parser._onToken = parser.onToken; + + parser.onToken = function(token, value) { + parser._onToken(token, value); + if (this.stack.length === 0) { + if (stream.root) { + if (!path) { stream.push(stream.root); } + + stream.root = null; + } + } + }; + + parser.onError = function(err) { + if (err.message.includes('at position')) { + err.message = 'Invalid JSON (' + err.message + ')'; + } + stream.destroy(err); + }; + + return stream; + + function setHeaderFooter(key, value) { + // header has not been emitted yet + if (header !== false) { + header = header || {}; + header[key] = value; + } + + // footer has not been emitted yet but header has + if (footer !== false && header === false) { + footer = footer || {}; + footer[key] = value; + } + } +}; diff --git a/package.json b/package.json index 8c72d72f..c169cd09 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,9 @@ "graceful-fs": "^4.1.3", "hexo-log": "^3.0.0", "is-plain-object": "^5.0.0", - "JSONStream": "^1.0.7", - "rfdc": "^1.1.4" + "jsonparse": "^1.3.1", + "rfdc": "^1.1.4", + "through2": "^4.0.2" }, "devDependencies": { "chai": "^4.2.0",