@mgarlik/json-filter 0.1.0 → 1.0.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.
@@ -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 };
@@ -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";
@@ -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
  */
@@ -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,76 @@ 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
- * 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);
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
- return val;
26
+ if (Array.isArray(resolved))
27
+ return resolved.includes(fieldVal);
28
+ return fieldVal === resolved;
41
29
  }
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;
30
+ function buildSourceKey(source) {
31
+ if (typeof source !== "object" || source === null)
32
+ return String(source);
33
+ return JSON.stringify(source);
106
34
  }
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;
35
+ function getCurrentDate() {
36
+ return new Date().toISOString().slice(0, 10);
115
37
  }
116
- function evalAnd(target, arr, context) {
117
- return arr.every(cond => matchesFilter(target, cond, context));
38
+ function getCurrentTime() {
39
+ return new Date().toTimeString().slice(0, 8);
118
40
  }
119
- function evalOr(target, arr, context) {
120
- return arr.some(cond => matchesFilter(target, cond, context));
41
+ function getCurrentDatetime() {
42
+ return new Date().toISOString().replace("T", " ").slice(0, 19);
43
+ }
44
+ function evalFirst(arr, context, target) {
45
+ if (!Array.isArray(arr))
46
+ return undefined;
47
+ for (const item of arr) {
48
+ const result = evaluateValue(item, context, target);
49
+ if (result !== undefined)
50
+ return result;
51
+ }
52
+ return undefined;
53
+ }
54
+ function evalRule(rule, context, target) {
55
+ if (typeof rule !== "object" || rule === null)
56
+ return Boolean(rule);
57
+ const keys = Object.keys(rule);
58
+ if (keys.length === 1 && keys[0].startsWith("$") && Array.isArray(rule[keys[0]])) {
59
+ const op = keys[0];
60
+ const arg = rule[op];
61
+ if (arg.length === 2) {
62
+ return evalOperator(op, undefined, arg, target, context);
63
+ }
64
+ }
65
+ return matchesFilter(target, rule, context);
121
66
  }
122
- /**
123
- * Evaluates a single field value against its condition.
124
- * Condition can be a primitive (implicit $eq) or an operator object.
125
- */
126
67
  function evalCondition(fieldVal, condition, target, context) {
127
68
  if (condition === null || typeof condition !== "object") {
128
- if (Array.isArray(fieldVal))
129
- return fieldVal.includes(condition);
130
- return fieldVal === condition;
69
+ return compareResolvedValues(fieldVal, evaluateValue(condition, context, target));
131
70
  }
132
71
  if (Array.isArray(condition)) {
133
- return fieldVal === condition;
72
+ return compareResolvedValues(fieldVal, evaluateValue(condition, context, target));
134
73
  }
135
74
  const keys = Object.keys(condition);
75
+ if (keys.length === 1 && keys[0] === "$source") {
76
+ return compareResolvedValues(fieldVal, evaluateValue(condition, context, target));
77
+ }
136
78
  for (const op of keys) {
137
79
  const arg = condition[op];
138
80
  if (!evalOperator(op, fieldVal, arg, target, context))
@@ -140,114 +82,112 @@ function evalCondition(fieldVal, condition, target, context) {
140
82
  }
141
83
  return true;
142
84
  }
143
- /**
144
- * Evaluates a single operator against a field value.
145
- */
146
85
  function evalOperator(op, fieldVal, arg, target, context) {
147
86
  switch (op) {
148
87
  case "$eq": {
149
88
  if (Array.isArray(arg) && arg.length === 2) {
150
- const left = resolveValue(arg[0], target, context);
151
- const right = resolveValue(arg[1], target, context);
89
+ const left = evaluateValue(arg[0], context, target);
90
+ const right = evaluateValue(arg[1], context, target);
152
91
  return left === right;
153
92
  }
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;
93
+ return compareResolvedValues(fieldVal, evaluateValue(arg, context, target));
164
94
  }
95
+ case "$neq":
96
+ return !compareResolvedValues(fieldVal, evaluateValue(arg, context, target));
165
97
  case "$gt":
166
- return fieldVal > resolveValue(arg, target, context);
98
+ return fieldVal > evaluateValue(arg, context, target);
167
99
  case "$gte":
168
- return fieldVal >= resolveValue(arg, target, context);
100
+ return fieldVal >= evaluateValue(arg, context, target);
169
101
  case "$lt":
170
- return fieldVal < resolveValue(arg, target, context);
102
+ return fieldVal < evaluateValue(arg, context, target);
171
103
  case "$lte":
172
- return fieldVal <= resolveValue(arg, target, context);
104
+ return fieldVal <= evaluateValue(arg, context, target);
173
105
  case "$between": {
174
106
  if (!Array.isArray(arg) || arg.length !== 2) {
175
107
  throw new Error("$between requires an array of [min, max]");
176
108
  }
177
- return fieldVal >= arg[0]
178
- && fieldVal <= arg[1];
109
+ return fieldVal >= evaluateValue(arg[0], context, target)
110
+ && fieldVal <= evaluateValue(arg[1], context, target);
111
+ }
112
+ case "$in": {
113
+ const resolved = evaluateValue(arg, context, target);
114
+ if (!Array.isArray(resolved))
115
+ return compareResolvedValues(fieldVal, resolved);
116
+ return compareResolvedValues(fieldVal, resolved);
179
117
  }
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))
118
+ case "$nin": {
119
+ const resolved = evaluateValue(arg, context, target);
120
+ if (!Array.isArray(resolved))
121
+ return !compareResolvedValues(fieldVal, resolved);
122
+ return !compareResolvedValues(fieldVal, resolved);
123
+ }
124
+ case "$ein": {
125
+ const resolved = evaluateValue(arg, context, target);
126
+ if (!Array.isArray(fieldVal) || !Array.isArray(resolved))
186
127
  return false;
187
- return fieldVal.every(v => arg.includes(v));
188
- case "$nein":
189
- if (!Array.isArray(fieldVal) || !Array.isArray(arg))
128
+ return fieldVal.every((v) => resolved.includes(v));
129
+ }
130
+ case "$nein": {
131
+ const resolved = evaluateValue(arg, context, target);
132
+ if (!Array.isArray(fieldVal) || !Array.isArray(resolved))
190
133
  return false;
191
- return fieldVal.some(v => !arg.includes(v));
134
+ return fieldVal.some((v) => !resolved.includes(v));
135
+ }
192
136
  case "$has":
193
- return typeof fieldVal === "string"
194
- && fieldVal.includes(String(resolveValue(arg, target, context)));
137
+ return typeof fieldVal === "string" && fieldVal.includes(String(evaluateValue(arg, context, target)));
195
138
  case "$nhas":
196
- return typeof fieldVal === "string"
197
- && !fieldVal.includes(String(resolveValue(arg, target, context)));
139
+ return typeof fieldVal === "string" && !fieldVal.includes(String(evaluateValue(arg, context, target)));
198
140
  case "$ihas": {
199
- const needle = String(resolveValue(arg, target, context)).toLowerCase();
141
+ const needle = String(evaluateValue(arg, context, target)).toLowerCase();
200
142
  return typeof fieldVal === "string" && fieldVal.toLowerCase().includes(needle);
201
143
  }
202
144
  case "$nihas": {
203
- const needle = String(resolveValue(arg, target, context)).toLowerCase();
145
+ const needle = String(evaluateValue(arg, context, target)).toLowerCase();
204
146
  return typeof fieldVal === "string" && !fieldVal.toLowerCase().includes(needle);
205
147
  }
206
148
  case "$sw":
207
- return typeof fieldVal === "string" && fieldVal.startsWith(String(arg));
149
+ return typeof fieldVal === "string" && fieldVal.startsWith(String(evaluateValue(arg, context, target)));
208
150
  case "$nsw":
209
- return typeof fieldVal === "string" && !fieldVal.startsWith(String(arg));
151
+ return typeof fieldVal === "string" && !fieldVal.startsWith(String(evaluateValue(arg, context, target)));
210
152
  case "$ew":
211
- return typeof fieldVal === "string" && fieldVal.endsWith(String(arg));
153
+ return typeof fieldVal === "string" && fieldVal.endsWith(String(evaluateValue(arg, context, target)));
212
154
  case "$new":
213
- return typeof fieldVal === "string" && !fieldVal.endsWith(String(arg));
155
+ return typeof fieldVal === "string" && !fieldVal.endsWith(String(evaluateValue(arg, context, target)));
214
156
  case "$regex":
215
- return new RegExp(String(arg)).test(String(fieldVal));
157
+ return new RegExp(String(evaluateValue(arg, context, target))).test(String(fieldVal));
216
158
  case "$iregex":
217
- return new RegExp(String(arg), "i").test(String(fieldVal));
159
+ return new RegExp(String(evaluateValue(arg, context, target)), "i").test(String(fieldVal));
218
160
  case "$exists":
219
161
  return arg ? fieldVal !== undefined : fieldVal === undefined;
220
162
  case "$and":
221
- return Array.isArray(arg) && arg.every(cond => evalCondition(fieldVal, cond, target, context));
163
+ return Array.isArray(arg) && arg.every((cond) => evalCondition(fieldVal, cond, target, context));
222
164
  case "$nand":
223
- return Array.isArray(arg) && !arg.every(cond => evalCondition(fieldVal, cond, target, context));
165
+ return Array.isArray(arg) && !arg.every((cond) => evalCondition(fieldVal, cond, target, context));
224
166
  case "$or":
225
- return Array.isArray(arg) && arg.some(cond => evalCondition(fieldVal, cond, target, context));
167
+ return Array.isArray(arg) && arg.some((cond) => evalCondition(fieldVal, cond, target, context));
226
168
  case "$nor":
227
- return Array.isArray(arg) && !arg.some(cond => evalCondition(fieldVal, cond, target, context));
169
+ return Array.isArray(arg) && !arg.some((cond) => evalCondition(fieldVal, cond, target, context));
228
170
  case "$not":
229
171
  return !evalCondition(fieldVal, arg, target, context);
230
172
  case "$some":
231
173
  if (!Array.isArray(fieldVal))
232
174
  return false;
233
- return fieldVal.some(item => matchesFilter((item ?? {}), (arg ?? {}), context));
175
+ return fieldVal.some((item) => matchesFilter((item ?? {}), (arg ?? {}), context));
234
176
  case "$nsome":
235
177
  if (!Array.isArray(fieldVal))
236
178
  return true;
237
- return !fieldVal.some(item => matchesFilter((item ?? {}), (arg ?? {}), context));
179
+ return !fieldVal.some((item) => matchesFilter((item ?? {}), (arg ?? {}), context));
238
180
  case "$every":
239
181
  if (!Array.isArray(fieldVal))
240
182
  return false;
241
- return fieldVal.every(item => matchesFilter((item ?? {}), (arg ?? {}), context));
183
+ return fieldVal.every((item) => matchesFilter((item ?? {}), (arg ?? {}), context));
242
184
  case "$nevery":
243
185
  if (!Array.isArray(fieldVal))
244
186
  return false;
245
- return !fieldVal.every(item => matchesFilter((item ?? {}), (arg ?? {}), context));
187
+ return !fieldVal.every((item) => matchesFilter((item ?? {}), (arg ?? {}), context));
246
188
  case "$length":
247
189
  case "$size": {
248
- const len = Array.isArray(fieldVal)
249
- ? fieldVal.length
250
- : (typeof fieldVal === "string" ? fieldVal.length : undefined);
190
+ const len = Array.isArray(fieldVal) ? fieldVal.length : (typeof fieldVal === "string" ? fieldVal.length : undefined);
251
191
  if (len === undefined)
252
192
  return false;
253
193
  return evalCondition(len, arg, target, context);
@@ -255,7 +195,7 @@ function evalOperator(op, fieldVal, arg, target, context) {
255
195
  case "$sum": {
256
196
  if (!Array.isArray(fieldVal))
257
197
  return false;
258
- const sum = fieldVal.reduce((a, b) => (Number(a) || 0) + (Number(b) || 0), 0);
198
+ const sum = fieldVal.reduce((acc, item) => (Number(acc) || 0) + (Number(item) || 0), 0);
259
199
  return evalCondition(sum, arg, target, context);
260
200
  }
261
201
  case "$date":
@@ -270,121 +210,149 @@ function evalOperator(op, fieldVal, arg, target, context) {
270
210
  throw new Error(`Unknown operator: ${op}`);
271
211
  }
272
212
  }
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 = {}) {
213
+ export function evaluateValue(expr, context = {}, target = {}) {
281
214
  if (expr === null || expr === undefined)
282
215
  return expr;
283
- if (typeof expr !== "object") {
284
- return resolveValue(expr, {}, context);
216
+ if (typeof expr === "string") {
217
+ return resolveContextRef(resolveTargetRef(expr, target), context);
285
218
  }
219
+ if (typeof expr !== "object")
220
+ return expr;
286
221
  if (Array.isArray(expr)) {
287
- return expr.map(e => evaluateValue(e, context));
222
+ return expr.map((item) => evaluateValue(item, context, target));
288
223
  }
289
224
  const obj = expr;
290
225
  const keys = Object.keys(obj);
226
+ if (keys.length === 1 && keys[0] === "$source") {
227
+ const source = obj["$source"];
228
+ const sourceResolver = context.sourceResolver;
229
+ if (!sourceResolver) {
230
+ throw new Error("$source requires context.sourceResolver");
231
+ }
232
+ const stack = context.sourceStack ?? [];
233
+ const sourceKey = buildSourceKey(source);
234
+ if (stack.includes(sourceKey)) {
235
+ throw new Error(`Cyclic $source reference detected for ${sourceKey}`);
236
+ }
237
+ return sourceResolver(source, { ...context, sourceStack: [...stack, sourceKey] });
238
+ }
291
239
  if (keys.includes("$first")) {
292
- return evalFirst(obj["$first"], context);
240
+ return evalFirst(obj["$first"], context, target);
293
241
  }
294
242
  if (keys.includes("$val") || keys.includes("$value")) {
295
243
  const val = obj["$val"] !== undefined ? obj["$val"] : obj["$value"];
296
244
  const rule = obj["$rule"];
297
- if (rule !== undefined && !evalRule(rule, context))
245
+ if (rule !== undefined && !evalRule(rule, context, target))
298
246
  return undefined;
299
- return evaluateValue(val, context);
247
+ return evaluateValue(val, context, target);
300
248
  }
301
249
  if (keys.includes("$concat")) {
302
250
  const list = obj["$concat"] || [];
303
- return list.map(s => String(evaluateValue(s, context))).join("");
251
+ return list.map((item) => String(evaluateValue(item, context, target))).join("");
304
252
  }
305
253
  if (keys.includes("$plus")) {
306
254
  const list = obj["$plus"] || [];
307
- return list.reduce((acc, v) => Number(acc) + Number(evaluateValue(v, context)), 0);
255
+ return list.reduce((acc, item) => Number(acc) + Number(evaluateValue(item, context, target)), 0);
308
256
  }
309
257
  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]);
258
+ const vals = (obj["$minus"] || []).map((item) => Number(evaluateValue(item, context, target)));
259
+ return vals.slice(1).reduce((acc, item) => acc - item, vals[0]);
312
260
  }
313
261
  if (keys.includes("$times")) {
314
262
  const list = obj["$times"] || [];
315
- return list.reduce((acc, v) => Number(acc) * Number(evaluateValue(v, context)), 1);
263
+ return list.reduce((acc, item) => Number(acc) * Number(evaluateValue(item, context, target)), 1);
316
264
  }
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]);
265
+ if (keys.includes("$div") || keys.includes("$divide")) {
266
+ const list = (obj["$div"] || obj["$divide"]) || [];
267
+ const vals = list.map((item) => Number(evaluateValue(item, context, target)));
268
+ return vals.slice(1).reduce((acc, item) => acc / item, vals[0]);
320
269
  }
321
270
  if (keys.includes("$min")) {
322
- const vals = (obj["$min"] || []).map(v => Number(evaluateValue(v, context)));
271
+ const vals = (obj["$min"] || []).map((item) => Number(evaluateValue(item, context, target)));
323
272
  return Math.min(...vals);
324
273
  }
325
274
  if (keys.includes("$max")) {
326
- const vals = (obj["$max"] || []).map(v => Number(evaluateValue(v, context)));
275
+ const vals = (obj["$max"] || []).map((item) => Number(evaluateValue(item, context, target)));
327
276
  return Math.max(...vals);
328
277
  }
329
278
  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;
279
+ const vals = (obj["$avg"] || []).map((item) => Number(evaluateValue(item, context, target)));
280
+ return vals.reduce((acc, item) => acc + item, 0) / vals.length;
332
281
  }
