@adminforth/dashboard 1.7.0 → 1.9.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.
Files changed (33) hide show
  1. package/README.md +81 -55
  2. package/custom/model/dashboard.types.ts +17 -9
  3. package/custom/skills/adminforth-dashboard/SKILL.md +28 -12
  4. package/dist/custom/model/dashboard.types.d.ts +15 -8
  5. package/dist/custom/model/dashboard.types.ts +17 -9
  6. package/dist/custom/queries/useDashboardConfig.d.ts +222 -4
  7. package/dist/custom/queries/useWidgetData.d.ts +222 -4
  8. package/dist/custom/skills/adminforth-dashboard/SKILL.md +28 -12
  9. package/dist/schema/api.d.ts +5440 -941
  10. package/dist/schema/api.js +2 -2
  11. package/dist/schema/widget.d.ts +432 -23
  12. package/dist/schema/widget.js +1 -1
  13. package/dist/schema/widgets/charts.d.ts +558 -28
  14. package/dist/schema/widgets/charts.js +2 -2
  15. package/dist/schema/widgets/common.d.ts +17 -6
  16. package/dist/schema/widgets/common.js +16 -7
  17. package/dist/schema/widgets/gauge-card.d.ts +38 -2
  18. package/dist/schema/widgets/kpi-card.d.ts +38 -2
  19. package/dist/schema/widgets/pivot-table.d.ts +38 -2
  20. package/dist/schema/widgets/table.d.ts +38 -2
  21. package/dist/services/calc-evaluator.d.ts +1 -0
  22. package/dist/services/calc-evaluator.js +28 -0
  23. package/dist/services/dashboardFilterService.d.ts +5 -0
  24. package/dist/services/dashboardFilterService.js +125 -0
  25. package/dist/services/widgetDataService.js +53 -201
  26. package/package.json +2 -1
  27. package/schema/api.ts +1 -2
  28. package/schema/widget.ts +0 -1
  29. package/schema/widgets/charts.ts +1 -2
  30. package/schema/widgets/common.ts +16 -7
  31. package/services/calc-evaluator.ts +33 -0
  32. package/services/dashboardFilterService.ts +162 -0
  33. package/services/widgetDataService.ts +88 -263
