Comparing version 0.0.0 to 0.1.0
{ | ||
"name": "nimma", | ||
"version": "0.0.0", | ||
"description": "JSONPath-based object querying that scales.", | ||
"version": "0.1.0", | ||
"description": "JSONPath engine that scales.", | ||
"keywords": [ | ||
@@ -14,13 +14,35 @@ "json", | ||
"engines": { | ||
"node": ">=10" | ||
"node": ">=12.20" | ||
}, | ||
"files": [ | ||
"dist/**", | ||
"src/index.d.ts" | ||
"dist/**" | ||
], | ||
"typings": "./src/index.d.ts", | ||
"main": "./dist/index.cjs", | ||
"types": "./dist/cjs/index.d.ts", | ||
"type": "commonjs", | ||
"main": "./dist/legacy/cjs/index.js", | ||
"exports": { | ||
"import": "./dist/index.mjs", | ||
"require": "./dist/index.cjs" | ||
".": { | ||
"import": "./dist/esm/index.mjs", | ||
"require": "./dist/cjs/index.js" | ||
}, | ||
"./runtime": { | ||
"import": "./dist/esm/runtime/index.mjs", | ||
"require": "./dist/cjs/runtime/index.js" | ||
}, | ||
"./fallbacks": { | ||
"import": "./dist/esm/fallbacks/index.mjs", | ||
"require": "./dist/cjs/fallbacks/index.js" | ||
}, | ||
"./legacy": { | ||
"import": "./dist/legacy/esm/index.mjs", | ||
"require": "./dist/legacy/cjs/index.js" | ||
}, | ||
"./legacy/runtime": { | ||
"import": "./dist/legacy/esm/runtime/index.mjs", | ||
"require": "./dist/legacy/cjs/runtime/index.js" | ||
}, | ||
"./legacy/fallbacks": { | ||
"import": "./dist/legacy/esm/fallbacks/index.mjs", | ||
"require": "./dist/legacy/cjs/fallbacks/index.js" | ||
} | ||
}, | ||
@@ -34,5 +56,6 @@ "license": "Apache-2.0", | ||
"scripts": { | ||
"prebuild": "node ./scripts/generate-parser.js", | ||
"build": "NODE_ENV=production rollup -c", | ||
"lint": "eslint --cache --cache-location .cache/ src/**/*.mjs", | ||
"prebuild": "pegjs --optimize speed -o src/parser/parser.cjs src/parser/parser.peg", | ||
"build": "export NODE_ENV=production; rollup -c && BABEL_ENV=legacy rollup -c", | ||
"postbuild": "(cd dist/legacy && sed -i \"s/nimma\\/runtime/nimma\\/legacy\\/runtime/\" cjs/codegen/tree/tree.js esm/codegen/tree/tree.mjs) && node ./scripts/copy-types.mjs", | ||
"lint": "ls-lint && eslint --cache --cache-location .cache/ src/**/*.mjs", | ||
"test": "NODE_ENV=test c8 mocha --experimental-modules --config .mocharc ./**/__tests__/**/*.test.mjs", | ||
@@ -42,20 +65,36 @@ "prepublish": "npm run lint && npm run test && npm run build" | ||
"devDependencies": { | ||
"c8": "^7.1.0", | ||
"chai": "^4.2.0", | ||
"eslint": "^6.8.0", | ||
"eslint-config-prettier": "^6.10.1", | ||
"eslint-plugin-chai-expect": "^2.1.0", | ||
"eslint-plugin-chai-friendly": "^0.5.0", | ||
"eslint-plugin-prettier": "^3.1.2", | ||
"husky": "^4.2.5", | ||
"jison": "^0.4.18", | ||
"mocha": "^7.1.1", | ||
"@babel/core": "^7.15.0", | ||
"@babel/eslint-parser": "^7.15.0", | ||
"@babel/plugin-transform-runtime": "^7.15.0", | ||
"@babel/preset-env": "^7.15.0", | ||
"@ls-lint/ls-lint": "^1.10.0", | ||
"@rollup/plugin-alias": "^3.1.5", | ||
"@rollup/plugin-babel": "^5.3.0", | ||
"@rollup/plugin-commonjs": "^19.0.2", | ||
"c8": "^7.8.0", | ||
"chai": "^4.3.4", | ||
"cpy": "^8.1.2", | ||
"eslint": "^7.32.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
"eslint-plugin-chai-expect": "^2.2.0", | ||
"eslint-plugin-chai-friendly": "^0.7.2", | ||
"eslint-plugin-prettier": "^3.4.1", | ||
"eslint-plugin-simple-import-sort": "^7.0.0", | ||
"husky": "^4.3.8", | ||
"it-each": "0.4.0", | ||
"jsonpath-plus": "^6.0.1", | ||
"lodash.topath": "^4.5.2", | ||
"mocha": "^8.0.1", | ||
"mocha-each": "^2.0.1", | ||
"prettier": "^2.0.4", | ||
"rollup": "^2.4.0" | ||
"pegjs": "0.10.0", | ||
"prettier": "^2.3.2", | ||
"rollup": "^2.56.2" | ||
}, | ||
"optionalDependencies": { | ||
"jsonpath-plus": "^6.0.1", | ||
"lodash.topath": "^4.5.2" | ||
}, | ||
"dependencies": { | ||
"astring": "^1.4.3", | ||
"jsep": "^0.3.4" | ||
"astring": "^1.7.5" | ||
} | ||
} |
229
README.md
# nimma | ||
> JSON Path expressions? I mog *nimma*, aba naja. :trollface: | ||
> JSON Path expressions? I mog *nimma*, aba naja. :trollface: | ||
@@ -11,3 +11,3 @@ ## Install | ||
or if npm is package manager of your choice | ||
or if npm is the package manager of your choice | ||
@@ -18,12 +18,231 @@ ```sh | ||
## Features | ||
- Very good JSONPath support - besides a few tiny exceptions, the whole spec is covered, | ||
- Supports the majority of JSONPath-plus additions, | ||
- Support for containments (`in`) and regex (`~=`) operators, as taken from [draft-ietf-jsonpath-base-01](https://datatracker.ietf.org/doc/html/draft-ietf-jsonpath-base), | ||
- Increased security - only a strict set of operations are supported in Filter Expressions - no global references, or assignments are permitted. | ||
## Usage | ||
Will add some later. | ||
```js | ||
import Nimma from 'https://cdn.skypack.dev/nimma'; | ||
## Thanks | ||
const n = new Nimma([ | ||
'$.info', | ||
'$.info.contact', | ||
'$.info^', | ||
'$.info^~', | ||
'$.servers[*].url', | ||
'$.servers[0:2]', | ||
'$.servers[:5]', | ||
"$.bar['children']", | ||
"$.bar['0']", | ||
"$.bar['children.bar']", | ||
'$.channels[*][publish,subscribe][?(@.schemaFormat === void 0)].payload', | ||
"$..[?( @property === 'get' || @property === 'put' || @property === 'post' )]", | ||
"$..paths..[?( @property === 'get' || @property === 'put' || @property === 'post' )]", | ||
`$.examples.*`, | ||
'$[1:-5:-2]', | ||
'$..foo..[?( @property >= 900 )]..foo', | ||
]); | ||
- David Chester for [JSONPath expression parser](https://github.com/dchester/jsonpath). | ||
// you can perform the query... | ||
n.query(document, { | ||
['$.info']({ value, path }) { | ||
// | ||
}, | ||
// and so on for each specified path | ||
}); | ||
// ... or write the generated code. It's advisable to write the code to further re-use. | ||
await cache.writeFile('./nimma-code.mjs', n.sourceCode); // once | ||
``` | ||
Here's how the sourceCode would look like for the above path expressions | ||
```js | ||
import {Scope, isObject, inBounds} from "nimma/runtime"; | ||
const tree = { | ||
"$.info": function (scope, fn) { | ||
const value = scope.sandbox.root; | ||
if (isObject(value)) { | ||
scope.fork(["info"])?.emit(fn, 0, false); | ||
} | ||
}, | ||
"$.info.contact": function (scope, fn) { | ||
const value = scope.sandbox.root?.["info"]; | ||
if (isObject(value)) { | ||
scope.fork(["info", "contact"])?.emit(fn, 0, false); | ||
} | ||
}, | ||
"$.info^": function (scope, fn) { | ||
const value = scope.sandbox.root; | ||
if (isObject(value)) { | ||
scope.fork(["info"])?.emit(fn, 1, false); | ||
} | ||
}, | ||
"$.info^~": function (scope, fn) { | ||
const value = scope.sandbox.root; | ||
if (isObject(value)) { | ||
scope.fork(["info"])?.emit(fn, 1, true); | ||
} | ||
}, | ||
"$.servers[*].url": function (scope, fn) { | ||
if (scope.depth !== 2) return; | ||
if (scope.path[0] !== "servers") return; | ||
if (scope.path[2] !== "url") return; | ||
scope.emit(fn, 0, false); | ||
}, | ||
"$.servers[0:2]": function (scope, fn) { | ||
if (scope.depth !== 1) return; | ||
if (scope.path[0] !== "servers") return; | ||
if (typeof scope.path[1] !== "number" || scope.path[1] >= 2) return; | ||
scope.emit(fn, 0, false); | ||
}, | ||
"$.servers[:5]": function (scope, fn) { | ||
if (scope.depth !== 1) return; | ||
if (scope.path[0] !== "servers") return; | ||
if (typeof scope.path[1] !== "number" || scope.path[1] >= 5) return; | ||
scope.emit(fn, 0, false); | ||
}, | ||
"$.bar['children']": function (scope, fn) { | ||
const value = scope.sandbox.root?.["bar"]; | ||
if (isObject(value)) { | ||
scope.fork(["bar", "children"])?.emit(fn, 0, false); | ||
} | ||
}, | ||
"$.bar['0']": function (scope, fn) { | ||
const value = scope.sandbox.root?.["bar"]; | ||
if (isObject(value)) { | ||
scope.fork(["bar", "0"])?.emit(fn, 0, false); | ||
} | ||
}, | ||
"$.bar['children.bar']": function (scope, fn) { | ||
const value = scope.sandbox.root?.["bar"]; | ||
if (isObject(value)) { | ||
scope.fork(["bar", "children.bar"])?.emit(fn, 0, false); | ||
} | ||
}, | ||
"$.channels[*][publish,subscribe][?(@.schemaFormat === void 0)].payload": function (scope, fn) { | ||
if (scope.depth !== 4) return; | ||
if (scope.path[0] !== "channels") return; | ||
if (scope.path[2] !== "publish" && scope.path[2] !== "subscribe") return; | ||
if (!(scope.sandbox.at(3).value.schemaFormat === void 0)) return; | ||
if (scope.path[4] !== "payload") return; | ||
scope.emit(fn, 0, false); | ||
}, | ||
"$..[?( @property === 'get' || @property === 'put' || @property === 'post' )]": function (scope, fn) { | ||
if (!(scope.sandbox.property === 'get' || scope.sandbox.property === 'put' || scope.sandbox.property === 'post')) return; | ||
scope.emit(fn, 0, false); | ||
}, | ||
"$..paths..[?( @property === 'get' || @property === 'put' || @property === 'post' )]": function (scope, fn) { | ||
if (scope.depth < 1) return; | ||
let pos = 0; | ||
if ((pos = scope.path.indexOf("paths", pos), pos === -1)) return; | ||
if (scope.depth < pos + 1 || (pos = !(scope.sandbox.property === 'get' || scope.sandbox.property === 'put' || scope.sandbox.property === 'post') ? -1 : scope.depth, pos === -1)) return; | ||
if (scope.depth !== pos) return; | ||
scope.emit(fn, 0, false); | ||
}, | ||
"$.examples.*": function (scope, fn) { | ||
if (scope.depth !== 1) return; | ||
if (scope.path[0] !== "examples") return; | ||
scope.emit(fn, 0, false); | ||
}, | ||
"$[1:-5:-2]": function (scope, fn) { | ||
if (scope.depth !== 0) return; | ||
if (typeof scope.path[0] !== "number" || !inBounds(scope.sandbox.parentValue, scope.path[0], 1, -5, -2)) return; | ||
scope.emit(fn, 0, false); | ||
}, | ||
"$..foo..[?( @property >= 900 )]..foo": function (scope, fn) { | ||
scope.bail("$..foo..[?( @property >= 900 )]..foo", scope => scope.emit(fn, 0, false), [{ | ||
fn: scope => scope.property !== "foo", | ||
deep: true | ||
}, { | ||
fn: scope => !(scope.sandbox.property >= 900), | ||
deep: true | ||
}, { | ||
fn: scope => scope.property !== "foo", | ||
deep: true | ||
}]); | ||
} | ||
}; | ||
export default function (input, callbacks) { | ||
const scope = new Scope(input); | ||
const _tree = scope.registerTree(tree); | ||
const _callbacks = scope.proxyCallbacks(callbacks, {}); | ||
try { | ||
_tree["$.info"](scope, _callbacks["$.info"]); | ||
_tree["$.info.contact"](scope, _callbacks["$.info.contact"]); | ||
_tree["$.info^"](scope, _callbacks["$.info^"]); | ||
_tree["$.info^~"](scope, _callbacks["$.info^~"]); | ||
_tree["$.bar['children']"](scope, _callbacks["$.bar['children']"]); | ||
_tree["$.bar['0']"](scope, _callbacks["$.bar['0']"]); | ||
_tree["$.bar['children.bar']"](scope, _callbacks["$.bar['children.bar']"]); | ||
_tree["$..foo..[?( @property >= 900 )]..foo"](scope, _callbacks["$..foo..[?( @property >= 900 )]..foo"]); | ||
scope.traverse(() => { | ||
_tree["$.servers[*].url"](scope, _callbacks["$.servers[*].url"]); | ||
_tree["$.servers[0:2]"](scope, _callbacks["$.servers[0:2]"]); | ||
_tree["$.servers[:5]"](scope, _callbacks["$.servers[:5]"]); | ||
_tree["$.channels[*][publish,subscribe][?(@.schemaFormat === void 0)].payload"](scope, _callbacks["$.channels[*][publish,subscribe][?(@.schemaFormat === void 0)].payload"]); | ||
_tree["$..[?( @property === 'get' || @property === 'put' || @property === 'post' )]"](scope, _callbacks["$..[?( @property === 'get' || @property === 'put' || @property === 'post' )]"]); | ||
_tree["$..paths..[?( @property === 'get' || @property === 'put' || @property === 'post' )]"](scope, _callbacks["$..paths..[?( @property === 'get' || @property === 'put' || @property === 'post' )]"]); | ||
_tree["$.examples.*"](scope, _callbacks["$.examples.*"]); | ||
_tree["$[1:-5:-2]"](scope, _callbacks["$[1:-5:-2]"]); | ||
}); | ||
} finally { | ||
scope.destroy(); | ||
} | ||
} | ||
``` | ||
Since it's a valid ES Module, you can easily load it again and there's no need for `new Nimma`. | ||
### Supported opts | ||
- output: ES2018 | ES2021 | auto | ||
- fallback | ||
- unsafe | ||
## Comparison vs jsonpath-plus and alikes | ||
Nimma, although being yet-another-json-path query engine, it's considerably different from its JS counterparts. | ||
Nimma takes dozens/hundreds/thousands of JSONPath expressions and attempt to form a proper JS code, | ||
while packages like jsonpath-plus or jsonpath take a JSONPath expression and loop over its segments during the query. | ||
They are meant to be executed on a single expression, whereas Nimma, for the most time, doesn't really care whether you supply it with 10s or 100s of paths. | ||
Futhermore, Nimma, despite remaining close to the ~spec~, well, "spec", does make certain minor assumptions - the most notable being here that the order of query doesn't matter. | ||
In order words, Nimma guarantees that all matching values will be returned, but doesn't assure any order. | ||
This may be a deal breaker for some, but I haven't spotted such people in my life. | ||
In reality, this would only matter if you used negative boundaries in Slice Expressions. | ||
In addition to that, it also doesn't accumulate the results - this duties lies on the consumer. | ||
These are tradeoffs that are likely to be negligible for the vast percentage of cases, yet they may play a role for some. | ||
Unlike the aforementioned libraries, Nimma forbids any arbitrary code execution. | ||
This is mostly thanks to a forked version of [jsep](https://github.com/EricSmekens/jsep) Nimma is equipped with, as well as a set of additional enforcements. | ||
Due to that, it's not possible to reference any object or function, even if it exists in the given environment. | ||
For instance, `$[?(Array.isArray(@)]` will throw an exception, same as `$[(?Object.prototype = {})]`, etc. | ||
As a result, it's generally safer to execute these expressions, however there's no security guarantee here by any means, | ||
and therefore it's still advisable to run Nimma in an isolated environment if JSONPath expressions cannot be trusted. | ||
Since Nimma serves a different purpose, a use of other libraries is not ruled out. | ||
It certainly doesn't aim to compete with any of them. | ||
In fact, Nimma relies on `jsonpath-plus` under rare circumstances (mostly when "^" or "~" is not placed at the end of the expression). | ||
### How does it actually work? | ||
Nimma consists of 3 major components. These are: | ||
- parser | ||
- codegen (iterator/feedback + baseline) | ||
- runtime (scope + sandbox + traverse) | ||
Parser takes any JSON Path expression and generates an AST that's consumed by the codegen in the next step. | ||
Codegen is a two-step process: | ||
- first, we have a quick pass of the tree to collect some feedback about it that will be used by the actual code generators | ||
- baseline processes the AST & the feedback gathered by the Iterator, and generates a decent ESTree-compliant AST representing that we dump later only | ||
- there's also a concept of "fast paths" implemented that are basically stubs for some common use cases to generate an even more efficient code | ||
## LICENSE | ||
[Apache License 2.0](https://github.com/P0lip/nimma/blob/master/LICENSE) |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
566478
185
17645
247
3
26
9
+ Addedjsonpath-plus@6.0.1(transitive)
+ Addedlodash.topath@4.5.2(transitive)
- Removedjsep@^0.3.4
- Removedjsep@0.3.5(transitive)
Updatedastring@^1.7.5