@adminforth/dashboard 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/custom/api/dashboardApi.ts +137 -1
  2. package/custom/model/dashboard.types.ts +32 -22
  3. package/custom/runtime/DashboardRuntime.vue +2 -3
  4. package/custom/skills/adminforth-dashboard/SKILL.md +66 -10
  5. package/custom/widgets/KpiCardWidget.vue +172 -9
  6. package/custom/widgets/chart/ChartWidget.vue +5 -5
  7. package/custom/widgets/registry.ts +4 -4
  8. package/dist/custom/api/dashboardApi.d.ts +46 -1
  9. package/dist/custom/api/dashboardApi.js +90 -0
  10. package/dist/custom/api/dashboardApi.ts +137 -1
  11. package/dist/custom/model/dashboard.types.d.ts +30 -14
  12. package/dist/custom/model/dashboard.types.js +2 -2
  13. package/dist/custom/model/dashboard.types.ts +32 -22
  14. package/dist/custom/queries/useDashboardConfig.d.ts +106 -104
  15. package/dist/custom/queries/useWidgetData.d.ts +106 -104
  16. package/dist/custom/runtime/DashboardRuntime.vue +2 -3
  17. package/dist/custom/skills/adminforth-dashboard/SKILL.md +66 -10
  18. package/dist/custom/widgets/KpiCardWidget.vue +172 -9
  19. package/dist/custom/widgets/chart/ChartWidget.vue +5 -5
  20. package/dist/custom/widgets/registry.js +4 -4
  21. package/dist/custom/widgets/registry.ts +4 -4
  22. package/dist/endpoint/widgets.js +99 -14
  23. package/dist/schema/api.d.ts +11426 -1634
  24. package/dist/schema/api.js +118 -21
  25. package/dist/schema/widget.d.ts +425 -1980
  26. package/dist/schema/widget.js +13 -374
  27. package/dist/schema/widgets/charts.d.ts +1689 -0
  28. package/dist/schema/widgets/charts.js +92 -0
  29. package/dist/schema/widgets/common.d.ts +275 -0
  30. package/dist/schema/widgets/common.js +171 -0
  31. package/dist/schema/widgets/gauge-card.d.ts +172 -0
  32. package/dist/schema/widgets/gauge-card.js +28 -0
  33. package/dist/schema/widgets/kpi-card.d.ts +212 -0
  34. package/dist/schema/widgets/kpi-card.js +43 -0
  35. package/dist/schema/widgets/pivot-table.d.ts +196 -0
  36. package/dist/schema/widgets/pivot-table.js +17 -0
  37. package/dist/schema/widgets/table.d.ts +130 -0
  38. package/dist/schema/widgets/table.js +12 -0
  39. package/dist/services/widgetDataService.js +96 -2
  40. package/endpoint/widgets.ts +173 -26
  41. package/package.json +1 -1
  42. package/schema/api.ts +148 -22
  43. package/schema/widget.ts +43 -425
  44. package/schema/widgets/charts.ts +113 -0
  45. package/schema/widgets/common.ts +194 -0
  46. package/schema/widgets/gauge-card.ts +34 -0
  47. package/schema/widgets/kpi-card.ts +49 -0
  48. package/schema/widgets/pivot-table.ts +24 -0
  49. package/schema/widgets/table.ts +18 -0
  50. package/services/widgetDataService.ts +129 -3
