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