@adminforth/dashboard 1.4.0 → 1.4.2

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 (86) hide show
  1. package/README.md +23 -4
  2. package/custom/api/dashboardApi.ts +6 -9
  3. package/custom/model/dashboard.types.ts +60 -275
  4. package/custom/model/dashboardTopics.ts +5 -0
  5. package/custom/package.json +1 -0
  6. package/custom/runtime/DashboardGroup.vue +2 -2
  7. package/custom/runtime/DashboardPage.vue +17 -7
  8. package/custom/runtime/DashboardRuntime.vue +20 -8
  9. package/custom/runtime/WidgetRenderer.vue +1 -2
  10. package/custom/runtime/WidgetShell.vue +3 -3
  11. package/custom/skills/adminforth-dashboard/SKILL.md +2 -2
  12. package/custom/widgets/{gauge-card/GaugeCardWidget.vue → GaugeCardWidget.vue} +63 -61
  13. package/custom/widgets/{kpi-card/KpiCardWidget.vue → KpiCardWidget.vue} +35 -33
  14. package/custom/widgets/{pivot-table/PivotTableWidget.vue → PivotTableWidget.vue} +71 -68
  15. package/custom/widgets/{table/TableWidget.vue → TableWidget.vue} +5 -5
  16. package/custom/widgets/chart/{bar/BarChart.vue → BarChart.vue} +2 -2
  17. package/custom/widgets/chart/ChartWidget.vue +4 -15
  18. package/{dist/custom/widgets/chart/funnel → custom/widgets/chart}/FunnelChart.vue +80 -78
  19. package/{dist/custom/widgets/chart/line → custom/widgets/chart}/LineChart.vue +2 -2
  20. package/custom/widgets/chart/{pie/PieChart.vue → PieChart.vue} +2 -2
  21. package/{dist/custom/widgets/chart/stacked-bar → custom/widgets/chart}/StackedBarChart.vue +97 -95
  22. package/custom/widgets/chart/chart.types.ts +0 -28
  23. package/dist/custom/api/dashboardApi.d.ts +4 -8
  24. package/dist/custom/api/dashboardApi.js +2 -6
  25. package/dist/custom/api/dashboardApi.ts +6 -9
  26. package/dist/custom/composables/useElementSize.js +7 -10
  27. package/dist/custom/model/dashboard.types.d.ts +38 -32
  28. package/dist/custom/model/dashboard.types.js +4 -161
  29. package/dist/custom/model/dashboard.types.ts +60 -275
  30. package/dist/custom/model/dashboardTopics.d.ts +2 -0
  31. package/dist/custom/model/dashboardTopics.js +4 -0
  32. package/dist/custom/model/dashboardTopics.ts +5 -0
  33. package/dist/custom/package.json +1 -0
  34. package/dist/custom/queries/useDashboardConfig.d.ts +96 -96
  35. package/dist/custom/queries/useDashboardConfig.js +9 -12
  36. package/dist/custom/queries/useWidgetData.d.ts +96 -96
  37. package/dist/custom/queries/useWidgetData.js +9 -12
  38. package/dist/custom/runtime/DashboardGroup.vue +2 -2
  39. package/dist/custom/runtime/DashboardPage.vue +17 -7
  40. package/dist/custom/runtime/DashboardRuntime.vue +20 -8
  41. package/dist/custom/runtime/WidgetRenderer.vue +1 -2
  42. package/dist/custom/runtime/WidgetShell.vue +3 -3
  43. package/dist/custom/skills/adminforth-dashboard/SKILL.md +2 -2
  44. package/dist/custom/widgets/{gauge-card/GaugeCardWidget.vue → GaugeCardWidget.vue} +63 -61
  45. package/dist/custom/widgets/{kpi-card/KpiCardWidget.vue → KpiCardWidget.vue} +35 -33
  46. package/dist/custom/widgets/{pivot-table/PivotTableWidget.vue → PivotTableWidget.vue} +71 -68
  47. package/dist/custom/widgets/{table/TableWidget.vue → TableWidget.vue} +5 -5
  48. package/dist/custom/widgets/chart/{bar/BarChart.vue → BarChart.vue} +2 -2
  49. package/dist/custom/widgets/chart/ChartWidget.vue +4 -15
  50. package/{custom/widgets/chart/funnel → dist/custom/widgets/chart}/FunnelChart.vue +80 -78
  51. package/{custom/widgets/chart/line → dist/custom/widgets/chart}/LineChart.vue +2 -2
  52. package/dist/custom/widgets/chart/{pie/PieChart.vue → PieChart.vue} +2 -2
  53. package/{custom/widgets/chart/stacked-bar → dist/custom/widgets/chart}/StackedBarChart.vue +97 -95
  54. package/dist/custom/widgets/chart/chart.types.d.ts +0 -2
  55. package/dist/custom/widgets/chart/chart.types.js +1 -25
  56. package/dist/custom/widgets/chart/chart.types.ts +0 -28
  57. package/dist/custom/widgets/chart/chart.utils.js +6 -14
  58. package/dist/custom/widgets/registry.js +14 -22
  59. package/dist/endpoint/dashboard.d.ts +2 -3
  60. package/dist/endpoint/dashboard.js +12 -32
  61. package/dist/endpoint/groups.d.ts +2 -21
  62. package/dist/endpoint/groups.js +18 -16
  63. package/dist/endpoint/widgets.d.ts +0 -3
  64. package/dist/endpoint/widgets.js +27 -74
  65. package/dist/index.js +1 -3
  66. package/dist/schema/api.d.ts +2090 -511
  67. package/dist/schema/api.js +18 -15
  68. package/dist/schema/widget.d.ts +1003 -250
  69. package/dist/schema/widget.js +102 -46
  70. package/dist/services/dashboardConfigService.d.ts +0 -10
  71. package/dist/services/dashboardConfigService.js +6 -21
  72. package/dist/services/widgetDataService.js +226 -196
  73. package/endpoint/dashboard.ts +13 -46
  74. package/endpoint/groups.ts +25 -42
  75. package/endpoint/widgets.ts +36 -95
  76. package/index.ts +0 -3
  77. package/package.json +3 -3
  78. package/schema/api.ts +19 -15
  79. package/schema/widget.ts +113 -52
  80. package/services/dashboardConfigService.ts +6 -25
  81. package/services/widgetDataService.ts +304 -229
  82. package/custom/widgets/chart/histogram/HistogramChart.vue +0 -21
  83. package/dist/custom/widgets/chart/histogram/HistogramChart.vue +0 -21
  84. package/dist/services/widgetConfigValidator.d.ts +0 -8
  85. package/dist/services/widgetConfigValidator.js +0 -27
  86. package/services/widgetConfigValidator.ts +0 -61