333
282
  if (keys.includes("$median")) {
334
- const sorted = [...(obj["$median"] || []).map(v => Number(evaluateValue(v, context)))]
335
- .sort((a, b) => a - b);
283
+ const sorted = [...(obj["$median"] || []).map((item) => Number(evaluateValue(item, context, target)))].sort((a, b) => a - b);
336
284
  const mid = Math.floor(sorted.length / 2);
337
- return sorted.length % 2 !== 0
338
- ? sorted[mid]
339
- : (sorted[mid - 1] + sorted[mid]) / 2;
285
+ return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
340
286
  }
341
287
  if (keys.includes("$last")) {
342
288
  const arr = obj["$last"];
343
- return Array.isArray(arr) ? evaluateValue(arr[arr.length - 1], context) : evaluateValue(arr, context);
289
+ return Array.isArray(arr) ? evaluateValue(arr[arr.length - 1], context, target) : evaluateValue(arr, context, target);
344
290
  }
345
- return expr;
291
+ return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, evaluateValue(value, context, target)]));
346
292
  }
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;
293
+ export function defineFilter(filter) {
294
+ return filter;
358
295
  }
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);
296
+ export function matchesFilter(target, filter, context = {}) {
297
+ if (filter === null || filter === undefined)
298
+ return true;
299
+ if (typeof filter !== "object")
300
+ return Boolean(filter);
301
+ const keys = Object.keys(filter);
302
+ for (const key of keys) {
303
+ const val = filter[key];
304
+ if (key === "$and") {
305
+ if (!Array.isArray(val) || !val.every((condition) => matchesFilter(target, condition, context)))
306
+ return false;
307
+ continue;
370
308
  }
309
+ if (key === "$nand") {
310
+ if (Array.isArray(val) && val.every((condition) => matchesFilter(target, condition, context)))
311
+ return false;
312
+ continue;
313
+ }
314
+ if (key === "$or") {
315
+ if (!Array.isArray(val) || !val.some((condition) => matchesFilter(target, condition, context)))
316
+ return false;
317
+ continue;
318
+ }
319
+ if (key === "$nor") {
320
+ if (Array.isArray(val) && val.some((condition) => matchesFilter(target, condition, context)))
321
+ return false;
322
+ continue;
323
+ }
324
+ if (key === "$not") {
325
+ if (matchesFilter(target, val, context))
326
+ return false;
327
+ continue;
328
+ }
329
+ if (key.startsWith("$ctx.") || (key.startsWith("$") && !key.startsWith("$this."))) {
330
+ const fieldVal = resolveContextRef(key, context);
331
+ if (!evalCondition(fieldVal, val, target, context))
332
+ return false;
333
+ continue;
334
+ }
335
+ if (key.startsWith("$this.")) {
336
+ const fieldVal = resolveTargetRef(key, target);
337
+ if (!evalCondition(fieldVal, val, target, context))
338
+ return false;
339
+ continue;
340
+ }
341
+ const optional = key.endsWith("?");
342
+ const cleanKey = optional ? key.slice(0, -1) : key;
343
+ const fieldVal = resolvePath(target, cleanKey);
344
+ if (optional && fieldVal === undefined)
345
+ continue;
346
+ if (!evalCondition(fieldVal, val, target, context))
347
+ return false;
371
348
  }
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);
349
+ return true;
382
350
  }
