@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.
- package/README.md +23 -4
- package/custom/api/dashboardApi.ts +6 -9
- package/custom/model/dashboard.types.ts +60 -275
- package/custom/model/dashboardTopics.ts +5 -0
- package/custom/package.json +1 -0
- package/custom/runtime/DashboardGroup.vue +2 -2
- package/custom/runtime/DashboardPage.vue +17 -7
- package/custom/runtime/DashboardRuntime.vue +20 -8
- package/custom/runtime/WidgetRenderer.vue +1 -2
- package/custom/runtime/WidgetShell.vue +3 -3
- package/custom/skills/adminforth-dashboard/SKILL.md +2 -2
- package/custom/widgets/{gauge-card/GaugeCardWidget.vue → GaugeCardWidget.vue} +63 -61
- package/custom/widgets/{kpi-card/KpiCardWidget.vue → KpiCardWidget.vue} +35 -33
- package/custom/widgets/{pivot-table/PivotTableWidget.vue → PivotTableWidget.vue} +71 -68
- package/custom/widgets/{table/TableWidget.vue → TableWidget.vue} +5 -5
- package/custom/widgets/chart/{bar/BarChart.vue → BarChart.vue} +2 -2
- package/custom/widgets/chart/ChartWidget.vue +4 -15
- package/{dist/custom/widgets/chart/funnel → custom/widgets/chart}/FunnelChart.vue +80 -78
- package/{dist/custom/widgets/chart/line → custom/widgets/chart}/LineChart.vue +2 -2
- package/custom/widgets/chart/{pie/PieChart.vue → PieChart.vue} +2 -2
- package/{dist/custom/widgets/chart/stacked-bar → custom/widgets/chart}/StackedBarChart.vue +97 -95
- package/custom/widgets/chart/chart.types.ts +0 -28
- package/dist/custom/api/dashboardApi.d.ts +4 -8
- package/dist/custom/api/dashboardApi.js +2 -6
- package/dist/custom/api/dashboardApi.ts +6 -9
- package/dist/custom/composables/useElementSize.js +7 -10
- package/dist/custom/model/dashboard.types.d.ts +38 -32
- package/dist/custom/model/dashboard.types.js +4 -161
- package/dist/custom/model/dashboard.types.ts +60 -275
- package/dist/custom/model/dashboardTopics.d.ts +2 -0
- package/dist/custom/model/dashboardTopics.js +4 -0
- package/dist/custom/model/dashboardTopics.ts +5 -0
- package/dist/custom/package.json +1 -0
- package/dist/custom/queries/useDashboardConfig.d.ts +96 -96
- package/dist/custom/queries/useDashboardConfig.js +9 -12
- package/dist/custom/queries/useWidgetData.d.ts +96 -96
- package/dist/custom/queries/useWidgetData.js +9 -12
- package/dist/custom/runtime/DashboardGroup.vue +2 -2
- package/dist/custom/runtime/DashboardPage.vue +17 -7
- package/dist/custom/runtime/DashboardRuntime.vue +20 -8
- package/dist/custom/runtime/WidgetRenderer.vue +1 -2
- package/dist/custom/runtime/WidgetShell.vue +3 -3
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +2 -2
- package/dist/custom/widgets/{gauge-card/GaugeCardWidget.vue → GaugeCardWidget.vue} +63 -61
- package/dist/custom/widgets/{kpi-card/KpiCardWidget.vue → KpiCardWidget.vue} +35 -33
- package/dist/custom/widgets/{pivot-table/PivotTableWidget.vue → PivotTableWidget.vue} +71 -68
- package/dist/custom/widgets/{table/TableWidget.vue → TableWidget.vue} +5 -5
- package/dist/custom/widgets/chart/{bar/BarChart.vue → BarChart.vue} +2 -2
- package/dist/custom/widgets/chart/ChartWidget.vue +4 -15
- package/{custom/widgets/chart/funnel → dist/custom/widgets/chart}/FunnelChart.vue +80 -78
- package/{custom/widgets/chart/line → dist/custom/widgets/chart}/LineChart.vue +2 -2
- package/dist/custom/widgets/chart/{pie/PieChart.vue → PieChart.vue} +2 -2
- package/{custom/widgets/chart/stacked-bar → dist/custom/widgets/chart}/StackedBarChart.vue +97 -95
- package/dist/custom/widgets/chart/chart.types.d.ts +0 -2
- package/dist/custom/widgets/chart/chart.types.js +1 -25
- package/dist/custom/widgets/chart/chart.types.ts +0 -28
- package/dist/custom/widgets/chart/chart.utils.js +6 -14
- package/dist/custom/widgets/registry.js +14 -22
- package/dist/endpoint/dashboard.d.ts +2 -3
- package/dist/endpoint/dashboard.js +12 -32
- package/dist/endpoint/groups.d.ts +2 -21
- package/dist/endpoint/groups.js +18 -16
- package/dist/endpoint/widgets.d.ts +0 -3
- package/dist/endpoint/widgets.js +27 -74
- package/dist/index.js +1 -3
- package/dist/schema/api.d.ts +2090 -511
- package/dist/schema/api.js +18 -15
- package/dist/schema/widget.d.ts +1003 -250
- package/dist/schema/widget.js +102 -46
- package/dist/services/dashboardConfigService.d.ts +0 -10
- package/dist/services/dashboardConfigService.js +6 -21
- package/dist/services/widgetDataService.js +226 -196
- package/endpoint/dashboard.ts +13 -46
- package/endpoint/groups.ts +25 -42
- package/endpoint/widgets.ts +36 -95
- package/index.ts +0 -3
- package/package.json +3 -3
- package/schema/api.ts +19 -15
- package/schema/widget.ts +113 -52
- package/services/dashboardConfigService.ts +6 -25
- package/services/widgetDataService.ts +304 -229
- package/custom/widgets/chart/histogram/HistogramChart.vue +0 -21
- package/dist/custom/widgets/chart/histogram/HistogramChart.vue +0 -21
- package/dist/services/widgetConfigValidator.d.ts +0 -8
- package/dist/services/widgetConfigValidator.js +0 -27
- package/services/widgetConfigValidator.ts +0 -61
|
@@ -9,9 +9,8 @@ import type {
|
|
|
9
9
|
DashboardWidgetConfig,
|
|
10
10
|
DashboardWidgetData,
|
|
11
11
|
DashboardVariables,
|
|
12
|
-
FilterExpression,
|
|
13
12
|
FunnelQueryConfig,
|
|
14
|
-
|
|
13
|
+
FilterExpression,
|
|
15
14
|
QueryAggregateSelectItem,
|
|
16
15
|
QueryCalcSelectItem,
|
|
17
16
|
QueryConfig,
|
|
@@ -35,16 +34,55 @@ type DashboardWidgetFilters =
|
|
|
35
34
|
| IAdminForthAndOrFilter
|
|
36
35
|
| Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>;
|
|
37
36
|
|
|
38
|
-
type
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
type AggregateRule =
|
|
38
|
+
| { operation: 'count' }
|
|
39
|
+
| { operation: Exclude<QueryAggregateSelectItem['agg'], 'count'>; field: string };
|
|
40
|
+
|
|
41
|
+
type AggregateGroupByRule =
|
|
42
|
+
| {
|
|
43
|
+
type: 'date_trunc';
|
|
44
|
+
field: string;
|
|
45
|
+
truncation: TimeGrain;
|
|
46
|
+
timezone?: string;
|
|
47
|
+
as: string;
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
type: 'field';
|
|
51
|
+
field: string;
|
|
52
|
+
as: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type AggregateResource = {
|
|
56
|
+
aggregate: (
|
|
57
|
+
filters: DashboardWidgetFilters,
|
|
58
|
+
aggregations: Record<string, AggregateRule>,
|
|
59
|
+
groupBy?: AggregateGroupByRule | AggregateGroupByRule[],
|
|
60
|
+
) => Promise<Record<string, unknown>[]>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type EffectiveGroupByItem = {
|
|
64
|
+
field: string;
|
|
65
|
+
as: string;
|
|
66
|
+
grain?: TimeGrain;
|
|
67
|
+
timezone?: string;
|
|
41
68
|
};
|
|
42
69
|
|
|
43
|
-
const NOW_MINUS_RE = /^(\d+)([dhw])$/;
|
|
44
70
|
const CALC_IDENTIFIER_RE = /\b[a-zA-Z_][a-zA-Z0-9_]*\b/g;
|
|
45
71
|
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;
|
|
46
72
|
const VARIABLE_PATH_PREFIX_RE = /^\$variables\.?/;
|
|
47
73
|
const SAFE_CALC_EXPRESSION_RE = /^[\d+\-*/().\s]+$/;
|
|
74
|
+
const FILTER_OPERATORS = {
|
|
75
|
+
eq: Filters.EQ,
|
|
76
|
+
neq: Filters.NEQ,
|
|
77
|
+
gt: Filters.GT,
|
|
78
|
+
gte: Filters.GTE,
|
|
79
|
+
lt: Filters.LT,
|
|
80
|
+
lte: Filters.LTE,
|
|
81
|
+
in: Filters.IN,
|
|
82
|
+
not_in: Filters.NOT_IN,
|
|
83
|
+
like: Filters.LIKE,
|
|
84
|
+
ilike: Filters.ILIKE,
|
|
85
|
+
} as const;
|
|
48
86
|
|
|
49
87
|
export type WidgetDataService = {
|
|
50
88
|
getWidgetData: (widget: DashboardWidgetConfig, options?: DashboardWidgetDataOptions) => Promise<DashboardWidgetData | null>;
|
|
@@ -90,12 +128,18 @@ async function getFunnelWidgetData(
|
|
|
90
128
|
): Promise<DashboardWidgetData> {
|
|
91
129
|
const rows = await Promise.all(query.steps.map(async (step) => {
|
|
92
130
|
const valueField = step.metric.as;
|
|
93
|
-
const
|
|
131
|
+
const [values = {}] = await getAggregateRows(
|
|
132
|
+
adminforth,
|
|
133
|
+
step.resource,
|
|
134
|
+
step.filters,
|
|
135
|
+
[step.metric],
|
|
136
|
+
[],
|
|
137
|
+
);
|
|
94
138
|
|
|
95
139
|
const row: Record<string, unknown> = {
|
|
96
140
|
name: step.name,
|
|
97
141
|
resource: step.resource,
|
|
98
|
-
[valueField]:
|
|
142
|
+
[valueField]: values[valueField] ?? 0,
|
|
99
143
|
};
|
|
100
144
|
|
|
101
145
|
for (const calc of query.calcs ?? []) {
|
|
@@ -121,9 +165,14 @@ async function getQueryWidgetData(
|
|
|
121
165
|
query: QueryConfig,
|
|
122
166
|
variables: DashboardVariables,
|
|
123
167
|
): Promise<DashboardWidgetData> {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
168
|
+
const selectedRows = isAggregateQuery(query)
|
|
169
|
+
? await buildAggregateQueryRows(adminforth, query, variables)
|
|
170
|
+
: buildPlainQueryRows(
|
|
171
|
+
await getResourceRows(adminforth, query.resource, query.filters, getBackendSort(query.order_by)),
|
|
172
|
+
query,
|
|
173
|
+
variables,
|
|
174
|
+
);
|
|
175
|
+
const orderedRows = sortRows(selectedRows, query.order_by);
|
|
127
176
|
const slicedRows = typeof query.limit === 'number'
|
|
128
177
|
? orderedRows.slice(query.offset ?? 0, (query.offset ?? 0) + query.limit)
|
|
129
178
|
: orderedRows.slice(query.offset ?? 0);
|
|
@@ -150,85 +199,97 @@ async function getQueryWidgetData(
|
|
|
150
199
|
async function getResourceRows(
|
|
151
200
|
adminforth: IAdminForth,
|
|
152
201
|
resourceId: string,
|
|
153
|
-
filters:
|
|
202
|
+
filters: FilterExpression | undefined,
|
|
154
203
|
sort?: IAdminForthSort | IAdminForthSort[],
|
|
155
204
|
) {
|
|
156
205
|
return adminforth.resource(resourceId).list(
|
|
157
|
-
|
|
206
|
+
getAdminForthFilters(filters),
|
|
158
207
|
undefined,
|
|
159
208
|
0,
|
|
160
209
|
sort,
|
|
161
210
|
);
|
|
162
211
|
}
|
|
163
212
|
|
|
164
|
-
function
|
|
213
|
+
function buildPlainQueryRows(rows: Record<string, unknown>[], query: QueryConfig, variables: DashboardVariables) {
|
|
165
214
|
const select = query.select ?? getDefaultSelect(rows);
|
|
166
|
-
const groupBy = query.groupBy ?? [];
|
|
167
|
-
|
|
168
|
-
if (isAggregateQuery(query)) {
|
|
169
|
-
return buildGroupedRows(rows, select, groupBy, variables, query.calcs);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
215
|
return rows.map((row) => buildPlainRow(row, select, query.calcs, variables));
|
|
173
216
|
}
|
|
174
217
|
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
groupBy: QueryGroupByItem[],
|
|
218
|
+
async function buildAggregateQueryRows(
|
|
219
|
+
adminforth: IAdminForth,
|
|
220
|
+
query: QueryConfig,
|
|
179
221
|
variables: DashboardVariables,
|
|
180
|
-
calcs: QueryCalcSelectItem[] = [],
|
|
181
222
|
) {
|
|
182
|
-
const
|
|
183
|
-
const effectiveGroupBy =
|
|
184
|
-
|
|
185
|
-
|
|
223
|
+
const select = query.select ?? [];
|
|
224
|
+
const effectiveGroupBy = getEffectiveGroupBy(query.group_by, select);
|
|
225
|
+
const aggregateSelect = select.filter(isAggregateSelectItem);
|
|
226
|
+
const rows = await getAggregateRows(
|
|
227
|
+
adminforth,
|
|
228
|
+
query.resource,
|
|
229
|
+
query.filters,
|
|
230
|
+
aggregateSelect,
|
|
231
|
+
effectiveGroupBy,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
return rows.map((row) => buildCalculatedRow(row, select, query.calcs, variables));
|
|
235
|
+
}
|
|
186
236
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
237
|
+
async function getAggregateRows(
|
|
238
|
+
adminforth: IAdminForth,
|
|
239
|
+
resourceId: string,
|
|
240
|
+
baseFilters: FilterExpression | undefined,
|
|
241
|
+
select: QueryAggregateSelectItem[],
|
|
242
|
+
groupBy: EffectiveGroupByItem[],
|
|
243
|
+
) {
|
|
244
|
+
const resource = adminforth.resource(resourceId) as unknown as AggregateResource;
|
|
245
|
+
const groups = new Map<string, Record<string, unknown>>();
|
|
246
|
+
const groupByRules = groupBy.length ? groupBy.map(toAggregateGroupByRule) : undefined;
|
|
247
|
+
const aggregateSelectGroups = groupAggregateSelectItems(select);
|
|
248
|
+
|
|
249
|
+
if (groupBy.length) {
|
|
250
|
+
const groupSeedAlias = getHiddenAggregateAlias(groupBy, select);
|
|
251
|
+
const groupSeedRows = await resource.aggregate(
|
|
252
|
+
getAdminForthFilters(baseFilters),
|
|
253
|
+
{ [groupSeedAlias]: { operation: 'count' } },
|
|
254
|
+
groupByRules,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
for (const row of groupSeedRows) {
|
|
258
|
+
ensureAggregateGroup(groups, row, groupBy);
|
|
259
|
+
}
|
|
190
260
|
}
|
|
191
261
|
|
|
192
|
-
for (const
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
262
|
+
for (const filterGroup of aggregateSelectGroups) {
|
|
263
|
+
const rows = await resource.aggregate(
|
|
264
|
+
mergeFilters(baseFilters, filterGroup.filters),
|
|
265
|
+
Object.fromEntries(filterGroup.items.map((item) => [item.as, toAggregationRule(item)])),
|
|
266
|
+
groupByRules,
|
|
267
|
+
);
|
|
197
268
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const key = JSON.stringify(values);
|
|
201
|
-
const group = groups.get(key) ?? { values, rows: [] };
|
|
269
|
+
for (const row of rows) {
|
|
270
|
+
const values = ensureAggregateGroup(groups, row, groupBy);
|
|
202
271
|
|
|
203
|
-
|
|
204
|
-
|
|
272
|
+
for (const item of filterGroup.items) {
|
|
273
|
+
values[item.as] = row[item.as] ?? 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
205
276
|
}
|
|
206
277
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
278
|
+
if (!groups.size && !groupBy.length && select.length) {
|
|
279
|
+
groups.set(JSON.stringify({}), {});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return Array.from(groups.values(), (row) => applyAggregateDefaults(row, select));
|
|
211
283
|
}
|
|
212
284
|
|
|
213
|
-
function
|
|
214
|
-
|
|
285
|
+
function buildCalculatedRow(
|
|
286
|
+
baseValues: Record<string, unknown>,
|
|
215
287
|
select: QuerySelectItem[],
|
|
216
|
-
calcs: QueryCalcSelectItem[],
|
|
288
|
+
calcs: QueryCalcSelectItem[] = [],
|
|
217
289
|
variables: DashboardVariables,
|
|
218
|
-
baseValues: Record<string, unknown> = {},
|
|
219
290
|
) {
|
|
220
291
|
const values: Record<string, unknown> = { ...baseValues };
|
|
221
292
|
|
|
222
|
-
for (const item of select) {
|
|
223
|
-
if (isAggregateSelectItem(item)) {
|
|
224
|
-
const filteredRows = item.filters
|
|
225
|
-
? rows.filter((row) => matchesFilterExpression(row, item.filters as FilterExpression))
|
|
226
|
-
: rows;
|
|
227
|
-
|
|
228
|
-
values[item.as] = calculateAggregate(filteredRows, item);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
293
|
for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
|
|
233
294
|
values[item.as] = evaluateCalc(item.calc, values, variables);
|
|
234
295
|
}
|
|
@@ -259,50 +320,6 @@ function buildPlainRow(
|
|
|
259
320
|
return values;
|
|
260
321
|
}
|
|
261
322
|
|
|
262
|
-
function calculateAggregate(rows: Record<string, unknown>[], item: QueryAggregateSelectItem) {
|
|
263
|
-
switch (item.agg) {
|
|
264
|
-
case 'count':
|
|
265
|
-
return rows.length;
|
|
266
|
-
case 'count_distinct':
|
|
267
|
-
return new Set(rows.map((row) => row[item.field!])).size;
|
|
268
|
-
case 'sum':
|
|
269
|
-
return aggregateNumbers(rows, item.field!, (values) => values.reduce((sum, value) => sum + value, 0));
|
|
270
|
-
case 'avg':
|
|
271
|
-
return aggregateNumbers(rows, item.field!, (values) => values.length
|
|
272
|
-
? values.reduce((sum, value) => sum + value, 0) / values.length
|
|
273
|
-
: 0);
|
|
274
|
-
case 'min':
|
|
275
|
-
return aggregateNumbers(rows, item.field!, (values) => values.length ? Math.min(...values) : 0);
|
|
276
|
-
case 'max':
|
|
277
|
-
return aggregateNumbers(rows, item.field!, (values) => values.length ? Math.max(...values) : 0);
|
|
278
|
-
case 'median':
|
|
279
|
-
return aggregateNumbers(rows, item.field!, calculateMedian);
|
|
280
|
-
default:
|
|
281
|
-
throw new Error(`Unsupported aggregation operation: ${(item as { agg: QueryAggregateOperation }).agg}`);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function aggregateNumbers(
|
|
286
|
-
rows: Record<string, unknown>[],
|
|
287
|
-
field: string,
|
|
288
|
-
aggregate: (values: number[]) => number,
|
|
289
|
-
) {
|
|
290
|
-
return aggregate(rows.map((row) => toFiniteNumber(row[field])).filter(Number.isFinite));
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function calculateMedian(values: number[]) {
|
|
294
|
-
if (!values.length) {
|
|
295
|
-
return 0;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const sorted = [...values].sort((left, right) => left - right);
|
|
299
|
-
const middle = Math.floor(sorted.length / 2);
|
|
300
|
-
|
|
301
|
-
return sorted.length % 2
|
|
302
|
-
? sorted[middle]
|
|
303
|
-
: (sorted[middle - 1] + sorted[middle]) / 2;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
323
|
function evaluateCalc(calc: string, values: Record<string, unknown>, variables: DashboardVariables) {
|
|
307
324
|
const expression = calc
|
|
308
325
|
.replace(LOOKUP_CALL_RE, (_match, path: string, keyField: string, defaultValue: string) => {
|
|
@@ -369,7 +386,7 @@ function getBackendSort(orderBy: QueryOrderByItem[] | undefined) {
|
|
|
369
386
|
|
|
370
387
|
function getColumns(rows: Record<string, unknown>[], query: QueryConfig) {
|
|
371
388
|
const selectColumns = [
|
|
372
|
-
...(query.
|
|
389
|
+
...(query.group_by ?? []).map(getGroupByAlias),
|
|
373
390
|
...(query.select ?? []).map(getSelectAlias),
|
|
374
391
|
...(query.calcs ?? []).map((item) => item.as),
|
|
375
392
|
].filter(Boolean);
|
|
@@ -383,7 +400,7 @@ function getDefaultSelect(rows: Record<string, unknown>[]): QuerySelectItem[] {
|
|
|
383
400
|
|
|
384
401
|
function isAggregateQuery(query: QueryConfig) {
|
|
385
402
|
return Boolean(
|
|
386
|
-
query.
|
|
403
|
+
query.group_by?.length
|
|
387
404
|
|| query.select?.some((item) => isAggregateSelectItem(item)),
|
|
388
405
|
);
|
|
389
406
|
}
|
|
@@ -408,206 +425,264 @@ function getSelectAlias(item: QuerySelectItem) {
|
|
|
408
425
|
return item.as;
|
|
409
426
|
}
|
|
410
427
|
|
|
411
|
-
function getGroupByField(item: QueryGroupByItem) {
|
|
412
|
-
return typeof item === 'string' ? item : item.field;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
428
|
function getGroupByAlias(item: QueryGroupByItem) {
|
|
416
429
|
return typeof item === 'string' ? item : item.as ?? item.field;
|
|
417
430
|
}
|
|
418
431
|
|
|
419
|
-
function
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
function formatGroupValue(value: unknown, grain: TimeGrain | undefined) {
|
|
424
|
-
if (!grain) {
|
|
425
|
-
return value;
|
|
432
|
+
function getEffectiveGroupBy(groupBy: QueryGroupByItem[] | undefined, select: QuerySelectItem[]) {
|
|
433
|
+
if (groupBy?.length) {
|
|
434
|
+
return groupBy.map((item) => normalizeGroupByItem(item));
|
|
426
435
|
}
|
|
427
436
|
|
|
428
|
-
|
|
437
|
+
return select
|
|
438
|
+
.filter(isFieldSelectItem)
|
|
439
|
+
.map((item) => normalizeGroupByItem({ field: item.field, as: item.as, grain: item.grain }));
|
|
440
|
+
}
|
|
429
441
|
|
|
430
|
-
|
|
431
|
-
|
|
442
|
+
function normalizeGroupByItem(item: QueryGroupByItem | Pick<QueryFieldSelectItem, 'field' | 'as' | 'grain'>): EffectiveGroupByItem {
|
|
443
|
+
if (typeof item === 'string') {
|
|
444
|
+
return { field: item, as: item };
|
|
432
445
|
}
|
|
433
446
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
447
|
+
return {
|
|
448
|
+
field: item.field,
|
|
449
|
+
as: item.as ?? item.field,
|
|
450
|
+
grain: item.grain,
|
|
451
|
+
timezone: 'timezone' in item ? item.timezone : undefined,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
437
454
|
|
|
438
|
-
|
|
439
|
-
|
|
455
|
+
function toAggregateGroupByRule(item: EffectiveGroupByItem): AggregateGroupByRule {
|
|
456
|
+
if (item.grain) {
|
|
457
|
+
return {
|
|
458
|
+
type: 'date_trunc',
|
|
459
|
+
field: item.field,
|
|
460
|
+
truncation: item.grain,
|
|
461
|
+
timezone: item.timezone,
|
|
462
|
+
as: item.as,
|
|
463
|
+
};
|
|
440
464
|
}
|
|
441
465
|
|
|
442
|
-
|
|
466
|
+
return {
|
|
467
|
+
type: 'field',
|
|
468
|
+
field: item.field,
|
|
469
|
+
as: item.as,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
443
472
|
|
|
444
|
-
|
|
445
|
-
|
|
473
|
+
function toAggregationRule(item: QueryAggregateSelectItem): AggregateRule {
|
|
474
|
+
switch (item.agg) {
|
|
475
|
+
case 'count':
|
|
476
|
+
return { operation: 'count' };
|
|
477
|
+
case 'count_distinct':
|
|
478
|
+
return { operation: 'count_distinct', field: item.field! };
|
|
479
|
+
case 'sum':
|
|
480
|
+
return { operation: 'sum', field: item.field! };
|
|
481
|
+
case 'avg':
|
|
482
|
+
return { operation: 'avg', field: item.field! };
|
|
483
|
+
case 'min':
|
|
484
|
+
return { operation: 'min', field: item.field! };
|
|
485
|
+
case 'max':
|
|
486
|
+
return { operation: 'max', field: item.field! };
|
|
487
|
+
case 'median':
|
|
488
|
+
return { operation: 'median', field: item.field! };
|
|
446
489
|
}
|
|
490
|
+
}
|
|
447
491
|
|
|
448
|
-
|
|
492
|
+
function extractAggregateGroupValues(row: Record<string, unknown>, groupBy: EffectiveGroupByItem[]) {
|
|
493
|
+
return Object.fromEntries(groupBy.map((item) => [
|
|
494
|
+
item.as,
|
|
495
|
+
formatGroupValue(row[item.as], item.grain),
|
|
496
|
+
]));
|
|
497
|
+
}
|
|
449
498
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
499
|
+
function ensureAggregateGroup(
|
|
500
|
+
groups: Map<string, Record<string, unknown>>,
|
|
501
|
+
row: Record<string, unknown>,
|
|
502
|
+
groupBy: EffectiveGroupByItem[],
|
|
503
|
+
) {
|
|
504
|
+
const groupValues = groupBy.length ? extractAggregateGroupValues(row, groupBy) : {};
|
|
505
|
+
const key = JSON.stringify(groupValues);
|
|
506
|
+
const existingGroup = groups.get(key);
|
|
455
507
|
|
|
456
|
-
if (
|
|
457
|
-
return
|
|
508
|
+
if (existingGroup) {
|
|
509
|
+
return existingGroup;
|
|
458
510
|
}
|
|
459
511
|
|
|
460
|
-
|
|
461
|
-
return
|
|
512
|
+
groups.set(key, groupValues);
|
|
513
|
+
return groupValues;
|
|
462
514
|
}
|
|
463
515
|
|
|
464
|
-
function
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (isRecord(filters)) {
|
|
470
|
-
return normalizeFilterNode(filters);
|
|
516
|
+
function applyAggregateDefaults(values: Record<string, unknown>, select: QueryAggregateSelectItem[]) {
|
|
517
|
+
for (const item of select) {
|
|
518
|
+
if (typeof values[item.as] === 'undefined') {
|
|
519
|
+
values[item.as] = 0;
|
|
520
|
+
}
|
|
471
521
|
}
|
|
472
522
|
|
|
473
|
-
return
|
|
523
|
+
return values;
|
|
474
524
|
}
|
|
475
525
|
|
|
476
|
-
function
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
526
|
+
function groupAggregateSelectItems(select: QueryAggregateSelectItem[]) {
|
|
527
|
+
const groups = new Map<string, {
|
|
528
|
+
filters: DashboardWidgetFilters;
|
|
529
|
+
items: QueryAggregateSelectItem[];
|
|
530
|
+
}>();
|
|
480
531
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
532
|
+
for (const item of select) {
|
|
533
|
+
const filters = getAdminForthFilters(item.filters);
|
|
534
|
+
const key = getFilterCacheKey(filters);
|
|
535
|
+
const group = groups.get(key) ?? { filters, items: [] };
|
|
484
536
|
|
|
485
|
-
|
|
486
|
-
|
|
537
|
+
group.items.push(item);
|
|
538
|
+
groups.set(key, group);
|
|
487
539
|
}
|
|
488
540
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
return Filters.EQ(filter.field, normalizeFilterValue(filter.eq));
|
|
492
|
-
}
|
|
541
|
+
return Array.from(groups.values());
|
|
542
|
+
}
|
|
493
543
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
544
|
+
function getFilterCacheKey(filters: DashboardWidgetFilters) {
|
|
545
|
+
if (Array.isArray(filters) && !filters.length) {
|
|
546
|
+
return '__base__';
|
|
547
|
+
}
|
|
497
548
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
549
|
+
return JSON.stringify(filters);
|
|
550
|
+
}
|
|
501
551
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
552
|
+
function mergeFilters(...filters: Array<FilterExpression | DashboardWidgetFilters | undefined>) {
|
|
553
|
+
const merged: Array<IAdminForthSingleFilter | IAdminForthAndOrFilter> = [];
|
|
505
554
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
555
|
+
for (const filter of filters) {
|
|
556
|
+
const normalized = getAdminForthFilters(filter);
|
|
509
557
|
|
|
510
|
-
if (
|
|
511
|
-
|
|
558
|
+
if (Array.isArray(normalized)) {
|
|
559
|
+
merged.push(...normalized);
|
|
560
|
+
continue;
|
|
512
561
|
}
|
|
513
562
|
|
|
514
|
-
if (
|
|
515
|
-
|
|
563
|
+
if (normalized) {
|
|
564
|
+
merged.push(normalized);
|
|
516
565
|
}
|
|
566
|
+
}
|
|
517
567
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
568
|
+
if (!merged.length) {
|
|
569
|
+
return [] as DashboardWidgetFilters;
|
|
521
570
|
}
|
|
522
571
|
|
|
523
|
-
return
|
|
572
|
+
return merged.length === 1 ? merged[0] : merged;
|
|
524
573
|
}
|
|
525
574
|
|
|
526
|
-
function
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
575
|
+
function getHiddenAggregateAlias(groupBy: EffectiveGroupByItem[], select: QueryAggregateSelectItem[]) {
|
|
576
|
+
const usedAliases = new Set([
|
|
577
|
+
...groupBy.map((item) => item.as),
|
|
578
|
+
...select.map((item) => item.as),
|
|
579
|
+
]);
|
|
580
|
+
let alias = '__adminforth_dashboard_group_seed__';
|
|
530
581
|
|
|
531
|
-
|
|
532
|
-
|
|
582
|
+
while (usedAliases.has(alias)) {
|
|
583
|
+
alias = `_${alias}`;
|
|
533
584
|
}
|
|
534
585
|
|
|
535
|
-
|
|
536
|
-
|
|
586
|
+
return alias;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function formatGroupValue(value: unknown, grain: TimeGrain | undefined) {
|
|
590
|
+
if (!grain) {
|
|
591
|
+
return value;
|
|
537
592
|
}
|
|
538
593
|
|
|
539
|
-
const
|
|
594
|
+
const date = new Date(String(value));
|
|
540
595
|
|
|
541
|
-
if (
|
|
542
|
-
return value
|
|
596
|
+
if (!Number.isFinite(date.getTime())) {
|
|
597
|
+
return value;
|
|
543
598
|
}
|
|
544
599
|
|
|
545
|
-
if (
|
|
546
|
-
return
|
|
600
|
+
if (grain === 'year') {
|
|
601
|
+
return `${date.getUTCFullYear()}`;
|
|
547
602
|
}
|
|
548
603
|
|
|
549
|
-
|
|
550
|
-
|
|
604
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
605
|
+
|
|
606
|
+
if (grain === 'month') {
|
|
607
|
+
return `${date.getUTCFullYear()}-${month}`;
|
|
551
608
|
}
|
|
552
609
|
|
|
553
|
-
|
|
554
|
-
|
|
610
|
+
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
611
|
+
|
|
612
|
+
if (grain === 'week') {
|
|
613
|
+
const weekStart = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
614
|
+
weekStart.setUTCDate(weekStart.getUTCDate() - weekStart.getUTCDay());
|
|
615
|
+
return weekStart.toISOString().slice(0, 10);
|
|
555
616
|
}
|
|
556
617
|
|
|
557
|
-
if (
|
|
558
|
-
return
|
|
618
|
+
if (grain === 'day') {
|
|
619
|
+
return `${date.getUTCFullYear()}-${month}-${day}`;
|
|
559
620
|
}
|
|
560
621
|
|
|
561
|
-
|
|
562
|
-
|
|
622
|
+
return value;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function getAdminForthFilters(filters: FilterExpression | DashboardWidgetFilters | undefined): DashboardWidgetFilters {
|
|
626
|
+
if (Array.isArray(filters)) {
|
|
627
|
+
return filters.map((filter) => isDashboardFilterExpression(filter)
|
|
628
|
+
? toAdminForthFilter(filter)
|
|
629
|
+
: filter);
|
|
563
630
|
}
|
|
564
631
|
|
|
565
|
-
if (
|
|
566
|
-
return
|
|
632
|
+
if (isDashboardFilterExpression(filters)) {
|
|
633
|
+
return toAdminForthFilter(filters);
|
|
567
634
|
}
|
|
568
635
|
|
|
569
|
-
if (
|
|
570
|
-
return
|
|
636
|
+
if (filters) {
|
|
637
|
+
return filters;
|
|
571
638
|
}
|
|
572
639
|
|
|
573
|
-
return
|
|
640
|
+
return [];
|
|
574
641
|
}
|
|
575
642
|
|
|
576
|
-
function
|
|
577
|
-
|
|
578
|
-
|
|
643
|
+
function isDashboardFilterExpression(value: unknown): value is FilterExpression {
|
|
644
|
+
if (Array.isArray(value)) {
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
579
647
|
|
|
580
|
-
if (
|
|
581
|
-
return
|
|
648
|
+
if (!isRecord(value)) {
|
|
649
|
+
return false;
|
|
582
650
|
}
|
|
583
651
|
|
|
584
|
-
return
|
|
652
|
+
return 'and' in value
|
|
653
|
+
|| 'or' in value
|
|
654
|
+
|| 'eq' in value
|
|
655
|
+
|| 'neq' in value
|
|
656
|
+
|| 'gt' in value
|
|
657
|
+
|| 'gte' in value
|
|
658
|
+
|| 'lt' in value
|
|
659
|
+
|| 'lte' in value
|
|
660
|
+
|| 'in' in value
|
|
661
|
+
|| 'not_in' in value
|
|
662
|
+
|| 'like' in value
|
|
663
|
+
|| 'ilike' in value;
|
|
585
664
|
}
|
|
586
665
|
|
|
587
|
-
function
|
|
588
|
-
if (
|
|
589
|
-
return
|
|
666
|
+
function toAdminForthFilter(filter: FilterExpression): IAdminForthSingleFilter | IAdminForthAndOrFilter {
|
|
667
|
+
if (Array.isArray(filter)) {
|
|
668
|
+
return Filters.AND(filter.map((item) => toAdminForthFilter(item)));
|
|
590
669
|
}
|
|
591
670
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (!match) {
|
|
595
|
-
return value;
|
|
671
|
+
if ('and' in filter) {
|
|
672
|
+
return Filters.AND(filter.and.map((item) => toAdminForthFilter(item)));
|
|
596
673
|
}
|
|
597
674
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
675
|
+
if ('or' in filter) {
|
|
676
|
+
return Filters.OR(filter.or.map((item) => toAdminForthFilter(item)));
|
|
677
|
+
}
|
|
601
678
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
} else {
|
|
607
|
-
date.setDate(date.getDate() - amount);
|
|
679
|
+
for (const [operator, createFilter] of Object.entries(FILTER_OPERATORS)) {
|
|
680
|
+
if (Object.prototype.hasOwnProperty.call(filter, operator)) {
|
|
681
|
+
return createFilter(filter.field, filter[operator as keyof typeof FILTER_OPERATORS]);
|
|
682
|
+
}
|
|
608
683
|
}
|
|
609
684
|
|
|
610
|
-
return
|
|
685
|
+
return Filters.AND([]);
|
|
611
686
|
}
|
|
612
687
|
|
|
613
688
|
function toFiniteNumber(value: unknown) {
|