@@ -8,11 +8,22 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { Filters, Sorts } from 'adminforth';
11
- const NOW_MINUS_RE = /^(\d+)([dhw])$/;
12
11
  const CALC_IDENTIFIER_RE = /\b[a-zA-Z_][a-zA-Z0-9_]*\b/g;
13
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;
14
13
  const VARIABLE_PATH_PREFIX_RE = /^\$variables\.?/;
15
14
  const SAFE_CALC_EXPRESSION_RE = /^[\d+\-*/().\s]+$/;
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
+ };
16
27
  export function getWidgetData(adminforth_1, widget_1) {
17
28
  return __awaiter(this, arguments, void 0, function* (adminforth, widget, options = {}) {
18
29
  var _a, _b;
@@ -40,15 +51,15 @@ function getFunnelWidgetData(adminforth, query, variables) {
40
51
  return __awaiter(this, void 0, void 0, function* () {
41
52
  var _a;
42
53
  const rows = yield Promise.all(query.steps.map((step) => __awaiter(this, void 0, void 0, function* () {
43
- var _a;
54
+ var _a, _b;
44
55
  const valueField = step.metric.as;
45
- const sourceRows = yield getResourceRows(adminforth, step.resource, step.filters);
56
+ const [values = {}] = yield getAggregateRows(adminforth, step.resource, step.filters, [step.metric], []);
46
57
  const row = {
47
58
  name: step.name,
48
59
  resource: step.resource,
49
- [valueField]: calculateAggregate(sourceRows, step.metric),
60
+ [valueField]: (_a = values[valueField]) !== null && _a !== void 0 ? _a : 0,
50
61
  };
51
- for (const calc of (_a = query.calcs) !== null && _a !== void 0 ? _a : []) {
62
+ for (const calc of (_b = query.calcs) !== null && _b !== void 0 ? _b : []) {
52
63
  row[calc.as] = evaluateCalc(calc.calc, row, variables);
53
64
  }
54
65
  return row;
@@ -67,9 +78,10 @@ function getFunnelWidgetData(adminforth, query, variables) {
67
78
  function getQueryWidgetData(adminforth, query, variables) {
68
79
  return __awaiter(this, void 0, void 0, function* () {
69
80
  var _a, _b, _c;
70
- const rows = yield getResourceRows(adminforth, query.resource, query.filters, getBackendSort(query.orderBy));
71
- const selectedRows = buildQueryRows(rows, query, variables);
72
- const orderedRows = sortRows(selectedRows, query.orderBy);
81
+ const selectedRows = isAggregateQuery(query)
82
+ ? yield buildAggregateQueryRows(adminforth, query, variables)
83
+ : buildPlainQueryRows(yield getResourceRows(adminforth, query.resource, query.filters, getBackendSort(query.order_by)), query, variables);
84
+ const orderedRows = sortRows(selectedRows, query.order_by);
73
85
  const slicedRows = typeof query.limit === 'number'
74
86
  ? orderedRows.slice((_a = query.offset) !== null && _a !== void 0 ? _a : 0, ((_b = query.offset) !== null && _b !== void 0 ? _b : 0) + query.limit)
75
87
  : orderedRows.slice((_c = query.offset) !== null && _c !== void 0 ? _c : 0);
@@ -87,52 +99,55 @@ function getQueryWidgetData(adminforth, query, variables) {
87
99
  }
88
100
  function getResourceRows(adminforth, resourceId, filters, sort) {
89
101
  return __awaiter(this, void 0, void 0, function* () {
90
- return adminforth.resource(resourceId).list(normalizeFilters(filters), undefined, 0, sort);
102
+ return adminforth.resource(resourceId).list(getAdminForthFilters(filters), undefined, 0, sort);
91
103
  });
92
104
  }
93
- function buildQueryRows(rows, query, variables) {
94
- var _a, _b;
105
+ function buildPlainQueryRows(rows, query, variables) {
106
+ var _a;
95
107
  const select = (_a = query.select) !== null && _a !== void 0 ? _a : getDefaultSelect(rows);
96
- const groupBy = (_b = query.groupBy) !== null && _b !== void 0 ? _b : [];
97
- if (isAggregateQuery(query)) {
98
- return buildGroupedRows(rows, select, groupBy, variables, query.calcs);
99
- }
100
108
  return rows.map((row) => buildPlainRow(row, select, query.calcs, variables));
101
109
  }
102
- function buildGroupedRows(rows, select, groupBy, variables, calcs = []) {
103
- var _a;
104
- const groups = new Map();
105
- const effectiveGroupBy = groupBy.length
106
- ? groupBy
107
- : select.filter(isFieldSelectItem).map((item) => ({ field: item.field, as: item.as, grain: item.grain }));
108
- if (!effectiveGroupBy.length) {
109
- const values = calculateGroupValues(rows, select, calcs, variables);
110
- return Object.keys(values).length ? [values] : [];
111
- }
112
- for (const row of rows) {
113
- const values = Object.fromEntries(effectiveGroupBy.map((item) => {
114
- const field = getGroupByField(item);
115
- const alias = getGroupByAlias(item);
116
- const grain = getGroupByGrain(item);
117
- return [alias, formatGroupValue(row[field], grain)];
118
- }));
119
- const key = JSON.stringify(values);
120
- const group = (_a = groups.get(key)) !== null && _a !== void 0 ? _a : { values, rows: [] };
121
- group.rows.push(row);
122
- groups.set(key, group);
123
- }
124
- return Array.from(groups.values()).map((group) => (Object.assign(Object.assign({}, group.values), calculateGroupValues(group.rows, select, calcs, variables, group.values))));
110
+ function buildAggregateQueryRows(adminforth, query, variables) {
111
+ return __awaiter(this, void 0, void 0, function* () {
112
+ var _a;
113
+ const select = (_a = query.select) !== null && _a !== void 0 ? _a : [];
114
+ const effectiveGroupBy = getEffectiveGroupBy(query.group_by, select);
115
+ const aggregateSelect = select.filter(isAggregateSelectItem);
116
+ const rows = yield getAggregateRows(adminforth, query.resource, query.filters, aggregateSelect, effectiveGroupBy);
117
+ return rows.map((row) => buildCalculatedRow(row, select, query.calcs, variables));
118
+ });
125
119
  }
126
- function calculateGroupValues(rows, select, calcs, variables, baseValues = {}) {
127
- const values = Object.assign({}, baseValues);
128
- for (const item of select) {
129
- if (isAggregateSelectItem(item)) {
130
- const filteredRows = item.filters
131
- ? rows.filter((row) => matchesFilterExpression(row, item.filters))
132
- : rows;
133
- values[item.as] = calculateAggregate(filteredRows, item);
120
+ function getAggregateRows(adminforth, resourceId, baseFilters, select, groupBy) {
121
+ return __awaiter(this, void 0, void 0, function* () {
122
+ var _a;
123
+ const resource = adminforth.resource(resourceId);
124
+ const groups = new Map();
125
+ const groupByRules = groupBy.length ? groupBy.map(toAggregateGroupByRule) : undefined;
126
+ const aggregateSelectGroups = groupAggregateSelectItems(select);
127
+ if (groupBy.length) {
128
+ const groupSeedAlias = getHiddenAggregateAlias(groupBy, select);
129
+ const groupSeedRows = yield resource.aggregate(getAdminForthFilters(baseFilters), { [groupSeedAlias]: { operation: 'count' } }, groupByRules);
130
+ for (const row of groupSeedRows) {
131
+ ensureAggregateGroup(groups, row, groupBy);
132
+ }
134
133
  }
135
- }
134
+ for (const filterGroup of aggregateSelectGroups) {
135
+ const rows = yield resource.aggregate(mergeFilters(baseFilters, filterGroup.filters), Object.fromEntries(filterGroup.items.map((item) => [item.as, toAggregationRule(item)])), groupByRules);
136
+ for (const row of rows) {
137
+ const values = ensureAggregateGroup(groups, row, groupBy);
138
+ for (const item of filterGroup.items) {
139
+ values[item.as] = (_a = row[item.as]) !== null && _a !== void 0 ? _a : 0;
140
+ }
141
+ }
142
+ }
143
+ if (!groups.size && !groupBy.length && select.length) {
144
+ groups.set(JSON.stringify({}), {});
145
+ }
146
+ return Array.from(groups.values(), (row) => applyAggregateDefaults(row, select));
147
+ });
148
+ }
149
+ function buildCalculatedRow(baseValues, select, calcs = [], variables) {
150
+ const values = Object.assign({}, baseValues);
136
151
  for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
137
152
  values[item.as] = evaluateCalc(item.calc, values, variables);
138
153
  }
@@ -153,41 +168,6 @@ function buildPlainRow(row, select, calcs = [], variables) {
153
168
  }
154
169
  return values;
155
170
  }
156
- function calculateAggregate(rows, item) {
157
- switch (item.agg) {
158
- case 'count':
159
- return rows.length;
160
- case 'count_distinct':
161
- return new Set(rows.map((row) => row[item.field])).size;
162
- case 'sum':
163
- return aggregateNumbers(rows, item.field, (values) => values.reduce((sum, value) => sum + value, 0));
164
- case 'avg':
165
- return aggregateNumbers(rows, item.field, (values) => values.length
166
- ? values.reduce((sum, value) => sum + value, 0) / values.length
167
- : 0);
168
- case 'min':
169
- return aggregateNumbers(rows, item.field, (values) => values.length ? Math.min(...values) : 0);
170
- case 'max':
171
- return aggregateNumbers(rows, item.field, (values) => values.length ? Math.max(...values) : 0);
172
- case 'median':
173
- return aggregateNumbers(rows, item.field, calculateMedian);
174
- default:
175
- throw new Error(`Unsupported aggregation operation: ${item.agg}`);
176
- }
177
- }
178
- function aggregateNumbers(rows, field, aggregate) {
179
- return aggregate(rows.map((row) => toFiniteNumber(row[field])).filter(Number.isFinite));
180
- }
181
- function calculateMedian(values) {
182
- if (!values.length) {
183
- return 0;
184
- }
185
- const sorted = [...values].sort((left, right) => left - right);
186
- const middle = Math.floor(sorted.length / 2);
187
- return sorted.length % 2
188
- ? sorted[middle]
189
- : (sorted[middle - 1] + sorted[middle]) / 2;
190
- }
191
171
  function evaluateCalc(calc, values, variables) {
192
172
  const expression = calc
193
173
  .replace(LOOKUP_CALL_RE, (_match, path, keyField, defaultValue) => {
@@ -243,7 +223,7 @@ function getBackendSort(orderBy) {
243
223
  function getColumns(rows, query) {
244
224
  var _a, _b, _c, _d;
245
225
  const selectColumns = [
246
- ...((_a = query.groupBy) !== null && _a !== void 0 ? _a : []).map(getGroupByAlias),
226
+ ...((_a = query.group_by) !== null && _a !== void 0 ? _a : []).map(getGroupByAlias),
247
227
  ...((_b = query.select) !== null && _b !== void 0 ? _b : []).map(getSelectAlias),
248
228
  ...((_c = query.calcs) !== null && _c !== void 0 ? _c : []).map((item) => item.as),
249
229
  ].filter(Boolean);
@@ -255,7 +235,7 @@ function getDefaultSelect(rows) {
255
235
  }
256
236
  function isAggregateQuery(query) {
257
237
  var _a, _b;
258
- return Boolean(((_a = query.groupBy) === null || _a === void 0 ? void 0 : _a.length)
238
+ return Boolean(((_a = query.group_by) === null || _a === void 0 ? void 0 : _a.length)
259
239
  || ((_b = query.select) === null || _b === void 0 ? void 0 : _b.some((item) => isAggregateSelectItem(item))));
260
240
  }
261
241
  function isFieldSelectItem(item) {
@@ -274,15 +254,133 @@ function getSelectAlias(item) {
274
254
  }
275
255
  return item.as;
276
256
  }
277
- function getGroupByField(item) {
278
- return typeof item === 'string' ? item : item.field;
279
- }
280
257
  function getGroupByAlias(item) {
281
258
  var _a;
282
259
  return typeof item === 'string' ? item : (_a = item.as) !== null && _a !== void 0 ? _a : item.field;
283
260
  }
284
- function getGroupByGrain(item) {
285
- return typeof item === 'string' ? undefined : item.grain;
261
+ function getEffectiveGroupBy(groupBy, select) {
262
+ if (groupBy === null || groupBy === void 0 ? void 0 : groupBy.length) {
263
+ return groupBy.map((item) => normalizeGroupByItem(item));
264
+ }
265
+ return select
266
+ .filter(isFieldSelectItem)
267
+ .map((item) => normalizeGroupByItem({ field: item.field, as: item.as, grain: item.grain }));
268
+ }
269
+ function normalizeGroupByItem(item) {
270
+ var _a;
271
+ if (typeof item === 'string') {
272
+ return { field: item, as: item };
273
+ }
274
+ return {
275
+ field: item.field,
276
+ as: (_a = item.as) !== null && _a !== void 0 ? _a : item.field,
277
+ grain: item.grain,
278
+ timezone: 'timezone' in item ? item.timezone : undefined,
279
+ };
280
+ }
281
+ function toAggregateGroupByRule(item) {
282
+ if (item.grain) {
283
+ return {
284
+ type: 'date_trunc',
285
+ field: item.field,
286
+ truncation: item.grain,
287
+ timezone: item.timezone,
288
+ as: item.as,
289
+ };
290
+ }
291
+ return {
292
+ type: 'field',
293
+ field: item.field,
294
+ as: item.as,
295
+ };
296
+ }
297
+ function toAggregationRule(item) {
298
+ switch (item.agg) {
299
+ case 'count':
300
+ return { operation: 'count' };
301
+ case 'count_distinct':
302
+ return { operation: 'count_distinct', field: item.field };
303
+ case 'sum':
304
+ return { operation: 'sum', field: item.field };
305
+ case 'avg':
306
+ return { operation: 'avg', field: item.field };
307
+ case 'min':
308
+ return { operation: 'min', field: item.field };
309
+ case 'max':
310
+ return { operation: 'max', field: item.field };
311
+ case 'median':
312
+ return { operation: 'median', field: item.field };
313
+ }
314
+ }
315
+ function extractAggregateGroupValues(row, groupBy) {
316
+ return Object.fromEntries(groupBy.map((item) => [
317
+ item.as,
318
+ formatGroupValue(row[item.as], item.grain),
319
+ ]));
320
+ }
321
+ function ensureAggregateGroup(groups, row, groupBy) {
322
+ const groupValues = groupBy.length ? extractAggregateGroupValues(row, groupBy) : {};
323
+ const key = JSON.stringify(groupValues);
324
+ const existingGroup = groups.get(key);
325
+ if (existingGroup) {
326
+ return existingGroup;
327
+ }
328
+ groups.set(key, groupValues);
329
+ return groupValues;
330
+ }
331
+ function applyAggregateDefaults(values, select) {
332
+ for (const item of select) {
333
+ if (typeof values[item.as] === 'undefined') {
334
+ values[item.as] = 0;
335
+ }
336
+ }
337
+ return values;
338
+ }
339
+ function groupAggregateSelectItems(select) {
340
+ var _a;
341
+ const groups = new Map();
342
+ for (const item of select) {
343
+ const filters = getAdminForthFilters(item.filters);
344
+ const key = getFilterCacheKey(filters);
345
+ const group = (_a = groups.get(key)) !== null && _a !== void 0 ? _a : { filters, items: [] };
346
+ group.items.push(item);
347
+ groups.set(key, group);
348
+ }
349
+ return Array.from(groups.values());
350
+ }
351
+ function getFilterCacheKey(filters) {
352
+ if (Array.isArray(filters) && !filters.length) {
353
+ return '__base__';
354
+ }
355
+ return JSON.stringify(filters);
356
+ }
357
+ function mergeFilters(...filters) {
358
+ const merged = [];
359
+ for (const filter of filters) {
360
+ const normalized = getAdminForthFilters(filter);
361
+ if (Array.isArray(normalized)) {
362
+ merged.push(...normalized);
363
+ continue;
364
+ }
365
+ if (normalized) {
366
+ merged.push(normalized);
367
+ }
368
+ }
369
+ if (!merged.length) {
370
+ return [];
371
+ }
372
+ return merged.length === 1 ? merged[0] : merged;
373
+ }
374
+ function getHiddenAggregateAlias(groupBy, select) {
375
+ const usedAliases = new Set([
376
+ ...groupBy.map((item) => item.as),
377
+ ...select.map((item) => item.as),
378
+ ]);
379
+ let alias = '__adminforth_dashboard_group_seed__';
380
+ while (usedAliases.has(alias)) {
381
+ alias = `_${alias}`;
382
+ }
383
+ return alias;
286
384
  }
287
385
  function formatGroupValue(value, grain) {
288
386
  if (!grain) {
@@ -295,9 +393,6 @@ function formatGroupValue(value, grain) {
295
393
  if (grain === 'year') {
296
394
  return `${date.getUTCFullYear()}`;
297
395
  }
298
- if (grain === 'quarter') {
299
- return `${date.getUTCFullYear()}-Q${Math.floor(date.getUTCMonth() / 3) + 1}`;
300
- }
301
396
  const month = String(date.getUTCMonth() + 1).padStart(2, '0');
302
397
  if (grain === 'month') {
303
398
  return `${date.getUTCFullYear()}-${month}`;
@@ -311,123 +406,58 @@ function formatGroupValue(value, grain) {
311
406
  if (grain === 'day') {
312
407
  return `${date.getUTCFullYear()}-${month}-${day}`;
313
408
  }
314
- const hour = String(date.getUTCHours()).padStart(2, '0');
315
- return `${date.getUTCFullYear()}-${month}-${day}T${hour}:00:00.000Z`;
409
+ return value;
316
410
  }
317
- function normalizeFilters(filters) {
411
+ function getAdminForthFilters(filters) {
318
412
  if (Array.isArray(filters)) {
319
- return filters.map((filter) => normalizeFilterNode(filter));
413
+ return filters.map((filter) => isDashboardFilterExpression(filter)
414
+ ? toAdminForthFilter(filter)
415
+ : filter);
320
416
  }
321
- if (isRecord(filters)) {
322
- return normalizeFilterNode(filters);
417
+ if (isDashboardFilterExpression(filters)) {
418
+ return toAdminForthFilter(filters);
323
419
  }
324
- return [];
325
- }
326
- function normalizeFilterNode(filter) {
327
- if (!isRecord(filter)) {
328
- return filter;
420
+ if (filters) {
421
+ return filters;
329
422
  }
330
- if (Array.isArray(filter.and)) {
331
- return Filters.AND(filter.and.map((item) => normalizeFilterNode(item)));
332
- }
333
- if (Array.isArray(filter.or)) {
334
- return Filters.OR(filter.or.map((item) => normalizeFilterNode(item)));
335
- }
336
- if (typeof filter.field === 'string') {
337
- if (Object.prototype.hasOwnProperty.call(filter, 'eq')) {
338
- return Filters.EQ(filter.field, normalizeFilterValue(filter.eq));
339
- }
340
- if (Object.prototype.hasOwnProperty.call(filter, 'neq')) {
341
- return Filters.NEQ(filter.field, normalizeFilterValue(filter.neq));
342
- }
343
- if (Object.prototype.hasOwnProperty.call(filter, 'gt')) {
344
- return Filters.GT(filter.field, normalizeFilterValue(filter.gt));
345
- }
346
- if (Object.prototype.hasOwnProperty.call(filter, 'gte')) {
347
- return Filters.GTE(filter.field, normalizeFilterValue(filter.gte));
348
- }
349
- if (Object.prototype.hasOwnProperty.call(filter, 'lt')) {
350
- return Filters.LT(filter.field, normalizeFilterValue(filter.lt));
351
- }
352
- if (Object.prototype.hasOwnProperty.call(filter, 'lte')) {
353
- return Filters.LTE(filter.field, normalizeFilterValue(filter.lte));
354
- }
355
- if (Object.prototype.hasOwnProperty.call(filter, 'in')) {
356
- return Filters.IN(filter.field, normalizeFilterValue(filter.in));
357
- }
358
- if (Object.prototype.hasOwnProperty.call(filter, 'not_in')) {
359
- return Filters.NOT_IN(filter.field, normalizeFilterValue(filter.not_in));
360
- }
361
- }
362
- return filter;
423
+ return [];
363
424
  }
364
- function matchesFilterExpression(row, filter) {
365
- var _a, _b, _c, _d;
425
+ function isDashboardFilterExpression(value) {
426
+ if (Array.isArray(value)) {
427
+ return true;
428
+ }
429
+ if (!isRecord(value)) {
430
+ return false;
431
+ }
432
+ return 'and' in value
433
+ || 'or' in value
434
+ || 'eq' in value
435
+ || 'neq' in value
436
+ || 'gt' in value
437
+ || 'gte' in value
438
+ || 'lt' in value
439
+ || 'lte' in value
440
+ || 'in' in value
441
+ || 'not_in' in value
442
+ || 'like' in value
443
+ || 'ilike' in value;
444
+ }
445
+ function toAdminForthFilter(filter) {
366
446
  if (Array.isArray(filter)) {
367
- return filter.every((item) => matchesFilterExpression(row, item));
447
+ return Filters.AND(filter.map((item) => toAdminForthFilter(item)));
368
448
  }
369
449
  if ('and' in filter) {
370
- return filter.and.every((item) => matchesFilterExpression(row, item));
450
+ return Filters.AND(filter.and.map((item) => toAdminForthFilter(item)));
371
451
  }
372
452
  if ('or' in filter) {
373
- return filter.or.some((item) => matchesFilterExpression(row, item));
374
- }
375
- const value = row[filter.field];
376
- if (Object.prototype.hasOwnProperty.call(filter, 'eq')) {
377
- return value === normalizeFilterValue(filter.eq);
378
- }
379
- if (Object.prototype.hasOwnProperty.call(filter, 'neq')) {
380
- return value !== normalizeFilterValue(filter.neq);
381
- }
382
- if (Object.prototype.hasOwnProperty.call(filter, 'gt')) {
383
- return compareComparableValues(value, normalizeFilterValue(filter.gt)) > 0;
384
- }
385
- if (Object.prototype.hasOwnProperty.call(filter, 'gte')) {
386
- return compareComparableValues(value, normalizeFilterValue(filter.gte)) >= 0;
387
- }
388
- if (Object.prototype.hasOwnProperty.call(filter, 'lt')) {
389
- return compareComparableValues(value, normalizeFilterValue(filter.lt)) < 0;
453
+ return Filters.OR(filter.or.map((item) => toAdminForthFilter(item)));
390
454
  }
391
- if (Object.prototype.hasOwnProperty.call(filter, 'lte')) {
392
- return compareComparableValues(value, normalizeFilterValue(filter.lte)) <= 0;
393
- }
394
- if (Object.prototype.hasOwnProperty.call(filter, 'in')) {
395
- return (_b = (_a = filter.in) === null || _a === void 0 ? void 0 : _a.includes(value)) !== null && _b !== void 0 ? _b : false;
396
- }
397
- if (Object.prototype.hasOwnProperty.call(filter, 'not_in')) {
398
- return !((_d = (_c = filter.not_in) === null || _c === void 0 ? void 0 : _c.includes(value)) !== null && _d !== void 0 ? _d : false);
399
- }
400
- return true;
401
- }
402
- function compareComparableValues(left, right) {
403
- const leftNumber = Number(left);
404
- const rightNumber = Number(right);
405
- if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) {
406
- return leftNumber - rightNumber;
407
- }
408
- return String(left !== null && left !== void 0 ? left : '').localeCompare(String(right !== null && right !== void 0 ? right : ''));
409
- }
410
- function normalizeFilterValue(value) {
411
- if (!isRecord(value) || typeof value.now_minus !== 'string') {
412
- return value;
413
- }
414
- const match = value.now_minus.match(NOW_MINUS_RE);
415
- if (!match) {
416
- return value;
417
- }
418
- const amount = Number(match[1]);
419
- const unit = match[2];
420
- const date = new Date();
421
- if (unit === 'h') {
422
- date.setHours(date.getHours() - amount);
423
- }
424
- else if (unit === 'w') {
425
- date.setDate(date.getDate() - amount * 7);
426
- }
427
- else {
428
- date.setDate(date.getDate() - amount);
455
+ for (const [operator, createFilter] of Object.entries(FILTER_OPERATORS)) {
456
+ if (Object.prototype.hasOwnProperty.call(filter, operator)) {
457
+ return createFilter(filter.field, filter[operator]);
458
+ }
429
459
  }
430
- return date.toISOString();
460
+ return Filters.AND([]);
431
461
  }
432
462
  function toFiniteNumber(value) {
433
463
  const numberValue = typeof value === 'number' ? value : Number(value);
@@ -1,35 +1,22 @@
1
1
  import type { AdminUser, IHttpServer } from 'adminforth';
2
- import { normalizeDashboardConfig } from '../custom/model/dashboard.types.js';
3
- import type { DashboardConfig, DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
2
+ import type { DashboardConfig } from '../custom/model/dashboard.types.js';
4
3
  import {
5
4
  DashboardApiResponseSchema,
6
- DashboardConfigZodSchema,
7
5
  SetDashboardConfigRequestSchema,
8
6
  SlugRequestSchema,
9
7
  } from '../schema/api.js';
10
- import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
11
8
  import type { DashboardRecord, PersistedDashboardResponse } from '../services/dashboardConfigService.js';
12
- import { buildDashboardResponse } from '../services/dashboardConfigService.js';
13
9
 
14
10
  type DashboardEndpointsContext = {
15
11
  canEditDashboard: (adminUser: AdminUser) => boolean;
16
12
  getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
13
+ parseStoredDashboardConfig: (config: unknown) => DashboardConfig;
17
14
  persistDashboardConfig: (
18
15
  dashboard: DashboardRecord,
19
16
  config: DashboardConfig,
20
17
  ) => Promise<PersistedDashboardResponse>;
21
- validateDashboardWidgetApiConfig: (
22
- widget: DashboardWidgetConfig,
23
- ) => DashboardWidgetConfigValidationError[];
24
18
  };
25
19
 
26
- function formatDashboardConfigValidationErrors(error: { issues: { path: PropertyKey[], message: string }[] }) {
27
- return error.issues.map((issue) => ({
28
- field: issue.path.length ? issue.path.map(String).join('.') : 'config',
29
- message: issue.message,
30
- }));
31
- }
32
-
33
20
  export function registerDashboardEndpoints(
34
21
  server: IHttpServer,
35
22
  ctx: DashboardEndpointsContext,
@@ -41,15 +28,20 @@ export function registerDashboardEndpoints(
41
28
  request_schema: SlugRequestSchema,
42
29
  response_schema: DashboardApiResponseSchema,
43
30
  handler: async ({ body, response }) => {
44
- const slug = String(body?.slug || 'default');
45
- const dashboard = await ctx.getDashboardRecord(slug);
31
+ const dashboard = await ctx.getDashboardRecord(body.slug);
46
32
 
47
33
  if (!dashboard) {
48
34
  response.setStatus(404);
49
35
  return { error: 'Dashboard not found' };
50
36
  }
51
37
 
52
- return buildDashboardResponse(dashboard);
38
+ return {
39
+ id: dashboard.id,
40
+ slug: dashboard.slug,
41
+ label: dashboard.label,
42
+ revision: dashboard.revision,
43
+ config: ctx.parseStoredDashboardConfig(dashboard.config),
44
+ };
53
45
  },
54
46
  });
55
47
 
@@ -65,41 +57,16 @@ export function registerDashboardEndpoints(
65
57
  return { error: 'Dashboard edit is not allowed' };
66
58
  }
67
59
 
68
- const slug = String(body?.slug || 'default');
69
- const dashboard = await ctx.getDashboardRecord(slug);
60
+ const dashboard = await ctx.getDashboardRecord(body.slug);
70
61
 
71
62
  if (!dashboard) {
72
63
  response.setStatus(404);
73
64
  return { error: 'Dashboard not found' };
74
65
  }
75
66
 
76
- const normalizedConfig = normalizeDashboardConfig(body?.config);
77
- const parsedConfig = DashboardConfigZodSchema.safeParse(normalizedConfig);
78
-
79
- if (!parsedConfig.success) {
80
- response.setStatus(422);
81
- return {
82
- error: 'Invalid dashboard config',
83
- validationErrors: formatDashboardConfigValidationErrors(parsedConfig.error),
84
- };
85
- }
86
-
87
- const widgetValidationErrors = parsedConfig.data.widgets.flatMap((widget, index) => (
88
- ctx.validateDashboardWidgetApiConfig(widget as DashboardWidgetConfig).map((error) => ({
89
- ...error,
90
- field: `widgets.${index}.${error.field}`,
91
- }))
92
- ));
93
-
94
- if (widgetValidationErrors.length) {
95
- response.setStatus(422);
96
- return {
97
- error: 'Invalid dashboard config',
98
- validationErrors: widgetValidationErrors,
99
- };
100
- }
67
+ const config = body.config as DashboardConfig;
101
68
 
102
- return ctx.persistDashboardConfig(dashboard, parsedConfig.data as DashboardConfig);
69
+ return ctx.persistDashboardConfig(dashboard, config);
103
70
  },
104
71
  });
105
72
  }