@adminforth/dashboard 1.2.0 → 1.3.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/README.md +47 -39
  2. package/custom/composables/useElementSize.ts +17 -2
  3. package/custom/model/dashboard.types.ts +327 -234
  4. package/custom/skills/adminforth-dashboard/SKILL.md +6 -2
  5. package/custom/widgets/chart/ChartWidget.vue +23 -55
  6. package/custom/widgets/chart/bar/BarChart.vue +20 -12
  7. package/custom/widgets/chart/chart.types.ts +17 -66
  8. package/custom/widgets/chart/chart.utils.ts +11 -0
  9. package/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
  10. package/custom/widgets/chart/line/LineChart.vue +23 -15
  11. package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
  12. package/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -12
  13. package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  14. package/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
  15. package/custom/widgets/table/TableWidget.vue +8 -3
  16. package/dist/custom/composables/useElementSize.js +14 -2
  17. package/dist/custom/composables/useElementSize.ts +17 -2
  18. package/dist/custom/model/dashboard.types.d.ts +178 -61
  19. package/dist/custom/model/dashboard.types.js +67 -92
  20. package/dist/custom/model/dashboard.types.ts +327 -234
  21. package/dist/custom/queries/useDashboardConfig.d.ts +832 -66
  22. package/dist/custom/queries/useWidgetData.d.ts +828 -62
  23. package/dist/custom/skills/adminforth-dashboard/SKILL.md +6 -2
  24. package/dist/custom/widgets/chart/ChartWidget.vue +23 -55
  25. package/dist/custom/widgets/chart/bar/BarChart.vue +20 -12
  26. package/dist/custom/widgets/chart/chart.types.d.ts +13 -22
  27. package/dist/custom/widgets/chart/chart.types.js +2 -25
  28. package/dist/custom/widgets/chart/chart.types.ts +17 -66
  29. package/dist/custom/widgets/chart/chart.utils.d.ts +1 -0
  30. package/dist/custom/widgets/chart/chart.utils.js +7 -0
  31. package/dist/custom/widgets/chart/chart.utils.ts +11 -0
  32. package/dist/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
  33. package/dist/custom/widgets/chart/line/LineChart.vue +23 -15
  34. package/dist/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
  35. package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -12
  36. package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  37. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
  38. package/dist/custom/widgets/table/TableWidget.vue +8 -3
  39. package/dist/endpoint/widgets.js +5 -2
  40. package/dist/schema/api.d.ts +2680 -736
  41. package/dist/schema/widget.d.ts +1588 -476
  42. package/dist/schema/widget.js +205 -139
  43. package/dist/services/widgetConfigValidator.js +16 -40
  44. package/dist/services/widgetDataService.js +359 -82
  45. package/endpoint/dashboard.ts +1 -1
  46. package/endpoint/widgets.ts +5 -2
  47. package/package.json +1 -1
  48. package/schema/widget.ts +222 -139
  49. package/services/widgetConfigValidator.ts +29 -53
  50. package/services/widgetDataService.ts +484 -100
package/schema/widget.ts CHANGED
@@ -13,61 +13,164 @@ const DashboardWidgetSizeSchema = z.enum([
13
13
  'full',
14
14
  ])
15
15
 
