@mgarlik/json-filter 0.1.0
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/README.md +38 -0
- package/dist/dotPath.d.ts +6 -0
- package/dist/dotPath.js +19 -0
- package/dist/filterDocuments.d.ts +12 -0
- package/dist/filterDocuments.js +24 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +20 -0
- package/dist/matchesFilter.d.ts +33 -0
- package/dist/matchesFilter.js +394 -0
- package/dist/recalculateWithDocument.d.ts +8 -0
- package/dist/recalculateWithDocument.js +310 -0
- package/dist/typecheck.d.ts +1 -0
- package/dist/typecheck.js +23 -0
- package/dist/types.d.ts +137 -0
- package/dist/types.js +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @mgarlik/json-filter
|
|
2
|
+
|
|
3
|
+
Shared TypeScript filter/rule engine for backend and frontend applications.
|
|
4
|
+
|
|
5
|
+
## Scripts
|
|
6
|
+
|
|
7
|
+
- `npm run build` - compile package to `dist/` and generate type declarations
|
|
8
|
+
- `npm test` - run unit tests with Vitest
|
|
9
|
+
- `npm run test:watch` - run tests in watch mode
|
|
10
|
+
- `npm run lint` - type-check source code
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { matchJson, type JsonFilter } from "@mgarlik/json-filter";
|
|
16
|
+
|
|
17
|
+
type Product = {
|
|
18
|
+
status: string;
|
|
19
|
+
price: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const filter: JsonFilter<Product> = {
|
|
23
|
+
status: { $eq: "active" },
|
|
24
|
+
price: { $gte: 100 },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const ok = matchJson({ status: "active", price: 120 }, filter);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Documentation
|
|
31
|
+
|
|
32
|
+
- [matchJson docs](docs/matchJson.md)
|
|
33
|
+
|
|
34
|
+
## Publish
|
|
35
|
+
|
|
36
|
+
1. Update version in `package.json`.
|
|
37
|
+
2. Run `npm run build`.
|
|
38
|
+
3. Publish with `npm publish`.
|
package/dist/dotPath.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vrati hodnotu z objektu podle dot-path cesty.
|
|
3
|
+
*
|
|
4
|
+
* @example getValueByDotPath({ a: { b: { c: 1 } } }, "a.b.c") => 1
|
|
5
|
+
*/
|
|
6
|
+
export function getValueByDotPath(obj, path) {
|
|
7
|
+
if (!obj || typeof obj !== "object" || typeof path !== "string") {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const keys = path.split(".");
|
|
11
|
+
let result = obj;
|
|
12
|
+
for (const key of keys) {
|
|
13
|
+
if (result === null || result === undefined) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
result = result[key];
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filtruje jeden dokument nebo pole dokumentu podle filtru.
|
|
3
|
+
*
|
|
4
|
+
* - Pokud je `documents` pole, vrati vyfiltrovane pole.
|
|
5
|
+
* - Pokud je `documents` jeden dokument, vrati dokument nebo `null`.
|
|
6
|
+
* - `null`/`undefined` na vstupu vraci beze zmeny.
|
|
7
|
+
*/
|
|
8
|
+
import type { contextType, filterType, targetType } from "./types";
|
|
9
|
+
declare function filterDocuments<TTarget extends targetType, TContext extends contextType = contextType>(documents: TTarget[], filter?: filterType<TTarget, TContext> | boolean | null, context?: TContext): TTarget[];
|
|
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
|
+
export { filterDocuments };
|
|
12
|
+
export default filterDocuments;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filtruje jeden dokument nebo pole dokumentu podle filtru.
|
|
3
|
+
*
|
|
4
|
+
* - Pokud je `documents` pole, vrati vyfiltrovane pole.
|
|
5
|
+
* - Pokud je `documents` jeden dokument, vrati dokument nebo `null`.
|
|
6
|
+
* - `null`/`undefined` na vstupu vraci beze zmeny.
|
|
7
|
+
*/
|
|
8
|
+
import { matchesFilter } from "./matchesFilter";
|
|
9
|
+
function filterDocuments(documents, filter, context = {}) {
|
|
10
|
+
if (documents == null)
|
|
11
|
+
return documents;
|
|
12
|
+
if (filter == null)
|
|
13
|
+
return Array.isArray(documents) ? [...documents] : documents;
|
|
14
|
+
if (filter === true)
|
|
15
|
+
return Array.isArray(documents) ? [...documents] : documents;
|
|
16
|
+
if (filter === false)
|
|
17
|
+
return Array.isArray(documents) ? [] : null;
|
|
18
|
+
if (Array.isArray(documents)) {
|
|
19
|
+
return documents.filter((item) => matchesFilter(item, filter, context));
|
|
20
|
+
}
|
|
21
|
+
return matchesFilter(documents, filter, context) ? documents : null;
|
|
22
|
+
}
|
|
23
|
+
export { filterDocuments };
|
|
24
|
+
export default filterDocuments;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +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";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export {
|
|
2
|
+
// matchesFilter,
|
|
3
|
+
matchesFilter as matchJson,
|
|
4
|
+
// defineFilter,
|
|
5
|
+
defineFilter as defineJsonFilter,
|
|
6
|
+
// filterArray,
|
|
7
|
+
filterArray as filterJsonArray,
|
|
8
|
+
// filterObject,
|
|
9
|
+
filterObject as filterJsonObjectMap,
|
|
10
|
+
// evaluateValue,
|
|
11
|
+
evaluateValue as evaluateJsonExpression, } from "./matchesFilter";
|
|
12
|
+
export {
|
|
13
|
+
// filterDocuments,
|
|
14
|
+
filterDocuments as filterJsonDocuments, } from "./filterDocuments";
|
|
15
|
+
export {
|
|
16
|
+
// getValueByDotPath,
|
|
17
|
+
getValueByDotPath as getJsonPathValue, } from "./dotPath";
|
|
18
|
+
export {
|
|
19
|
+
// default as recalculateObjectWithDocument,
|
|
20
|
+
default as transformJsonDocument, } from "./recalculateWithDocument";
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
*/
|
|
17
|
+
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;
|
|
26
|
+
/**
|
|
27
|
+
* Vyfiltruje pole JSON dokumentu podle filtru.
|
|
28
|
+
*/
|
|
29
|
+
export declare function filterArray(documents: documentsArrayType, filter: filterType<targetType>, context?: contextType): documentsArrayType;
|
|
30
|
+
/**
|
|
31
|
+
* Vyfiltruje mapu JSON dokumentu (`Record<string, doc>`) podle filtru.
|
|
32
|
+
*/
|
|
33
|
+
export declare function filterObject(documents: documentsMapType, filter: filterType<targetType>, context?: contextType): documentsMapType;
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a dot-notation path on an object.
|
|
3
|
+
* "brand.name" -> obj.brand.name
|
|
4
|
+
*/
|
|
5
|
+
function resolvePath(obj, path) {
|
|
6
|
+
return path.split(".").reduce((cur, key) => {
|
|
7
|
+
if (cur == null || typeof cur !== "object")
|
|
8
|
+
return undefined;
|
|
9
|
+
return cur[key];
|
|
10
|
+
}, obj);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolves a context reference like "$profile.type"
|
|
14
|
+
* from the provided context object.
|
|
15
|
+
*/
|
|
16
|
+
function resolveContextRef(ref, context) {
|
|
17
|
+
if (typeof ref !== "string" || !ref.startsWith("$"))
|
|
18
|
+
return ref;
|
|
19
|
+
if (ref.startsWith("$ctx.")) {
|
|
20
|
+
return resolvePath(context, ref.slice(5));
|
|
21
|
+
}
|
|
22
|
+
const path = ref.slice(1);
|
|
23
|
+
return resolvePath(context, path);
|
|
24
|
+
}
|
|
25
|
+
function resolveTargetRef(ref, target) {
|
|
26
|
+
if (typeof ref !== "string" || !ref.startsWith("$this."))
|
|
27
|
+
return ref;
|
|
28
|
+
return resolvePath(target, ref.slice(6));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolves a value - either a context reference or a literal.
|
|
32
|
+
*/
|
|
33
|
+
function resolveValue(val, target, context) {
|
|
34
|
+
if (typeof val === "string" && val.startsWith("$this.")) {
|
|
35
|
+
return resolveTargetRef(val, target);
|
|
36
|
+
}
|
|
37
|
+
if (typeof val === "string" && val.startsWith("$")) {
|
|
38
|
+
return resolveContextRef(val, context);
|
|
39
|
+
}
|
|
40
|
+
return val;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Vyhodnoti, jestli dokument odpovida zadanemu filtru.
|
|
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;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Pomocna funkce pro definici filtru se zachovanim plneho typovani.
|
|
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;
|
|
115
|
+
}
|
|
116
|
+
function evalAnd(target, arr, context) {
|
|
117
|
+
return arr.every(cond => matchesFilter(target, cond, context));
|
|
118
|
+
}
|
|
119
|
+
function evalOr(target, arr, context) {
|
|
120
|
+
return arr.some(cond => matchesFilter(target, cond, context));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Evaluates a single field value against its condition.
|
|
124
|
+
* Condition can be a primitive (implicit $eq) or an operator object.
|
|
125
|
+
*/
|
|
126
|
+
function evalCondition(fieldVal, condition, target, context) {
|
|
127
|
+
if (condition === null || typeof condition !== "object") {
|
|
128
|
+
if (Array.isArray(fieldVal))
|
|
129
|
+
return fieldVal.includes(condition);
|
|
130
|
+
return fieldVal === condition;
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(condition)) {
|
|
133
|
+
return fieldVal === condition;
|
|
134
|
+
}
|
|
135
|
+
const keys = Object.keys(condition);
|
|
136
|
+
for (const op of keys) {
|
|
137
|
+
const arg = condition[op];
|
|
138
|
+
if (!evalOperator(op, fieldVal, arg, target, context))
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Evaluates a single operator against a field value.
|
|
145
|
+
*/
|
|
146
|
+
function evalOperator(op, fieldVal, arg, target, context) {
|
|
147
|
+
switch (op) {
|
|
148
|
+
case "$eq": {
|
|
149
|
+
if (Array.isArray(arg) && arg.length === 2) {
|
|
150
|
+
const left = resolveValue(arg[0], target, context);
|
|
151
|
+
const right = resolveValue(arg[1], target, context);
|
|
152
|
+
return left === right;
|
|
153
|
+
}
|
|
154
|
+
const resolved = resolveValue(arg, target, context);
|
|
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;
|
|
164
|
+
}
|
|
165
|
+
case "$gt":
|
|
166
|
+
return fieldVal > resolveValue(arg, target, context);
|
|
167
|
+
case "$gte":
|
|
168
|
+
return fieldVal >= resolveValue(arg, target, context);
|
|
169
|
+
case "$lt":
|
|
170
|
+
return fieldVal < resolveValue(arg, target, context);
|
|
171
|
+
case "$lte":
|
|
172
|
+
return fieldVal <= resolveValue(arg, target, context);
|
|
173
|
+
case "$between": {
|
|
174
|
+
if (!Array.isArray(arg) || arg.length !== 2) {
|
|
175
|
+
throw new Error("$between requires an array of [min, max]");
|
|
176
|
+
}
|
|
177
|
+
return fieldVal >= arg[0]
|
|
178
|
+
&& fieldVal <= arg[1];
|
|
179
|
+
}
|
|
180
|
+
case "$in":
|
|
181
|
+
return Array.isArray(arg) && arg.includes(fieldVal);
|
|
182
|
+
case "$nin":
|
|
183
|
+
return Array.isArray(arg) && !arg.includes(fieldVal);
|
|
184
|
+
case "$ein":
|
|
185
|
+
if (!Array.isArray(fieldVal) || !Array.isArray(arg))
|
|
186
|
+
return false;
|
|
187
|
+
return fieldVal.every(v => arg.includes(v));
|
|
188
|
+
case "$nein":
|
|
189
|
+
if (!Array.isArray(fieldVal) || !Array.isArray(arg))
|
|
190
|
+
return false;
|
|
191
|
+
return fieldVal.some(v => !arg.includes(v));
|
|
192
|
+
case "$has":
|
|
193
|
+
return typeof fieldVal === "string"
|
|
194
|
+
&& fieldVal.includes(String(resolveValue(arg, target, context)));
|
|
195
|
+
case "$nhas":
|
|
196
|
+
return typeof fieldVal === "string"
|
|
197
|
+
&& !fieldVal.includes(String(resolveValue(arg, target, context)));
|
|
198
|
+
case "$ihas": {
|
|
199
|
+
const needle = String(resolveValue(arg, target, context)).toLowerCase();
|
|
200
|
+
return typeof fieldVal === "string" && fieldVal.toLowerCase().includes(needle);
|
|
201
|
+
}
|
|
202
|
+
case "$nihas": {
|
|
203
|
+
const needle = String(resolveValue(arg, target, context)).toLowerCase();
|
|
204
|
+
return typeof fieldVal === "string" && !fieldVal.toLowerCase().includes(needle);
|
|
205
|
+
}
|
|
206
|
+
case "$sw":
|
|
207
|
+
return typeof fieldVal === "string" && fieldVal.startsWith(String(arg));
|
|
208
|
+
case "$nsw":
|
|
209
|
+
return typeof fieldVal === "string" && !fieldVal.startsWith(String(arg));
|
|
210
|
+
case "$ew":
|
|
211
|
+
return typeof fieldVal === "string" && fieldVal.endsWith(String(arg));
|
|
212
|
+
case "$new":
|
|
213
|
+
return typeof fieldVal === "string" && !fieldVal.endsWith(String(arg));
|
|
214
|
+
case "$regex":
|
|
215
|
+
return new RegExp(String(arg)).test(String(fieldVal));
|
|
216
|
+
case "$iregex":
|
|
217
|
+
return new RegExp(String(arg), "i").test(String(fieldVal));
|
|
218
|
+
case "$exists":
|
|
219
|
+
return arg ? fieldVal !== undefined : fieldVal === undefined;
|
|
220
|
+
case "$and":
|
|
221
|
+
return Array.isArray(arg) && arg.every(cond => evalCondition(fieldVal, cond, target, context));
|
|
222
|
+
case "$nand":
|
|
223
|
+
return Array.isArray(arg) && !arg.every(cond => evalCondition(fieldVal, cond, target, context));
|
|
224
|
+
case "$or":
|
|
225
|
+
return Array.isArray(arg) && arg.some(cond => evalCondition(fieldVal, cond, target, context));
|
|
226
|
+
case "$nor":
|
|
227
|
+
return Array.isArray(arg) && !arg.some(cond => evalCondition(fieldVal, cond, target, context));
|
|
228
|
+
case "$not":
|
|
229
|
+
return !evalCondition(fieldVal, arg, target, context);
|
|
230
|
+
case "$some":
|
|
231
|
+
if (!Array.isArray(fieldVal))
|
|
232
|
+
return false;
|
|
233
|
+
return fieldVal.some(item => matchesFilter((item ?? {}), (arg ?? {}), context));
|
|
234
|
+
case "$nsome":
|
|
235
|
+
if (!Array.isArray(fieldVal))
|
|
236
|
+
return true;
|
|
237
|
+
return !fieldVal.some(item => matchesFilter((item ?? {}), (arg ?? {}), context));
|
|
238
|
+
case "$every":
|
|
239
|
+
if (!Array.isArray(fieldVal))
|
|
240
|
+
return false;
|
|
241
|
+
return fieldVal.every(item => matchesFilter((item ?? {}), (arg ?? {}), context));
|
|
242
|
+
case "$nevery":
|
|
243
|
+
if (!Array.isArray(fieldVal))
|
|
244
|
+
return false;
|
|
245
|
+
return !fieldVal.every(item => matchesFilter((item ?? {}), (arg ?? {}), context));
|
|
246
|
+
case "$length":
|
|
247
|
+
case "$size": {
|
|
248
|
+
const len = Array.isArray(fieldVal)
|
|
249
|
+
? fieldVal.length
|
|
250
|
+
: (typeof fieldVal === "string" ? fieldVal.length : undefined);
|
|
251
|
+
if (len === undefined)
|
|
252
|
+
return false;
|
|
253
|
+
return evalCondition(len, arg, target, context);
|
|
254
|
+
}
|
|
255
|
+
case "$sum": {
|
|
256
|
+
if (!Array.isArray(fieldVal))
|
|
257
|
+
return false;
|
|
258
|
+
const sum = fieldVal.reduce((a, b) => (Number(a) || 0) + (Number(b) || 0), 0);
|
|
259
|
+
return evalCondition(sum, arg, target, context);
|
|
260
|
+
}
|
|
261
|
+
case "$date":
|
|
262
|
+
return evalCondition(getCurrentDate(), arg, target, context);
|
|
263
|
+
case "$time":
|
|
264
|
+
return evalCondition(getCurrentTime(), arg, target, context);
|
|
265
|
+
case "$datetime":
|
|
266
|
+
return evalCondition(getCurrentDatetime(), arg, target, context);
|
|
267
|
+
case "$day":
|
|
268
|
+
return evalCondition(new Date().getDay(), arg, target, context);
|
|
269
|
+
default:
|
|
270
|
+
throw new Error(`Unknown operator: ${op}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
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 = {}) {
|
|
281
|
+
if (expr === null || expr === undefined)
|
|
282
|
+
return expr;
|
|
283
|
+
if (typeof expr !== "object") {
|
|
284
|
+
return resolveValue(expr, {}, context);
|
|
285
|
+
}
|
|
286
|
+
if (Array.isArray(expr)) {
|
|
287
|
+
return expr.map(e => evaluateValue(e, context));
|
|
288
|
+
}
|
|
289
|
+
const obj = expr;
|
|
290
|
+
const keys = Object.keys(obj);
|
|
291
|
+
if (keys.includes("$first")) {
|
|
292
|
+
return evalFirst(obj["$first"], context);
|
|
293
|
+
}
|
|
294
|
+
if (keys.includes("$val") || keys.includes("$value")) {
|
|
295
|
+
const val = obj["$val"] !== undefined ? obj["$val"] : obj["$value"];
|
|
296
|
+
const rule = obj["$rule"];
|
|
297
|
+
if (rule !== undefined && !evalRule(rule, context))
|
|
298
|
+
return undefined;
|
|
299
|
+
return evaluateValue(val, context);
|
|
300
|
+
}
|
|
301
|
+
if (keys.includes("$concat")) {
|
|
302
|
+
const list = obj["$concat"] || [];
|
|
303
|
+
return list.map(s => String(evaluateValue(s, context))).join("");
|
|
304
|
+
}
|
|
305
|
+
if (keys.includes("$plus")) {
|
|
306
|
+
const list = obj["$plus"] || [];
|
|
307
|
+
return list.reduce((acc, v) => Number(acc) + Number(evaluateValue(v, context)), 0);
|
|
308
|
+
}
|
|
309
|
+
if (keys.includes("$minus")) {
|
|
310
|
+
const vals = (obj["$minus"] || []).map(v => Number(evaluateValue(v, context)));
|
|
311
|
+
return vals.slice(1).reduce((acc, v) => acc - v, vals[0]);
|
|
312
|
+
}
|
|
313
|
+
if (keys.includes("$times")) {
|
|
314
|
+
const list = obj["$times"] || [];
|
|
315
|
+
return list.reduce((acc, v) => Number(acc) * Number(evaluateValue(v, context)), 1);
|
|
316
|
+
}
|
|
317
|
+
if (keys.includes("$div")) {
|
|
318
|
+
const vals = (obj["$div"] || []).map(v => Number(evaluateValue(v, context)));
|
|
319
|
+
return vals.slice(1).reduce((acc, v) => acc / v, vals[0]);
|
|
320
|
+
}
|
|
321
|
+
if (keys.includes("$min")) {
|
|
322
|
+
const vals = (obj["$min"] || []).map(v => Number(evaluateValue(v, context)));
|
|
323
|
+
return Math.min(...vals);
|
|
324
|
+
}
|
|
325
|
+
if (keys.includes("$max")) {
|
|
326
|
+
const vals = (obj["$max"] || []).map(v => Number(evaluateValue(v, context)));
|
|
327
|
+
return Math.max(...vals);
|
|
328
|
+
}
|
|
329
|
+
if (keys.includes("$avg")) {
|
|
330
|
+
const vals = (obj["$avg"] || []).map(v => Number(evaluateValue(v, context)));
|
|
331
|
+
return vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
332
|
+
}
|
|
333
|
+
if (keys.includes("$median")) {
|
|
334
|
+
const sorted = [...(obj["$median"] || []).map(v => Number(evaluateValue(v, context)))]
|
|
335
|
+
.sort((a, b) => a - b);
|
|
336
|
+
const mid = Math.floor(sorted.length / 2);
|
|
337
|
+
return sorted.length % 2 !== 0
|
|
338
|
+
? sorted[mid]
|
|
339
|
+
: (sorted[mid - 1] + sorted[mid]) / 2;
|
|
340
|
+
}
|
|
341
|
+
if (keys.includes("$last")) {
|
|
342
|
+
const arr = obj["$last"];
|
|
343
|
+
return Array.isArray(arr) ? evaluateValue(arr[arr.length - 1], context) : evaluateValue(arr, context);
|
|
344
|
+
}
|
|
345
|
+
return expr;
|
|
346
|
+
}
|
|
347
|
+
function evalFirst(arr, context) {
|
|
348
|
+
if (!Array.isArray(arr))
|
|
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;
|
|
358
|
+
}
|
|
359
|
+
function evalRule(rule, context) {
|
|
360
|
+
if (typeof rule !== "object" || rule === null)
|
|
361
|
+
return Boolean(rule);
|
|
362
|
+
const keys = Object.keys(rule);
|
|
363
|
+
if (keys.length === 1
|
|
364
|
+
&& keys[0].startsWith("$")
|
|
365
|
+
&& Array.isArray(rule[keys[0]])) {
|
|
366
|
+
const op = keys[0];
|
|
367
|
+
const arg = rule[op];
|
|
368
|
+
if (arg.length === 2) {
|
|
369
|
+
return evalOperator(op, undefined, arg, {}, context);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return matchesFilter({}, rule, context);
|
|
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);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Vyfiltruje pole JSON dokumentu podle filtru.
|
|
385
|
+
*/
|
|
386
|
+
export function filterArray(documents, filter, context = {}) {
|
|
387
|
+
return documents.filter(doc => matchesFilter(doc, filter, context));
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Vyfiltruje mapu JSON dokumentu (`Record<string, doc>`) podle filtru.
|
|
391
|
+
*/
|
|
392
|
+
export function filterObject(documents, filter, context = {}) {
|
|
393
|
+
return Object.fromEntries(Object.entries(documents).filter(([, doc]) => matchesFilter(doc, filter, context)));
|
|
394
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rekurzivne vyhodnocuje transformacni vyraz nad JSON dokumentem.
|
|
3
|
+
*
|
|
4
|
+
* Podporuje operatory jako `$rule`, `$and`, `$or`, `$sum`, `$avg`, `$concat`
|
|
5
|
+
* a reference hodnot pres dot-path (`$field.subfield`).
|
|
6
|
+
*/
|
|
7
|
+
declare function recalculateObjectWithDocument(obj: any, document: any): any;
|
|
8
|
+
export default recalculateObjectWithDocument;
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { getValueByDotPath } from "./dotPath.js";
|
|
2
|
+
import { matchesFilter } from "./matchesFilter.js";
|
|
3
|
+
function getObjectValue(ddd, obj, documents) {
|
|
4
|
+
const data = Array.isArray(ddd)
|
|
5
|
+
? ddd.length > 0
|
|
6
|
+
? [...ddd]
|
|
7
|
+
: undefined
|
|
8
|
+
: typeof ddd === "object"
|
|
9
|
+
? Object.keys(ddd).length > 0
|
|
10
|
+
? { ...ddd }
|
|
11
|
+
: undefined
|
|
12
|
+
: ddd;
|
|
13
|
+
if (!data)
|
|
14
|
+
return undefined;
|
|
15
|
+
// console.log("DATA v getObjectValue", data)
|
|
16
|
+
// console.log("getObjectValue", obj)
|
|
17
|
+
switch (Object.keys(obj)[0]) {
|
|
18
|
+
case "$pipe":
|
|
19
|
+
let result = data;
|
|
20
|
+
// console.log("PIPE")
|
|
21
|
+
for (const action of obj.$pipe) {
|
|
22
|
+
// console.log("Action", action, result)
|
|
23
|
+
if (result)
|
|
24
|
+
result = getObjectValue(result, action, documents);
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
case "$filter":
|
|
28
|
+
// console.log("Data", data[0], obj.$filter)
|
|
29
|
+
const filterRes = data
|
|
30
|
+
.map((d) => {
|
|
31
|
+
if (matchesFilter(d, obj.$filter, documents))
|
|
32
|
+
return d;
|
|
33
|
+
})
|
|
34
|
+
.filter((u) => u);
|
|
35
|
+
// console.log("FILTER RESULT", filterRes)
|
|
36
|
+
return filterRes;
|
|
37
|
+
case "$sort":
|
|
38
|
+
console.log("Sorting", obj.$sort);
|
|
39
|
+
if (typeof obj.$sort === "object") {
|
|
40
|
+
const rr = data.sortBy(obj.$sort);
|
|
41
|
+
// console.log("Sorting result", JSON.stringify(rr, null, 2))
|
|
42
|
+
return rr;
|
|
43
|
+
}
|
|
44
|
+
else
|
|
45
|
+
return obj.$sort === "ASC" ? data.sort((a, b) => a - b) : data.sort((a, b) => b - a);
|
|
46
|
+
case "$limit":
|
|
47
|
+
let limit = Array.isArray(obj.$limit) ? [...obj.$limit] : [obj.$limit];
|
|
48
|
+
// console.log("Limiting", limit)
|
|
49
|
+
if (limit.length === 1)
|
|
50
|
+
limit.unshift(0);
|
|
51
|
+
else
|
|
52
|
+
limit[1] = limit[1] + limit[0];
|
|
53
|
+
const res = data.slice(...limit);
|
|
54
|
+
// console.log("Limit res", res)
|
|
55
|
+
return res;
|
|
56
|
+
case "$project":
|
|
57
|
+
// console.log("Project", obj.$project)
|
|
58
|
+
if (!Array.isArray(obj.$project))
|
|
59
|
+
obj.$project = [obj.$project];
|
|
60
|
+
return data.map((o) => Object.fromEntries(Object.entries(o).filter(([key]) => obj.$project.includes(key))));
|
|
61
|
+
case "$extract":
|
|
62
|
+
// console.log("Extract", obj.$extract)
|
|
63
|
+
// return data.map(o => o[obj.$extract])
|
|
64
|
+
const sres = data.map((o) => {
|
|
65
|
+
const res = getValueByDotPath(o, obj.$extract);
|
|
66
|
+
// console.log("EXTRACT RES", res)
|
|
67
|
+
return res;
|
|
68
|
+
});
|
|
69
|
+
// console.log("SRES JE ", sres.flat())
|
|
70
|
+
return sres.flat();
|
|
71
|
+
// return sres
|
|
72
|
+
case "$extractValue":
|
|
73
|
+
const rv = data.map((o) => o[obj.$extract]);
|
|
74
|
+
if (rv.length === 1)
|
|
75
|
+
return rv[0];
|
|
76
|
+
else
|
|
77
|
+
return rv;
|
|
78
|
+
case "$val":
|
|
79
|
+
case "$value":
|
|
80
|
+
// $val/$value return the current data as-is (or extract if specified)
|
|
81
|
+
return data;
|
|
82
|
+
default:
|
|
83
|
+
// console.log("JE TU NEZNAMY", Object.keys(obj)[0])
|
|
84
|
+
}
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Rekurzivne vyhodnocuje transformacni vyraz nad JSON dokumentem.
|
|
89
|
+
*
|
|
90
|
+
* Podporuje operatory jako `$rule`, `$and`, `$or`, `$sum`, `$avg`, `$concat`
|
|
91
|
+
* a reference hodnot pres dot-path (`$field.subfield`).
|
|
92
|
+
*/
|
|
93
|
+
function recalculateObjectWithDocument(obj, document) {
|
|
94
|
+
if (obj === undefined)
|
|
95
|
+
return;
|
|
96
|
+
if (Array.isArray(obj)) {
|
|
97
|
+
var results = [];
|
|
98
|
+
obj.forEach((item) => {
|
|
99
|
+
const result = recalculateObjectWithDocument(item, document);
|
|
100
|
+
if (result !== undefined) {
|
|
101
|
+
results.push(result);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
else if (typeof obj === "object") {
|
|
107
|
+
const keys = Object.keys(obj);
|
|
108
|
+
if (keys.some((item) => item.startsWith("$"))) {
|
|
109
|
+
if (keys.includes("$rule")) {
|
|
110
|
+
// console.log("Test je ", obj["$rule"])
|
|
111
|
+
const res = matchesFilter(document, obj["$rule"]);
|
|
112
|
+
// console.log("Rule", obj["$rule"])
|
|
113
|
+
// const res = recalculateObjectWithDocument(obj["$rule"], document)
|
|
114
|
+
if (res && obj["$value"] !== undefined) {
|
|
115
|
+
// console.log("Volam recaluclate", obj["$value"])
|
|
116
|
+
return recalculateObjectWithDocument(obj["$value"], document);
|
|
117
|
+
}
|
|
118
|
+
else if (obj["$value"] === undefined) {
|
|
119
|
+
// console.log("Tady", obj["$rule"], res)
|
|
120
|
+
return res;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (keys.includes("$or")) {
|
|
124
|
+
return obj["$or"].some((condition) => matchesFilter(document, condition));
|
|
125
|
+
}
|
|
126
|
+
else if (keys.includes("$and")) {
|
|
127
|
+
return obj["$and"].every((condition) => matchesFilter(document, condition));
|
|
128
|
+
}
|
|
129
|
+
else if (keys.includes("$min")) {
|
|
130
|
+
const res = recalculateObjectWithDocument(obj["$min"], document);
|
|
131
|
+
if (res.length === 0)
|
|
132
|
+
return undefined;
|
|
133
|
+
// console.log("Vybiram z ", res)
|
|
134
|
+
return Math.min(...res);
|
|
135
|
+
}
|
|
136
|
+
else if (keys.includes("$max")) {
|
|
137
|
+
const res = recalculateObjectWithDocument(obj["$max"], document);
|
|
138
|
+
if (res.length === 0)
|
|
139
|
+
return undefined;
|
|
140
|
+
return Math.max(...res);
|
|
141
|
+
}
|
|
142
|
+
else if (keys.includes("$sum")) {
|
|
143
|
+
const res = recalculateObjectWithDocument(obj["$sum"], document);
|
|
144
|
+
if (res)
|
|
145
|
+
return res.reduce((acc, val) => acc + val, 0);
|
|
146
|
+
else
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
else if (keys.includes("$avg")) {
|
|
150
|
+
const res = recalculateObjectWithDocument(obj["$avg"], document);
|
|
151
|
+
const sum = res.reduce((acc, val) => acc + val, 0);
|
|
152
|
+
return sum / res.length;
|
|
153
|
+
}
|
|
154
|
+
else if (keys.includes("$median")) {
|
|
155
|
+
const res = recalculateObjectWithDocument(obj["$median"], document);
|
|
156
|
+
const sortedArray = res.slice().sort((a, b) => a - b);
|
|
157
|
+
let median;
|
|
158
|
+
const middleIndex = Math.floor(sortedArray.length / 2);
|
|
159
|
+
if (sortedArray.length % 2 === 0) {
|
|
160
|
+
// Even number of elements
|
|
161
|
+
median = (sortedArray[middleIndex - 1] + sortedArray[middleIndex]) / 2;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// Odd number of elements
|
|
165
|
+
median = sortedArray[middleIndex];
|
|
166
|
+
}
|
|
167
|
+
return median;
|
|
168
|
+
}
|
|
169
|
+
else if (keys.includes("$val") || keys.includes("$value")) {
|
|
170
|
+
const res = recalculateObjectWithDocument(obj["$val"] || obj["$value"], document);
|
|
171
|
+
if (res.length === 1)
|
|
172
|
+
return res[0];
|
|
173
|
+
return res;
|
|
174
|
+
}
|
|
175
|
+
else if (keys.includes("$first")) {
|
|
176
|
+
const res = recalculateObjectWithDocument(obj["$first"], document);
|
|
177
|
+
return res?.[0];
|
|
178
|
+
}
|
|
179
|
+
else if (keys.includes("$last")) {
|
|
180
|
+
const res = recalculateObjectWithDocument(obj["$last"], document);
|
|
181
|
+
return res?.[res.length - 1];
|
|
182
|
+
}
|
|
183
|
+
else if (keys.includes("$concat")) {
|
|
184
|
+
const res = recalculateObjectWithDocument(obj["$concat"], document);
|
|
185
|
+
return res.join("");
|
|
186
|
+
}
|
|
187
|
+
else if (keys.includes("$plus")) {
|
|
188
|
+
const res = recalculateObjectWithDocument(obj["$plus"], document);
|
|
189
|
+
return res.reduce((accumulator, currentValue) => accumulator + Number(currentValue), 0);
|
|
190
|
+
}
|
|
191
|
+
else if (keys.includes("$minus")) {
|
|
192
|
+
const res = recalculateObjectWithDocument(obj["$minus"], document);
|
|
193
|
+
if (res?.length > 0) {
|
|
194
|
+
const first = res.shift();
|
|
195
|
+
return res.reduce((accumulator, currentValue) => accumulator - Number(currentValue), first);
|
|
196
|
+
}
|
|
197
|
+
return res;
|
|
198
|
+
}
|
|
199
|
+
else if (keys.includes("$times")) {
|
|
200
|
+
const res = recalculateObjectWithDocument(obj["$times"], document);
|
|
201
|
+
return res.reduce((accumulator, currentValue) => accumulator * Number(currentValue), 1);
|
|
202
|
+
}
|
|
203
|
+
else if (keys.includes("$div") || keys.includes("$divide")) {
|
|
204
|
+
const res = recalculateObjectWithDocument(obj["$div"] || obj["$divide"], document);
|
|
205
|
+
// if (res?.length >= 2) {
|
|
206
|
+
const first = res.shift();
|
|
207
|
+
return res.reduce((accumulator, currentValue) => accumulator / Number(currentValue), first);
|
|
208
|
+
// } else return false
|
|
209
|
+
}
|
|
210
|
+
else if (keys.includes("$mod")) {
|
|
211
|
+
const res = recalculateObjectWithDocument(obj["$mod"], document);
|
|
212
|
+
return res[0] % res[1];
|
|
213
|
+
}
|
|
214
|
+
else if (keys.includes("$round")) {
|
|
215
|
+
const res = recalculateObjectWithDocument(obj["$round"], document);
|
|
216
|
+
if (Array.isArray(res)) {
|
|
217
|
+
const decimals = res[1] || 0;
|
|
218
|
+
return Math.round(res[0] * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
|
219
|
+
}
|
|
220
|
+
return Math.round(res);
|
|
221
|
+
}
|
|
222
|
+
else if (keys.includes("$ceil")) {
|
|
223
|
+
const res = recalculateObjectWithDocument(obj["$ceil"], document);
|
|
224
|
+
if (Array.isArray(res)) {
|
|
225
|
+
const decimals = res[1] || 0;
|
|
226
|
+
return Math.ceil(res[0] * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
|
227
|
+
}
|
|
228
|
+
return Math.floor(res);
|
|
229
|
+
}
|
|
230
|
+
else if (keys.includes("$floor")) {
|
|
231
|
+
const res = recalculateObjectWithDocument(obj["$floor"], document);
|
|
232
|
+
return Math.floor(res);
|
|
233
|
+
}
|
|
234
|
+
else if (keys.includes("$gt")) {
|
|
235
|
+
const res = recalculateObjectWithDocument(obj["$gt"], document);
|
|
236
|
+
return res[0] > res[1];
|
|
237
|
+
}
|
|
238
|
+
else if (keys.includes("$gte")) {
|
|
239
|
+
const res = recalculateObjectWithDocument(obj["$gte"], document);
|
|
240
|
+
return res[0] >= res[1];
|
|
241
|
+
}
|
|
242
|
+
else if (keys.includes("$lt")) {
|
|
243
|
+
const res = recalculateObjectWithDocument(obj["$lt"], document);
|
|
244
|
+
return res[0] < res[1];
|
|
245
|
+
}
|
|
246
|
+
else if (keys.includes("$lte")) {
|
|
247
|
+
const res = recalculateObjectWithDocument(obj["$lte"], document);
|
|
248
|
+
return res[0] <= res[1];
|
|
249
|
+
}
|
|
250
|
+
else if (keys.includes("$eq")) {
|
|
251
|
+
const res = recalculateObjectWithDocument(obj["$eq"], document);
|
|
252
|
+
return res[0] === res[1];
|
|
253
|
+
}
|
|
254
|
+
else if (keys.includes("$neq")) {
|
|
255
|
+
const res = recalculateObjectWithDocument(obj["$neq"], document);
|
|
256
|
+
return res[0] !== res[1];
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// console.log("String obj", obj)
|
|
260
|
+
// console.log("Data:", document)
|
|
261
|
+
let data = getValueByDotPath(document, Object.keys(obj)[0].substring(1));
|
|
262
|
+
if (typeof obj === "object") {
|
|
263
|
+
data = getObjectValue(data, obj[Object.keys(obj)[0]], document);
|
|
264
|
+
}
|
|
265
|
+
return data;
|
|
266
|
+
}
|
|
267
|
+
// const a = {
|
|
268
|
+
// "$and": [
|
|
269
|
+
// { "name": { "$regex": { "$neq": "LED" } } },
|
|
270
|
+
// { "name": { "$regex": "UV" } },
|
|
271
|
+
// { "name": { "$regex": "Lamp" } },
|
|
272
|
+
// { "categories": "ce82bfa4-b2c7-11ec-9c66-246e96436e9c" }
|
|
273
|
+
// ]
|
|
274
|
+
// }
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
// let result = {};
|
|
278
|
+
let result = {};
|
|
279
|
+
// console.log("Obj", obj)
|
|
280
|
+
// console.log("Documents", keys, JSON.stringify(document, null, 2))
|
|
281
|
+
// console.log("klic", obj, JSON.stringify(document, null, 2))
|
|
282
|
+
// console.log("KKK", getValueByDotPath("$" + keys[0], document))
|
|
283
|
+
// TODO: Zmenil jsem z:
|
|
284
|
+
// recalculateObjectWithDocument(getValueByDotPath(obj[key]), document)))
|
|
285
|
+
// Ale nevim jestli to je spravne
|
|
286
|
+
keys.forEach((key) => {
|
|
287
|
+
const value = obj[key];
|
|
288
|
+
// If value is a string starting with $, treat it as a path reference
|
|
289
|
+
if (typeof value === "string" && value.startsWith("$")) {
|
|
290
|
+
const path = value.substring(1);
|
|
291
|
+
result[key] = recalculateObjectWithDocument(getValueByDotPath(document, path), document);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// Otherwise treat it as an expression to evaluate
|
|
295
|
+
result[key] = recalculateObjectWithDocument(value, document);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
// console.log("Result je", result)
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Je to jen hodnota, vracime ji
|
|
303
|
+
else {
|
|
304
|
+
if (typeof obj === "string" && obj.startsWith("$"))
|
|
305
|
+
return getValueByDotPath(document, obj.substring(1));
|
|
306
|
+
else
|
|
307
|
+
return obj;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
export default recalculateObjectWithDocument;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { matchesFilter } from "./matchesFilter";
|
|
2
|
+
const target = { price: 120, stock: 3, minPrice: 10 };
|
|
3
|
+
const context = { minPrice: 20, maxPrice: 150 };
|
|
4
|
+
matchesFilter(target, {
|
|
5
|
+
$and: [
|
|
6
|
+
{ price: { $gte: "$this.minPrice" } },
|
|
7
|
+
{ price: { $lte: "$ctx.maxPrice" } },
|
|
8
|
+
],
|
|
9
|
+
}, context);
|
|
10
|
+
matchesFilter(target, {
|
|
11
|
+
$and: [
|
|
12
|
+
// @ts-expect-error invalid target reference path
|
|
13
|
+
{ price: { $gte: "$this.nesmysl" } },
|
|
14
|
+
{ price: { $lte: "$ctx.maxPrice" } },
|
|
15
|
+
],
|
|
16
|
+
}, context);
|
|
17
|
+
matchesFilter(target, {
|
|
18
|
+
$and: [
|
|
19
|
+
{ price: { $gte: "$this.minPrice" } },
|
|
20
|
+
// @ts-expect-error invalid context reference path
|
|
21
|
+
{ price: { $lte: "$ctx.nesmysl" } },
|
|
22
|
+
],
|
|
23
|
+
}, context);
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Zakladni skalarny typ podporovany ve filtrech. */
|
|
2
|
+
export type scalarType = string | number | boolean | null;
|
|
3
|
+
/** Kontext predavany jako zdroj hodnot pro reference `$ctx.*`. */
|
|
4
|
+
export type contextType = Record<string, unknown>;
|
|
5
|
+
/** Cilovy JSON dokument, nad kterym se filtr vyhodnocuje. */
|
|
6
|
+
export type targetType = Record<string, unknown>;
|
|
7
|
+
type objectLikeType = Record<string, unknown>;
|
|
8
|
+
type dotPathType<TObj> = TObj extends objectLikeType ? {
|
|
9
|
+
[K in Extract<keyof TObj, string>]: TObj[K] extends objectLikeType ? K | `${K}.${dotPathType<TObj[K]>}` : K;
|
|
10
|
+
}[Extract<keyof TObj, string>] : never;
|
|
11
|
+
type optionalFieldKeyType<TTarget extends targetType> = `${dotPathType<TTarget>}?`;
|
|
12
|
+
type pathValueType<TObj, TPath extends string> = TPath extends `${infer THead}.${infer TTail}` ? THead extends keyof TObj ? pathValueType<TObj[THead], TTail> : unknown : TPath extends keyof TObj ? TObj[TPath] : unknown;
|
|
13
|
+
type scalarFieldValueType<TValue> = Extract<TValue, scalarType>;
|
|
14
|
+
type arrayElementType<TValue> = TValue extends readonly (infer TElement)[] ? TElement : never;
|
|
15
|
+
type numericOrStringType<TValue> = Extract<TValue, number | string>;
|
|
16
|
+
type someFilterType<TValue> = TValue extends readonly (infer TItem)[] ? TItem extends Record<string, unknown> ? filterType<TItem> : never : never;
|
|
17
|
+
/** Reference na hodnotu z ciloveho dokumentu, napr. `$this.profile.type`. */
|
|
18
|
+
export type targetReferenceType<TTarget extends targetType> = `$this.${dotPathType<TTarget>}`;
|
|
19
|
+
/** Reference na hodnotu z kontextu, napr. `$ctx.user.role`. */
|
|
20
|
+
export type contextReferenceType<TContext extends contextType = contextType> = `$ctx.${dotPathType<TContext>}`;
|
|
21
|
+
export type valueReferenceType<TTarget extends targetType = targetType, TContext extends contextType = contextType> = targetReferenceType<TTarget> | contextReferenceType<TContext>;
|
|
22
|
+
/**
|
|
23
|
+
* Stringová hodnota nebo reference s validací.
|
|
24
|
+
* Pokud začíná `$this.` nebo `$ctx.`, musí být validní cesta.
|
|
25
|
+
* Jinak je to libovolný string (ne-reference).
|
|
26
|
+
*/
|
|
27
|
+
type stringOrValidReferenceType<TTarget extends targetType, TContext extends contextType> = `$this.${dotPathType<TTarget>}` | `$ctx.${dotPathType<TContext>}` | (string & {});
|
|
28
|
+
type scalarOrReferenceType<TValue, TTarget extends targetType, TContext extends contextType> = TValue extends string ? stringOrValidReferenceType<TTarget, TContext> : scalarFieldValueType<TValue> | valueReferenceType<TTarget, TContext>;
|
|
29
|
+
export type conditionType<TValue = unknown, TTarget extends targetType = targetType, TContext extends contextType = contextType> = scalarType | undefined | conditionObjectType<TValue, TTarget, TContext> | Array<conditionType<TValue, TTarget, TContext>>;
|
|
30
|
+
/** Objektova podoba podminky (operator -> hodnota). */
|
|
31
|
+
export type conditionObjectType<TValue = unknown, TTarget extends targetType = targetType, TContext extends contextType = contextType> = {
|
|
32
|
+
/**
|
|
33
|
+
* Rovnost hodnoty (`field === value`).
|
|
34
|
+
* Pokud string začíná `$this.` nebo `$ctx.`, musí být validní cesta.
|
|
35
|
+
*/
|
|
36
|
+
$eq?: TValue extends string ? (`$this.${dotPathType<TTarget>}` | `$ctx.${dotPathType<TContext>}` | (string & {})) : scalarOrReferenceType<TValue, TTarget, TContext> | [unknown, unknown];
|
|
37
|
+
/** Nerovnost hodnoty (`field !== value`). */
|
|
38
|
+
$neq?: scalarOrReferenceType<TValue, TTarget, TContext>;
|
|
39
|
+
/** Vetsi nez (`field > value`). */
|
|
40
|
+
$gt?: numericOrStringType<TValue> | valueReferenceType<TTarget, TContext>;
|
|
41
|
+
/** Vetsi nebo rovno (`field >= value`). */
|
|
42
|
+
$gte?: numericOrStringType<TValue> | valueReferenceType<TTarget, TContext>;
|
|
43
|
+
/** Mensi nez (`field < value`). */
|
|
44
|
+
$lt?: numericOrStringType<TValue> | valueReferenceType<TTarget, TContext>;
|
|
45
|
+
/** Mensi nebo rovno (`field <= value`). */
|
|
46
|
+
$lte?: numericOrStringType<TValue> | valueReferenceType<TTarget, TContext>;
|
|
47
|
+
/** Rozsah vcetne hranic (`min <= field <= max`). */
|
|
48
|
+
$between?: [unknown, unknown];
|
|
49
|
+
/** Hodnota je v seznamu (`field in list`). */
|
|
50
|
+
$in?: Array<scalarOrReferenceType<TValue, TTarget, TContext>>;
|
|
51
|
+
/** Hodnota neni v seznamu (`field not in list`). */
|
|
52
|
+
$nin?: Array<scalarOrReferenceType<TValue, TTarget, TContext>>;
|
|
53
|
+
/** Vsechny prvky pole jsou v seznamu. */
|
|
54
|
+
$ein?: TValue extends readonly unknown[] ? Array<scalarOrReferenceType<arrayElementType<TValue>, TTarget, TContext>> : never;
|
|
55
|
+
/** Alespon jeden prvek pole neni v seznamu. */
|
|
56
|
+
$nein?: TValue extends readonly unknown[] ? Array<scalarOrReferenceType<arrayElementType<TValue>, TTarget, TContext>> : never;
|
|
57
|
+
/** String obsahuje podretezec (case-sensitive). */
|
|
58
|
+
$has?: scalarOrReferenceType<string, TTarget, TContext>;
|
|
59
|
+
/** String neobsahuje podretezec (case-sensitive). */
|
|
60
|
+
$nhas?: scalarOrReferenceType<string, TTarget, TContext>;
|
|
61
|
+
/** String obsahuje podretezec (case-insensitive). */
|
|
62
|
+
$ihas?: scalarOrReferenceType<string, TTarget, TContext>;
|
|
63
|
+
/** String neobsahuje podretezec (case-insensitive). */
|
|
64
|
+
$nihas?: scalarOrReferenceType<string, TTarget, TContext>;
|
|
65
|
+
/** String zacina na hodnotu. */
|
|
66
|
+
$sw?: scalarOrReferenceType<string, TTarget, TContext>;
|
|
67
|
+
/** String nezacina na hodnotu. */
|
|
68
|
+
$nsw?: scalarOrReferenceType<string, TTarget, TContext>;
|
|
69
|
+
/** String konci na hodnotu. */
|
|
70
|
+
$ew?: scalarOrReferenceType<string, TTarget, TContext>;
|
|
71
|
+
/** String nekonci na hodnotu. */
|
|
72
|
+
$new?: scalarOrReferenceType<string, TTarget, TContext>;
|
|
73
|
+
/** RegExp porovnani (case-sensitive). */
|
|
74
|
+
$regex?: string;
|
|
75
|
+
/** RegExp porovnani (case-insensitive). */
|
|
76
|
+
$iregex?: string;
|
|
77
|
+
/** Kontrola existence hodnoty (`true` = existuje, `false` = neexistuje). */
|
|
78
|
+
$exists?: boolean;
|
|
79
|
+
/** Vsechny podminky musi platit. */
|
|
80
|
+
$and?: Array<conditionType<TValue, TTarget, TContext>>;
|
|
81
|
+
/** Negace operatoru $and. */
|
|
82
|
+
$nand?: Array<conditionType<TValue, TTarget, TContext>>;
|
|
83
|
+
/** Alespon jedna podminka musi platit. */
|
|
84
|
+
$or?: Array<conditionType<TValue, TTarget, TContext>>;
|
|
85
|
+
/** Negace operatoru $or. */
|
|
86
|
+
$nor?: Array<conditionType<TValue, TTarget, TContext>>;
|
|
87
|
+
/** Negace podminky. */
|
|
88
|
+
$not?: conditionType<TValue, TTarget, TContext>;
|
|
89
|
+
/** U pole objektu: alespon jeden prvek odpovida filtru. */
|
|
90
|
+
$some?: someFilterType<TValue>;
|
|
91
|
+
/** U pole objektu: zadny prvek neodpovida filtru. */
|
|
92
|
+
$nsome?: someFilterType<TValue>;
|
|
93
|
+
/** U pole objektu: vsechny prvky odpovidaji filtru. */
|
|
94
|
+
$every?: someFilterType<TValue>;
|
|
95
|
+
/** U pole objektu: neplati, ze vsechny prvky odpovidaji filtru. */
|
|
96
|
+
$nevery?: someFilterType<TValue>;
|
|
97
|
+
/** Podminka nad delkou pole nebo stringu. */
|
|
98
|
+
$length?: conditionType<number, TTarget, TContext>;
|
|
99
|
+
/** Alias pro $length. */
|
|
100
|
+
$size?: conditionType<number, TTarget, TContext>;
|
|
101
|
+
/** Soucet prvku pole a porovnani vysledku. */
|
|
102
|
+
$sum?: conditionType<number, TTarget, TContext>;
|
|
103
|
+
/** Porovnani s aktualnim datem (YYYY-MM-DD). */
|
|
104
|
+
$date?: conditionType<string, TTarget, TContext>;
|
|
105
|
+
/** Porovnani s aktualnim casem (HH:mm:ss). */
|
|
106
|
+
$time?: conditionType<string, TTarget, TContext>;
|
|
107
|
+
/** Porovnani s aktualnim datetime. */
|
|
108
|
+
$datetime?: conditionType<string, TTarget, TContext>;
|
|
109
|
+
/** Den v tydnu (`0-6`, nedele=0). */
|
|
110
|
+
$day?: conditionType<number, TTarget, TContext>;
|
|
111
|
+
};
|
|
112
|
+
/** Hlavni typ filtru pouzivany ve `matchesFilter` a `filterDocuments`. */
|
|
113
|
+
export type filterType<TTarget extends targetType = targetType, TContext extends contextType = contextType> = {
|
|
114
|
+
/** Vsechny podfiltry musi platit. */
|
|
115
|
+
$and?: Array<filterType<TTarget, TContext>>;
|
|
116
|
+
/** Negace operatoru $and. */
|
|
117
|
+
$nand?: Array<filterType<TTarget, TContext>>;
|
|
118
|
+
/** Alespon jeden podfiltr musi platit. */
|
|
119
|
+
$or?: Array<filterType<TTarget, TContext>>;
|
|
120
|
+
/** Negace operatoru $or. */
|
|
121
|
+
$nor?: Array<filterType<TTarget, TContext>>;
|
|
122
|
+
/** Negace jednoho podfiltru. */
|
|
123
|
+
$not?: filterType<TTarget, TContext>;
|
|
124
|
+
} & {
|
|
125
|
+
[K in dotPathType<TTarget>]?: conditionType<pathValueType<TTarget, K>, TTarget, TContext>;
|
|
126
|
+
} & {
|
|
127
|
+
[K in optionalFieldKeyType<TTarget>]?: conditionType<pathValueType<TTarget, K extends `${infer P}?` ? P : never>, TTarget, TContext>;
|
|
128
|
+
} & {
|
|
129
|
+
[K in contextReferenceType<TContext>]?: conditionType<any, TTarget, TContext>;
|
|
130
|
+
};
|
|
131
|
+
/** Pole JSON dokumentu. */
|
|
132
|
+
export type documentsArrayType = Array<Record<string, unknown>>;
|
|
133
|
+
/** Mapa JSON dokumentu (`Record<id, document>`). */
|
|
134
|
+
export type documentsMapType = Record<string, Record<string, unknown>>;
|
|
135
|
+
/** Typ hodnotoveho vyrazu pro `evaluateValue` a souvisejici API. */
|
|
136
|
+
export type valueExpressionType = unknown;
|
|
137
|
+
export {};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mgarlik/json-filter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "JSON filter engine for backend and frontend applications",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"lint": "tsc -p tsconfig.json --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"filter",
|
|
28
|
+
"rules",
|
|
29
|
+
"validation",
|
|
30
|
+
"typescript"
|
|
31
|
+
],
|
|
32
|
+
"license": "ISC",
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"typescript": "^5.6.3",
|
|
38
|
+
"vitest": "^2.1.5"
|
|
39
|
+
}
|
|
40
|
+
}
|