@@ -0,0 +1,113 @@
1
+ import { z } from 'zod'
2
+ import {
3
+ ChartFieldRefSchema,
4
+ FunnelQueryConfigSchema,
5
+ QueryConfigSchema,
6
+ WidgetBaseSchema,
7
+ } from './common.js'
8
+
9
+ const ChartBaseSchema = z.object({
10
+ title: z.string().optional(),
11
+ }).strict()
12
+
13
+ const ChartBucketSchema = z.object({
14
+ label: z.string().min(1, 'Bucket label is required'),
15
+ min: z.number().optional(),
16
+ max: z.number().optional(),
17
+ }).strict()
18
+
19
+ const ChartSeriesRefSchema = z.object({
20
+ field: z.string(),
21
+ label: z.string().optional(),
22
+ }).strict()
23
+
24
+ export const LineChartSchema = ChartBaseSchema.extend({
25
+ type: z.literal('line'),
26
+ x: ChartFieldRefSchema,
27
+ y: z.array(ChartFieldRefSchema).min(1),
28
+ series: ChartSeriesRefSchema.optional(),
29
+ color: z.string().optional(),
30
+ colors: z.array(z.string()).optional(),
31
+ })
32
+
33
+ export const BarChartSchema = ChartBaseSchema.extend({
34
+ type: z.literal('bar'),
35
+ x: ChartFieldRefSchema,
36
+ y: ChartFieldRefSchema,
37
+ color: z.string().optional(),
38
+ })
39
+
40
+ export const StackedBarChartSchema = ChartBaseSchema.extend({
41
+ type: z.literal('stacked_bar'),
42
+ x: ChartFieldRefSchema,
43
+ y: z.union([ChartFieldRefSchema, z.array(ChartFieldRefSchema).min(1)]),
44
+ series: ChartSeriesRefSchema.optional(),
45
+ colors: z.array(z.string()).optional(),
46
+ })
47
+
48
+ export const PieChartSchema = ChartBaseSchema.extend({
49
+ type: z.literal('pie'),
50
+ label: ChartFieldRefSchema,
51
+ value: ChartFieldRefSchema,
52
+ colors: z.array(z.string()).optional(),
53
+ })
54
+
55
+ export const HistogramChartSchema = ChartBaseSchema.extend({
56
+ type: z.literal('histogram'),
57
+ x: ChartFieldRefSchema,
58
+ y: ChartFieldRefSchema,
59
+ buckets: z.array(ChartBucketSchema).optional(),
60
+ color: z.string().optional(),
61
+ })
62
+
63
+ export const FunnelChartSchema = ChartBaseSchema.extend({
64
+ type: z.literal('funnel'),
65
+ label: ChartFieldRefSchema.optional(),
66
+ value: ChartFieldRefSchema.optional(),
67
+ colors: z.array(z.string()).optional(),
68
+ })
69
+
70
+ export const LineChartWidgetConfigSchema = WidgetBaseSchema.extend({
71
+ target: z.literal('chart'),
72
+ chart: LineChartSchema,
73
+ query: QueryConfigSchema,
74
+ })
75
+
76
+ export const BarChartWidgetConfigSchema = WidgetBaseSchema.extend({
77
+ target: z.literal('chart'),
78
+ chart: BarChartSchema,
79
+ query: QueryConfigSchema,
80
+ })
81
+
82
+ export const StackedBarChartWidgetConfigSchema = WidgetBaseSchema.extend({
83
+ target: z.literal('chart'),
84
+ chart: StackedBarChartSchema,
85
+ query: QueryConfigSchema,
86
+ })
87
+
88
+ export const PieChartWidgetConfigSchema = WidgetBaseSchema.extend({
89
+ target: z.literal('chart'),
90
+ chart: PieChartSchema,
91
+ query: QueryConfigSchema,
92
+ })
93
+
94
+ export const HistogramChartWidgetConfigSchema = WidgetBaseSchema.extend({
95
+ target: z.literal('chart'),
96
+ chart: HistogramChartSchema,
97
+ query: QueryConfigSchema,
98
+ })
99
+
100
+ export const FunnelChartWidgetConfigSchema = WidgetBaseSchema.extend({
101
+ target: z.literal('chart'),
102
+ chart: FunnelChartSchema,
103
+ query: FunnelQueryConfigSchema,
104
+ })
105
+
106
+ export const ChartWidgetTargetConfigSchema = z.union([
107
+ LineChartWidgetConfigSchema,
108
+ BarChartWidgetConfigSchema,
109
+ StackedBarChartWidgetConfigSchema,
110
+ PieChartWidgetConfigSchema,
111
+ HistogramChartWidgetConfigSchema,
112
+ FunnelChartWidgetConfigSchema,
113
+ ])
@@ -0,0 +1,194 @@
1
+ import { z } from 'zod'
2
+
3
+ export const DashboardWidgetSizeSchema = z.enum([
4
+ 'small',
5
+ 'medium',
6
+ 'large',
7
+ 'wide',
8
+ 'full',
9
+ ])
10
+
11
+ export const ValueFormatSchema = z.enum([
12
+ 'number',
13
+ 'integer',
14
+ 'compact_number',
15
+ 'currency',
16
+ 'percent',
17
+ 'percent_delta',
18
+ 'number_delta',
19
+ 'currency_delta',
20
+ ]).optional()
21
+
22
+ export const VariablesConfigSchema = z.record(z.string(), z.unknown())
23
+
24
+ export const ChartFieldRefSchema = z.object({
25
+ field: z.string(),
26
+ label: z.string().optional(),
27
+ format: ValueFormatSchema,
28
+ }).strict()
29
+
30
+ const RelativeDateValueSchema = z.union([
31
+ z.object({
32
+ now: z.literal(true),
33
+ }).strict(),
34
+ z.object({
35
+ now_minus: z.string().regex(/^\d+(h|d|w|mo|y)$/),
36
+ }).strict(),
37
+ ])
38
+
39
+ const FilterValueSchema: z.ZodType = z.lazy(() => z.union([
40
+ RelativeDateValueSchema,
41
+ z.string(),
42
+ z.number(),
43
+ z.boolean(),
44
+ z.null(),
45
+ z.array(FilterValueSchema),
46
+ z.record(z.string(), FilterValueSchema),
47
+ ]))
48
+
49
+ export const FilterExpressionSchema: z.ZodType = z.lazy(() => z.union([
50
+ z.array(FilterExpressionSchema),
51
+ z.object({
52
+ and: z.array(FilterExpressionSchema),
53
+ }).strict(),
54
+ z.object({
55
+ or: z.array(FilterExpressionSchema),
56
+ }).strict(),
57
+ z.object({
58
+ field: z.string(),
59
+ eq: FilterValueSchema.optional(),
60
+ neq: FilterValueSchema.optional(),
61
+ gt: FilterValueSchema.optional(),
62
+ gte: FilterValueSchema.optional(),
63
+ lt: FilterValueSchema.optional(),
64
+ lte: FilterValueSchema.optional(),
65
+ in: z.array(FilterValueSchema).optional(),
66
+ not_in: z.array(FilterValueSchema).optional(),
67
+ like: FilterValueSchema.optional(),
68
+ ilike: FilterValueSchema.optional(),
69
+ }).strict(),
70
+ ]))
71
+
72
+ export const QueryAggregateOperationSchema = z.enum([
73
+ 'sum',
74
+ 'count',
75
+ 'count_distinct',
76
+ 'avg',
77
+ 'min',
78
+ 'max',
79
+ 'median',
80
+ ])
81
+
82
+ export const QueryFieldSelectItemSchema = z.object({
83
+ field: z.string(),
84
+ as: z.string().optional(),
85
+ grain: z.enum(['day', 'week', 'month', 'year']).optional(),
86
+ }).strict()
87
+
88
+ export const QueryAggregateSelectItemSchema = z.object({
89
+ agg: QueryAggregateOperationSchema,
90
+ field: z.string().optional(),
91
+ as: z.string(),
92
+ filters: FilterExpressionSchema.optional(),
93
+ }).strict().superRefine((item, ctx) => {
94
+ if (!['count'].includes(item.agg) && !item.field) {
95
+ ctx.addIssue({
96
+ code: 'custom',
97
+ path: ['field'],
98
+ message: `field is required for ${item.agg}`,
99
+ })
100
+ }
101
+ })
102
+
103
+ export const QueryCalcSelectItemSchema = z.object({
104
+ calc: z.string(),
105
+ as: z.string(),
106
+ }).strict()
107
+
108
+ export const QuerySelectItemSchema = z.union([
109
+ QueryFieldSelectItemSchema,
110
+ QueryAggregateSelectItemSchema,
111
+ QueryCalcSelectItemSchema,
112
+ ])
113
+
114
+ export const QueryGroupByItemSchema = z.union([
115
+ z.string(),
116
+ z.object({
117
+ field: z.string(),
118
+ as: z.string().optional(),
119
+ grain: z.enum(['day', 'week', 'month', 'year']).optional(),
120
+ timezone: z.string().optional(),
121
+ }).strict(),
122
+ ])
123
+
124
+ export const QueryOrderByItemSchema = z.object({
125
+ field: z.string(),
126
+ direction: z.enum(['asc', 'desc']).optional(),
127
+ }).strict()
128
+
129
+ const BucketConfigSchema = z.object({
130
+ field: z.string(),
131
+ buckets: z.array(z.object({
132
+ label: z.string(),
133
+ min: z.number().optional(),
134
+ max: z.number().optional(),
135
+ }).strict()),
136
+ }).strict()
137
+
138
+ export const QueryCalcItemSchema = z.object({
139
+ calc: z.string(),
140
+ as: z.string(),
141
+ }).strict()
142
+
143
+ export const QueryConfigSchema = z.object({
144
+ resource: z.string(),
145
+ select: z.array(QuerySelectItemSchema).optional(),
146
+ sparkline: z.object({
147
+ field: z.string(),
148
+ grain: z.enum(['day', 'week', 'month', 'year']),
149
+ as: z.string(),
150
+ fill_missing: z.record(z.string(), z.unknown()).optional(),
151
+ }).strict().optional(),
152
+ filters: FilterExpressionSchema.optional(),
153
+ group_by: z.array(QueryGroupByItemSchema).optional(),
154
+ order_by: z.array(QueryOrderByItemSchema).optional(),
155
+ limit: z.number().int().positive().optional(),
156
+ offset: z.number().int().nonnegative().optional(),
157
+ bucket: BucketConfigSchema.optional(),
158
+ calcs: z.array(QueryCalcItemSchema).optional(),
159
+ formatting: z.record(z.string(), z.unknown()).optional(),
160
+ }).strict()
161
+
162
+ const FunnelQueryStepSchema = z.object({
163
+ name: z.string(),
164
+ resource: z.string(),
165
+ metric: QueryAggregateSelectItemSchema,
166
+ filters: FilterExpressionSchema.optional(),
167
+ }).strict()
168
+
169
+ export const FunnelQueryConfigSchema = z.object({
170
+ steps: z.array(FunnelQueryStepSchema).min(1),
171
+ calcs: z.array(QueryCalcItemSchema).optional(),
172
+ }).strict()
173
+
174
+ export const WidgetPersistedFieldsSchema = z.object({
175
+ id: z.string(),
176
+ group_id: z.string(),
177
+ order: z.number(),
178
+ }).strict()
179
+
180
+ export const WidgetEditableBaseSchema = z.object({
181
+ label: z.string().optional(),
182
+ variables: VariablesConfigSchema.optional(),
183
+ size: DashboardWidgetSizeSchema.optional(),
184
+ width: z.number().positive('Width must be greater than 0').optional(),
185
+ height: z.number().positive('Height must be greater than 0').optional(),
186
+ min_width: z.number().nonnegative('Min width must be a non-negative number').optional(),
187
+ max_width: z.number().nonnegative('Max width must be a non-negative number').nullable().optional(),
188
+ }).strict()
189
+
190
+ export const WidgetBaseSchema = WidgetPersistedFieldsSchema.merge(WidgetEditableBaseSchema)
191
+
192
+ export const EmptyWidgetConfigSchema = WidgetBaseSchema.extend({
193
+ target: z.literal('empty'),
194
+ })
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod'
2
+ import {
3
+ QueryConfigSchema,
4
+ ValueFormatSchema,
5
+ WidgetBaseSchema,
6
+ } from './common.js'
7
+
8
+ export const GaugeCardViewConfigSchema = z.object({
9
+ title: z.string().optional(),
10
+ value: z.object({
11
+ field: z.string(),
12
+ format: ValueFormatSchema,
13
+ prefix: z.string().optional(),
14
+ suffix: z.string().optional(),
15
+ }).strict(),
16
+ target: z.object({
17
+ value: z.number().optional(),
18
+ field: z.string().optional(),
19
+ label: z.string().optional(),
20
+ }).strict().optional(),
21
+ progress: z.object({
22
+ value_field: z.string(),
23
+ target_value: z.number().optional(),
24
+ target_field: z.string().optional(),
25
+ format: ValueFormatSchema,
26
+ }).strict().optional(),
27
+ color: z.string().optional(),
28
+ }).strict()
29
+
30
+ export const GaugeCardWidgetConfigSchema = WidgetBaseSchema.extend({
31
+ target: z.literal('gauge_card'),
32
+ card: GaugeCardViewConfigSchema,
33
+ query: QueryConfigSchema,
34
+ })
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod'
2
+ import {
3
+ QueryConfigSchema,
4
+ ValueFormatSchema,
5
+ WidgetBaseSchema,
6
+ } from './common.js'
7
+
8
+ export const KpiCardViewConfigSchema = z.object({
9
+ title: z.string().optional(),
10
+ value: z.object({
11
+ field: z.string(),
12
+ format: ValueFormatSchema,
13
+ prefix: z.string().optional(),
14
+ suffix: z.string().optional(),
15
+ }).strict(),
16
+ subtitle: z.object({
17
+ text: z.string().optional(),
18
+ field: z.string().optional(),
19
+ }).strict().optional(),
20
+ comparison: z.object({
21
+ field: z.string(),
22
+ format: ValueFormatSchema,
23
+ positive_is_good: z.boolean().optional(),
24
+ compact: z.object({
25
+ show: z.boolean().optional(),
26
+ template: z.string().optional(),
27
+ }).strict().optional(),
28
+ tooltip: z.object({
29
+ label: z.string().optional(),
30
+ template: z.string().optional(),
31
+ }).strict().optional(),
32
+ }).strict().optional(),
33
+ sparkline: z.object({
34
+ type: z.enum(['line']).optional(),
35
+ field: z.string(),
36
+ x: z.string(),
37
+ show_axes: z.boolean().optional(),
38
+ show_labels: z.boolean().optional(),
39
+ fill: z.object({
40
+ type: z.enum(['gradient', 'solid']).optional(),
41
+ }).strict().optional(),
42
+ }).strict().optional(),
43
+ }).strict()
44
+
45
+ export const KpiCardWidgetConfigSchema = WidgetBaseSchema.extend({
46
+ target: z.literal('kpi_card'),
47
+ card: KpiCardViewConfigSchema,
48
+ query: QueryConfigSchema,
49
+ })
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod'
2
+ import {
3
+ ChartFieldRefSchema,
4
+ QueryConfigSchema,
5
+ ValueFormatSchema,
6
+ WidgetBaseSchema,
7
+ } from './common.js'
8
+
9
+ export const PivotTableViewConfigSchema = z.object({
10
+ rows: z.array(ChartFieldRefSchema).min(1),
11
+ columns: z.array(ChartFieldRefSchema).optional(),
12
+ values: z.array(z.object({
13
+ field: z.string(),
14
+ label: z.string().optional(),
15
+ format: ValueFormatSchema,
16
+ aggregation: z.enum(['sum', 'count', 'avg', 'min', 'max']).optional(),
17
+ }).strict()).min(1),
18
+ }).strict()
19
+
20
+ export const PivotTableWidgetConfigSchema = WidgetBaseSchema.extend({
21
+ target: z.literal('pivot_table'),
22
+ pivot: PivotTableViewConfigSchema,
23
+ query: QueryConfigSchema,
24
+ })
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod'
2
+ import {
3
+ ChartFieldRefSchema,
4
+ QueryConfigSchema,
5
+ WidgetBaseSchema,
6
+ } from './common.js'
7
+
8
+ export const TableViewConfigSchema = z.object({
9
+ columns: z.array(ChartFieldRefSchema).optional(),
10
+ pagination: z.boolean().optional(),
11
+ page_size: z.number().int().positive().optional(),
12
+ }).strict()
13
+
14
+ export const TableWidgetConfigSchema = WidgetBaseSchema.extend({
15
+ target: z.literal('table'),
16
+ table: TableViewConfigSchema.optional(),
17
+ query: QueryConfigSchema,
18
+ })
@@ -70,7 +70,8 @@ type EffectiveGroupByItem = {
70
70
  const CALC_IDENTIFIER_RE = /\b[a-zA-Z_][a-zA-Z0-9_]*\b/g;
71
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;
72
72
  const VARIABLE_PATH_PREFIX_RE = /^\$variables\.?/;
73
- const SAFE_CALC_EXPRESSION_RE = /^[\d+\-*/().\s]+$/;
73
+ const SAFE_CALC_EXPRESSION_RE = /^[\d+\-*/().\s?:<>=!]+$/;
74
+ const RELATIVE_DURATION_RE = /^(\d+)(h|d|w|mo|y)$/;
74
75
  const FILTER_OPERATORS = {
75
76
  eq: Filters.EQ,
76
77
  neq: Filters.NEQ,
@@ -165,6 +166,12 @@ async function getQueryWidgetData(
165
166
  query: QueryConfig,
166
167
  variables: DashboardVariables,
167
168
  ): Promise<DashboardWidgetData> {
169
+ const metricSelect = getSingleAggregateMetricSelect(query);
170
+
171
+ if (metricSelect) {
172
+ return getMetricWidgetData(adminforth, query, metricSelect);
173
+ }
174
+
168
175
  const selectedRows = isAggregateQuery(query)
169
176
  ? await buildAggregateQueryRows(adminforth, query, variables)
170
177
  : buildPlainQueryRows(
@@ -196,6 +203,64 @@ async function getQueryWidgetData(
196
203
  };
197
204
  }
198
205
 
206
+ async function getMetricWidgetData(
207
+ adminforth: IAdminForth,
208
+ query: QueryConfig,
209
+ metric: QueryAggregateSelectItem,
210
+ ): Promise<DashboardWidgetData> {
211
+ const [currentValues = {}] = await getAggregateRows(
212
+ adminforth,
213
+ query.resource,
214
+ query.filters,
215
+ [metric],
216
+ [],
217
+ );
218
+ const values: Record<string, unknown> = {
219
+ [metric.as]: currentValues[metric.as] ?? 0,
220
+ };
221
+
222
+ const rows = query.sparkline
223
+ ? await getMetricSparklineRows(adminforth, query, metric, getAdminForthFilters(query.filters))
224
+ : [values];
225
+ const columns = Array.from(new Set([
226
+ metric.as,
227
+ ...(query.sparkline ? [query.sparkline.as] : []),
228
+ ]));
229
+
230
+ return {
231
+ kind: 'aggregate',
232
+ columns,
233
+ rows,
234
+ values,
235
+ };
236
+ }
237
+
238
+ async function getMetricSparklineRows(
239
+ adminforth: IAdminForth,
240
+ query: QueryConfig,
241
+ metric: QueryAggregateSelectItem,
242
+ filters: DashboardWidgetFilters,
243
+ ) {
244
+ const sparkline = query.sparkline!;
245
+ const groupBy = [{
246
+ field: sparkline.field,
247
+ as: sparkline.as,
248
+ grain: sparkline.grain,
249
+ }];
250
+ const rows = await getAggregateRows(
251
+ adminforth,
252
+ query.resource,
253
+ filters,
254
+ [metric],
255
+ groupBy,
256
+ );
257
+
258
+ return rows.map((row) => ({
259
+ ...query.sparkline?.fill_missing,
260
+ ...row,
261
+ }));
262
+ }
263
+
199
264
  async function getResourceRows(
200
265
  adminforth: IAdminForth,
201
266
  resourceId: string,
@@ -237,7 +302,7 @@ async function buildAggregateQueryRows(
237
302
  async function getAggregateRows(
238
303
  adminforth: IAdminForth,
239
304
  resourceId: string,
240
- baseFilters: FilterExpression | undefined,
305
+ baseFilters: FilterExpression | DashboardWidgetFilters | undefined,
241
306
  select: QueryAggregateSelectItem[],
242
307
  groupBy: EffectiveGroupByItem[],
243
308
  ) {
@@ -405,6 +470,21 @@ function isAggregateQuery(query: QueryConfig) {
405
470
  );
406
471
  }
407
472
 
473
+ function getSingleAggregateMetricSelect(query: QueryConfig) {
474
+ if (query.group_by?.length) {
475
+ return undefined;
476
+ }
477
+
478
+ const select = query.select ?? [];
479
+ const aggregateItems = select.filter(isAggregateSelectItem);
480
+
481
+ if (aggregateItems.length !== 1 || aggregateItems.length !== select.length) {
482
+ return undefined;
483
+ }
484
+
485
+ return aggregateItems[0];
486
+ }
487
+
408
488
  function isFieldSelectItem(item: QuerySelectItem): item is QueryFieldSelectItem {
409
489
  return 'field' in item && !('agg' in item);
410
490
  }
@@ -678,13 +758,59 @@ function toAdminForthFilter(filter: FilterExpression): IAdminForthSingleFilter |
678
758
 
679
759
  for (const [operator, createFilter] of Object.entries(FILTER_OPERATORS)) {
680
760
  if (Object.prototype.hasOwnProperty.call(filter, operator)) {
681
- return createFilter(filter.field, filter[operator as keyof typeof FILTER_OPERATORS]);
761
+ return createFilter(filter.field, resolveFilterValue(filter[operator as keyof typeof FILTER_OPERATORS]));
682
762
  }
683
763
  }
684
764
 
685
765
  return Filters.AND([]);
686
766
  }
687
767
 
768
+ function resolveFilterValue(value: unknown): unknown {
769
+ if (Array.isArray(value)) {
770
+ return value.map((item) => resolveFilterValue(item));
771
+ }
772
+
773
+ if (!isRecord(value)) {
774
+ return value;
775
+ }
776
+
777
+ if (value.now === true) {
778
+ return new Date().toISOString();
779
+ }
780
+
781
+ if (typeof value.now_minus === 'string') {
782
+ return subtractDuration(new Date(), value.now_minus).toISOString();
783
+ }
784
+
785
+ return value;
786
+ }
787
+
788
+ function subtractDuration(now: Date, duration: string) {
789
+ const match = duration.match(RELATIVE_DURATION_RE);
790
+
791
+ if (!match) {
792
+ throw new Error(`Unsupported relative date duration: ${duration}`);
793
+ }
794
+
795
+ const amount = Number(match[1]);
796
+ const unit = match[2];
797
+ const date = new Date(now);
798
+
799
+ if (unit === 'h') {
800
+ date.setUTCHours(date.getUTCHours() - amount);
801
+ } else if (unit === 'd') {
802
+ date.setUTCDate(date.getUTCDate() - amount);
803
+ } else if (unit === 'w') {
804
+ date.setUTCDate(date.getUTCDate() - amount * 7);
805
+ } else if (unit === 'mo') {
806
+ date.setUTCMonth(date.getUTCMonth() - amount);
807
+ } else if (unit === 'y') {
808
+ date.setUTCFullYear(date.getUTCFullYear() - amount);
809
+ }
810
+
811
+ return date;
812
+ }
813
+
688
814
  function toFiniteNumber(value: unknown) {
689
815
  const numberValue = typeof value === 'number' ? value : Number(value);
690
816
  return Number.isFinite(numberValue) ? numberValue : 0;