383
351
  /**
384
352
  * Vyfiltruje pole JSON dokumentu podle filtru.
385
353
  */
386
354
  export function filterArray(documents, filter, context = {}) {
387
- return documents.filter(doc => matchesFilter(doc, filter, context));
355
+ return documents.filter((doc) => matchesFilter(doc, filter, context));
388
356
  }
389
357
  /**
390
358
  * Vyfiltruje mapu JSON dokumentu (`Record<string, doc>`) podle filtru.
package/dist/typecheck.js CHANGED
@@ -1,4 +1,4 @@
1
- import { matchesFilter } from "./matchesFilter";
1
+ import { matchesFilter } from "./matchesFilter.js";
2
2
  const target = { price: 120, stock: 3, minPrice: 10 };
3
3
  const context = { minPrice: 20, maxPrice: 150 };
4
4
  matchesFilter(target, {
package/dist/types.d.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  /** Zakladni skalarny typ podporovany ve filtrech. */
2
2
  export type scalarType = string | number | boolean | null;
3
3
  /** Kontext predavany jako zdroj hodnot pro reference `$ctx.*`. */
4
- export type contextType = Record<string, unknown>;
4
+ export type contextType = Record<string, unknown> & {
5
+ /** Volitelny resolver pro dynamicke reference typu `$source`. */
6
+ sourceResolver?: (source: unknown, context: contextType) => unknown;
7
+ /** Interni stack pro detekci cyklu pri reseni `$source`. */
8
+ sourceStack?: string[];
9
+ };
5
10
  /** Cilovy JSON dokument, nad kterym se filtr vyhodnocuje. */
