@jsonquerylang/jsonquery
Advanced tools
Comparing version 2.0.0 to 3.0.1
@@ -1,3 +0,3 @@ | ||
import { Function, JSONQuery, JSONQueryOptions } from './types'; | ||
export declare function compile(query: JSONQuery, options?: JSONQueryOptions): Function; | ||
import type { Fun, JSONQuery, JSONQueryCompileOptions } from './types'; | ||
export declare function compile(query: JSONQuery, options?: JSONQueryCompileOptions): Fun; | ||
//# sourceMappingURL=compile.d.ts.map |
@@ -1,3 +0,4 @@ | ||
import { FunctionBuildersMap } from './types'; | ||
import type { FunctionBuilder, FunctionBuildersMap } from './types'; | ||
export declare function buildFunction(fn: (...args: unknown[]) => unknown): FunctionBuilder; | ||
export declare const functions: FunctionBuildersMap; | ||
//# sourceMappingURL=functions.d.ts.map |
export { jsonquery } from './jsonquery'; | ||
export { compile } from './compile'; | ||
export { stringify } from './stringify'; | ||
export { parse } from './parse'; | ||
export { buildFunction } from './functions'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -1,3 +0,3 @@ | ||
import { JSONQuery, JSONQueryOptions } from './types'; | ||
export declare function jsonquery(data: unknown, query: JSONQuery, options?: JSONQueryOptions): unknown; | ||
import type { JSONQuery, JSONQueryOptions } from './types'; | ||
export declare function jsonquery(data: unknown, query: string | JSONQuery, options?: JSONQueryOptions): unknown; | ||
//# sourceMappingURL=jsonquery.d.ts.map |
@@ -1,67 +0,87 @@ | ||
const m = (t) => Array.isArray(t), a = (t) => t && typeof t == "object" && !m(t), b = (t) => typeof t == "string"; | ||
function s(t) { | ||
return (...n) => { | ||
const e = n.map((c) => u(c)), r = e[0], o = e[1]; | ||
return e.length === 1 ? (c) => t(r(c)) : e.length === 2 ? (c) => t(r(c), o(c)) : (c) => t(...e.map((i) => i(c))); | ||
const E = (t) => Array.isArray(t), L = (t) => typeof t == "string"; | ||
function m(t) { | ||
return (...e) => { | ||
const r = e.map((i) => b(i)), n = r[0], s = r[1]; | ||
return r.length === 1 ? (i) => t(n(i)) : r.length === 2 ? (i) => t(n(i), s(i)) : (i) => t(...r.map((j) => j(i))); | ||
}; | ||
} | ||
const l = { | ||
const N = { | ||
pipe: (...t) => { | ||
const e = t.map((r) => b(r)); | ||
return (r) => e.reduce((n, s) => s(n), r); | ||
}, | ||
object: (t) => { | ||
const e = Object.keys(t).map((r) => [r, b(t[r])]); | ||
return (r) => { | ||
const n = {}; | ||
for (const [s, i] of e) | ||
n[s] = i(r); | ||
return n; | ||
}; | ||
}, | ||
array: (...t) => { | ||
const e = t.map((r) => b(r)); | ||
return (r) => e.map((n) => n(r)); | ||
}, | ||
get: (...t) => { | ||
if (t.length === 0) | ||
return (n) => n; | ||
return (e) => e; | ||
if (t.length === 1) { | ||
const n = t[0]; | ||
return (e) => e == null ? void 0 : e[n]; | ||
const e = t[0]; | ||
return (r) => r == null ? void 0 : r[e]; | ||
} | ||
return (n) => { | ||
let e = n; | ||
for (const r of t) | ||
e = e == null ? void 0 : e[r]; | ||
return e; | ||
return (e) => { | ||
let r = e; | ||
for (const n of t) | ||
r = r == null ? void 0 : r[n]; | ||
return r; | ||
}; | ||
}, | ||
map: (t) => { | ||
const n = u(t); | ||
return (e) => e.map(n); | ||
const e = b(t); | ||
return (r) => r.map(e); | ||
}, | ||
filter: (...t) => { | ||
const n = u(t.length === 1 ? t[0] : t); | ||
return (e) => e.filter(n); | ||
filter: (t) => { | ||
const e = b(t); | ||
return (r) => r.filter(e); | ||
}, | ||
sort: (t = ["get"], n) => { | ||
const e = u(t), r = n === "desc" ? -1 : 1; | ||
function o(c, i) { | ||
const f = e(c), p = e(i); | ||
return f > p ? r : f < p ? -r : 0; | ||
sort: (t = ["get"], e) => { | ||
const r = b(t), n = e === "desc" ? -1 : 1; | ||
function s(i, j) { | ||
const w = r(i), y = r(j); | ||
return w > y ? n : w < y ? -n : 0; | ||
} | ||
return (c) => c.slice().sort(o); | ||
return (i) => i.slice().sort(s); | ||
}, | ||
pick: (...t) => { | ||
const n = t.map( | ||
([r, ...o]) => [o[o.length - 1], l.get(...o)] | ||
), e = (r, o) => { | ||
const c = {}; | ||
return o.forEach(([i, f]) => c[i] = f(r)), c; | ||
const e = t.map( | ||
([n, ...s]) => [s[s.length - 1], N.get(...s)] | ||
), r = (n, s) => { | ||
const i = {}; | ||
for (const [j, w] of s) | ||
i[j] = w(n); | ||
return i; | ||
}; | ||
return (r) => m(r) ? r.map((o) => e(o, n)) : e(r, n); | ||
return (n) => E(n) ? n.map((s) => r(s, e)) : r(n, e); | ||
}, | ||
groupBy: (t) => { | ||
const n = u(t); | ||
return (e) => { | ||
const r = {}; | ||
for (const o of e) { | ||
const c = n(o); | ||
r[c] ? r[c].push(o) : r[c] = [o]; | ||
const e = b(t); | ||
return (r) => { | ||
const n = {}; | ||
for (const s of r) { | ||
const i = e(s); | ||
n[i] ? n[i].push(s) : n[i] = [s]; | ||
} | ||
return r; | ||
return n; | ||
}; | ||
}, | ||
keyBy: (t) => { | ||
const n = u(t); | ||
return (e) => { | ||
const r = {}; | ||
return e.forEach((o) => { | ||
const c = n(o); | ||
r[c] = r[c] ?? o; | ||
}), r; | ||
const e = b(t); | ||
return (r) => { | ||
const n = {}; | ||
for (const s of r) { | ||
const i = e(s); | ||
n[i] = n[i] ?? s; | ||
} | ||
return n; | ||
}; | ||
@@ -71,85 +91,236 @@ }, | ||
uniq: () => (t) => [...new Set(t)], | ||
uniqBy: (t) => (n) => Object.values(l.groupBy(t)(n)).map((e) => e[0]), | ||
limit: (t) => (n) => n.slice(0, t), | ||
uniqBy: (t) => (e) => Object.values(N.groupBy(t)(e)).map((r) => r[0]), | ||
limit: (t) => (e) => e.slice(0, t), | ||
size: () => (t) => t.length, | ||
keys: () => Object.keys, | ||
values: () => Object.values, | ||
prod: () => (t) => t.reduce((n, e) => n * e), | ||
sum: () => (t) => t.reduce((n, e) => n + e), | ||
average: () => (t) => l.sum()(t) / t.length, | ||
prod: () => (t) => t.reduce((e, r) => e * r), | ||
sum: () => (t) => t.reduce((e, r) => e + r), | ||
average: () => (t) => N.sum()(t) / t.length, | ||
min: () => (t) => Math.min(...t), | ||
max: () => (t) => Math.max(...t), | ||
in: (t, n) => { | ||
const e = u(t); | ||
return (r) => n.includes(e(r)); | ||
in: (t, e) => { | ||
const r = b(t), n = b(e); | ||
return (s) => n(s).includes(r(s)); | ||
}, | ||
"not in": (t, n) => { | ||
const e = u(t); | ||
return (r) => !n.includes(e(r)); | ||
"not in": (t, e) => { | ||
const r = N.in(t, e); | ||
return (n) => !r(n); | ||
}, | ||
regex: (t, n, e) => { | ||
const r = new RegExp(n, e), o = u(t); | ||
return (c) => r.test(o(c)); | ||
regex: (t, e, r) => { | ||
const n = new RegExp(e, r), s = b(t); | ||
return (i) => n.test(s(i)); | ||
}, | ||
and: s((t, n) => t && n), | ||
or: s((t, n) => t || n), | ||
not: s((t) => !t), | ||
exists: s((t) => t !== void 0), | ||
eq: s((t, n) => t === n), | ||
gt: s((t, n) => t > n), | ||
gte: s((t, n) => t >= n), | ||
lt: s((t, n) => t < n), | ||
lte: s((t, n) => t <= n), | ||
ne: s((t, n) => t !== n), | ||
add: s((t, n) => t + n), | ||
subtract: s((t, n) => t - n), | ||
multiply: s((t, n) => t * n), | ||
divide: s((t, n) => t / n), | ||
pow: s((t, n) => t ** n), | ||
mod: s((t, n) => t % n), | ||
abs: s(Math.abs), | ||
round: s((t, n = 0) => +(Math.round(+(t + "e" + n)) + "e" + -n)) | ||
}, g = [l]; | ||
function u(t, n) { | ||
g.unshift({ ...g[0], ...n == null ? void 0 : n.functions }); | ||
and: m((t, e) => t && e), | ||
or: m((t, e) => t || e), | ||
not: m((t) => !t), | ||
exists: m((t) => t !== void 0), | ||
eq: m((t, e) => t === e), | ||
gt: m((t, e) => t > e), | ||
gte: m((t, e) => t >= e), | ||
lt: m((t, e) => t < e), | ||
lte: m((t, e) => t <= e), | ||
ne: m((t, e) => t !== e), | ||
add: m((t, e) => t + e), | ||
subtract: m((t, e) => t - e), | ||
multiply: m((t, e) => t * e), | ||
divide: m((t, e) => t / e), | ||
pow: m((t, e) => t ** e), | ||
mod: m((t, e) => t % e), | ||
abs: m(Math.abs), | ||
round: m((t, e = 0) => +`${Math.round(+`${t}e${e}`)}e${-e}`) | ||
}, S = []; | ||
function b(t, e) { | ||
S.unshift({ ...N, ...S[0], ...e == null ? void 0 : e.functions }); | ||
try { | ||
const e = h(t, g[0]); | ||
return (r) => { | ||
const r = E(t) ? P(t, S[0]) : () => t; | ||
return (n) => { | ||
try { | ||
return e(r); | ||
} catch (o) { | ||
throw o.jsonquery = [{ data: r, query: t }, ...o.jsonquery ?? []], o; | ||
return r(n); | ||
} catch (s) { | ||
throw s.jsonquery = [{ data: n, query: t }, ...s.jsonquery ?? []], s; | ||
} | ||
}; | ||
} finally { | ||
g.shift(); | ||
S.shift(); | ||
} | ||
} | ||
function h(t, n) { | ||
return m(t) ? b(t[0]) ? j(t, n) : y(t) : a(t) ? d(t) : () => t; | ||
function P(t, e) { | ||
const [r, ...n] = t, s = e[r]; | ||
if (!s) | ||
throw new Error(`Unknown function '${r}'`); | ||
return s(...n); | ||
} | ||
function j(t, n) { | ||
const [e, ...r] = t, o = n[e]; | ||
if (!o) | ||
throw new Error(`Unknown function "${e}"`); | ||
return o(...r); | ||
} | ||
function y(t) { | ||
const n = t.map((e) => u(e)); | ||
return (e) => n.reduce((r, o) => o(r), e); | ||
} | ||
function d(t) { | ||
const n = Object.keys(t).map((e) => [e, u(t[e])]); | ||
return (e) => { | ||
const r = {}; | ||
return n.forEach(([o, c]) => r[o] = c(e)), r; | ||
const R = { | ||
and: "and", | ||
or: "or", | ||
eq: "==", | ||
gt: ">", | ||
gte: ">=", | ||
lt: "<", | ||
lte: "<=", | ||
ne: "!=", | ||
add: "+", | ||
subtract: "-", | ||
multiply: "*", | ||
divide: "/", | ||
pow: "^", | ||
mod: "%", | ||
in: "in", | ||
"not in": "not in" | ||
}, W = /^[a-zA-Z_$][a-zA-Z\d_$]*$/, I = /^[a-zA-Z_$][a-zA-Z\d_$]*/, U = /^"(?:[^"\\]|\\.)*"/, F = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/, J = /^(0|[1-9][0-9]*)/, z = /^(true|false|null)/, B = /^[ \n\t\r]+/; | ||
function M(t, e) { | ||
const r = () => { | ||
h(); | ||
const o = n(); | ||
if (h(), t[c] === "|") { | ||
const g = [o]; | ||
for (; t[c] === "|"; ) | ||
c++, h(), g.push(n()); | ||
return ["pipe", ...g]; | ||
} | ||
return o; | ||
}, n = () => { | ||
const o = { ...R, ...e == null ? void 0 : e.operators }, g = s(); | ||
h(); | ||
for (const x of Object.keys(o).sort((k, v) => v.length - k.length)) { | ||
const k = o[x]; | ||
if (t.substring(c, c + k.length) === k) { | ||
c += k.length, h(); | ||
const v = s(); | ||
return [x, g, v]; | ||
} | ||
} | ||
return g; | ||
}, s = () => { | ||
if (t[c] === "(") { | ||
c++; | ||
const o = r(); | ||
return p(")"), o; | ||
} | ||
return i(); | ||
}, i = () => { | ||
const o = []; | ||
if (t[c] === ".") { | ||
for (; t[c] === "."; ) | ||
c++, o.push( | ||
a() ?? u() ?? f() ?? $("Property expected") | ||
); | ||
return ["get", ...o]; | ||
} | ||
return j(); | ||
}, j = () => { | ||
const o = c, g = u(); | ||
if (h(), !g || t[c] !== "(") | ||
return c = o, w(); | ||
c++, !(e != null && e.functions[g]) && !N[g] && $(`Unknown function '${g}'`), h(); | ||
const x = t[c] !== ")" ? [r()] : []; | ||
for (; c < t.length && t[c] !== ")"; ) | ||
h(), p(","), x.push(r()); | ||
return p(")"), [g, ...x]; | ||
}, w = () => { | ||
if (t[c] === "{") { | ||
c++, h(); | ||
const o = {}; | ||
let g = !0; | ||
for (; c < t.length && t[c] !== "}"; ) { | ||
g ? g = !1 : (p(","), h()); | ||
const x = a() ?? u() ?? f() ?? $("Key expected"); | ||
h(), p(":"), o[x] = r(); | ||
} | ||
return p("}"), ["object", o]; | ||
} | ||
return y(); | ||
}, y = () => { | ||
if (t[c] === "[") { | ||
c++, h(); | ||
const o = []; | ||
let g = !0; | ||
for (; c < t.length && t[c] !== "]"; ) | ||
g ? g = !1 : (p(","), h()), o.push(r()); | ||
return p("]"), ["array", ...o]; | ||
} | ||
return a() ?? l() ?? _(); | ||
}, a = () => d(U, JSON.parse), u = () => d(I, (o) => o), l = () => d(F, JSON.parse), f = () => d(J, JSON.parse), _ = () => { | ||
const o = d(z, JSON.parse); | ||
if (o !== void 0) | ||
return o; | ||
$("Value expected"); | ||
}, O = () => { | ||
h(), c < t.length && $(`Unexpected part '${t.substring(c)}'`); | ||
}, d = (o, g) => { | ||
const x = t.substring(c).match(o); | ||
if (x) | ||
return c += x[0].length, g(x[0]); | ||
}, h = () => d(B, (o) => o), p = (o) => { | ||
t[c] !== o && $(`Character '${o}' expected`), c++; | ||
}, $ = (o, g = c) => { | ||
throw new SyntaxError(`${o} (pos: ${g})`); | ||
}; | ||
let c = 0; | ||
const A = r(); | ||
return O(), A; | ||
} | ||
function k(t, n, e) { | ||
return u(n, e)(t); | ||
function D(t, e, r) { | ||
return b(L(e) ? M(e, r) : e, r)(t); | ||
} | ||
const T = 40, Z = " ", K = (t, e) => { | ||
const r = (e == null ? void 0 : e.indentation) ?? Z, n = (a, u) => E(a) ? s(a, u) : JSON.stringify(a), s = (a, u) => { | ||
var h; | ||
const [l, ...f] = a; | ||
if (l === "get" && f.length > 0) | ||
return j(f); | ||
if (l === "pipe") { | ||
const p = f.map(($) => n($, u + r)); | ||
return y(p, ["", " | ", ""], ["", ` | ||
${u + r}| `, ""]); | ||
} | ||
if (l === "object") | ||
return i(f[0], u); | ||
if (l === "array") { | ||
const p = f.map(($) => n($, u)); | ||
return y( | ||
p, | ||
["[", ", ", "]"], | ||
[`[ | ||
${u + r}`, `, | ||
${u + r}`, ` | ||
${u}]`] | ||
); | ||
} | ||
const _ = ((h = e == null ? void 0 : e.operators) == null ? void 0 : h[l]) ?? R[l]; | ||
if (_ && f.length === 2) { | ||
const [p, $] = f, c = n(p, u), A = n($, u); | ||
return `(${c} ${_} ${A})`; | ||
} | ||
const O = f.length === 1 ? u : u + r, d = f.map((p) => n(p, O)); | ||
return f.length === 1 && d[0][0] === "(" ? `${l}${d}` : y( | ||
d, | ||
[`${l}(`, ", ", ")"], | ||
f.length === 1 ? [`${l}(`, `, | ||
${u}`, ")"] : [`${l}( | ||
${O}`, `, | ||
${O}`, ` | ||
${u})`] | ||
); | ||
}, i = (a, u) => { | ||
const l = u + r, f = Object.entries(a).map(([_, O]) => `${w(_)}: ${n(O, l)}`); | ||
return y( | ||
f, | ||
["{ ", ", ", " }"], | ||
[`{ | ||
${l}`, `, | ||
${l}`, ` | ||
${u}}`] | ||
); | ||
}, j = (a) => a.map((u) => `.${w(u)}`).join(""), w = (a) => W.test(a) ? a : JSON.stringify(a), y = (a, [u, l, f], [_, O, d]) => u.length + a.reduce((p, $) => p + $.length + l.length, 0) - l.length + f.length <= ((e == null ? void 0 : e.maxLineLength) ?? T) ? u + a.join(l) + f : _ + a.join(O) + d; | ||
return n(t, ""); | ||
}; | ||
export { | ||
u as compile, | ||
k as jsonquery | ||
m as buildFunction, | ||
b as compile, | ||
D as jsonquery, | ||
M as parse, | ||
K as stringify | ||
}; | ||
//# sourceMappingURL=jsonquery.js.map |
@@ -13,7 +13,20 @@ export type JSONQueryPipe = JSONQuery[]; | ||
functions?: FunctionBuildersMap; | ||
operators?: Record<string, string>; | ||
} | ||
export type Function = (data: unknown) => unknown; | ||
export type FunctionBuilder = (...args: JSONQuery[]) => Function; | ||
export interface JSONQueryCompileOptions { | ||
functions?: FunctionBuildersMap; | ||
} | ||
export interface JSONQueryStringifyOptions { | ||
operators?: Record<string, string>; | ||
maxLineLength?: number; | ||
indentation?: string; | ||
} | ||
export interface JSONQueryParseOptions { | ||
functions?: Record<string, boolean> | FunctionBuildersMap; | ||
operators?: Record<string, string>; | ||
} | ||
export type Fun = (data: unknown) => unknown; | ||
export type FunctionBuilder = (...args: JSONQuery[]) => Fun; | ||
export type FunctionBuildersMap = Record<string, FunctionBuilder>; | ||
export type Getter = [key: string, Function]; | ||
export type Getter = [key: string, Fun]; | ||
//# sourceMappingURL=types.d.ts.map |
{ | ||
"name": "@jsonquerylang/jsonquery", | ||
"version": "2.0.0", | ||
"version": "3.0.1", | ||
"description": "A small, flexible, and expandable JSON query language", | ||
@@ -39,2 +39,3 @@ "keywords": [ | ||
"test-ci": "vitest run src", | ||
"coverage": "vitest run src --coverage", | ||
"build": "npm-run-all build:**", | ||
@@ -44,4 +45,4 @@ "build:esm": "vite build", | ||
"build:validate": "vitest run test-lib", | ||
"lint": "prettier . --check", | ||
"format": "prettier . --write", | ||
"lint": "biome check", | ||
"format": "biome check --write", | ||
"format:readme": "prettier README.md --write --ignore-path notneeded", | ||
@@ -53,9 +54,10 @@ "build-and-test": "npm-run-all test-ci lint build", | ||
"devDependencies": { | ||
"@biomejs/biome": "1.9.2", | ||
"@vitest/coverage-v8": "2.1.1", | ||
"npm-run-all": "4.1.5", | ||
"prettier": "3.3.3", | ||
"semantic-release": "24.1.0", | ||
"typescript": "5.5.4", | ||
"vite": "5.4.2", | ||
"vitest": "2.0.5" | ||
"semantic-release": "24.1.1", | ||
"typescript": "5.6.2", | ||
"vite": "5.4.7", | ||
"vitest": "2.1.1" | ||
} | ||
} |
411
README.md
@@ -9,10 +9,9 @@ # JSON Query | ||
![JSON Query Overview](https://jsonquerylang.org/jsonquery-overview.svg) | ||
![JSON Query Overview](docs/jsonquery-overview.svg) | ||
## Features | ||
- Small (just `1.4 kB` when minified and gzipped!) | ||
- Feature rich (40+ powerful functions and operators) | ||
- Serializable (it is JSON) | ||
- Easy to parse | ||
- Small: just `2.9 kB` when minified and gzipped! The JSON query engine without parse/stringify is only `1.3 kB`. | ||
- Feature rich (40+ powerful functions) | ||
- Easy to interoperate with thanks to the intermediate JSON format. | ||
- Expressive | ||
@@ -28,2 +27,3 @@ - Expandable | ||
- [Syntax](#syntax) | ||
- [JSON Format](#json-format) | ||
- [JavaScript API](#javascript-api) | ||
@@ -41,3 +41,3 @@ - [Gotchas](#gotchas) | ||
``` | ||
```text | ||
npm install @jsonquerylang/jsonquery | ||
@@ -65,8 +65,8 @@ ``` | ||
// sort them by age, and pick just the name and age out of the objects. | ||
const names = jsonquery(data, [ | ||
["get", "friends"], | ||
["filter", ["eq", ["get", "city"], "New York"]], | ||
["sort", ["get", "age"]], | ||
["pick", ["get", "name"], ["get", "age"]] | ||
]) | ||
const names = jsonquery(data, ` | ||
.friends | ||
| filter(.city == "New York") | ||
| sort(.age) | ||
| pick(.name, .age) | ||
`) | ||
// names = [ | ||
@@ -82,13 +82,9 @@ // { "name": "Chris", "age": 23 }, | ||
// properties `age` in all items. | ||
const result = jsonquery(data, [ | ||
["get", "friends"], | ||
{ | ||
"names": ["map", ["get", "name"]], | ||
"count": ["size"], | ||
"averageAge": [ | ||
["map", ["get", "age"]], | ||
["average"] | ||
] | ||
const result = jsonquery(data, ` | ||
.friends | { | ||
names: map(.name), | ||
count: size(), | ||
averageAge: map(.age) | average() | ||
} | ||
]) | ||
`) | ||
// result = { | ||
@@ -105,6 +101,3 @@ // "names": ["Chris", "Emily", "Joe", "Kevin", "Michelle", "Robert", "Sarah"], | ||
] | ||
const totalPrice = jsonquery(shoppingCart, [ | ||
["map", ["multiply", ["get", "price"], ["get", "quantity"]]], | ||
["sum"] | ||
]) | ||
const totalPrice = jsonquery(shoppingCart, 'map(.price * .quantity) | sum()') | ||
// totalPrice = 8.6 | ||
@@ -118,8 +111,10 @@ ``` | ||
const customFunctions = { | ||
times: (value) => (data) => data.map((item) => item * value) | ||
const options = { | ||
functions: { | ||
times: (value) => (data) => data.map((item) => item * value) | ||
} | ||
} | ||
const data = [1, 2, 3] | ||
const result = jsonquery(data, ["times", 3], customFunctions) | ||
const result = jsonquery(data, 'times(3)', options) | ||
// [3, 6, 9] | ||
@@ -130,4 +125,26 @@ ``` | ||
The `jsonquery` query language is written in JSON and has the following building blocks: _functions_, _pipes_, and _objects_. When writing a JSON Query, you compose a ["pipe"](https://medium.com/@efeminella/the-pipe-operator-a-glimpse-into-the-future-of-functional-javascript-7ebb578887a4) or a ["chain"](https://en.wikipedia.org/wiki/Method_chaining) of operations to be applied to the data. It resembles chaining like in [Lodash](https://lodash.com/docs/4.17.15#chain) or just [in JavaScript](https://medium.com/backticks-tildes/understanding-method-chaining-in-javascript-647a9004bd4f) itself using methods like `map` and `filter`. | ||
The `jsonquery` language looks quite similar to JavaScript and other JSON query languages. This makes it easy to learn. When writing a query, you compose a ["pipe"](https://medium.com/@efeminella/the-pipe-operator-a-glimpse-into-the-future-of-functional-javascript-7ebb578887a4) or a ["chain"](https://en.wikipedia.org/wiki/Method_chaining) of operations to be applied to the data. It resembles chaining like in [Lodash](https://lodash.com/docs/4.17.15#chain) or just [in JavaScript](https://medium.com/backticks-tildes/understanding-method-chaining-in-javascript-647a9004bd4f) itself using methods like `map` and `filter`. | ||
Queries are written in a plain text format which is compact and easy to read for humans. The text format is parsed into an intermediate JSON format which is easy to operate on programmatically. This JSON format is executed by the query engine. | ||
The text format has functions, operators, property getters, pipes to execute multiple queries in series, and objects to execute multiple queries in parallel or transform the input. For example: | ||
```text | ||
filter(.age >= 18) | sort(.age) | ||
``` | ||
The text format can be converted (back and forth) into a JSON format consisting purely of composed function calls. A function call is described by an array containing the function name followed by its arguments, like `[name, arg1, arg2, ...]`. Here is the JSON equivalent of the previous example: | ||
```json | ||
[ | ||
"pipe", | ||
["filter", ["gte", ["get", "age"], 18]], | ||
["sort", ["get", "age"]] | ||
] | ||
``` | ||
The JSON format is mostly used under the hood. It allows for easy integrations like a GUI or executing the query in a different environment or language without having to implement a parser for the text format. Read more in the [JSON Format](#json-format) section. | ||
### Syntax overview | ||
The examples in the following section are based on querying the following data: | ||
@@ -146,86 +163,200 @@ | ||
``` | ||
Syntax overview: | ||
| Category | Syntax | Example | | ||
|------------------------|-------------------------------------------|------------------------------------------------| | ||
| [Function](#functions) | `[name, argument1, argument2, ...]` | `["sort", ["get", "age"], "asc"]` | | ||
| [Pipe](#pipes) | `[query1, query1, ...]` | `[["sort", "age"], ["pick", "name", "age"]]` | | ||
| [Object](#objects) | `{"prop1": query1, "prop2": query2, ...}` | `{"names": ["map", "name"], "total": ["sum"]}` | | ||
The following table gives an overview of the JSON query text format: | ||
The following sections explain the syntax in more detail. | ||
| Type | Syntax | Example | | ||
|-------------------------|----------------------------------------------|--------------------------------------------------| | ||
| [Function](#functions) | `name(argument1, argument2, ...)` | `sort(.age, "asc")` | | ||
| [Operator](#operators) | `(left operator right)` | `filter(.age >= 18)` | | ||
| [Pipe](#pipes) | <code>query1 | query2 | ...</code> | <code>sort(.age) | pick(.name, .age)</code> | | ||
| [Object](#objects) | `{ prop1: query1, prop2: query2, ... }` | `{ names: map(.name), total: sum() }` | | ||
| [Array](#arrays) | `[ item1, item2, ... ]` | `[ "New York", "Atlanta" ]` | | ||
| [Property](#properties) | `.prop1`</br>`.prop1.prop2`</br>`."prop1"` | `.age`</br>`.address.city`</br>`."first name"` | | ||
| [String](#values) | `"string"` | `"Hello world"` | | ||
| [Number](#values) | A floating point number | `2.4` | | ||
| [Boolean](#values) | `true` or `false` | `true` | | ||
| [null](#values) | `null` | `null` | | ||
The syntax is explained in details in the following sections. | ||
### Functions | ||
At the core of the query language, we have a _function_ call which described by an array with the function name as first item followed by optional function arguments. The following example will look up the `sort` function and then call it like `sort(data, (item) => item.age, 'asc')`. Here, `data` is the input and should be an array with objects which will be sorted in ascending by the property `age`: | ||
Function calls have the same syntax as in most programming languages: | ||
```json | ||
["sort", ["get", "age"], "asc"] | ||
```text | ||
name(argument1, argument2, ...) | ||
``` | ||
An important function is the function `get`. It allows to get a property from an object: | ||
The following example will `sort` the data in ascending order, sorted by the property `age`. | ||
```json | ||
["get", "age"] | ||
```text | ||
sort(.age, "asc") | ||
``` | ||
A nested property can be retrieved by specifying multiple properties. The following path for example describes the value of a nested property `city` inside an object `address`: | ||
Important to understand is that the functions are executed as a method in a chain: the sorting is applied to the data input, and forwarded to the next method in the chain (if any). The following example first filters the data, and next sorts it: | ||
```json | ||
["get", "address", "city"] | ||
```text | ||
filter(.age >= 21) | sort(.age, "asc") | ||
``` | ||
To get the current value itself, just specify `["get"]` without properties: | ||
See section [Function reference](reference/functions.md) for a detailed overview of all available functions and operators. | ||
```json | ||
["multiply", ["get"], 2] | ||
### Operators | ||
JSON Query supports all basic operators. Operators must be wrapped in parentheses `(...)`, must have both a left and right hand side, and do not have precedence since parentheses are required. The syntax is: | ||
```text | ||
(left operator right) | ||
``` | ||
See section [Function reference](reference/functions.md) for a detailed overview of all available functions. | ||
The following example tests whether a property `age` is greater than or equal to `18`: | ||
```text | ||
(.age >= 18) | ||
``` | ||
Operators are for example used to specify filter conditions: | ||
```text | ||
filter(.age >= 18) | ||
``` | ||
When composing multiple operators, it is necessary to use parentheses: | ||
```text | ||
filter((.age >= 18) and (.age <= 65)) | ||
``` | ||
See section [Function reference](reference/functions.md) for a detailed overview of all available functions and operators. | ||
### Pipes | ||
A _pipe_ is an array containing a series of _functions_, _objects_, or _pipes_. The entries in the pipeline are executed one by one, and the output of the first is the input for the next. The following example will first filter the items of an array that have a nested property `city` in the object `address` with the value `"New York"`, and next, sort the filtered items by the property `age`: | ||
A _pipe_ is a series of multiple query operations separated by a pipe character `|`. The syntax is: | ||
```json | ||
[ | ||
["filter", ["eq", ["get" ,"address", "city"], "New York"]], | ||
["sort", ["get" ,"age"]] | ||
] | ||
```text | ||
query1 | query2 | ... | ||
``` | ||
The entries in the pipeline are executed one by one, and the output of the first is the input for the next. The following example will first filter the items of an array that have a nested property `city` in the object `address` with the value `"New York"`, and next, sort the filtered items by the property `age`: | ||
```text | ||
filter(.address.city == "New York") | sort(.age) | ||
``` | ||
### Objects | ||
An _object_ is defined as a regular JSON object with a property name as key, and a _function_, _pipe_, or _object_ as value. Objects can be used to transform data or to execute multiple query pipelines in parallel. | ||
An _object_ is defined as a regular JSON object with a property name as key, and query as value. Objects can be used to transform data or to execute multiple queries in parallel. | ||
The following example will map over the items of the array and create a new object with properties `firstName` and `city` for every item: | ||
```text | ||
{ prop1: query1, prop2: query2, ... } | ||
``` | ||
```json | ||
["map", { | ||
"firstName": ["get", "name"], | ||
"city": ["get", "address", "city"] | ||
}] | ||
The following example will transform the data by mapping over the items of the array and creating a new object with properties `firstName` and `city` for every item: | ||
```text | ||
map({ | ||
firstName: .name, | ||
city: .address.city | ||
}) | ||
``` | ||
The following example will output an object with properties `names`, `count`, and `averageAge` containing the results of their query: a list with names, the total number of array items, and the average value of the properties `age` in all items: | ||
The following example runs multiple queries in parallel. It outputs an object with properties `names`, `count`, and `averageAge` containing the results of their query: a list with names, the total number of array items, and the average value of the properties `age` in all items: | ||
```json | ||
```text | ||
{ | ||
"names": ["map", ["get", "name"]], | ||
"count": ["size"], | ||
"averageAge": [ | ||
["map", ["get", "age"]], | ||
["average"] | ||
] | ||
names: map(.name), | ||
count: size(), | ||
averageAge: map(.age) | average() | ||
} | ||
``` | ||
A property can be unquoted when it only contains characters `a-z`, `A-Z`, `_` and `$`, and all but the first character can be a number `0-9`. When the property contains other characters, like spaces, it needs to be enclosed in double quotes and escaped like JSON keys: | ||
```text | ||
{ | ||
"first name": map(.name) | ||
} | ||
``` | ||
### Arrays | ||
Arrays are defined like JSON arrays: enclosed in square brackets, with items separated by a comma: | ||
```text | ||
[query1, query2, ...] | ||
``` | ||
Arrays can for example be used for the operators `in` and `not in`: | ||
```text | ||
filter(.city in ["New York", "Atlanta"]) | ||
``` | ||
### Properties | ||
An important feature is the property getter. It allows to get a property from an object: | ||
```text | ||
.age | ||
``` | ||
A nested property can be retrieved by specifying multiple properties. The following path for example describes the value of a nested property `city` inside an object `address`: | ||
```text | ||
.address.city | ||
``` | ||
A property can be unquoted when it only contains characters `a-z`, `A-Z`, `_` and `$`, and all but the first character can be a number `0-9`. When the property contains other characters, like spaces, it needs to be enclosed in double quotes and escaped like JSON keys: | ||
```text | ||
."first name" | ||
``` | ||
To get the current value itself, use the function `get()` without arguments. | ||
### Values | ||
JSON Query supports the following primitive values, the same as in [JSON](https://www.json.org): `string`, `number`, `boolean`, `null`. | ||
| Type | Example | | ||
|---------|-------------------------------------------------------------------| | ||
| string | `"Hello world"`</br>`"Multi line text\nwith \"quoted\" contents"` | | ||
| number | `42`</br>`2.74`</br>`-1.2e3`</br> | | ||
| boolean | `true`</br>`false` | | ||
| null | `null` | | ||
## JSON format | ||
The text format describe above can be converted into an intermediate JSON format consisting purely of composed function calls and vice versa. A function call is described by an array containing the function name followed by its arguments, like `[name, arg1, arg2, ...]`. The following table gives an overview of the text format and the equivalent JSON format. | ||
| Type | Text format | JSON format | | ||
|----------|----------------------------------------------|---------------------------------------------------------------------------| | ||
| Function | `name(argument1, argument2, ...)` | `["name", argument1, argument2, ...]` | | ||
| Operator | `(left operator right)` | `["operator", left, right]` | | ||
| Pipe | <code>query1 | query2 | ...</code> | `["pipe", query1, query2, ...]` | | ||
| Object | `{ prop1: query1, prop2: query2, ... }` | `["object", { "prop1": query1, "prop2": query2, ... }]` | | ||
| Array | `[ item1, item2, ... ]` | `["array", item1, item2, ... ]` | | ||
| Property | `.prop1`</br>`.prop1.prop2`</br>`."prop1"` | `["get", "prop1"]`</br>`["get", "prop1", "prop2"]`</br>`["get", "prop1"]` | | ||
| String | `"string"` | `"string"` | | ||
| Number | A floating point number | A floating point number | | ||
| Boolean | `true` or `false` | `true` or `false` | | ||
| null | `null` | `null` | | ||
## JavaScript API | ||
The library exports the following functions: | ||
- [`jsonquery`](#jsonquery) is the core function of the library, which parses, compiles, and evaluates a query in one go. | ||
- [`compile`](#compile) to compile and evaluate a query. | ||
- [`parse`](#parse) to parse a query in text format into JSON. | ||
- [`stringify`](#stringify) to convert a query in JSON into the text format. | ||
- [`buildFunction`](#buildfunction) a helper function to create a custom function. | ||
### jsonquery | ||
The `jsonquery` library has one core function where you pass the data, the query, and optionally an object with custom functions to extend the built-in functions: | ||
The function `jsonquery` allows to pass data and a query in one go and parse, compile and execute it: | ||
```text | ||
jsonquery(data: JSON, query: string | JSONQuery, options: JSONQueryOptions) : JSON | ||
``` | ||
jsonquery(data: JSON, query: JSONQuery, options: JSONQueryOptions) : JSON | ||
``` | ||
@@ -235,3 +366,3 @@ Here: | ||
- `data` is the JSON document that will be queried, often an array with objects. | ||
- `query` is a JSON document containing a JSON query as described in the section below. | ||
- `query` is a JSON document containing a JSON query, either the text format or the parsed JSON format. | ||
- `options` is an optional object that can contain the following properties: | ||
@@ -243,3 +374,3 @@ - `functions` is an optional map with custom function creators. A function creator has optional arguments as input and must return a function that can be used to process the query data. For example: | ||
functions: { | ||
// usage example: ["times", 3] | ||
// usage example: 'times(3)' | ||
times: (value) => (data) => data.map((item) => item * value) | ||
@@ -255,3 +386,3 @@ } | ||
functions: { | ||
// usage example: ["filter", ["age", ">", 20 ]] | ||
// usage example: 'filter(.age > 20)' | ||
filter: (predicate) => { | ||
@@ -262,7 +393,21 @@ const _predicate = compile(predicate) | ||
} | ||
} | ||
} | ||
``` | ||
You can have a look at the source code of the functions in `/src/functions.ts` for more examples. | ||
- `operators` is an optional map with operators, for example `{ eq: '==' }`. The defined operators can be used in a text query. Only operators with both a left and right hand side are supported, like `a == b`. They can only be executed when there is a corresponding function. For example: | ||
```js | ||
import { buildFunction } from 'jsonquery' | ||
const options = { | ||
operators: { | ||
notEqual: '<>' | ||
}, | ||
functions: { | ||
notEqual: buildFunction((a, b) => a !== b) | ||
} | ||
} | ||
``` | ||
Here an example of using the function `jsonquery`: | ||
@@ -288,5 +433,5 @@ | ||
The JavaScript library also exports a `compile` function: | ||
The compile function compiles and executes a query in JSON format. Function `parse` can be used to parse a text query into JSON before passing it to `compile`. | ||
``` | ||
```text | ||
compile(query: JSONQuery, options: JSONQueryOptions) => (data: JSON) => JSON | ||
@@ -315,2 +460,86 @@ ``` | ||
### parse | ||
Function `parse` parses a query in text format into JSON. Function `stringify` can be used to do the opposite. | ||
```text | ||
parse(query: text, options: JSONQueryParseOptions) : JSONQuery | ||
``` | ||
Example: | ||
```js | ||
import { parse } from '@jsonquerylang/jsonquery' | ||
const text = 'filter(.age > 20)' | ||
const json = parse(text) | ||
// json = ["filter", ["gt", ["get", "age"], 20]] | ||
``` | ||
### stringify | ||
Function `stringify` turns a query in JSON format into the equivalent text format. Function `parse` can be used to parse the text into JSON again. | ||
```text | ||
stringify(query: JSONQuery, options: JSONQueryStringifyOptions) : string | ||
``` | ||
Example: | ||
```js | ||
import { stringify } from '@jsonquerylang/jsonquery' | ||
const json = ["filter", ["gt", ["get", "age"], 20]] | ||
const text = stringify(json) | ||
// text = 'filter(.age > 20)' | ||
``` | ||
### buildFunction | ||
The function `buildFunction` is a helper function to create a custom function. It can only be used for functions (mostly operators), not for methods that need access the previous data as input. | ||
The query engine passes the raw arguments to all functions, and the functions have to compile the arguments themselves when they are dynamic. For example: | ||
```ts | ||
const options = { | ||
operators: { | ||
notEqual: '<>' | ||
}, | ||
functions: { | ||
notEqual: (a: JSONQuery, b: JSONQuery) => { | ||
const aCompiled = compile(a) | ||
const bCompiled = compile(b) | ||
return (data: unknown) => { | ||
const aEvaluated = aCompiled(data) | ||
const bEvaluated = bCompiled(data) | ||
return aEvaluated !== bEvaluated | ||
} | ||
} | ||
} | ||
} | ||
const data = { x: 2, y: 3} | ||
const result = jsonquery(data, '(.x + .y) <> 6', options) // true | ||
``` | ||
To automatically compile and evaluate the arguments of the function, the helper function `buildFunction` can be used: | ||
```ts | ||
import { jsonquery, buildFunction } from '@jsonquerylang/jsonquery' | ||
const options = { | ||
operators: { | ||
notEqual: '<>' | ||
}, | ||
functions: { | ||
notEqual: buildFunction((a: number, b: number) => a !== b) | ||
} | ||
} | ||
const data = { x: 2, y: 3} | ||
const result = jsonquery(data, '(.x + .y) <> 6', options) // true | ||
``` | ||
### error handling | ||
@@ -373,19 +602,4 @@ | ||
The JSON Query language has some gotchas. What can be confusing at first is to understand how data is piped through the query. A traditional function call is for example `max(myValues)`, so you may expect to have to write this in JSON Query like `["max", "myValues"]`. However, JSON Query has a functional approach where we create a pipeline like: `data -> max -> result`. So, you will have to write a pipe first getting this property and then calling abs: `[["get", "myValues"], ["max"]]"`. | ||
The JSON Query language has some gotchas. What can be confusing at first is to understand how data is piped through the query. A traditional function call is for example `max(myValues)`, so you may expect to have to write this in JSON Query like `["max", "myValues"]`. However, JSON Query has a functional approach where we create a pipeline like: `data -> max -> result`. So, you will have to write a pipe which first gets this property and next calls the function max: `.myValues | max()`. | ||
It's easy to forget to specify a property getter and instead, just specify a string with the property name, like: | ||
```js | ||
const data = [ | ||
{"name": "Chris", "age": 23, "city": "New York"}, | ||
{"name": "Emily", "age": 19, "city": "Atlanta"}, | ||
{"name": "Joe", "age": 16, "city": "New York"} | ||
] | ||
const result = jsonquery(data, ["filter", ["eq", "city", "New York"]]) | ||
// result: empty array | ||
// expecteed: an array with two items | ||
// solution: specify "city" as a getter like ["filter", ["eq", ["get" "city"], "New York"]] | ||
``` | ||
## Development | ||
@@ -395,3 +609,3 @@ | ||
``` | ||
```text | ||
npm run test | ||
@@ -401,2 +615,3 @@ npm run test-ci | ||
npm run format | ||
npm run coverage | ||
npm run build | ||
@@ -425,3 +640,3 @@ npm run build-and-test | ||
4. **Parsable** | ||
When a query language is simple to parse, it is easy to write integrations and adapters for it. For example, it is possible to write a visual user interface to write queries, and the query language can be implemented in various environments (frontend, backend). | ||
@@ -428,0 +643,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
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
76231
23
434
630
7