@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.
@@ -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,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
- * 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 getCurrentDate() {
31
+ return new Date().toISOString().slice(0, 10);
106
32
  }
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;
33
+ function getCurrentTime() {
34
+ return new Date().toTimeString().slice(0, 8);
115
35
  }
116
- function evalAnd(target, arr, context) {
117
- return arr.every(cond => matchesFilter(target, cond, context));
36
+ function getCurrentDatetime() {
37
+ return new Date().toISOString().replace("T", " ").slice(0, 19);
118
38
  }
119
- function evalOr(target, arr, context) {
120
- return arr.some(cond => matchesFilter(target, cond, context));
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
- if (Array.isArray(fieldVal))
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 === condition;
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 = resolveValue(arg[0], target, context);
151
- const right = resolveValue(arg[1], target, context);
84
+ const left = evaluateValue(arg[0], context, target);
85
+ const right = evaluateValue(arg[1], context, target);
152
86
  return left === right;
153
87
  }
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;
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 > resolveValue(arg, target, context);
93
+ return fieldVal > evaluateValue(arg, context, target);
167
94
  case "$gte":
168
- return fieldVal >= resolveValue(arg, target, context);
95
+ return fieldVal >= evaluateValue(arg, context, target);
169
96
  case "$lt":
170
- return fieldVal < resolveValue(arg, target, context);
97
+ return fieldVal < evaluateValue(arg, context, target);
171
98
  case "$lte":
172
- return fieldVal <= resolveValue(arg, target, context);
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
- 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))
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 => arg.includes(v));
188
- case "$nein":
189
- if (!Array.isArray(fieldVal) || !Array.isArray(arg))
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 => !arg.includes(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(resolveValue(arg, target, context)).toLowerCase();
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(resolveValue(arg, target, context)).toLowerCase();
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((a, b) => (Number(a) || 0) + (Number(b) || 0), 0);
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 !== "object") {
284
- return resolveValue(expr, {}, context);
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(e => evaluateValue(e, context));
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(s => String(evaluateValue(s, context))).join("");
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, v) => Number(acc) + Number(evaluateValue(v, context)), 0);
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(v => Number(evaluateValue(v, context)));
311
- return vals.slice(1).reduce((acc, v) => acc - v, vals[0]);
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, v) => Number(acc) * Number(evaluateValue(v, context)), 1);
248
+ return list.reduce((acc, item) => Number(acc) * Number(evaluateValue(item, context, target)), 1);
316
249
  }
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]);
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(v => Number(evaluateValue(v, context)));
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(v => Number(evaluateValue(v, context)));
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(v => Number(evaluateValue(v, context)));
331
- return vals.reduce((a, b) => a + b, 0) / vals.length;
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(v => Number(evaluateValue(v, context)))]
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 expr;
276
+ return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, evaluateValue(value, context, target)]));
346
277
  }
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;
278
+ export function defineFilter(filter) {
279
+ return filter;
358
280
  }
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);
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 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);
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
@@ -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
@@ -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>>;
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.1",
4
4
  "description": "JSON filter engine for backend and frontend applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",