16
- export const AggregationOperationZodSchema = z.enum([
16
+ const ValueFormatSchema = z.enum([
17
+ 'number',
18
+ 'compact_number',
19
+ 'currency',
20
+ 'percent',
21
+ 'percent_delta',
22
+ 'number_delta',
23
+ 'currency_delta',
24
+ ]).optional()
25
+
26
+ const ChartFieldRefSchema = z.object({
27
+ field: z.string(),
28
+ label: z.string().optional(),
29
+ format: ValueFormatSchema,
30
+ }).strict()
31
+
32
+ const FieldRefSchema = z.union([
33
+ z.string(),
34
+ z.object({
35
+ field: z.string(),
36
+ label: z.string().optional(),
37
+ format: ValueFormatSchema,
38
+ }).strict(),
39
+ ])
40
+
41
+ const FilterExpressionSchema: z.ZodType = z.lazy(() => z.union([
42
+ z.array(FilterExpressionSchema),
43
+ z.object({
44
+ and: z.array(FilterExpressionSchema),
45
+ }).strict(),
46
+ z.object({
47
+ or: z.array(FilterExpressionSchema),
48
+ }).strict(),
49
+ z.object({
50
+ field: z.string(),
51
+ eq: z.unknown().optional(),
52
+ neq: z.unknown().optional(),
53
+ gt: z.unknown().optional(),
54
+ gte: z.unknown().optional(),
55
+ lt: z.unknown().optional(),
56
+ lte: z.unknown().optional(),
57
+ in: z.array(z.unknown()).optional(),
58
+ not_in: z.array(z.unknown()).optional(),
59
+ like: z.unknown().optional(),
60
+ ilike: z.unknown().optional(),
61
+ }).strict(),
62
+ ]))
63
+
64
+ const QueryAggregateOperationSchema = z.enum([
17
65
  'sum',
18
66
  'count',
67
+ 'count_distinct',
19
68
  'avg',
20
69
  'min',
21
70
  'max',
22
71
  'median',
23
72
  ])
24
73
 
25
- export const AggregationRuleZodSchema = z.object({
26
- operation: AggregationOperationZodSchema,
74
+ const QueryFieldSelectItemSchema = z.object({
75
+ field: z.string(),
76
+ as: z.string().optional(),
77
+ grain: z.enum(['hour', 'day', 'week', 'month', 'quarter', 'year']).optional(),
78
+ }).strict()
79
+
80
+ const QueryAggregateSelectItemSchema = z.object({
81
+ agg: QueryAggregateOperationSchema,
27
82
  field: z.string().optional(),
28
- }).superRefine((rule, ctx) => {
29
- if (rule.operation !== 'count' && !rule.field) {
83
+ as: z.string(),
84
+ filters: FilterExpressionSchema.optional(),
85
+ }).strict().superRefine((item, ctx) => {
86
+ if (!['count'].includes(item.agg) && !item.field) {
30
87
  ctx.addIssue({
31
88
  code: z.ZodIssueCode.custom,
32
89
  path: ['field'],
33
- message: `field is required for ${rule.operation}`,
90
+ message: `field is required for ${item.agg}`,
34
91
  })
35
92
  }
36
93
  })
37
94
 
38
- export const GroupByRuleZodSchema = z.discriminatedUnion('type', [
39
- z.object({
40
- type: z.literal('field'),
41
- field: z.string(),
42
- }),
95
+ const QueryCalcSelectItemSchema = z.object({
96
+ calc: z.string(),
97
+ as: z.string(),
98
+ }).strict()
99
+
100
+ const QuerySelectItemSchema = z.union([
101
+ QueryFieldSelectItemSchema,
102
+ QueryAggregateSelectItemSchema,
103
+ QueryCalcSelectItemSchema,
104
+ ])
105
+
106
+ const QueryGroupByItemSchema = z.union([
107
+ z.string(),
43
108
  z.object({
44
- type: z.literal('date_trunc'),
45
109
  field: z.string(),
46
- truncation: z.enum(['day', 'week', 'month', 'year']),
110
+ as: z.string().optional(),
111
+ grain: z.enum(['hour', 'day', 'week', 'month', 'quarter', 'year']).optional(),
47
112
  timezone: z.string().optional(),
48
- }),
113
+ }).strict(),
49
114
  ])
50
115
 
51
- export const AggregateDataSourceZodSchema = z.object({
52
- type: z.literal('aggregate'),
53
- resourceId: z.string(),
54
- aggregations: z.record(z.string(), AggregationRuleZodSchema),
55
- groupBy: GroupByRuleZodSchema.optional(),
56
- filters: z.unknown().optional(),
116
+ const QueryOrderByItemSchema = z.object({
117
+ field: z.string(),
118
+ direction: z.enum(['asc', 'desc']).optional(),
57
119
  }).strict()
58
120
 
59
- export const ResourceDataSourceZodSchema = z.object({
60
- type: z.literal('resource'),
61
- resourceId: z.string(),
62
- columns: z.array(z.string()).optional(),
63
- filters: z.unknown().optional(),
64
- sort: z.unknown().optional(),
121
+ const TimeSeriesConfigSchema = z.object({
122
+ field: z.string(),
123
+ grain: z.enum(['hour', 'day', 'week', 'month', 'quarter', 'year']),
124
+ timezone: z.string().optional(),
65
125
  }).strict()
66
126
 
67
- export const WidgetDataSourceZodSchema = z.discriminatedUnion('type', [
68
- ResourceDataSourceZodSchema,
69
- AggregateDataSourceZodSchema,
70
- ])
127
+ const PeriodConfigSchema = z.object({
128
+ field: z.string(),
129
+ gte: z.unknown().optional(),
130
+ lt: z.unknown().optional(),
131
+ }).strict()
132
+
133
+ const BucketConfigSchema = z.object({
134
+ field: z.string(),
135
+ buckets: z.array(z.object({
136
+ label: z.string(),
137
+ min: z.number().optional(),
138
+ max: z.number().optional(),
139
+ }).strict()),
140
+ }).strict()
141
+
142
+ const QueryCalcItemSchema = z.object({
143
+ calc: z.string(),
144
+ as: z.string(),
145
+ }).strict()
146
+
147
+ const FormattingConfigSchema = z.record(z.string(), z.unknown())
148
+
149
+ export const QueryConfigSchema = z.object({
150
+ resource: z.string(),
151
+ select: z.array(QuerySelectItemSchema).optional(),
152
+ filters: FilterExpressionSchema.optional(),
153
+ groupBy: z.array(QueryGroupByItemSchema).optional(),
154
+ orderBy: z.array(QueryOrderByItemSchema).optional(),
155
+ limit: z.number().int().positive().optional(),
156
+ offset: z.number().int().nonnegative().optional(),
157
+ timeSeries: TimeSeriesConfigSchema.optional(),
158
+ period: PeriodConfigSchema.optional(),
159
+ bucket: BucketConfigSchema.optional(),
160
+ calcs: z.array(QueryCalcItemSchema).optional(),
161
+ formatting: FormattingConfigSchema.optional(),
162
+ }).strict()
163
+
164
+ const FunnelQueryStepSchema = z.object({
165
+ name: z.string(),
166
+ resource: z.string(),
167
+ metric: QueryAggregateSelectItemSchema,
168
+ filters: FilterExpressionSchema.optional(),
169
+ }).strict()
170
+
171
+ export const FunnelQueryConfigSchema = z.object({
172
+ steps: z.array(FunnelQueryStepSchema).min(1),
173
+ }).strict()
71
174
 
72
175
  const WidgetBaseSchema = z.object({
73
176
  id: z.string().optional(),
@@ -79,9 +182,14 @@ const WidgetBaseSchema = z.object({
79
182
  minWidth: z.number().nonnegative('Min width must be a non-negative number').optional(),
80
183
  maxWidth: z.number().nonnegative('Max width must be a non-negative number').nullable().optional(),
81
184
  order: z.number().optional(),
82
- dataSource: WidgetDataSourceZodSchema.optional(),
83
185
  })
84
186
 
187
+ const TableViewConfigSchema = z.object({
188
+ columns: z.array(FieldRefSchema).optional(),
189
+ pagination: z.boolean().optional(),
190
+ pageSize: z.number().int().positive().optional(),
191
+ }).strict()
192
+
85
193
  const ChartBaseSchema = z.object({
86
194
  title: z.string().optional(),
87
195
  })
@@ -90,58 +198,56 @@ const ChartBucketSchema = z.object({
90
198
  label: z.string().min(1, 'Bucket label is required'),
91
199
  min: z.number().optional(),
92
200
  max: z.number().optional(),
93
- })
201
+ }).strict()
94
202
 
95
- const ChartSeriesSchema = z.object({
96
- name: z.string().min(1, 'Series name is required'),
97
- field: z.string().min(1, 'Series field is required'),
98
- color: z.string().optional(),
99
- })
203
+ const ChartSeriesRefSchema = z.object({
204
+ field: z.string(),
205
+ label: z.string().optional(),
206
+ }).strict()
100
207
 
101
208
  const LineChartSchema = ChartBaseSchema.extend({
102
209
  type: z.literal('line'),
103
- x_field: z.string().optional(),
104
- y_field: z.string().optional(),
105
- series_name: z.string().optional(),
210
+ x: ChartFieldRefSchema,
211
+ y: z.array(ChartFieldRefSchema).min(1),
212
+ series: ChartSeriesRefSchema.optional(),
106
213
  color: z.string().optional(),
214
+ colors: z.array(z.string()).optional(),
107
215
  })
108
216
 
109
217
  const BarChartSchema = ChartBaseSchema.extend({
110
218
  type: z.literal('bar'),
111
- label_field: z.string().optional(),
112
- value_field: z.string().optional(),
113
- bucket_field: z.string().optional(),
114
- buckets: z.array(ChartBucketSchema).optional(),
219
+ x: ChartFieldRefSchema,
220
+ y: ChartFieldRefSchema,
115
221
  color: z.string().optional(),
116
222
  })
117
223
 
118
224
  const StackedBarChartSchema = ChartBaseSchema.extend({
119
225
  type: z.literal('stacked_bar'),
120
- x_field: z.string().optional(),
121
- series: z.array(ChartSeriesSchema).optional(),
226
+ x: ChartFieldRefSchema,
227
+ y: ChartFieldRefSchema,
228
+ series: ChartSeriesRefSchema,
122
229
  colors: z.array(z.string()).optional(),
123
230
  })
124
231
 
125
232
  const PieChartSchema = ChartBaseSchema.extend({
126
233
  type: z.literal('pie'),
127
- label_field: z.string().optional(),
128
- value_field: z.string().optional(),
234
+ label: ChartFieldRefSchema,
235
+ value: ChartFieldRefSchema,
129
236
  colors: z.array(z.string()).optional(),
130
237
  })
131
238
 
132
239
  const HistogramChartSchema = ChartBaseSchema.extend({
133
240
  type: z.literal('histogram'),
134
- label_field: z.string().optional(),
135
- value_field: z.string().optional(),
136
- bucket_field: z.string().optional(),
241
+ x: ChartFieldRefSchema,
242
+ y: ChartFieldRefSchema,
137
243
  buckets: z.array(ChartBucketSchema).optional(),
138
244
  color: z.string().optional(),
139
245
  })
140
246
 
141
247
  const FunnelChartSchema = ChartBaseSchema.extend({
142
248
  type: z.literal('funnel'),
143
- label_field: z.string().optional(),
144
- value_field: z.string().optional(),
249
+ label: ChartFieldRefSchema.optional(),
250
+ value: ChartFieldRefSchema.optional(),
145
251
  colors: z.array(z.string()).optional(),
146
252
  })
147
253
 
@@ -154,121 +260,98 @@ export const ChartConfigSchema = z.discriminatedUnion('type', [
154
260
  FunnelChartSchema,
155
261
  ])
156
262
 
263
+ const KpiCardViewConfigSchema = z.object({
264
+ title: z.string().optional(),
265
+ value: z.object({
266
+ field: z.string(),
267
+ format: ValueFormatSchema,
268
+ prefix: z.string().optional(),
269
+ suffix: z.string().optional(),
270
+ }).strict(),
271
+ subtitle: z.object({
272
+ text: z.string().optional(),
273
+ field: z.string().optional(),
274
+ }).strict().optional(),
275
+ comparison: z.unknown().optional(),
276
+ sparkline: z.unknown().optional(),
277
+ }).strict()
278
+
279
+ const GaugeCardViewConfigSchema = z.object({
280
+ title: z.string().optional(),
281
+ value: z.object({
282
+ field: z.string(),
283
+ format: ValueFormatSchema,
284
+ prefix: z.string().optional(),
285
+ suffix: z.string().optional(),
286
+ }).strict(),
287
+ target: z.object({
288
+ value: z.number().optional(),
289
+ field: z.string().optional(),
290
+ label: z.string().optional(),
291
+ }).strict().optional(),
292
+ progress: z.object({
293
+ valueField: z.string(),
294
+ targetValue: z.number().optional(),
295
+ targetField: z.string().optional(),
296
+ format: ValueFormatSchema,
297
+ }).strict().optional(),
298
+ color: z.string().optional(),
299
+ }).strict()
300
+
301
+ const PivotTableViewConfigSchema = z.object({
302
+ rows: z.array(FieldRefSchema).min(1),
303
+ columns: z.array(FieldRefSchema).optional(),
304
+ values: z.array(z.object({
305
+ field: z.string(),
306
+ label: z.string().optional(),
307
+ format: ValueFormatSchema,
308
+ aggregation: z.enum(['sum', 'count', 'avg', 'min', 'max']).optional(),
309
+ }).strict()).min(1),
310
+ }).strict()
311
+
157
312
  export const EmptyWidgetConfigSchema = WidgetBaseSchema.extend({
158
313
  target: z.literal('empty'),
159
314
  })
160
315
 
161
316
  const TableWidgetConfigSchema = WidgetBaseSchema.extend({
162
317
  target: z.literal('table'),
163
- table: z.unknown().optional(),
164
- }).superRefine((widget, ctx) => {
165
- if (!widget.dataSource) {
166
- ctx.addIssue({
167
- code: z.ZodIssueCode.custom,
168
- path: ['dataSource'],
169
- message: 'Table widget must have dataSource config',
170
- })
171
- }
172
-
173
- if (widget.dataSource?.type === 'aggregate') {
174
- ctx.addIssue({
175
- code: z.ZodIssueCode.custom,
176
- path: ['dataSource'],
177
- message: 'Table widget dataSource must use resource type',
178
- })
179
- }
318
+ table: TableViewConfigSchema.optional(),
319
+ query: QueryConfigSchema,
180
320
  })
181
321
 
182
322
  const ChartWidgetTargetConfigSchema = WidgetBaseSchema.extend({
183
323
  target: z.literal('chart'),
184
324
  chart: ChartConfigSchema,
325
+ query: z.union([QueryConfigSchema, FunnelQueryConfigSchema]),
185
326
  }).superRefine((widget, ctx) => {
186
- if (!widget.dataSource) {
187
- ctx.addIssue({
188
- code: z.ZodIssueCode.custom,
189
- path: ['dataSource'],
190
- message: 'Chart widget must have dataSource config',
191
- })
192
- }
327
+ const isFunnelChart = widget.chart.type === 'funnel'
328
+ const isFunnelQuery = 'steps' in widget.query
193
329
 
194
- if (widget.dataSource?.type === 'resource') {
330
+ if (isFunnelChart !== isFunnelQuery) {
195
331
  ctx.addIssue({
196
332
  code: z.ZodIssueCode.custom,
197
- path: ['dataSource'],
198
- message: 'Chart widget dataSource must use aggregate type',
199
- })
200
- }
201
-
202
- if (widget.dataSource?.type === 'aggregate' && !widget.dataSource.groupBy) {
203
- ctx.addIssue({
204
- code: z.ZodIssueCode.custom,
205
- path: ['dataSource', 'groupBy'],
206
- message: 'Chart widget aggregate dataSource must define groupBy',
333
+ path: ['query'],
334
+ message: 'Funnel charts must use steps query, other charts must use resource query',
207
335
  })
208
336
  }
209
337
  })
210
338
 
211
339
  const KpiCardWidgetConfigSchema = WidgetBaseSchema.extend({
212
340
  target: z.literal('kpi_card'),
213
- kpi_card: z.unknown().optional(),
214
- }).superRefine((widget, ctx) => {
215
- if (!widget.dataSource) {
216
- ctx.addIssue({
217
- code: z.ZodIssueCode.custom,
218
- path: ['dataSource'],
219
- message: 'KPI card widget must have dataSource config',
220
- })
221
- }
222
-
223
- if (widget.dataSource?.type === 'aggregate' && widget.dataSource.groupBy) {
224
- ctx.addIssue({
225
- code: z.ZodIssueCode.custom,
226
- path: ['dataSource', 'groupBy'],
227
- message: 'KPI card aggregate dataSource must not define groupBy',
228
- })
229
- }
341
+ card: KpiCardViewConfigSchema,
342
+ query: QueryConfigSchema,
230
343
  })
231
344
 
232
345
  const GaugeCardWidgetConfigSchema = WidgetBaseSchema.extend({
233
346
  target: z.literal('gauge_card'),
234
- gauge_card: z.unknown().optional(),
235
- }).superRefine((widget, ctx) => {
236
- if (!widget.dataSource) {
237
- ctx.addIssue({
238
- code: z.ZodIssueCode.custom,
239
- path: ['dataSource'],
240
- message: 'Gauge card widget must have dataSource config',
241
- })
242
- }
243
-
244
- if (widget.dataSource?.type === 'aggregate' && widget.dataSource.groupBy) {
245
- ctx.addIssue({
246
- code: z.ZodIssueCode.custom,
247
- path: ['dataSource', 'groupBy'],
248
- message: 'Gauge card aggregate dataSource must not define groupBy',
249
- })
250
- }
347
+ card: GaugeCardViewConfigSchema,
348
+ query: QueryConfigSchema,
251
349
  })
252
350
 
253
351
  const PivotTableWidgetConfigSchema = WidgetBaseSchema.extend({
254
352
  target: z.literal('pivot_table'),
255
- pivot_table: z.unknown().optional(),
256
- }).superRefine((widget, ctx) => {
257
- if (!widget.dataSource) {
258
- ctx.addIssue({
259
- code: z.ZodIssueCode.custom,
260
- path: ['dataSource'],
261
- message: 'Pivot table widget must have dataSource config',
262
- })
263
- }
264
-
265
- if (widget.dataSource?.type === 'aggregate' && !widget.dataSource.groupBy) {
266
- ctx.addIssue({
267
- code: z.ZodIssueCode.custom,
268
- path: ['dataSource', 'groupBy'],
269
- message: 'Pivot table aggregate dataSource must define groupBy',
270
- })
271
- }
353
+ pivot: PivotTableViewConfigSchema,
354
+ query: QueryConfigSchema,
272
355
  })
273
356
 
274
357
  export const WidgetConfigSchema = z.discriminatedUnion('target', [
@@ -1,6 +1,5 @@
1
1
  import type { IAdminForth } from 'adminforth';
2
- import type { DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
3
- import { normalizeChartWidgetConfig } from '../custom/widgets/chart/chart.types.js';
2
+ import type { DashboardWidgetConfig, QueryConfig } from '../custom/model/dashboard.types.js';
4
3
  import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
5
4
 
6
5
  export type WidgetConfigValidatorService = {
@@ -13,50 +12,44 @@ export function validateDashboardWidgetApiConfig(
13
12
  adminforth: IAdminForth,
14
13
  widget: DashboardWidgetConfig,
15
14
  ): DashboardWidgetConfigValidationError[] {
16
- if (widget.target !== 'chart') {
15
+ if (!('query' in widget)) {
17
16
  return [];
18
17
  }
19
18
 
20
- const errors: DashboardWidgetConfigValidationError[] = [];
21
-
22
- const chart = normalizeChartWidgetConfig(widget.chart);
23
-
24
- if (!chart) {
25
- errors.push({
26
- field: 'chart',
27
- message: 'Chart widget must have chart config',
28
- });
29
- return errors;
19
+ if ('steps' in widget.query) {
20
+ return widget.query.steps.flatMap((step, index) => validateResource(
21
+ adminforth,
22
+ step.resource,
23
+ `query.steps.${index}.resource`,
24
+ ));
30
25
  }
31
26
 
32
- const aggregateDataSource = getAggregateDataSource(widget.dataSource);
33
-
34
- if (aggregateDataSource) {
35
- const resource = adminforth.config.resources.find((item) => item.resourceId === aggregateDataSource.resourceId);
27
+ return validateQueryConfig(adminforth, widget.query, 'query');
28
+ }
36
29
 
37
- if (!resource) {
38
- errors.push({
39
- field: 'data_source.resource_id',
40
- message: `Resource "${aggregateDataSource.resourceId}" is not registered`,
41
- });
42
- }
30
+ function validateQueryConfig(
31
+ adminforth: IAdminForth,
32
+ query: QueryConfig,
33
+ fieldPrefix: string,
34
+ ): DashboardWidgetConfigValidationError[] {
35
+ return validateResource(adminforth, query.resource, `${fieldPrefix}.resource`);
36
+ }
43
37
 
44
- if (!aggregateDataSource.groupBy) {
45
- errors.push({
46
- field: 'data_source.group_by',
47
- message: 'Chart aggregate dataSource must define groupBy',
48
- });
49
- }
38
+ function validateResource(
39
+ adminforth: IAdminForth,
40
+ resourceId: string,
41
+ field: string,
42
+ ): DashboardWidgetConfigValidationError[] {
43
+ const resource = adminforth.config.resources.find((item) => item.resourceId === resourceId);
50
44
 
51
- return errors;
45
+ if (resource) {
46
+ return [];
52
47
  }
53
48
 
54
- errors.push({
55
- field: 'data_source',
56
- message: 'Chart widget must have aggregate dataSource config',
57
- });
58
-
59
- return errors;
49
+ return [{
50
+ field,
51
+ message: `Resource "${resourceId}" is not registered`,
52
+ }];
60
53
  }
61
54
 
62
55
  export function createWidgetConfigValidatorService(
@@ -66,20 +59,3 @@ export function createWidgetConfigValidatorService(
66
59
  validateDashboardWidgetApiConfig: (widget) => validateDashboardWidgetApiConfig(adminforth, widget),
67
60
  };
68
61
  }
69
-
70
- function getAggregateDataSource(dataSource: unknown) {
71
- if (
72
- typeof dataSource !== 'object'
73
- || dataSource === null
74
- || (dataSource as { type?: string }).type !== 'aggregate'
75
- || typeof (dataSource as { resourceId?: unknown }).resourceId !== 'string'
76
- ) {
77
- return null;
78
- }
79
-
80
- return dataSource as {
81
- type: 'aggregate';
82
- resourceId: string;
83
- groupBy?: unknown;
84
- };
85
- }