@dizmo/dcs-client-library 4.2.3 → 4.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -10,3 +10,4 @@ export { needs } from "./modules/needs";
10
10
  export { packages } from "./modules/packages";
11
11
  export { translations } from "./modules/translations";
12
12
  export { components } from "./modules/components";
13
+ export { filterLogic } from "./modules/filterLogic";
package/dist/index.js CHANGED
@@ -14,3 +14,4 @@ export { needs } from "./modules/needs";
14
14
  export { packages } from "./modules/packages";
15
15
  export { translations } from "./modules/translations";
16
16
  export { components } from "./modules/components";
17
+ export { filterLogic } from "./modules/filterLogic";
@@ -0,0 +1,30 @@
1
+ import type { Filter, OperatorKey, OperatorDef, FilterCondition, AttrCategory } from "../types/types.js";
2
+ declare function getCategory(attrType: string): AttrCategory;
3
+ declare function getOperatorDefs(attrType: string): OperatorDef[];
4
+ declare function getDefaultOperator(attrType: string): OperatorKey;
5
+ declare function fieldPath(attributeId: string): string;
6
+ declare function stripFieldPath(path: string): string;
7
+ declare function coerceValue(raw: string, attrType: string): string | number | boolean | null;
8
+ declare function formatDatetimeLocal(value: number): string;
9
+ declare function buildFilterFromCondition(condition: FilterCondition, attrType: string): Filter | null;
10
+ declare function buildFilter(conditions: FilterCondition[], attrTypes: Record<string, string>): Filter | null;
11
+ declare function parseFilter(filter: Filter, attrTypes: Record<string, string>): FilterCondition[];
12
+ declare function decomposeFilter(filter: Filter): Map<string, Filter>;
13
+ declare function composeFilter(attrFilters: Map<string, Filter | null>): Filter | null;
14
+ declare function assertExhaustive(x: never, message?: string): never;
15
+ export declare const filterLogic: {
16
+ getCategory: typeof getCategory;
17
+ getOperatorDefs: typeof getOperatorDefs;
18
+ getDefaultOperator: typeof getDefaultOperator;
19
+ buildFilterFromCondition: typeof buildFilterFromCondition;
20
+ buildFilter: typeof buildFilter;
21
+ parseFilter: typeof parseFilter;
22
+ decomposeFilter: typeof decomposeFilter;
23
+ composeFilter: typeof composeFilter;
24
+ coerceValue: typeof coerceValue;
25
+ fieldPath: typeof fieldPath;
26
+ stripFieldPath: typeof stripFieldPath;
27
+ formatDatetimeLocal: typeof formatDatetimeLocal;
28
+ assertExhaustive: typeof assertExhaustive;
29
+ };
30
+ export {};
@@ -0,0 +1,582 @@
1
+ // ─── Category mapping ────────────────────────────────────────────
2
+ function getCategory(attrType) {
3
+ switch (attrType) {
4
+ case "integer":
5
+ case "float":
6
+ case "number":
7
+ case "slider":
8
+ case "stars":
9
+ case "datetime":
10
+ return "numeric";
11
+ case "string":
12
+ case "text":
13
+ case "heading1":
14
+ case "url":
15
+ case "email":
16
+ case "phone":
17
+ case "markdown":
18
+ return "text";
19
+ case "color":
20
+ return "color";
21
+ case "image_url":
22
+ case "document_url":
23
+ return "existence";
24
+ case "date":
25
+ return "date";
26
+ case "checkbox":
27
+ return "boolean";
28
+ case "selectbox":
29
+ case "radiobuttons":
30
+ return "select";
31
+ case "multi_selectbox":
32
+ case "multi_checkbox":
33
+ case "tags":
34
+ return "multi";
35
+ case "user":
36
+ return "user";
37
+ case "location":
38
+ return "skip";
39
+ default:
40
+ return "text";
41
+ }
42
+ }
43
+ // ─── Operator definitions ────────────────────────────────────────
44
+ const CATEGORY_OPERATORS = {
45
+ numeric: [
46
+ { key: "equals", labelKey: "js_op-equals", labelFallback: "Equals", valueCount: 1 },
47
+ { key: "notEquals", labelKey: "js_op-is-not", labelFallback: "Is Not", valueCount: 1 },
48
+ { key: "greater", labelKey: "js_op-greater-than", labelFallback: "Greater Than", valueCount: 1 },
49
+ { key: "greaterOrEqual", labelKey: "js_op-greater-or-equal", labelFallback: "Greater or Equal", valueCount: 1 },
50
+ { key: "less", labelKey: "js_op-less-than", labelFallback: "Less Than", valueCount: 1 },
51
+ { key: "lessOrEqual", labelKey: "js_op-less-or-equal", labelFallback: "Less or Equal", valueCount: 1 },
52
+ { key: "between", labelKey: "js_op-between", labelFallback: "Between", valueCount: 2 },
53
+ { key: "notBetween", labelKey: "js_op-not-between", labelFallback: "Not Between", valueCount: 2 },
54
+ { key: "empty", labelKey: "js_op-is-empty", labelFallback: "Is Empty", valueCount: 0 },
55
+ { key: "notEmpty", labelKey: "js_op-is-not-empty", labelFallback: "Is Not Empty", valueCount: 0 },
56
+ ],
57
+ text: [
58
+ { key: "equals", labelKey: "js_op-equals", labelFallback: "Equals", valueCount: 1 },
59
+ { key: "notEquals", labelKey: "js_op-is-not", labelFallback: "Is Not", valueCount: 1 },
60
+ { key: "stringContains", labelKey: "js_op-contains", labelFallback: "Contains", valueCount: 1 },
61
+ { key: "startsWith", labelKey: "js_op-starts-with", labelFallback: "Starts With", valueCount: 1 },
62
+ { key: "endsWith", labelKey: "js_op-ends-with", labelFallback: "Ends With", valueCount: 1 },
63
+ { key: "empty", labelKey: "js_op-is-empty", labelFallback: "Is Empty", valueCount: 0 },
64
+ { key: "notEmpty", labelKey: "js_op-is-not-empty", labelFallback: "Is Not Empty", valueCount: 0 },
65
+ ],
66
+ date: [
67
+ { key: "equals", labelKey: "js_op-equals", labelFallback: "Equals", valueCount: 1 },
68
+ { key: "notEquals", labelKey: "js_op-is-not", labelFallback: "Is Not", valueCount: 1 },
69
+ { key: "greater", labelKey: "js_op-after", labelFallback: "After", valueCount: 1 },
70
+ { key: "greaterOrEqual", labelKey: "js_op-on-or-after", labelFallback: "On or After", valueCount: 1 },
71
+ { key: "less", labelKey: "js_op-before", labelFallback: "Before", valueCount: 1 },
72
+ { key: "lessOrEqual", labelKey: "js_op-on-or-before", labelFallback: "On or Before", valueCount: 1 },
73
+ { key: "between", labelKey: "js_op-between", labelFallback: "Between", valueCount: 2 },
74
+ { key: "notBetween", labelKey: "js_op-not-between", labelFallback: "Not Between", valueCount: 2 },
75
+ { key: "empty", labelKey: "js_op-is-not-selected", labelFallback: "Is Not Selected", valueCount: 0 },
76
+ { key: "notEmpty", labelKey: "js_op-is-selected", labelFallback: "Is Selected", valueCount: 0 },
77
+ ],
78
+ boolean: [
79
+ { key: "is_true", labelKey: "js_op-is-true", labelFallback: "Is True", valueCount: 0 },
80
+ { key: "is_false", labelKey: "js_op-is-false", labelFallback: "Is False", valueCount: 0 },
81
+ { key: "defined", labelKey: "js_op-is-defined", labelFallback: "Is Defined", valueCount: 0 },
82
+ { key: "undefined", labelKey: "js_op-is-not-defined", labelFallback: "Is Not Defined", valueCount: 0 },
83
+ ],
84
+ select: [
85
+ { key: "equals", labelKey: "js_op-equals", labelFallback: "Equals", valueCount: 1 },
86
+ { key: "notEquals", labelKey: "js_op-is-not", labelFallback: "Is Not", valueCount: 1 },
87
+ { key: "empty", labelKey: "js_op-is-not-selected", labelFallback: "Is Not Selected", valueCount: 0 },
88
+ { key: "notEmpty", labelKey: "js_op-is-selected", labelFallback: "Is Selected", valueCount: 0 },
89
+ ],
90
+ multi: [
91
+ { key: "arrayContains", labelKey: "js_op-contains", labelFallback: "Contains", valueCount: 1 },
92
+ { key: "notArrayContains", labelKey: "js_op-not-has", labelFallback: "Not Has", valueCount: 1 },
93
+ { key: "length", labelKey: "js_op-length", labelFallback: "Length", valueCount: 1 },
94
+ { key: "empty", labelKey: "js_op-is-not-selected", labelFallback: "Is Not Selected", valueCount: 0 },
95
+ { key: "notEmpty", labelKey: "js_op-is-selected", labelFallback: "Is Selected", valueCount: 0 },
96
+ ],
97
+ user: [
98
+ { key: "equals", labelKey: "js_op-equals", labelFallback: "Equals", valueCount: 1 },
99
+ { key: "notEquals", labelKey: "js_op-is-not", labelFallback: "Is Not", valueCount: 1 },
100
+ ],
101
+ existence: [
102
+ { key: "notEmpty", labelKey: "js_op-exists", labelFallback: "Exists", valueCount: 0 },
103
+ { key: "empty", labelKey: "js_op-does-not-exist", labelFallback: "Does not Exist", valueCount: 0 },
104
+ ],
105
+ color: [
106
+ { key: "equals", labelKey: "js_op-equals", labelFallback: "Equals", valueCount: 1 },
107
+ { key: "notEquals", labelKey: "js_op-is-not", labelFallback: "Is Not", valueCount: 1 },
108
+ { key: "notEmpty", labelKey: "js_op-exists", labelFallback: "Exists", valueCount: 0 },
109
+ { key: "empty", labelKey: "js_op-does-not-exist", labelFallback: "Does not Exist", valueCount: 0 },
110
+ ],
111
+ skip: [],
112
+ };
113
+ // Per-type overrides where a type diverges from its category
114
+ const TYPE_OVERRIDES = {
115
+ slider: CATEGORY_OPERATORS.numeric.filter((op) => op.key !== "empty" && op.key !== "notEmpty"),
116
+ radiobuttons: CATEGORY_OPERATORS.select.filter((op) => op.key !== "empty" && op.key !== "notEmpty"),
117
+ };
118
+ function getOperatorDefs(attrType) {
119
+ if (attrType in TYPE_OVERRIDES)
120
+ return TYPE_OVERRIDES[attrType];
121
+ return CATEGORY_OPERATORS[getCategory(attrType)];
122
+ }
123
+ // ─── Default operators ───────────────────────────────────────────
124
+ const DEFAULT_OPERATOR = {
125
+ string: "stringContains",
126
+ text: "stringContains",
127
+ heading1: "stringContains",
128
+ phone: "stringContains",
129
+ url: "stringContains",
130
+ email: "stringContains",
131
+ markdown: "stringContains",
132
+ number: "between",
133
+ integer: "between",
134
+ float: "between",
135
+ datetime: "between",
136
+ date: "between",
137
+ slider: "between",
138
+ checkbox: "is_true",
139
+ selectbox: "equals",
140
+ radiobuttons: "equals",
141
+ multi_checkbox: "arrayContains",
142
+ multi_selectbox: "arrayContains",
143
+ tags: "arrayContains",
144
+ color: "equals",
145
+ user: "equals",
146
+ image_url: "notEmpty",
147
+ document_url: "notEmpty",
148
+ stars: "equals",
149
+ };
150
+ function getDefaultOperator(attrType) {
151
+ return DEFAULT_OPERATOR[attrType] ?? "equals";
152
+ }
153
+ // ─── Path helpers ────────────────────────────────────────────────
154
+ function fieldPath(attributeId) {
155
+ return `attributes.${attributeId}`;
156
+ }
157
+ function stripFieldPath(path) {
158
+ return path.startsWith("attributes.") ? path.slice("attributes.".length) : path;
159
+ }
160
+ // ─── Value helpers ───────────────────────────────────────────────
161
+ function coerceValue(raw, attrType) {
162
+ const cat = getCategory(attrType);
163
+ if (cat === "numeric") {
164
+ if (attrType === "datetime" && raw)
165
+ return +new Date(raw);
166
+ return raw === "" ? 0 : Number(raw);
167
+ }
168
+ return raw;
169
+ }
170
+ function formatDatetimeLocal(value) {
171
+ const d = new Date(value);
172
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}T${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
173
+ }
174
+ // ─── Build filter from a single condition ────────────────────────
175
+ function buildFilterFromCondition(condition, attrType) {
176
+ const path = fieldPath(condition.attributeId);
177
+ const op = condition.operator;
178
+ const cat = getCategory(attrType);
179
+ const raw1 = condition.values[0] ?? "";
180
+ const raw2 = condition.values[1] ?? "";
181
+ const val1 = coerceValue(raw1, attrType);
182
+ const val2 = coerceValue(raw2, attrType);
183
+ switch (op) {
184
+ case "equals":
185
+ return { equals: [path, val1] };
186
+ case "greater":
187
+ return { greater: [path, val1] };
188
+ case "greaterOrEqual":
189
+ return { greaterOrEqual: [path, val1] };
190
+ case "less":
191
+ return { less: [path, val1] };
192
+ case "lessOrEqual":
193
+ return { lessOrEqual: [path, val1] };
194
+ case "between":
195
+ return {
196
+ and: [
197
+ { greaterOrEqual: [path, val1] },
198
+ { lessOrEqual: [path, val2] },
199
+ ],
200
+ };
201
+ case "notBetween":
202
+ return {
203
+ not: {
204
+ and: [
205
+ { greaterOrEqual: [path, val1] },
206
+ { lessOrEqual: [path, val2] },
207
+ ],
208
+ },
209
+ };
210
+ case "stringContains":
211
+ return { stringContains: [path, String(val1)] };
212
+ case "startsWith":
213
+ return { startsWith: [path, String(val1)] };
214
+ case "endsWith":
215
+ return { endsWith: [path, String(val1)] };
216
+ case "arrayContains":
217
+ return { arrayContains: [path, val1] };
218
+ case "notArrayContains":
219
+ return { not: { arrayContains: [path, val1] } };
220
+ case "length":
221
+ return { length: [path, Number(val1) || 0] };
222
+ case "notEquals":
223
+ return { not: { equals: [path, val1] } };
224
+ case "empty":
225
+ if (cat === "multi")
226
+ return { length: [path, 0] };
227
+ return { equals: [path, ""] };
228
+ case "notEmpty":
229
+ if (cat === "multi")
230
+ return { not: { length: [path, 0] } };
231
+ return { not: { equals: [path, ""] } };
232
+ case "is_true":
233
+ return { equals: [path, true] };
234
+ case "is_false":
235
+ return { equals: [path, false] };
236
+ case "defined":
237
+ return { or: [{ equals: [path, true] }, { equals: [path, false] }] };
238
+ case "undefined":
239
+ return { not: { or: [{ equals: [path, true] }, { equals: [path, false] }] } };
240
+ default:
241
+ return null;
242
+ }
243
+ }
244
+ // ─── Build composed filter from multiple conditions ──────────────
245
+ function buildFilter(conditions, attrTypes) {
246
+ const parts = [];
247
+ for (const cond of conditions) {
248
+ const attrType = attrTypes[cond.attributeId];
249
+ if (!attrType)
250
+ continue;
251
+ const f = buildFilterFromCondition(cond, attrType);
252
+ if (f !== null)
253
+ parts.push(f);
254
+ }
255
+ if (parts.length === 0)
256
+ return null;
257
+ if (parts.length === 1)
258
+ return parts[0];
259
+ return { and: parts };
260
+ }
261
+ // ─── Parse filter → conditions ───────────────────────────────────
262
+ function parseFilter(filter, attrTypes) {
263
+ if (filter === null || filter === undefined)
264
+ return [];
265
+ let conditions;
266
+ if (typeof filter === "object" && "and" in filter) {
267
+ conditions = filter.and;
268
+ }
269
+ else {
270
+ conditions = [filter];
271
+ }
272
+ const result = [];
273
+ const consumed = new Set();
274
+ for (let i = 0; i < conditions.length; i++) {
275
+ if (consumed.has(i))
276
+ continue;
277
+ const cond = conditions[i];
278
+ if (cond === null || cond === undefined)
279
+ continue;
280
+ // Detect "between" sibling form: greaterOrEqual + lessOrEqual on same field
281
+ if (typeof cond === "object" && "greaterOrEqual" in cond) {
282
+ const [path, val1] = cond.greaterOrEqual;
283
+ const pairIdx = conditions.findIndex((c, j) => j > i &&
284
+ !consumed.has(j) &&
285
+ c !== null &&
286
+ c !== undefined &&
287
+ typeof c === "object" &&
288
+ "lessOrEqual" in c &&
289
+ c.lessOrEqual[0] === path);
290
+ if (pairIdx !== -1) {
291
+ const pair = conditions[pairIdx];
292
+ consumed.add(pairIdx);
293
+ const attrId = stripFieldPath(path);
294
+ const attrType = attrTypes[attrId];
295
+ result.push({
296
+ attributeId: attrId,
297
+ operator: "between",
298
+ values: [
299
+ formatValueForRestore(val1, attrType),
300
+ formatValueForRestore(pair.lessOrEqual[1], attrType),
301
+ ],
302
+ });
303
+ continue;
304
+ }
305
+ }
306
+ // Detect "between" nested form: { and: [{ greaterOrEqual: ... }, { lessOrEqual: ... }] }
307
+ if (typeof cond === "object" && "and" in cond) {
308
+ const children = cond.and;
309
+ const ge = children.find((f) => f !== null && typeof f === "object" && "greaterOrEqual" in f);
310
+ const le = children.find((f) => f !== null && typeof f === "object" && "lessOrEqual" in f);
311
+ if (ge && le && ge.greaterOrEqual[0] === le.lessOrEqual[0]) {
312
+ const attrId = stripFieldPath(ge.greaterOrEqual[0]);
313
+ const attrType = attrTypes[attrId];
314
+ result.push({
315
+ attributeId: attrId,
316
+ operator: "between",
317
+ values: [
318
+ formatValueForRestore(ge.greaterOrEqual[1], attrType),
319
+ formatValueForRestore(le.lessOrEqual[1], attrType),
320
+ ],
321
+ });
322
+ continue;
323
+ }
324
+ // Multi arrayContains nested form: { and: [{ arrayContains: ... }, ...] }
325
+ if (children.length > 0 &&
326
+ children.every((f) => f !== null && typeof f === "object" && "arrayContains" in f)) {
327
+ const first = children[0];
328
+ const attrId = stripFieldPath(first.arrayContains[0]);
329
+ // Each child is a separate arrayContains condition
330
+ for (const child of children) {
331
+ const ac = child;
332
+ result.push({
333
+ attributeId: attrId,
334
+ operator: "arrayContains",
335
+ values: [String(ac.arrayContains[1])],
336
+ });
337
+ }
338
+ continue;
339
+ }
340
+ }
341
+ // Simple comparison operators
342
+ const simpleKeys = [
343
+ "equals",
344
+ "greater",
345
+ "greaterOrEqual",
346
+ "less",
347
+ "lessOrEqual",
348
+ "stringContains",
349
+ "startsWith",
350
+ "endsWith",
351
+ "arrayContains",
352
+ "length",
353
+ ];
354
+ let matched = false;
355
+ for (const key of simpleKeys) {
356
+ if (typeof cond !== "object" || !(key in cond))
357
+ continue;
358
+ matched = true;
359
+ const [path, val] = cond[key];
360
+ const attrId = stripFieldPath(path);
361
+ const attrType = attrTypes[attrId];
362
+ const cat = attrType ? getCategory(attrType) : "text";
363
+ // Boolean: equals true/false → is_true/is_false
364
+ if (key === "equals" && cat === "boolean") {
365
+ result.push({
366
+ attributeId: attrId,
367
+ operator: val ? "is_true" : "is_false",
368
+ values: [],
369
+ });
370
+ break;
371
+ }
372
+ // Empty: equals "" or null → empty
373
+ if (key === "equals" && (val === "" || val === null)) {
374
+ result.push({ attributeId: attrId, operator: "empty", values: [] });
375
+ break;
376
+ }
377
+ // Empty: length 0 on multi → empty
378
+ if (key === "length" && val === 0 && cat === "multi") {
379
+ result.push({ attributeId: attrId, operator: "empty", values: [] });
380
+ break;
381
+ }
382
+ result.push({
383
+ attributeId: attrId,
384
+ operator: key,
385
+ values: [formatValueForRestore(val, attrType)],
386
+ });
387
+ break;
388
+ }
389
+ if (matched)
390
+ continue;
391
+ // not → notEquals, notEmpty, notArrayContains, notBetween, undefined
392
+ if (typeof cond === "object" && "not" in cond) {
393
+ const inner = cond.not;
394
+ if (!inner || typeof inner !== "object")
395
+ continue;
396
+ if ("equals" in inner) {
397
+ const [path, val] = inner.equals;
398
+ const attrId = stripFieldPath(path);
399
+ if (val === "" || val === null) {
400
+ result.push({ attributeId: attrId, operator: "notEmpty", values: [] });
401
+ }
402
+ else {
403
+ const attrType = attrTypes[attrId];
404
+ result.push({
405
+ attributeId: attrId,
406
+ operator: "notEquals",
407
+ values: [formatValueForRestore(val, attrType)],
408
+ });
409
+ }
410
+ continue;
411
+ }
412
+ if ("length" in inner) {
413
+ const [path] = inner.length;
414
+ result.push({ attributeId: stripFieldPath(path), operator: "notEmpty", values: [] });
415
+ continue;
416
+ }
417
+ if ("arrayContains" in inner) {
418
+ const [path, val] = inner.arrayContains;
419
+ result.push({
420
+ attributeId: stripFieldPath(path),
421
+ operator: "notArrayContains",
422
+ values: [String(val)],
423
+ });
424
+ continue;
425
+ }
426
+ if ("and" in inner) {
427
+ const items = inner.and;
428
+ const ge = items.find((f) => f !== null && typeof f === "object" && "greaterOrEqual" in f);
429
+ const le = items.find((f) => f !== null && typeof f === "object" && "lessOrEqual" in f);
430
+ if (ge && le) {
431
+ const attrId = stripFieldPath(ge.greaterOrEqual[0]);
432
+ const attrType = attrTypes[attrId];
433
+ result.push({
434
+ attributeId: attrId,
435
+ operator: "notBetween",
436
+ values: [
437
+ formatValueForRestore(ge.greaterOrEqual[1], attrType),
438
+ formatValueForRestore(le.lessOrEqual[1], attrType),
439
+ ],
440
+ });
441
+ }
442
+ else if (items.length > 0 &&
443
+ items.every((f) => f !== null && typeof f === "object" && "arrayContains" in f)) {
444
+ // notArrayContains multi-value: { not: { and: [arrayContains, ...] } }
445
+ const first = items[0];
446
+ const attrId = stripFieldPath(first.arrayContains[0]);
447
+ for (const child of items) {
448
+ const ac = child;
449
+ result.push({
450
+ attributeId: attrId,
451
+ operator: "notArrayContains",
452
+ values: [String(ac.arrayContains[1])],
453
+ });
454
+ }
455
+ }
456
+ continue;
457
+ }
458
+ if ("or" in inner) {
459
+ const items = inner.or;
460
+ if (items.length === 2) {
461
+ const eq1 = items.find((f) => f !== null && typeof f === "object" && "equals" in f && f.equals[1] === true);
462
+ const eq2 = items.find((f) => f !== null && typeof f === "object" && "equals" in f && f.equals[1] === false);
463
+ if (eq1 && eq2) {
464
+ result.push({
465
+ attributeId: stripFieldPath(eq1.equals[0]),
466
+ operator: "undefined",
467
+ values: [],
468
+ });
469
+ }
470
+ }
471
+ continue;
472
+ }
473
+ continue;
474
+ }
475
+ // or: [equals true, equals false] → defined
476
+ if (typeof cond === "object" && "or" in cond) {
477
+ const items = cond.or;
478
+ if (items.length === 2) {
479
+ const eq1 = items.find((f) => f !== null && typeof f === "object" && "equals" in f && f.equals[1] === true);
480
+ const eq2 = items.find((f) => f !== null && typeof f === "object" && "equals" in f && f.equals[1] === false);
481
+ if (eq1 && eq2) {
482
+ result.push({
483
+ attributeId: stripFieldPath(eq1.equals[0]),
484
+ operator: "defined",
485
+ values: [],
486
+ });
487
+ }
488
+ }
489
+ continue;
490
+ }
491
+ }
492
+ return result;
493
+ }
494
+ /** Format a raw filter value back to a string suitable for UI inputs */
495
+ function formatValueForRestore(val, attrType) {
496
+ if (attrType === "datetime" && typeof val === "number" && val > 0) {
497
+ return formatDatetimeLocal(val);
498
+ }
499
+ return String(val ?? "");
500
+ }
501
+ // ─── Decompose / compose filters by field path ──────────────────
502
+ function getFilterFieldPath(filter) {
503
+ if (!filter || typeof filter !== "object")
504
+ return null;
505
+ if ("not" in filter)
506
+ return getFilterFieldPath(filter.not);
507
+ if ("and" in filter) {
508
+ const children = filter.and;
509
+ return children.length > 0 ? getFilterFieldPath(children[0]) : null;
510
+ }
511
+ if ("or" in filter) {
512
+ const children = filter.or;
513
+ return children.length > 0 ? getFilterFieldPath(children[0]) : null;
514
+ }
515
+ const key = Object.keys(filter)[0];
516
+ if (!key)
517
+ return null;
518
+ const args = filter[key];
519
+ return Array.isArray(args) ? args[0] : null;
520
+ }
521
+ function decomposeFilter(filter) {
522
+ const map = new Map();
523
+ if (!filter)
524
+ return map;
525
+ if (typeof filter === "object" && "and" in filter && Array.isArray(filter.and)) {
526
+ const byPath = new Map();
527
+ for (const child of filter.and) {
528
+ const fp = getFilterFieldPath(child);
529
+ if (fp) {
530
+ if (!byPath.has(fp))
531
+ byPath.set(fp, []);
532
+ byPath.get(fp).push(child);
533
+ }
534
+ }
535
+ for (const [fp, filters] of byPath) {
536
+ if (filters.length === 1) {
537
+ map.set(fp, filters[0]);
538
+ }
539
+ else {
540
+ map.set(fp, { and: filters });
541
+ }
542
+ }
543
+ }
544
+ else {
545
+ const fp = getFilterFieldPath(filter);
546
+ if (fp)
547
+ map.set(fp, filter);
548
+ }
549
+ return map;
550
+ }
551
+ function composeFilter(attrFilters) {
552
+ const parts = [];
553
+ for (const filter of attrFilters.values()) {
554
+ if (filter !== null)
555
+ parts.push(filter);
556
+ }
557
+ if (parts.length === 0)
558
+ return null;
559
+ if (parts.length === 1)
560
+ return parts[0];
561
+ return { and: parts };
562
+ }
563
+ // ─── Exhaustive check helper ─────────────────────────────────────
564
+ function assertExhaustive(x, message) {
565
+ throw new Error(message ?? `Unhandled filter case: ${JSON.stringify(x)}`);
566
+ }
567
+ // ─── Public API ──────────────────────────────────────────────────
568
+ export const filterLogic = {
569
+ getCategory,
570
+ getOperatorDefs,
571
+ getDefaultOperator,
572
+ buildFilterFromCondition,
573
+ buildFilter,
574
+ parseFilter,
575
+ decomposeFilter,
576
+ composeFilter,
577
+ coerceValue,
578
+ fieldPath,
579
+ stripFieldPath,
580
+ formatDatetimeLocal,
581
+ assertExhaustive,
582
+ };
@@ -28,6 +28,19 @@ export type Filter = {
28
28
  } | {
29
29
  in: [string, string[]];
30
30
  } | null;
31
+ export type OperatorKey = "equals" | "notEquals" | "greater" | "greaterOrEqual" | "less" | "lessOrEqual" | "between" | "notBetween" | "empty" | "notEmpty" | "stringContains" | "startsWith" | "endsWith" | "arrayContains" | "notArrayContains" | "length" | "is_true" | "is_false" | "defined" | "undefined";
32
+ export type OperatorDef = {
33
+ key: OperatorKey;
34
+ labelKey: string;
35
+ labelFallback: string;
36
+ valueCount: 0 | 1 | 2;
37
+ };
38
+ export type FilterCondition = {
39
+ attributeId: string;
40
+ operator: OperatorKey;
41
+ values: string[];
42
+ };
43
+ export type AttrCategory = "numeric" | "text" | "date" | "boolean" | "select" | "multi" | "user" | "existence" | "color" | "skip";
31
44
  export type AllocationFilter = {
32
45
  root?: Filter;
33
46
  relationship?: Filter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dizmo/dcs-client-library",
3
- "version": "4.2.3",
3
+ "version": "4.2.4",
4
4
  "description": "A library that allows connection and management of dcs",
5
5
  "source": "source/index.ts",
6
6
  "main": "dist/index.js",