@@ -7,33 +7,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { Filters, Sorts } from 'adminforth';
11
- const CALC_IDENTIFIER_RE = /\b[a-zA-Z_][a-zA-Z0-9_]*\b/g;
12
- const LOOKUP_CALL_RE = /lookup\(\s*(\$variables(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*,\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g;
13
- const VARIABLE_PATH_PREFIX_RE = /^\$variables\.?/;
14
- const SAFE_CALC_EXPRESSION_RE = /^[\d+\-*/().\s?:<>=!]+$/;
15
- const RELATIVE_DURATION_RE = /^(\d+)(h|d|w|mo|y)$/;
16
- const FILTER_OPERATORS = {
17
- eq: Filters.EQ,
18
- neq: Filters.NEQ,
19
- gt: Filters.GT,
20
- gte: Filters.GTE,
21
- lt: Filters.LT,
22
- lte: Filters.LTE,
23
- in: Filters.IN,
24
- not_in: Filters.NOT_IN,
25
- like: Filters.LIKE,
26
- ilike: Filters.ILIKE,
27
- };
10
+ import { Sorts } from 'adminforth';
11
+ import { getAdminForthFilters, mergeFilters, } from './dashboardFilterService.js';
12
+ import { evaluateCalc } from './calc-evaluator.js';
28
13
  export function getWidgetData(adminforth_1, widget_1) {
29
14
  return __awaiter(this, arguments, void 0, function* (adminforth, widget, options = {}) {
30
- var _a, _b;
15
+ var _a;
31
16
  if (!('query' in widget)) {
32
17
  return null;
33
18
  }
34
- const data = 'steps' in widget.query
35
- ? yield getFunnelWidgetData(adminforth, widget.query, (_a = options.variables) !== null && _a !== void 0 ? _a : {})
36
- : yield getQueryWidgetData(adminforth, widget.query, (_b = options.variables) !== null && _b !== void 0 ? _b : {});
19
+ const data = yield getQueryWidgetData(adminforth, widget.query, (_a = options.variables) !== null && _a !== void 0 ? _a : {});
37
20
  if (widget.target !== 'table' || !options.pagination) {
38
21
  return data;
39
22
  }
@@ -48,40 +31,15 @@ export function getWidgetData(adminforth_1, widget_1) {
48
31
  } });
49
32
  });
50
33
  }
51
- function getFunnelWidgetData(adminforth, query, variables) {
52
- return __awaiter(this, void 0, void 0, function* () {
53
- var _a;
54
- const rows = yield Promise.all(query.steps.map((step) => __awaiter(this, void 0, void 0, function* () {
55
- var _a, _b;
56
- const valueField = step.metric.as;
57
- const [values = {}] = yield getAggregateRows(adminforth, step.resource, step.filters, [step.metric], []);
58
- const row = {
59
- name: step.name,
60
- resource: step.resource,
61
- [valueField]: (_a = values[valueField]) !== null && _a !== void 0 ? _a : 0,
62
- };
63
- for (const calc of (_b = query.calcs) !== null && _b !== void 0 ? _b : []) {
64
- row[calc.as] = evaluateCalc(calc.calc, row, variables);
65
- }
66
- return row;
67
- })));
68
- return {
69
- kind: 'aggregate',
70
- columns: [
71
- 'name',
72
- ...Array.from(new Set(query.steps.map((step) => step.metric.as))),
73
- ...Array.from(new Set(((_a = query.calcs) !== null && _a !== void 0 ? _a : []).map((calc) => calc.as))),
74
- ],
75
- rows,
76
- };
77
- });
78
- }
79
34
  function getQueryWidgetData(adminforth, query, variables) {
80
35
  return __awaiter(this, void 0, void 0, function* () {
81
36
  var _a, _b, _c;
82
- const metricSelect = getSingleAggregateMetricSelect(query);
83
- if (metricSelect) {
84
- return getMetricWidgetData(adminforth, query, metricSelect);
37
+ if (isStepsQuery(query)) {
38
+ return getStepsQueryData(adminforth, query, variables);
39
+ }
40
+ const singleAggregateSelect = getSingleAggregateSelectItem(query);
41
+ if (singleAggregateSelect) {
42
+ return getSingleAggregateWidgetData(adminforth, query, singleAggregateSelect);
85
43
  }
86
44
  const selectedRows = isAggregateQuery(query)
87
45
  ? yield buildAggregateQueryRows(adminforth, query, variables)
@@ -102,18 +60,44 @@ function getQueryWidgetData(adminforth, query, variables) {
102
60
  };
103
61
  });
104
62
  }
105
- function getMetricWidgetData(adminforth, query, metric) {
63
+ function getStepsQueryData(adminforth, query, variables) {
64
+ return __awaiter(this, void 0, void 0, function* () {
65
+ var _a, _b, _c, _d;
66
+ const rows = yield Promise.all(query.steps.map((step) => __awaiter(this, void 0, void 0, function* () {
67
+ const select = getStepSelect(step);
68
+ const [values = {}] = yield getAggregateRows(adminforth, step.resource, step.filters, select, []);
69
+ const row = buildCalculatedRow(Object.assign({ name: step.name, resource: step.resource }, values), select, query.calcs, variables);
70
+ return row;
71
+ })));
72
+ const orderedRows = sortRows(rows, query.order_by);
73
+ const slicedRows = typeof query.limit === 'number'
74
+ ? orderedRows.slice((_a = query.offset) !== null && _a !== void 0 ? _a : 0, ((_b = query.offset) !== null && _b !== void 0 ? _b : 0) + query.limit)
75
+ : orderedRows.slice((_c = query.offset) !== null && _c !== void 0 ? _c : 0);
76
+ const columns = Array.from(new Set([
77
+ 'name',
78
+ 'resource',
79
+ ...query.steps.flatMap((step) => getStepSelect(step).map((item) => item.as)),
80
+ ...((_d = query.calcs) !== null && _d !== void 0 ? _d : []).map((item) => item.as),
81
+ ]));
82
+ return {
83
+ kind: 'aggregate',
84
+ columns,
85
+ rows: slicedRows,
86
+ };
87
+ });
88
+ }
89
+ function getSingleAggregateWidgetData(adminforth, query, aggregate) {
106
90
  return __awaiter(this, void 0, void 0, function* () {
107
91
  var _a;
108
- const [currentValues = {}] = yield getAggregateRows(adminforth, query.resource, query.filters, [metric], []);
92
+ const [currentValues = {}] = yield getAggregateRows(adminforth, query.resource, query.filters, [aggregate], []);
109
93
  const values = {
110
- [metric.as]: (_a = currentValues[metric.as]) !== null && _a !== void 0 ? _a : 0,
94
+ [aggregate.as]: (_a = currentValues[aggregate.as]) !== null && _a !== void 0 ? _a : 0,
111
95
  };
112
96
  const rows = query.sparkline
113
- ? yield getMetricSparklineRows(adminforth, query, metric, getAdminForthFilters(query.filters))
97
+ ? yield getSingleAggregateSparklineRows(adminforth, query, aggregate, getAdminForthFilters(query.filters))
114
98
  : [values];
115
99
  const columns = Array.from(new Set([
116
- metric.as,
100
+ aggregate.as,
117
101
  ...(query.sparkline ? [query.sparkline.as] : []),
118
102
  ]));
119
103
  return {
@@ -124,7 +108,7 @@ function getMetricWidgetData(adminforth, query, metric) {
124
108
  };
125
109
  });
126
110
  }
127
- function getMetricSparklineRows(adminforth, query, metric, filters) {
111
+ function getSingleAggregateSparklineRows(adminforth, query, aggregate, filters) {
128
112
  return __awaiter(this, void 0, void 0, function* () {
129
113
  const sparkline = query.sparkline;
130
114
  const groupBy = [{
@@ -132,7 +116,7 @@ function getMetricSparklineRows(adminforth, query, metric, filters) {
132
116
  as: sparkline.as,
133
117
  grain: sparkline.grain,
134
118
  }];
135
- const rows = yield getAggregateRows(adminforth, query.resource, filters, [metric], groupBy);
119
+ const rows = yield getAggregateRows(adminforth, query.resource, filters, [aggregate], groupBy);
136
120
  return rows.map((row) => {
137
121
  var _a;
138
122
  return (Object.assign(Object.assign({}, (_a = query.sparkline) === null || _a === void 0 ? void 0 : _a.fill_missing), row));
@@ -191,7 +175,7 @@ function getAggregateRows(adminforth, resourceId, baseFilters, select, groupBy)
191
175
  function buildCalculatedRow(baseValues, select, calcs = [], variables) {
192
176
  const values = Object.assign({}, baseValues);
193
177
  for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
194
- values[item.as] = evaluateCalc(item.calc, values, variables);
178
+ values[item.as] = evaluateCalc(item.calc, values);
195
179
  }
196
180
  return values;
197
181
  }
@@ -206,33 +190,10 @@ function buildPlainRow(row, select, calcs = [], variables) {
206
190
  }
207
191
  }
208
192
  for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
209
- values[item.as] = evaluateCalc(item.calc, values, variables);
193
+ values[item.as] = evaluateCalc(item.calc, values);
210
194
  }
211
195
  return values;
212
196
  }
213
- function evaluateCalc(calc, values, variables) {
214
- const expression = calc
215
- .replace(LOOKUP_CALL_RE, (_match, path, keyField, defaultValue) => {
216
- var _a;
217
- const map = resolveVariablePath(variables, path);
218
- const key = String((_a = values[keyField]) !== null && _a !== void 0 ? _a : '');
219
- return String(toFiniteNumber(isRecord(map) && Object.prototype.hasOwnProperty.call(map, key)
220
- ? map[key]
221
- : Number(defaultValue)));
222
- })
223
- .replace(CALC_IDENTIFIER_RE, (name) => String(toFiniteNumber(values[name])));
224
- if (!SAFE_CALC_EXPRESSION_RE.test(expression)) {
225
- throw new Error(`Unsupported calc expression: ${calc}`);
226
- }
227
- return Function(`"use strict"; return (${expression});`)();
228
- }
229
- function resolveVariablePath(variables, path) {
230
- return path
231
- .replace(VARIABLE_PATH_PREFIX_RE, '')
232
- .split('.')
233
- .filter(Boolean)
234
- .reduce((current, segment) => isRecord(current) ? current[segment] : undefined, variables);
235
- }
236
197
  function sortRows(rows, orderBy = []) {
237
198
  if (!orderBy.length) {
238
199
  return rows;
@@ -280,7 +241,7 @@ function isAggregateQuery(query) {
280
241
  return Boolean(((_a = query.group_by) === null || _a === void 0 ? void 0 : _a.length)
281
242
  || ((_b = query.select) === null || _b === void 0 ? void 0 : _b.some((item) => isAggregateSelectItem(item))));
282
243
  }
283
- function getSingleAggregateMetricSelect(query) {
244
+ function getSingleAggregateSelectItem(query) {
284
245
  var _a, _b;
285
246
  if ((_a = query.group_by) === null || _a === void 0 ? void 0 : _a.length) {
286
247
  return undefined;
@@ -292,6 +253,12 @@ function getSingleAggregateMetricSelect(query) {
292
253
  }
293
254
  return aggregateItems[0];
294
255
  }
256
+ function isStepsQuery(query) {
257
+ return query.source === 'steps';
258
+ }
259
+ function getStepSelect(step) {
260
+ return step.select;
261
+ }
295
262
  function isFieldSelectItem(item) {
296
263
  return 'field' in item && !('agg' in item);
297
264
  }
@@ -408,23 +375,6 @@ function getFilterCacheKey(filters) {
408
375
  }
409
376
  return JSON.stringify(filters);
410
377
  }
411
- function mergeFilters(...filters) {
412
- const merged = [];
413
- for (const filter of filters) {
414
- const normalized = getAdminForthFilters(filter);
415
- if (Array.isArray(normalized)) {
416
- merged.push(...normalized);
417
- continue;
418
- }
419
- if (normalized) {
420
- merged.push(normalized);
421
- }
422
- }
423
- if (!merged.length) {
424
- return [];
425
- }
426
- return merged.length === 1 ? merged[0] : merged;
427
- }
428
378
  function getHiddenAggregateAlias(groupBy, select) {
429
379
  const usedAliases = new Set([
430
380
  ...groupBy.map((item) => item.as),
@@ -462,104 +412,6 @@ function formatGroupValue(value, grain) {
462
412
  }
463
413
  return value;
464
414
  }
465
- function getAdminForthFilters(filters) {
466
- if (Array.isArray(filters)) {
467
- return filters.map((filter) => isDashboardFilterExpression(filter)
468
- ? toAdminForthFilter(filter)
469
- : filter);
470
- }
471
- if (isDashboardFilterExpression(filters)) {
472
- return toAdminForthFilter(filters);
473
- }
474
- if (filters) {
475
- return filters;
476
- }
477
- return [];
478
- }
479
- function isDashboardFilterExpression(value) {
480
- if (Array.isArray(value)) {
481
- return true;
482
- }
483
- if (!isRecord(value)) {
484
- return false;
485
- }
486
- return 'and' in value
487
- || 'or' in value
488
- || 'eq' in value
489
- || 'neq' in value
490
- || 'gt' in value
491
- || 'gte' in value
492
- || 'lt' in value
493
- || 'lte' in value
494
- || 'in' in value
495
- || 'not_in' in value
496
- || 'like' in value
497
- || 'ilike' in value;
498
- }
499
- function toAdminForthFilter(filter) {
500
- if (Array.isArray(filter)) {
501
- return Filters.AND(filter.map((item) => toAdminForthFilter(item)));
502
- }
503
- if ('and' in filter) {
504
- return Filters.AND(filter.and.map((item) => toAdminForthFilter(item)));
505
- }
506
- if ('or' in filter) {
507
- return Filters.OR(filter.or.map((item) => toAdminForthFilter(item)));
508
- }
509
- for (const [operator, createFilter] of Object.entries(FILTER_OPERATORS)) {
510
- if (Object.prototype.hasOwnProperty.call(filter, operator)) {
511
- return createFilter(filter.field, resolveFilterValue(filter[operator]));
512
- }
513
- }
514
- return Filters.AND([]);
515
- }
516
- function resolveFilterValue(value) {
517
- if (Array.isArray(value)) {
518
- return value.map((item) => resolveFilterValue(item));
519
- }
520
- if (!isRecord(value)) {
521
- return value;
522
- }
523
- if (value.now === true) {
524
- return new Date().toISOString();
525
- }
526
- if (typeof value.now_minus === 'string') {
527
- return subtractDuration(new Date(), value.now_minus).toISOString();
528
- }
529
- return value;
530
- }
531
- function subtractDuration(now, duration) {
532
- const match = duration.match(RELATIVE_DURATION_RE);
533
- if (!match) {
534
- throw new Error(`Unsupported relative date duration: ${duration}`);
535
- }
536
- const amount = Number(match[1]);
537
- const unit = match[2];
538
- const date = new Date(now);
539
- if (unit === 'h') {
540
- date.setUTCHours(date.getUTCHours() - amount);
541
- }
542
- else if (unit === 'd') {
543
- date.setUTCDate(date.getUTCDate() - amount);
544
- }
545
- else if (unit === 'w') {
546
- date.setUTCDate(date.getUTCDate() - amount * 7);
547
- }
548
- else if (unit === 'mo') {
549
- date.setUTCMonth(date.getUTCMonth() - amount);
550
- }
551
- else if (unit === 'y') {
552
- date.setUTCFullYear(date.getUTCFullYear() - amount);
553
- }
554
- return date;
555
- }
556
- function toFiniteNumber(value) {
557
- const numberValue = typeof value === 'number' ? value : Number(value);
558
- return Number.isFinite(numberValue) ? numberValue : 0;
559
- }
560
- function isRecord(value) {
561
- return typeof value === 'object' && value !== null;
562
- }
563
415
  export function createWidgetDataService(adminforth) {
564
416
  return {
565
417
  getWidgetData: (widget, options) => getWidgetData(adminforth, widget, options),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/dashboard",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -56,6 +56,7 @@
56
56
  }
57
57
  ],
58
58
  "dependencies": {
59
+ "expr-eval-fork": "^3.0.3",
59
60
  "vue": "^3.5.34",
60
61
  "vue-router": "^5.0.7",
61
62
  "yaml": "^2.9.0",
package/schema/api.ts CHANGED
@@ -2,7 +2,6 @@ import { toJSONSchema, z } from 'zod'
2
2
  import {
3
3
  BarChartSchema,
4
4
  FunnelChartSchema,
5
- FunnelQueryConfigSchema,
6
5
  GaugeCardViewConfigSchema,
7
6
  HistogramChartSchema,
8
7
  KpiCardViewConfigSchema,
@@ -153,7 +152,7 @@ const ConfigurableHistogramChartWidgetConfigSchema = WidgetEditableBaseSchema.ex
153
152
  const ConfigurableFunnelChartWidgetConfigSchema = WidgetEditableBaseSchema.extend({
154
153
  target: z.literal('chart'),
155
154
  chart: FunnelChartSchema,
156
- query: FunnelQueryConfigSchema,
155
+ query: QueryConfigSchema,
157
156
  })
158
157
 
159
158
  const ConfigurablePivotTableWidgetConfigSchema = WidgetEditableBaseSchema.extend({
package/schema/widget.ts CHANGED
@@ -33,7 +33,6 @@ export {
33
33
  } from './widgets/table.js'
34
34
  export {
35
35
  EmptyWidgetConfigSchema,
36
- FunnelQueryConfigSchema,
37
36
  QueryConfigSchema,
38
37
  WidgetEditableBaseSchema,
39
38
  } from './widgets/common.js'
@@ -1,7 +1,6 @@
1
1
  import { z } from 'zod'
2
2
  import {
3
3
  ChartFieldRefSchema,
4
- FunnelQueryConfigSchema,
5
4
  QueryConfigSchema,
6
5
  WidgetBaseSchema,
7
6
  } from './common.js'
@@ -100,7 +99,7 @@ export const HistogramChartWidgetConfigSchema = WidgetBaseSchema.extend({
100
99
  export const FunnelChartWidgetConfigSchema = WidgetBaseSchema.extend({
101
100
  target: z.literal('chart'),
102
101
  chart: FunnelChartSchema,
103
- query: FunnelQueryConfigSchema,
102
+ query: QueryConfigSchema,
104
103
  })
105
104
 
106
105
  export const ChartWidgetTargetConfigSchema = z.union([
@@ -140,7 +140,8 @@ export const QueryCalcItemSchema = z.object({
140
140
  as: z.string(),
141
141
  }).strict()
142
142
 
143
- export const QueryConfigSchema = z.object({
143
+ const ResourceQueryConfigSchema = z.object({
144
+ source: z.literal('resource').optional(),
144
145
  resource: z.string(),
145
146
  select: z.array(QuerySelectItemSchema).optional(),
146
147
  sparkline: z.object({
@@ -159,17 +160,25 @@ export const QueryConfigSchema = z.object({
159
160
  formatting: z.record(z.string(), z.unknown()).optional(),
160
161
  }).strict()
161
162
 
162
- const FunnelQueryStepSchema = z.object({
163
+ const StepsQuerySelectStepSchema = z.object({
163
164
  name: z.string(),
164
165
  resource: z.string(),
165
- metric: QueryAggregateSelectItemSchema,
166
+ select: z.array(QueryAggregateSelectItemSchema).min(1),
166
167
  filters: FilterExpressionSchema.optional(),
167
168
  }).strict()
168
169
 
169
- export const FunnelQueryConfigSchema = z.object({
170
- steps: z.array(FunnelQueryStepSchema).min(1),
171
- calcs: z.array(QueryCalcItemSchema).optional(),
172
- }).strict()
170
+ export const QueryConfigSchema = z.union([
171
+ ResourceQueryConfigSchema,
172
+ z.object({
173
+ source: z.literal('steps'),
174
+ steps: z.array(StepsQuerySelectStepSchema).min(1),
175
+ calcs: z.array(QueryCalcItemSchema).optional(),
176
+ order_by: z.array(QueryOrderByItemSchema).optional(),
177
+ limit: z.number().int().positive().optional(),
178
+ offset: z.number().int().nonnegative().optional(),
179
+ formatting: z.record(z.string(), z.unknown()).optional(),
180
+ }).strict(),
181
+ ])
173
182
 
174
183
  export const WidgetPersistedFieldsSchema = z.object({
175
184
  id: z.string(),
@@ -0,0 +1,33 @@
1
+ import { Parser } from 'expr-eval-fork';
2
+
3
+ const CALC_PARSER_OPTIONS = {
4
+ allowMemberAccess: false,
5
+ operators: {
6
+ assignment: false,
7
+ concatenate: false,
8
+ conditional: true,
9
+ comparison: true,
10
+ fndef: false,
11
+ in: false,
12
+ logical: true,
13
+ random: false,
14
+ },
15
+ } as const;
16
+
17
+ const CALC_PARSER = new Parser(CALC_PARSER_OPTIONS);
18
+
19
+ export function evaluateCalc(calc: string, values: Record<string, unknown>) {
20
+ return CALC_PARSER.parse(calc).evaluate(normalizeCalcValues(values));
21
+ }
22
+
23
+ function normalizeCalcValues(values: Record<string, unknown>) {
24
+ return Object.fromEntries(Object.entries(values).map(([key, value]) => [
25
+ key,
26
+ typeof value === 'string' ? value : toFiniteNumber(value),
27
+ ]));
28
+ }
29
+
30
+ function toFiniteNumber(value: unknown) {
31
+ const numberValue = typeof value === 'number' ? value : Number(value);
32
+ return Number.isFinite(numberValue) ? numberValue : 0;
33
+ }
@@ -0,0 +1,162 @@
1
+ import { Filters } from 'adminforth';
2
+ import type {
3
+ IAdminForthAndOrFilter,
4
+ IAdminForthSingleFilter,
5
+ } from 'adminforth';
6
+ import type { FilterExpression } from '../custom/model/dashboard.types.js';
7
+
8
+ export type DashboardQueryFilters =
9
+ | IAdminForthSingleFilter
10
+ | IAdminForthAndOrFilter
11
+ | Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>;
12
+
13
+ const RELATIVE_DURATION_RE = /^(\d+)(h|d|w|mo|y)$/;
14
+
15
+ const FILTER_OPERATORS = {
16
+ eq: Filters.EQ,
17
+ neq: Filters.NEQ,
18
+ gt: Filters.GT,
19
+ gte: Filters.GTE,
20
+ lt: Filters.LT,
21
+ lte: Filters.LTE,
22
+ in: Filters.IN,
23
+ not_in: Filters.NOT_IN,
24
+ like: Filters.LIKE,
25
+ ilike: Filters.ILIKE,
26
+ } as const;
27
+
28
+ export function getAdminForthFilters(filters: FilterExpression | DashboardQueryFilters | undefined): DashboardQueryFilters {
29
+ if (Array.isArray(filters)) {
30
+ return filters.map((filter) => isDashboardFilterExpression(filter)
31
+ ? toAdminForthFilter(filter)
32
+ : filter);
33
+ }
34
+
35
+ if (isDashboardFilterExpression(filters)) {
36
+ return toAdminForthFilter(filters);
37
+ }
38
+
39
+ if (filters) {
40
+ return filters;
41
+ }
42
+
43
+ return [];
44
+ }
45
+
46
+ export function mergeFilters(...filters: Array<FilterExpression | DashboardQueryFilters | undefined>) {
47
+ const merged: Array<IAdminForthSingleFilter | IAdminForthAndOrFilter> = [];
48
+
49
+ for (const filter of filters) {
50
+ const normalized = getAdminForthFilters(filter);
51
+
52
+ if (Array.isArray(normalized)) {
53
+ merged.push(...normalized);
54
+ continue;
55
+ }
56
+
57
+ if (normalized) {
58
+ merged.push(normalized);
59
+ }
60
+ }
61
+
62
+ if (!merged.length) {
63
+ return [] as DashboardQueryFilters;
64
+ }
65
+
66
+ return merged.length === 1 ? merged[0] : merged;
67
+ }
68
+
69
+ function isDashboardFilterExpression(value: unknown): value is FilterExpression {
70
+ if (Array.isArray(value)) {
71
+ return true;
72
+ }
73
+
74
+ if (!isRecord(value)) {
75
+ return false;
76
+ }
77
+
78
+ return 'and' in value
79
+ || 'or' in value
80
+ || 'eq' in value
81
+ || 'neq' in value
82
+ || 'gt' in value
83
+ || 'gte' in value
84
+ || 'lt' in value
85
+ || 'lte' in value
86
+ || 'in' in value
87
+ || 'not_in' in value
88
+ || 'like' in value
89
+ || 'ilike' in value;
90
+ }
91
+
92
+ function toAdminForthFilter(filter: FilterExpression): IAdminForthSingleFilter | IAdminForthAndOrFilter {
93
+ if (Array.isArray(filter)) {
94
+ return Filters.AND(filter.map((item) => toAdminForthFilter(item)));
95
+ }
96
+
97
+ if ('and' in filter) {
98
+ return Filters.AND(filter.and.map((item) => toAdminForthFilter(item)));
99
+ }
100
+
101
+ if ('or' in filter) {
102
+ return Filters.OR(filter.or.map((item) => toAdminForthFilter(item)));
103
+ }
104
+
105
+ for (const [operator, createFilter] of Object.entries(FILTER_OPERATORS)) {
106
+ if (Object.prototype.hasOwnProperty.call(filter, operator)) {
107
+ return createFilter(filter.field, resolveFilterValue(filter[operator as keyof typeof FILTER_OPERATORS]));
108
+ }
109
+ }
110
+
111
+ return Filters.AND([]);
112
+ }
113
+
114
+ function resolveFilterValue(value: unknown): unknown {
115
+ if (Array.isArray(value)) {
116
+ return value.map((item) => resolveFilterValue(item));
117
+ }
118
+
119
+ if (!isRecord(value)) {
120
+ return value;
121
+ }
122
+
123
+ if (value.now === true) {
124
+ return new Date().toISOString();
125
+ }
126
+
127
+ if (typeof value.now_minus === 'string') {
128
+ return subtractDuration(new Date(), value.now_minus).toISOString();
129
+ }
130
+
131
+ return value;
132
+ }
133
+
134
+ function subtractDuration(now: Date, duration: string) {
135
+ const match = duration.match(RELATIVE_DURATION_RE);
136
+
137
+ if (!match) {
138
+ throw new Error(`Unsupported relative date duration: ${duration}`);
139
+ }
140
+
141
+ const amount = Number(match[1]);
142
+ const unit = match[2];
143
+ const date = new Date(now);
144
+
145
+ if (unit === 'h') {
146
+ date.setUTCHours(date.getUTCHours() - amount);
147
+ } else if (unit === 'd') {
148
+ date.setUTCDate(date.getUTCDate() - amount);
149
+ } else if (unit === 'w') {
150
+ date.setUTCDate(date.getUTCDate() - amount * 7);
151
+ } else if (unit === 'mo') {
152
+ date.setUTCMonth(date.getUTCMonth() - amount);
153
+ } else if (unit === 'y') {
154
+ date.setUTCFullYear(date.getUTCFullYear() - amount);
155
+ }
156
+
157
+ return date;
158
+ }
159
+
160
+ function isRecord(value: unknown): value is Record<string, any> {
161
+ return typeof value === 'object' && value !== null;
162
+ }