@mgarlik/json-filter 0.1.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/filterDocuments.d.ts +1 -1
- package/dist/filterDocuments.js +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -4
- package/dist/matchesFilter.d.ts +3 -24
- package/dist/matchesFilter.js +180 -227
- package/dist/typecheck.js +1 -1
- package/dist/types.d.ts +9 -1
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - Pokud je `documents` jeden dokument, vrati dokument nebo `null`.
|
|
6
6
|
* - `null`/`undefined` na vstupu vraci beze zmeny.
|
|
7
7
|
*/
|
|
8
|
-
import type { contextType, filterType, targetType } from "./types";
|
|
8
|
+
import type { contextType, filterType, targetType } from "./types.js";
|
|
9
9
|
declare function filterDocuments<TTarget extends targetType, TContext extends contextType = contextType>(documents: TTarget[], filter?: filterType<TTarget, TContext> | boolean | null, context?: TContext): TTarget[];
|
|
10
10
|
declare function filterDocuments<TTarget extends targetType, TContext extends contextType = contextType>(documents: TTarget | null | undefined, filter?: filterType<TTarget, TContext> | boolean | null, context?: TContext): TTarget | null | undefined;
|
|
11
11
|
export { filterDocuments };
|
package/dist/filterDocuments.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - Pokud je `documents` jeden dokument, vrati dokument nebo `null`.
|
|
6
6
|
* - `null`/`undefined` na vstupu vraci beze zmeny.
|
|
7
7
|
*/
|
|
8
|
-
import { matchesFilter } from "./matchesFilter";
|
|
8
|
+
import { matchesFilter } from "./matchesFilter.js";
|
|
9
9
|
function filterDocuments(documents, filter, context = {}) {
|
|
10
10
|
if (documents == null)
|
|
11
11
|
return documents;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { matchesFilter as matchJson, defineFilter as defineJsonFilter, filterArray as filterJsonArray, filterObject as filterJsonObjectMap, evaluateValue as evaluateJsonExpression, } from "./matchesFilter";
|
|
2
|
-
export { filterDocuments as filterJsonDocuments, } from "./filterDocuments";
|
|
3
|
-
export { getValueByDotPath as getJsonPathValue, } from "./dotPath";
|
|
4
|
-
export { default as transformJsonDocument, } from "./recalculateWithDocument";
|
|
5
|
-
export type { scalarType, scalarType as JsonScalar, contextType, contextType as JsonFilterContext, targetType, targetType as JsonDocument, conditionType, conditionType as JsonCondition, conditionObjectType, conditionObjectType as JsonConditionObject, filterType, filterType as JsonFilter, documentsArrayType, documentsArrayType as JsonDocumentsArray, documentsMapType, documentsMapType as JsonDocumentsMap, valueExpressionType, valueExpressionType as JsonExpression, } from "./types";
|
|
1
|
+
export { matchesFilter as matchJson, defineFilter as defineJsonFilter, filterArray as filterJsonArray, filterObject as filterJsonObjectMap, evaluateValue as evaluateJsonExpression, } from "./matchesFilter.js";
|
|
2
|
+
export { filterDocuments as filterJsonDocuments, } from "./filterDocuments.js";
|
|
3
|
+
export { getValueByDotPath as getJsonPathValue, } from "./dotPath.js";
|
|
4
|
+
export { default as transformJsonDocument, } from "./recalculateWithDocument.js";
|
|
5
|
+
export type { scalarType, scalarType as JsonScalar, contextType, contextType as JsonFilterContext, targetType, targetType as JsonDocument, conditionType, conditionType as JsonCondition, conditionObjectType, conditionObjectType as JsonConditionObject, filterType, filterType as JsonFilter, documentsArrayType, documentsArrayType as JsonDocumentsArray, documentsMapType, documentsMapType as JsonDocumentsMap, valueExpressionType, valueExpressionType as JsonExpression, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -8,13 +8,13 @@ filterArray as filterJsonArray,
|
|
|
8
8
|
// filterObject,
|
|
9
9
|
filterObject as filterJsonObjectMap,
|
|
10
10
|
// evaluateValue,
|
|
11
|
-
evaluateValue as evaluateJsonExpression, } from "./matchesFilter";
|
|
11
|
+
evaluateValue as evaluateJsonExpression, } from "./matchesFilter.js";
|
|
12
12
|
export {
|
|
13
13
|
// filterDocuments,
|
|
14
|
-
filterDocuments as filterJsonDocuments, } from "./filterDocuments";
|
|
14
|
+
filterDocuments as filterJsonDocuments, } from "./filterDocuments.js";
|
|
15
15
|
export {
|
|
16
16
|
// getValueByDotPath,
|
|
17
|
-
getValueByDotPath as getJsonPathValue, } from "./dotPath";
|
|
17
|
+
getValueByDotPath as getJsonPathValue, } from "./dotPath.js";
|
|
18
18
|
export {
|
|
19
19
|
// default as recalculateObjectWithDocument,
|
|
20
|
-
default as transformJsonDocument, } from "./recalculateWithDocument";
|
|
20
|
+
default as transformJsonDocument, } from "./recalculateWithDocument.js";
|
package/dist/matchesFilter.d.ts
CHANGED
|
@@ -1,28 +1,7 @@
|
|
|
1
|
-
import type { contextType, documentsArrayType, documentsMapType, filterType, targetType, valueExpressionType } from "./types";
|
|
2
|
-
|
|
3
|
-
* Vyhodnoti, jestli dokument odpovida zadanemu filtru.
|
|
4
|
-
*
|
|
5
|
-
* @param target Dokument, nad kterym se filtr vyhodnocuje.
|
|
6
|
-
* @param filter Filtr nebo boolean hodnota. `null`/`undefined` znamena "vse projde".
|
|
7
|
-
* @param context Volitelny kontext pro reference jako `$ctx.*`.
|
|
8
|
-
* @returns `true`, pokud dokument splnuje filtr.
|
|
9
|
-
*/
|
|
10
|
-
export declare function matchesFilter<TTarget extends targetType, TContext extends contextType = contextType>(target: TTarget, filter: filterType<TTarget, TContext> | null | undefined | boolean, context?: TContext): boolean;
|
|
11
|
-
/**
|
|
12
|
-
* Pomocna funkce pro definici filtru se zachovanim plneho typovani.
|
|
13
|
-
*
|
|
14
|
-
* Hodí se hlavne kdyz filtr ulozis do promenne pred predanim do `matchesFilter`,
|
|
15
|
-
* aby VS Code spravne naseptaval cesty (`$this.*`, `$ctx.*`) a operatory.
|
|
16
|
-
*/
|
|
1
|
+
import type { contextType, documentsArrayType, documentsMapType, filterType, targetType, valueExpressionType } from "./types.js";
|
|
2
|
+
export declare function evaluateValue(expr: valueExpressionType, context?: contextType, target?: targetType): unknown;
|
|
17
3
|
export declare function defineFilter<TTarget extends targetType, TContext extends contextType = contextType>(filter: filterType<TTarget, TContext>): filterType<TTarget, TContext>;
|
|
18
|
-
|
|
19
|
-
* Vyhodnoti hodnotovy vyraz (napr. `$plus`, `$concat`, `$val`) v kontextu dat.
|
|
20
|
-
*
|
|
21
|
-
* @param expr Vyraz k vyhodnoceni.
|
|
22
|
-
* @param context Kontext pro reference jako `$ctx.*`.
|
|
23
|
-
* @returns Vysledna hodnota vyrazu.
|
|
24
|
-
*/
|
|
25
|
-
export declare function evaluateValue(expr: valueExpressionType, context?: contextType): unknown;
|
|
4
|
+
export declare function matchesFilter<TTarget extends targetType, TContext extends contextType = contextType>(target: TTarget, filter: filterType<TTarget, TContext> | null | undefined | boolean, context?: TContext): boolean;
|
|
26
5
|
/**
|
|
27
6
|
* Vyfiltruje pole JSON dokumentu podle filtru.
|
|
28
7
|
*/
|
package/dist/matchesFilter.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolves a dot-notation path on an object.
|
|
3
|
-
* "brand.name" -> obj.brand.name
|
|
4
|
-
*/
|
|
5
1
|
function resolvePath(obj, path) {
|
|
6
2
|
return path.split(".").reduce((cur, key) => {
|
|
7
3
|
if (cur == null || typeof cur !== "object")
|
|
@@ -9,130 +5,71 @@ function resolvePath(obj, path) {
|
|
|
9
5
|
return cur[key];
|
|
10
6
|
}, obj);
|
|
11
7
|
}
|
|
12
|
-
/**
|
|
13
|
-
* Resolves a context reference like "$profile.type"
|
|
14
|
-
* from the provided context object.
|
|
15
|
-
*/
|
|
16
8
|
function resolveContextRef(ref, context) {
|
|
17
9
|
if (typeof ref !== "string" || !ref.startsWith("$"))
|
|
18
10
|
return ref;
|
|
19
|
-
if (ref.startsWith("$ctx."))
|
|
11
|
+
if (ref.startsWith("$ctx."))
|
|
20
12
|
return resolvePath(context, ref.slice(5));
|
|
21
|
-
|
|
22
|
-
const path = ref.slice(1);
|
|
23
|
-
return resolvePath(context, path);
|
|
13
|
+
return resolvePath(context, ref.slice(1));
|
|
24
14
|
}
|
|
25
15
|
function resolveTargetRef(ref, target) {
|
|
26
16
|
if (typeof ref !== "string" || !ref.startsWith("$this."))
|
|
27
17
|
return ref;
|
|
28
18
|
return resolvePath(target, ref.slice(6));
|
|
29
19
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return resolveTargetRef(val, target);
|
|
36
|
-
}
|
|
37
|
-
if (typeof val === "string" && val.startsWith("$")) {
|
|
38
|
-
return resolveContextRef(val, context);
|
|
20
|
+
function compareResolvedValues(fieldVal, resolved) {
|
|
21
|
+
if (Array.isArray(fieldVal)) {
|
|
22
|
+
if (Array.isArray(resolved))
|
|
23
|
+
return resolved.some((item) => fieldVal.includes(item));
|
|
24
|
+
return fieldVal.includes(resolved);
|
|
39
25
|
}
|
|
40
|
-
|
|
26
|
+
if (Array.isArray(resolved))
|
|
27
|
+
return resolved.includes(fieldVal);
|
|
28
|
+
return fieldVal === resolved;
|
|
41
29
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*
|
|
45
|
-
* @param target Dokument, nad kterym se filtr vyhodnocuje.
|
|
46
|
-
* @param filter Filtr nebo boolean hodnota. `null`/`undefined` znamena "vse projde".
|
|
47
|
-
* @param context Volitelny kontext pro reference jako `$ctx.*`.
|
|
48
|
-
* @returns `true`, pokud dokument splnuje filtr.
|
|
49
|
-
*/
|
|
50
|
-
export function matchesFilter(target, filter, context = {}) {
|
|
51
|
-
if (filter === null || filter === undefined)
|
|
52
|
-
return true;
|
|
53
|
-
if (typeof filter !== "object")
|
|
54
|
-
return Boolean(filter);
|
|
55
|
-
const keys = Object.keys(filter);
|
|
56
|
-
for (const key of keys) {
|
|
57
|
-
const val = filter[key];
|
|
58
|
-
if (key === "$and") {
|
|
59
|
-
if (!evalAnd(target, val, context))
|
|
60
|
-
return false;
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if (key === "$nand") {
|
|
64
|
-
if (evalAnd(target, val, context))
|
|
65
|
-
return false;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (key === "$or") {
|
|
69
|
-
if (!evalOr(target, val, context))
|
|
70
|
-
return false;
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (key === "$nor") {
|
|
74
|
-
if (evalOr(target, val, context))
|
|
75
|
-
return false;
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
if (key === "$not") {
|
|
79
|
-
if (matchesFilter(target, val, context))
|
|
80
|
-
return false;
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
// Value reference as field: "$ctx.profile.type" or legacy "$profile.type"
|
|
84
|
-
if (key.startsWith("$ctx.") || (key.startsWith("$") && !key.startsWith("$this."))) {
|
|
85
|
-
const fieldVal = resolveContextRef(key, context);
|
|
86
|
-
if (!evalCondition(fieldVal, val, target, context))
|
|
87
|
-
return false;
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (key.startsWith("$this.")) {
|
|
91
|
-
const fieldVal = resolveTargetRef(key, target);
|
|
92
|
-
if (!evalCondition(fieldVal, val, target, context))
|
|
93
|
-
return false;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
// Optional field (suffix ?)
|
|
97
|
-
const optional = key.endsWith("?");
|
|
98
|
-
const cleanKey = optional ? key.slice(0, -1) : key;
|
|
99
|
-
const fieldVal = resolvePath(target, cleanKey);
|
|
100
|
-
if (optional && fieldVal === undefined)
|
|
101
|
-
continue;
|
|
102
|
-
if (!evalCondition(fieldVal, val, target, context))
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
return true;
|
|
30
|
+
function getCurrentDate() {
|
|
31
|
+
return new Date().toISOString().slice(0, 10);
|
|
106
32
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
*
|
|
110
|
-
* Hodí se hlavne kdyz filtr ulozis do promenne pred predanim do `matchesFilter`,
|
|
111
|
-
* aby VS Code spravne naseptaval cesty (`$this.*`, `$ctx.*`) a operatory.
|
|
112
|
-
*/
|
|
113
|
-
export function defineFilter(filter) {
|
|
114
|
-
return filter;
|
|
33
|
+
function getCurrentTime() {
|
|
34
|
+
return new Date().toTimeString().slice(0, 8);
|
|
115
35
|
}
|
|
116
|
-
function
|
|
117
|
-
return
|
|
36
|
+
function getCurrentDatetime() {
|
|
37
|
+
return new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
118
38
|
}
|
|
119
|
-
function
|
|
120
|
-
|
|
39
|
+
function evalFirst(arr, context, target) {
|
|
40
|
+
if (!Array.isArray(arr))
|
|
41
|
+
return undefined;
|
|
42
|
+
for (const item of arr) {
|
|
43
|
+
const result = evaluateValue(item, context, target);
|
|
44
|
+
if (result !== undefined)
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
function evalRule(rule, context, target) {
|
|
50
|
+
if (typeof rule !== "object" || rule === null)
|
|
51
|
+
return Boolean(rule);
|
|
52
|
+
const keys = Object.keys(rule);
|
|
53
|
+
if (keys.length === 1 && keys[0].startsWith("$") && Array.isArray(rule[keys[0]])) {
|
|
54
|
+
const op = keys[0];
|
|
55
|
+
const arg = rule[op];
|
|
56
|
+
if (arg.length === 2) {
|
|
57
|
+
return evalOperator(op, undefined, arg, target, context);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return matchesFilter(target, rule, context);
|
|
121
61
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Evaluates a single field value against its condition.
|
|
124
|
-
* Condition can be a primitive (implicit $eq) or an operator object.
|
|
125
|
-
*/
|
|
126
62
|
function evalCondition(fieldVal, condition, target, context) {
|
|
127
63
|
if (condition === null || typeof condition !== "object") {
|
|
128
|
-
|
|
129
|
-
return fieldVal.includes(condition);
|
|
130
|
-
return fieldVal === condition;
|
|
64
|
+
return compareResolvedValues(fieldVal, evaluateValue(condition, context, target));
|
|
131
65
|
}
|
|
132
66
|
if (Array.isArray(condition)) {
|
|
133
|
-
return fieldVal
|
|
67
|
+
return compareResolvedValues(fieldVal, evaluateValue(condition, context, target));
|
|
134
68
|
}
|
|
135
69
|
const keys = Object.keys(condition);
|
|
70
|
+
if (keys.length === 1 && keys[0] === "$source") {
|
|
71
|
+
throw new Error("Unresolved $source in filter condition. Resolve $source before calling json-filter.");
|
|
72
|
+
}
|
|
136
73
|
for (const op of keys) {
|
|
137
74
|
const arg = condition[op];
|
|
138
75
|
if (!evalOperator(op, fieldVal, arg, target, context))
|
|
@@ -140,114 +77,112 @@ function evalCondition(fieldVal, condition, target, context) {
|
|
|
140
77
|
}
|
|
141
78
|
return true;
|
|
142
79
|
}
|
|
143
|
-
/**
|
|
144
|
-
* Evaluates a single operator against a field value.
|
|
145
|
-
*/
|
|
146
80
|
function evalOperator(op, fieldVal, arg, target, context) {
|
|
147
81
|
switch (op) {
|
|
148
82
|
case "$eq": {
|
|
149
83
|
if (Array.isArray(arg) && arg.length === 2) {
|
|
150
|
-
const left =
|
|
151
|
-
const right =
|
|
84
|
+
const left = evaluateValue(arg[0], context, target);
|
|
85
|
+
const right = evaluateValue(arg[1], context, target);
|
|
152
86
|
return left === right;
|
|
153
87
|
}
|
|
154
|
-
|
|
155
|
-
if (Array.isArray(fieldVal))
|
|
156
|
-
return fieldVal.includes(resolved);
|
|
157
|
-
return fieldVal === resolved;
|
|
158
|
-
}
|
|
159
|
-
case "$neq": {
|
|
160
|
-
const resolved = resolveValue(arg, target, context);
|
|
161
|
-
if (Array.isArray(fieldVal))
|
|
162
|
-
return !fieldVal.includes(resolved);
|
|
163
|
-
return fieldVal !== resolved;
|
|
88
|
+
return compareResolvedValues(fieldVal, evaluateValue(arg, context, target));
|
|
164
89
|
}
|
|
90
|
+
case "$neq":
|
|
91
|
+
return !compareResolvedValues(fieldVal, evaluateValue(arg, context, target));
|
|
165
92
|
case "$gt":
|
|
166
|
-
return fieldVal >
|
|
93
|
+
return fieldVal > evaluateValue(arg, context, target);
|
|
167
94
|
case "$gte":
|
|
168
|
-
return fieldVal >=
|
|
95
|
+
return fieldVal >= evaluateValue(arg, context, target);
|
|
169
96
|
case "$lt":
|
|
170
|
-
return fieldVal <
|
|
97
|
+
return fieldVal < evaluateValue(arg, context, target);
|
|
171
98
|
case "$lte":
|
|
172
|
-
return fieldVal <=
|
|
99
|
+
return fieldVal <= evaluateValue(arg, context, target);
|
|
173
100
|
case "$between": {
|
|
174
101
|
if (!Array.isArray(arg) || arg.length !== 2) {
|
|
175
102
|
throw new Error("$between requires an array of [min, max]");
|
|
176
103
|
}
|
|
177
|
-
return fieldVal >= arg[0]
|
|
178
|
-
&& fieldVal <= arg[1];
|
|
104
|
+
return fieldVal >= evaluateValue(arg[0], context, target)
|
|
105
|
+
&& fieldVal <= evaluateValue(arg[1], context, target);
|
|
179
106
|
}
|
|
180
|
-
case "$in":
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
107
|
+
case "$in": {
|
|
108
|
+
const resolved = evaluateValue(arg, context, target);
|
|
109
|
+
if (!Array.isArray(resolved))
|
|
110
|
+
return compareResolvedValues(fieldVal, resolved);
|
|
111
|
+
return compareResolvedValues(fieldVal, resolved);
|
|
112
|
+
}
|
|
113
|
+
case "$nin": {
|
|
114
|
+
const resolved = evaluateValue(arg, context, target);
|
|
115
|
+
if (!Array.isArray(resolved))
|
|
116
|
+
return !compareResolvedValues(fieldVal, resolved);
|
|
117
|
+
return !compareResolvedValues(fieldVal, resolved);
|
|
118
|
+
}
|
|
119
|
+
case "$ein": {
|
|
120
|
+
const resolved = evaluateValue(arg, context, target);
|
|
121
|
+
if (!Array.isArray(fieldVal) || !Array.isArray(resolved))
|
|
186
122
|
return false;
|
|
187
|
-
return fieldVal.every(v =>
|
|
188
|
-
|
|
189
|
-
|
|
123
|
+
return fieldVal.every((v) => resolved.includes(v));
|
|
124
|
+
}
|
|
125
|
+
case "$nein": {
|
|
126
|
+
const resolved = evaluateValue(arg, context, target);
|
|
127
|
+
if (!Array.isArray(fieldVal) || !Array.isArray(resolved))
|
|
190
128
|
return false;
|
|
191
|
-
return fieldVal.some(v => !
|
|
129
|
+
return fieldVal.some((v) => !resolved.includes(v));
|
|
130
|
+
}
|
|
192
131
|
case "$has":
|
|
193
|
-
return typeof fieldVal === "string"
|
|
194
|
-
&& fieldVal.includes(String(resolveValue(arg, target, context)));
|
|
132
|
+
return typeof fieldVal === "string" && fieldVal.includes(String(evaluateValue(arg, context, target)));
|
|
195
133
|
case "$nhas":
|
|
196
|
-
return typeof fieldVal === "string"
|
|
197
|
-
&& !fieldVal.includes(String(resolveValue(arg, target, context)));
|
|
134
|
+
return typeof fieldVal === "string" && !fieldVal.includes(String(evaluateValue(arg, context, target)));
|
|
198
135
|
case "$ihas": {
|
|
199
|
-
const needle = String(
|
|
136
|
+
const needle = String(evaluateValue(arg, context, target)).toLowerCase();
|
|
200
137
|
return typeof fieldVal === "string" && fieldVal.toLowerCase().includes(needle);
|
|
201
138
|
}
|
|
202
139
|
case "$nihas": {
|
|
203
|
-
const needle = String(
|
|
140
|
+
const needle = String(evaluateValue(arg, context, target)).toLowerCase();
|
|
204
141
|
return typeof fieldVal === "string" && !fieldVal.toLowerCase().includes(needle);
|
|
205
142
|
}
|
|
206
143
|
case "$sw":
|
|
207
|
-
return typeof fieldVal === "string" && fieldVal.startsWith(String(arg));
|
|
144
|
+
return typeof fieldVal === "string" && fieldVal.startsWith(String(evaluateValue(arg, context, target)));
|
|
208
145
|
case "$nsw":
|
|
209
|
-
return typeof fieldVal === "string" && !fieldVal.startsWith(String(arg));
|
|
146
|
+
return typeof fieldVal === "string" && !fieldVal.startsWith(String(evaluateValue(arg, context, target)));
|
|
210
147
|
case "$ew":
|
|
211
|
-
return typeof fieldVal === "string" && fieldVal.endsWith(String(arg));
|
|
148
|
+
return typeof fieldVal === "string" && fieldVal.endsWith(String(evaluateValue(arg, context, target)));
|
|
212
149
|
case "$new":
|
|
213
|
-
return typeof fieldVal === "string" && !fieldVal.endsWith(String(arg));
|
|
150
|
+
return typeof fieldVal === "string" && !fieldVal.endsWith(String(evaluateValue(arg, context, target)));
|
|
214
151
|
case "$regex":
|
|
215
|
-
return new RegExp(String(arg)).test(String(fieldVal));
|
|
152
|
+
return new RegExp(String(evaluateValue(arg, context, target))).test(String(fieldVal));
|
|
216
153
|
case "$iregex":
|
|
217
|
-
return new RegExp(String(arg), "i").test(String(fieldVal));
|
|
154
|
+
return new RegExp(String(evaluateValue(arg, context, target)), "i").test(String(fieldVal));
|
|
218
155
|
case "$exists":
|
|
219
156
|
return arg ? fieldVal !== undefined : fieldVal === undefined;
|
|
220
157
|
case "$and":
|
|
221
|
-
return Array.isArray(arg) && arg.every(cond => evalCondition(fieldVal, cond, target, context));
|
|
158
|
+
return Array.isArray(arg) && arg.every((cond) => evalCondition(fieldVal, cond, target, context));
|
|
222
159
|
case "$nand":
|
|
223
|
-
return Array.isArray(arg) && !arg.every(cond => evalCondition(fieldVal, cond, target, context));
|
|
160
|
+
return Array.isArray(arg) && !arg.every((cond) => evalCondition(fieldVal, cond, target, context));
|
|
224
161
|
case "$or":
|
|
225
|
-
return Array.isArray(arg) && arg.some(cond => evalCondition(fieldVal, cond, target, context));
|
|
162
|
+
return Array.isArray(arg) && arg.some((cond) => evalCondition(fieldVal, cond, target, context));
|
|
226
163
|
case "$nor":
|
|
227
|
-
return Array.isArray(arg) && !arg.some(cond => evalCondition(fieldVal, cond, target, context));
|
|
164
|
+
return Array.isArray(arg) && !arg.some((cond) => evalCondition(fieldVal, cond, target, context));
|
|
228
165
|
case "$not":
|
|
229
166
|
return !evalCondition(fieldVal, arg, target, context);
|
|
230
167
|
case "$some":
|
|
231
168
|
if (!Array.isArray(fieldVal))
|
|
232
169
|
return false;
|
|
233
|
-
return fieldVal.some(item => matchesFilter((item ?? {}), (arg ?? {}), context));
|
|
170
|
+
return fieldVal.some((item) => matchesFilter((item ?? {}), (arg ?? {}), context));
|
|
234
171
|
case "$nsome":
|
|
235
172
|
if (!Array.isArray(fieldVal))
|
|
236
173
|
return true;
|
|
237
|
-
return !fieldVal.some(item => matchesFilter((item ?? {}), (arg ?? {}), context));
|
|
174
|
+
return !fieldVal.some((item) => matchesFilter((item ?? {}), (arg ?? {}), context));
|
|
238
175
|
case "$every":
|
|
239
176
|
if (!Array.isArray(fieldVal))
|
|
240
177
|
return false;
|
|
241
|
-
return fieldVal.every(item => matchesFilter((item ?? {}), (arg ?? {}), context));
|
|
178
|
+
return fieldVal.every((item) => matchesFilter((item ?? {}), (arg ?? {}), context));
|
|
242
179
|
case "$nevery":
|
|
243
180
|
if (!Array.isArray(fieldVal))
|
|
244
181
|
return false;
|
|
245
|
-
return !fieldVal.every(item => matchesFilter((item ?? {}), (arg ?? {}), context));
|
|
182
|
+
return !fieldVal.every((item) => matchesFilter((item ?? {}), (arg ?? {}), context));
|
|
246
183
|
case "$length":
|
|
247
184
|
case "$size": {
|
|
248
|
-
const len = Array.isArray(fieldVal)
|
|
249
|
-
? fieldVal.length
|
|
250
|
-
: (typeof fieldVal === "string" ? fieldVal.length : undefined);
|
|
185
|
+
const len = Array.isArray(fieldVal) ? fieldVal.length : (typeof fieldVal === "string" ? fieldVal.length : undefined);
|
|
251
186
|
if (len === undefined)
|
|
252
187
|
return false;
|
|
253
188
|
return evalCondition(len, arg, target, context);
|
|
@@ -255,7 +190,7 @@ function evalOperator(op, fieldVal, arg, target, context) {
|
|
|
255
190
|
case "$sum": {
|
|
256
191
|
if (!Array.isArray(fieldVal))
|
|
257
192
|
return false;
|
|
258
|
-
const sum = fieldVal.reduce((
|
|
193
|
+
const sum = fieldVal.reduce((acc, item) => (Number(acc) || 0) + (Number(item) || 0), 0);
|
|
259
194
|
return evalCondition(sum, arg, target, context);
|
|
260
195
|
}
|
|
261
196
|
case "$date":
|
|
@@ -270,121 +205,139 @@ function evalOperator(op, fieldVal, arg, target, context) {
|
|
|
270
205
|
throw new Error(`Unknown operator: ${op}`);
|
|
271
206
|
}
|
|
272
207
|
}
|
|
273
|
-
|
|
274
|
-
* Vyhodnoti hodnotovy vyraz (napr. `$plus`, `$concat`, `$val`) v kontextu dat.
|
|
275
|
-
*
|
|
276
|
-
* @param expr Vyraz k vyhodnoceni.
|
|
277
|
-
* @param context Kontext pro reference jako `$ctx.*`.
|
|
278
|
-
* @returns Vysledna hodnota vyrazu.
|
|
279
|
-
*/
|
|
280
|
-
export function evaluateValue(expr, context = {}) {
|
|
208
|
+
export function evaluateValue(expr, context = {}, target = {}) {
|
|
281
209
|
if (expr === null || expr === undefined)
|
|
282
210
|
return expr;
|
|
283
|
-
if (typeof expr
|
|
284
|
-
return
|
|
211
|
+
if (typeof expr === "string") {
|
|
212
|
+
return resolveContextRef(resolveTargetRef(expr, target), context);
|
|
285
213
|
}
|
|
214
|
+
if (typeof expr !== "object")
|
|
215
|
+
return expr;
|
|
286
216
|
if (Array.isArray(expr)) {
|
|
287
|
-
return expr.map(
|
|
217
|
+
return expr.map((item) => evaluateValue(item, context, target));
|
|
288
218
|
}
|
|
289
219
|
const obj = expr;
|
|
290
220
|
const keys = Object.keys(obj);
|
|
221
|
+
if (keys.length === 1 && keys[0] === "$source") {
|
|
222
|
+
throw new Error("Unresolved $source expression. Resolve $source before calling json-filter.");
|
|
223
|
+
}
|
|
291
224
|
if (keys.includes("$first")) {
|
|
292
|
-
return evalFirst(obj["$first"], context);
|
|
225
|
+
return evalFirst(obj["$first"], context, target);
|
|
293
226
|
}
|
|
294
227
|
if (keys.includes("$val") || keys.includes("$value")) {
|
|
295
228
|
const val = obj["$val"] !== undefined ? obj["$val"] : obj["$value"];
|
|
296
229
|
const rule = obj["$rule"];
|
|
297
|
-
if (rule !== undefined && !evalRule(rule, context))
|
|
230
|
+
if (rule !== undefined && !evalRule(rule, context, target))
|
|
298
231
|
return undefined;
|
|
299
|
-
return evaluateValue(val, context);
|
|
232
|
+
return evaluateValue(val, context, target);
|
|
300
233
|
}
|
|
301
234
|
if (keys.includes("$concat")) {
|
|
302
235
|
const list = obj["$concat"] || [];
|
|
303
|
-
return list.map(
|
|
236
|
+
return list.map((item) => String(evaluateValue(item, context, target))).join("");
|
|
304
237
|
}
|
|
305
238
|
if (keys.includes("$plus")) {
|
|
306
239
|
const list = obj["$plus"] || [];
|
|
307
|
-
return list.reduce((acc,
|
|
240
|
+
return list.reduce((acc, item) => Number(acc) + Number(evaluateValue(item, context, target)), 0);
|
|
308
241
|
}
|
|
309
242
|
if (keys.includes("$minus")) {
|
|
310
|
-
const vals = (obj["$minus"] || []).map(
|
|
311
|
-
return vals.slice(1).reduce((acc,
|
|
243
|
+
const vals = (obj["$minus"] || []).map((item) => Number(evaluateValue(item, context, target)));
|
|
244
|
+
return vals.slice(1).reduce((acc, item) => acc - item, vals[0]);
|
|
312
245
|
}
|
|
313
246
|
if (keys.includes("$times")) {
|
|
314
247
|
const list = obj["$times"] || [];
|
|
315
|
-
return list.reduce((acc,
|
|
248
|
+
return list.reduce((acc, item) => Number(acc) * Number(evaluateValue(item, context, target)), 1);
|
|
316
249
|
}
|
|
317
|
-
if (keys.includes("$div")) {
|
|
318
|
-
const
|
|
319
|
-
|
|
250
|
+
if (keys.includes("$div") || keys.includes("$divide")) {
|
|
251
|
+
const list = (obj["$div"] || obj["$divide"]) || [];
|
|
252
|
+
const vals = list.map((item) => Number(evaluateValue(item, context, target)));
|
|
253
|
+
return vals.slice(1).reduce((acc, item) => acc / item, vals[0]);
|
|
320
254
|
}
|
|
321
255
|
if (keys.includes("$min")) {
|
|
322
|
-
const vals = (obj["$min"] || []).map(
|
|
256
|
+
const vals = (obj["$min"] || []).map((item) => Number(evaluateValue(item, context, target)));
|
|
323
257
|
return Math.min(...vals);
|
|
324
258
|
}
|
|
325
259
|
if (keys.includes("$max")) {
|
|
326
|
-
const vals = (obj["$max"] || []).map(
|
|
260
|
+
const vals = (obj["$max"] || []).map((item) => Number(evaluateValue(item, context, target)));
|
|
327
261
|
return Math.max(...vals);
|
|
328
262
|
}
|
|
329
263
|
if (keys.includes("$avg")) {
|
|
330
|
-
const vals = (obj["$avg"] || []).map(
|
|
331
|
-
return vals.reduce((
|
|
264
|
+
const vals = (obj["$avg"] || []).map((item) => Number(evaluateValue(item, context, target)));
|
|
265
|
+
return vals.reduce((acc, item) => acc + item, 0) / vals.length;
|
|
332
266
|
}
|
|
333
267
|
if (keys.includes("$median")) {
|
|
334
|
-
const sorted = [...(obj["$median"] || []).map(
|
|
335
|
-
.sort((a, b) => a - b);
|
|
268
|
+
const sorted = [...(obj["$median"] || []).map((item) => Number(evaluateValue(item, context, target)))].sort((a, b) => a - b);
|
|
336
269
|
const mid = Math.floor(sorted.length / 2);
|
|
337
|
-
return sorted.length % 2 !== 0
|
|
338
|
-
? sorted[mid]
|
|
339
|
-
: (sorted[mid - 1] + sorted[mid]) / 2;
|
|
270
|
+
return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
340
271
|
}
|
|
341
272
|
if (keys.includes("$last")) {
|
|
342
273
|
const arr = obj["$last"];
|
|
343
|
-
return Array.isArray(arr) ? evaluateValue(arr[arr.length - 1], context) : evaluateValue(arr, context);
|
|
274
|
+
return Array.isArray(arr) ? evaluateValue(arr[arr.length - 1], context, target) : evaluateValue(arr, context, target);
|
|
344
275
|
}
|
|
345
|
-
return
|
|
276
|
+
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, evaluateValue(value, context, target)]));
|
|
346
277
|
}
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
return undefined;
|
|
350
|
-
for (const item of arr) {
|
|
351
|
-
if (typeof item !== "object" || item === null)
|
|
352
|
-
return item;
|
|
353
|
-
const result = evaluateValue(item, context);
|
|
354
|
-
if (result !== undefined)
|
|
355
|
-
return result;
|
|
356
|
-
}
|
|
357
|
-
return undefined;
|
|
278
|
+
export function defineFilter(filter) {
|
|
279
|
+
return filter;
|
|
358
280
|
}
|
|
359
|
-
function
|
|
360
|
-
if (
|
|
361
|
-
return
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
281
|
+
export function matchesFilter(target, filter, context = {}) {
|
|
282
|
+
if (filter === null || filter === undefined)
|
|
283
|
+
return true;
|
|
284
|
+
if (typeof filter !== "object")
|
|
285
|
+
return Boolean(filter);
|
|
286
|
+
const keys = Object.keys(filter);
|
|
287
|
+
for (const key of keys) {
|
|
288
|
+
const val = filter[key];
|
|
289
|
+
if (key === "$and") {
|
|
290
|
+
if (!Array.isArray(val) || !val.every((condition) => matchesFilter(target, condition, context)))
|
|
291
|
+
return false;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (key === "$nand") {
|
|
295
|
+
if (Array.isArray(val) && val.every((condition) => matchesFilter(target, condition, context)))
|
|
296
|
+
return false;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (key === "$or") {
|
|
300
|
+
if (!Array.isArray(val) || !val.some((condition) => matchesFilter(target, condition, context)))
|
|
301
|
+
return false;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (key === "$nor") {
|
|
305
|
+
if (Array.isArray(val) && val.some((condition) => matchesFilter(target, condition, context)))
|
|
306
|
+
return false;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (key === "$not") {
|
|
310
|
+
if (matchesFilter(target, val, context))
|
|
311
|
+
return false;
|
|
312
|
+
continue;
|
|
370
313
|
}
|
|
314
|
+
if (key.startsWith("$ctx.") || (key.startsWith("$") && !key.startsWith("$this."))) {
|
|
315
|
+
const fieldVal = resolveContextRef(key, context);
|
|
316
|
+
if (!evalCondition(fieldVal, val, target, context))
|
|
317
|
+
return false;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (key.startsWith("$this.")) {
|
|
321
|
+
const fieldVal = resolveTargetRef(key, target);
|
|
322
|
+
if (!evalCondition(fieldVal, val, target, context))
|
|
323
|
+
return false;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const optional = key.endsWith("?");
|
|
327
|
+
const cleanKey = optional ? key.slice(0, -1) : key;
|
|
328
|
+
const fieldVal = resolvePath(target, cleanKey);
|
|
329
|
+
if (optional && fieldVal === undefined)
|
|
330
|
+
continue;
|
|
331
|
+
if (!evalCondition(fieldVal, val, target, context))
|
|
332
|
+
return false;
|
|
371
333
|
}
|
|
372
|
-
return
|
|
373
|
-
}
|
|
374
|
-
function getCurrentDate() {
|
|
375
|
-
return new Date().toISOString().slice(0, 10);
|
|
376
|
-
}
|
|
377
|
-
function getCurrentTime() {
|
|
378
|
-
return new Date().toTimeString().slice(0, 8);
|
|
379
|
-
}
|
|
380
|
-
function getCurrentDatetime() {
|
|
381
|
-
return new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
334
|
+
return true;
|
|
382
335
|
}
|
|
383
336
|
/**
|
|
384
337
|
* Vyfiltruje pole JSON dokumentu podle filtru.
|
|
385
338
|
*/
|
|
386
339
|
export function filterArray(documents, filter, context = {}) {
|
|
387
|
-
return documents.filter(doc => matchesFilter(doc, filter, context));
|
|
340
|
+
return documents.filter((doc) => matchesFilter(doc, filter, context));
|
|
388
341
|
}
|
|
389
342
|
/**
|
|
390
343
|
* Vyfiltruje mapu JSON dokumentu (`Record<string, doc>`) podle filtru.
|
package/dist/typecheck.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -100,7 +100,13 @@ export type conditionObjectType<TValue = unknown, TTarget extends targetType = t
|
|
|
100
100
|
$size?: conditionType<number, TTarget, TContext>;
|
|
101
101
|
/** Soucet prvku pole a porovnani vysledku. */
|
|
102
102
|
$sum?: conditionType<number, TTarget, TContext>;
|
|
103
|
-
/** Porovnani s aktualnim datem (YYYY-MM-DD).
|
|
103
|
+
/** Porovnani s aktualnim datem (YYYY-MM-DD).
|
|
104
|
+
* @example
|
|
105
|
+
* // Porovná, jestli pole `dateFinish` je dříve než dnešní datum:
|
|
106
|
+
* {
|
|
107
|
+
* dateFinish: { $lte: { $date: {} } as any },
|
|
108
|
+
* }
|
|
109
|
+
*/
|
|
104
110
|
$date?: conditionType<string, TTarget, TContext>;
|
|
105
111
|
/** Porovnani s aktualnim casem (HH:mm:ss). */
|
|
106
112
|
$time?: conditionType<string, TTarget, TContext>;
|
|
@@ -127,6 +133,8 @@ export type filterType<TTarget extends targetType = targetType, TContext extends
|
|
|
127
133
|
[K in optionalFieldKeyType<TTarget>]?: conditionType<pathValueType<TTarget, K extends `${infer P}?` ? P : never>, TTarget, TContext>;
|
|
128
134
|
} & {
|
|
129
135
|
[K in contextReferenceType<TContext>]?: conditionType<any, TTarget, TContext>;
|
|
136
|
+
} & {
|
|
137
|
+
[key: string]: conditionType<any, TTarget, TContext> | undefined;
|
|
130
138
|
};
|
|
131
139
|
/** Pole JSON dokumentu. */
|
|
132
140
|
export type documentsArrayType = Array<Record<string, unknown>>;
|