6
11
  export type targetType = Record<string, unknown>;
7
12
  type objectLikeType = Record<string, unknown>;
@@ -19,14 +24,30 @@ export type targetReferenceType<TTarget extends targetType> = `$this.${dotPathTy
19
24
  /** Reference na hodnotu z kontextu, napr. `$ctx.user.role`. */
20
25
  export type contextReferenceType<TContext extends contextType = contextType> = `$ctx.${dotPathType<TContext>}`;
21
26
  export type valueReferenceType<TTarget extends targetType = targetType, TContext extends contextType = contextType> = targetReferenceType<TTarget> | contextReferenceType<TContext>;
27
+ /** Zdrojova reference, ktera se vyhodnoti na jednu hodnotu nebo pole hodnot. */
28
+ export type sourceValueType<TTarget extends targetType = targetType, TContext extends contextType = contextType> = {
29
+ $source: {
30
+ dataProvider: string;
31
+ filter?: filterType<TTarget, TContext>;
32
+ val: string;
33
+ map?: never;
34
+ };
35
+ } | {
36
+ $source: {
37
+ dataProvider: string;
38
+ filter?: filterType<TTarget, TContext>;
39
+ map: string;
40
+ val?: never;
41
+ };
42
+ };
22
43
  /**
23
44
  * Stringová hodnota nebo reference s validací.
24
45
  * Pokud začíná `$this.` nebo `$ctx.`, musí být validní cesta.
25
46
  * Jinak je to libovolný string (ne-reference).
26
47
  */
27
48
  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>>;
49
+ type scalarOrReferenceType<TValue, TTarget extends targetType, TContext extends contextType> = TValue extends string ? stringOrValidReferenceType<TTarget, TContext> : scalarFieldValueType<TValue> | valueReferenceType<TTarget, TContext> | sourceValueType<TTarget, TContext>;
50
+ export type conditionType<TValue = unknown, TTarget extends targetType = targetType, TContext extends contextType = contextType> = scalarType | undefined | sourceValueType<TTarget, TContext> | conditionObjectType<TValue, TTarget, TContext> | Array<conditionType<TValue, TTarget, TContext>>;
30
51
  /** Objektova podoba podminky (operator -> hodnota). */
31
52
  export type conditionObjectType<TValue = unknown, TTarget extends targetType = targetType, TContext extends contextType = contextType> = {
32
53
  /**
@@ -47,13 +68,13 @@ export type conditionObjectType<TValue = unknown, TTarget extends targetType = t
47
68
  /** Rozsah vcetne hranic (`min <= field <= max`). */
48
69
  $between?: [unknown, unknown];
49
70
  /** Hodnota je v seznamu (`field in list`). */
50
- $in?: Array<scalarOrReferenceType<TValue, TTarget, TContext>>;
71
+ $in?: Array<scalarOrReferenceType<TValue, TTarget, TContext> | sourceValueType<TTarget, TContext>>;
51
72
  /** Hodnota neni v seznamu (`field not in list`). */
52
- $nin?: Array<scalarOrReferenceType<TValue, TTarget, TContext>>;
73
+ $nin?: Array<scalarOrReferenceType<TValue, TTarget, TContext> | sourceValueType<TTarget, TContext>>;
53
74
  /** Vsechny prvky pole jsou v seznamu. */
54
- $ein?: TValue extends readonly unknown[] ? Array<scalarOrReferenceType<arrayElementType<TValue>, TTarget, TContext>> : never;
75
+ $ein?: TValue extends readonly unknown[] ? Array<scalarOrReferenceType<arrayElementType<TValue>, TTarget, TContext> | sourceValueType<TTarget, TContext>> : never;
55
76
  /** Alespon jeden prvek pole neni v seznamu. */
56
- $nein?: TValue extends readonly unknown[] ? Array<scalarOrReferenceType<arrayElementType<TValue>, TTarget, TContext>> : never;
77
+ $nein?: TValue extends readonly unknown[] ? Array<scalarOrReferenceType<arrayElementType<TValue>, TTarget, TContext> | sourceValueType<TTarget, TContext>> : never;
57
78
  /** String obsahuje podretezec (case-sensitive). */
58
79
  $has?: scalarOrReferenceType<string, TTarget, TContext>;
59
80
  /** String neobsahuje podretezec (case-sensitive). */
@@ -100,7 +121,13 @@ export type conditionObjectType<TValue = unknown, TTarget extends targetType = t
100
121
  $size?: conditionType<number, TTarget, TContext>;
101
122
  /** Soucet prvku pole a porovnani vysledku. */
102
123
  $sum?: conditionType<number, TTarget, TContext>;
103
- /** Porovnani s aktualnim datem (YYYY-MM-DD). */
124
+ /** Porovnani s aktualnim datem (YYYY-MM-DD).
125
+ * @example
126
+ * // Porovná, jestli pole `dateFinish` je dříve než dnešní datum:
127
+ * {
128
+ * dateFinish: { $lte: { $date: {} } as any },
129
+ * }
130
+ */
104
131
  $date?: conditionType<string, TTarget, TContext>;
105
132
  /** Porovnani s aktualnim casem (HH:mm:ss). */
106
133
  $time?: conditionType<string, TTarget, TContext>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mgarlik/json-filter",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "JSON filter engine for backend and frontend applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",