@adminforth/dashboard 1.0.0 → 1.2.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 (55) hide show
  1. package/README.md +99 -54
  2. package/custom/api/dashboardApi.ts +9 -0
  3. package/custom/model/dashboard.types.ts +353 -2
  4. package/custom/queries/useWidgetData.ts +8 -4
  5. package/custom/runtime/DashboardRuntime.vue +2 -1
  6. package/custom/runtime/WidgetRenderer.vue +2 -1
  7. package/custom/runtime/WidgetShell.vue +8 -4
  8. package/custom/skills/adminforth-dashboard/SKILL.md +4 -4
  9. package/custom/widgets/chart/ChartWidget.vue +45 -12
  10. package/custom/widgets/chart/chart.types.ts +83 -0
  11. package/custom/widgets/chart/chart.utils.ts +2 -2
  12. package/custom/widgets/gauge-card/GaugeCardWidget.vue +63 -12
  13. package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  14. package/custom/widgets/pivot-table/PivotTableWidget.vue +32 -12
  15. package/custom/widgets/table/TableWidget.vue +155 -30
  16. package/dist/custom/api/dashboardApi.d.ts +7 -1
  17. package/dist/custom/api/dashboardApi.js +4 -6
  18. package/dist/custom/api/dashboardApi.ts +9 -0
  19. package/dist/custom/model/dashboard.types.d.ts +70 -1
  20. package/dist/custom/model/dashboard.types.js +173 -1
  21. package/dist/custom/model/dashboard.types.ts +353 -2
  22. package/dist/custom/queries/useDashboardConfig.d.ts +42 -2
  23. package/dist/custom/queries/useWidgetData.d.ts +44 -3
  24. package/dist/custom/queries/useWidgetData.js +3 -3
  25. package/dist/custom/queries/useWidgetData.ts +8 -4
  26. package/dist/custom/runtime/DashboardRuntime.vue +2 -1
  27. package/dist/custom/runtime/WidgetRenderer.vue +2 -1
  28. package/dist/custom/runtime/WidgetShell.vue +8 -4
  29. package/dist/custom/skills/adminforth-dashboard/SKILL.md +4 -4
  30. package/dist/custom/widgets/chart/ChartWidget.vue +45 -12
  31. package/dist/custom/widgets/chart/chart.types.d.ts +15 -0
  32. package/dist/custom/widgets/chart/chart.types.js +46 -0
  33. package/dist/custom/widgets/chart/chart.types.ts +83 -0
  34. package/dist/custom/widgets/chart/chart.utils.d.ts +1 -1
  35. package/dist/custom/widgets/chart/chart.utils.js +2 -2
  36. package/dist/custom/widgets/chart/chart.utils.ts +2 -2
  37. package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +63 -12
  38. package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  39. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +32 -12
  40. package/dist/custom/widgets/table/TableWidget.vue +155 -30
  41. package/dist/endpoint/widgets.d.ts +6 -1
  42. package/dist/endpoint/widgets.js +41 -6
  43. package/dist/schema/api.d.ts +874 -444
  44. package/dist/schema/api.js +11 -2
  45. package/dist/schema/widget.d.ts +538 -132
  46. package/dist/schema/widget.js +138 -14
  47. package/dist/services/widgetConfigValidator.js +26 -40
  48. package/dist/services/widgetDataService.d.ts +7 -14
  49. package/dist/services/widgetDataService.js +115 -11
  50. package/endpoint/widgets.ts +56 -6
  51. package/package.json +1 -1
  52. package/schema/api.ts +11 -1
  53. package/schema/widget.ts +145 -15
  54. package/services/widgetConfigValidator.ts +36 -44
  55. package/services/widgetDataService.ts +175 -28
@@ -6,6 +6,56 @@ const DashboardWidgetSizeSchema = z.enum([
6
6
  'wide',
7
7
  'full',
8
8
  ]);
9
+ export const AggregationOperationZodSchema = z.enum([
10
+ 'sum',
11
+ 'count',
12
+ 'avg',
13
+ 'min',
14
+ 'max',
15
+ 'median',
16
+ ]);
17
+ export const AggregationRuleZodSchema = z.object({
18
+ operation: AggregationOperationZodSchema,
19
+ field: z.string().optional(),
20
+ }).superRefine((rule, ctx) => {
21
+ if (rule.operation !== 'count' && !rule.field) {
22
+ ctx.addIssue({
23
+ code: z.ZodIssueCode.custom,
24
+ path: ['field'],
25
+ message: `field is required for ${rule.operation}`,
26
+ });
27
+ }
28
+ });
29
+ export const GroupByRuleZodSchema = z.discriminatedUnion('type', [
30
+ z.object({
31
+ type: z.literal('field'),
32
+ field: z.string(),
33
+ }),
34
+ z.object({
35
+ type: z.literal('date_trunc'),
36
+ field: z.string(),
37
+ truncation: z.enum(['day', 'week', 'month', 'year']),
38
+ timezone: z.string().optional(),
39
+ }),
40
+ ]);
41
+ export const AggregateDataSourceZodSchema = z.object({
42
+ type: z.literal('aggregate'),
43
+ resourceId: z.string(),
44
+ aggregations: z.record(z.string(), AggregationRuleZodSchema),
45
+ groupBy: GroupByRuleZodSchema.optional(),
46
+ filters: z.unknown().optional(),
47
+ }).strict();
48
+ export const ResourceDataSourceZodSchema = z.object({
49
+ type: z.literal('resource'),
50
+ resourceId: z.string(),
51
+ columns: z.array(z.string()).optional(),
52
+ filters: z.unknown().optional(),
53
+ sort: z.unknown().optional(),
54
+ }).strict();
55
+ export const WidgetDataSourceZodSchema = z.discriminatedUnion('type', [
56
+ ResourceDataSourceZodSchema,
57
+ AggregateDataSourceZodSchema,
58
+ ]);
9
59
  const WidgetBaseSchema = z.object({
10
60
  id: z.string().optional(),
11
61
  group_id: z.string().optional(),
@@ -16,6 +66,7 @@ const WidgetBaseSchema = z.object({
16
66
  minWidth: z.number().nonnegative('Min width must be a non-negative number').optional(),
17
67
  maxWidth: z.number().nonnegative('Max width must be a non-negative number').nullable().optional(),
18
68
  order: z.number().optional(),
69
+ dataSource: WidgetDataSourceZodSchema.optional(),
19
70
  });
20
71
  const ChartBaseSchema = z.object({
21
72
  title: z.string().optional(),
@@ -79,42 +130,115 @@ export const ChartConfigSchema = z.discriminatedUnion('type', [
79
130
  HistogramChartSchema,
80
131
  FunnelChartSchema,
81
132
  ]);
82
- export const DashboardWidgetQuerySchema = z.object({
83
- resource: z.string().min(1, 'Query resource must be a non-empty string'),
84
- select: z.array(z.string()).optional(),
85
- order: z.object({
86
- field: z.string().min(1, 'Order field is required'),
87
- direction: z.enum(['asc', 'desc']),
88
- }).optional(),
89
- limit: z.number().optional(),
90
- });
91
133
  export const EmptyWidgetConfigSchema = WidgetBaseSchema.extend({
92
134
  target: z.literal('empty'),
93
135
  });
94
136
  const TableWidgetConfigSchema = WidgetBaseSchema.extend({
95
137
  target: z.literal('table'),
96
138
  table: z.unknown().optional(),
97
- query: DashboardWidgetQuerySchema.optional(),
139
+ }).superRefine((widget, ctx) => {
140
+ var _a;
141
+ if (!widget.dataSource) {
142
+ ctx.addIssue({
143
+ code: z.ZodIssueCode.custom,
144
+ path: ['dataSource'],
145
+ message: 'Table widget must have dataSource config',
146
+ });
147
+ }
148
+ if (((_a = widget.dataSource) === null || _a === void 0 ? void 0 : _a.type) === 'aggregate') {
149
+ ctx.addIssue({
150
+ code: z.ZodIssueCode.custom,
151
+ path: ['dataSource'],
152
+ message: 'Table widget dataSource must use resource type',
153
+ });
154
+ }
98
155
  });
99
156
  const ChartWidgetTargetConfigSchema = WidgetBaseSchema.extend({
100
157
  target: z.literal('chart'),
101
158
  chart: ChartConfigSchema,
102
- query: DashboardWidgetQuerySchema,
159
+ }).superRefine((widget, ctx) => {
160
+ var _a, _b;
161
+ if (!widget.dataSource) {
162
+ ctx.addIssue({
163
+ code: z.ZodIssueCode.custom,
164
+ path: ['dataSource'],
165
+ message: 'Chart widget must have dataSource config',
166
+ });
167
+ }
168
+ if (((_a = widget.dataSource) === null || _a === void 0 ? void 0 : _a.type) === 'resource') {
169
+ ctx.addIssue({
170
+ code: z.ZodIssueCode.custom,
171
+ path: ['dataSource'],
172
+ message: 'Chart widget dataSource must use aggregate type',
173
+ });
174
+ }
175
+ if (((_b = widget.dataSource) === null || _b === void 0 ? void 0 : _b.type) === 'aggregate' && !widget.dataSource.groupBy) {
176
+ ctx.addIssue({
177
+ code: z.ZodIssueCode.custom,
178
+ path: ['dataSource', 'groupBy'],
179
+ message: 'Chart widget aggregate dataSource must define groupBy',
180
+ });
181
+ }
103
182
  });
104
183
  const KpiCardWidgetConfigSchema = WidgetBaseSchema.extend({
105
184
  target: z.literal('kpi_card'),
106
185
  kpi_card: z.unknown().optional(),
107
- query: DashboardWidgetQuerySchema.optional(),
186
+ }).superRefine((widget, ctx) => {
187
+ var _a;
188
+ if (!widget.dataSource) {
189
+ ctx.addIssue({
190
+ code: z.ZodIssueCode.custom,
191
+ path: ['dataSource'],
192
+ message: 'KPI card widget must have dataSource config',
193
+ });
194
+ }
195
+ if (((_a = widget.dataSource) === null || _a === void 0 ? void 0 : _a.type) === 'aggregate' && widget.dataSource.groupBy) {
196
+ ctx.addIssue({
197
+ code: z.ZodIssueCode.custom,
198
+ path: ['dataSource', 'groupBy'],
199
+ message: 'KPI card aggregate dataSource must not define groupBy',
200
+ });
201
+ }
108
202
  });
109
203
  const GaugeCardWidgetConfigSchema = WidgetBaseSchema.extend({
110
204
  target: z.literal('gauge_card'),
111
205
  gauge_card: z.unknown().optional(),
112
- query: DashboardWidgetQuerySchema.optional(),
206
+ }).superRefine((widget, ctx) => {
207
+ var _a;
208
+ if (!widget.dataSource) {
209
+ ctx.addIssue({
210
+ code: z.ZodIssueCode.custom,
211
+ path: ['dataSource'],
212
+ message: 'Gauge card widget must have dataSource config',
213
+ });
214
+ }
215
+ if (((_a = widget.dataSource) === null || _a === void 0 ? void 0 : _a.type) === 'aggregate' && widget.dataSource.groupBy) {
216
+ ctx.addIssue({
217
+ code: z.ZodIssueCode.custom,
218
+ path: ['dataSource', 'groupBy'],
219
+ message: 'Gauge card aggregate dataSource must not define groupBy',
220
+ });
221
+ }
113
222
  });
114
223
  const PivotTableWidgetConfigSchema = WidgetBaseSchema.extend({
115
224
  target: z.literal('pivot_table'),
116
225
  pivot_table: z.unknown().optional(),
117
- query: DashboardWidgetQuerySchema.optional(),
226
+ }).superRefine((widget, ctx) => {
227
+ var _a;
228
+ if (!widget.dataSource) {
229
+ ctx.addIssue({
230
+ code: z.ZodIssueCode.custom,
231
+ path: ['dataSource'],
232
+ message: 'Pivot table widget must have dataSource config',
233
+ });
234
+ }
235
+ if (((_a = widget.dataSource) === null || _a === void 0 ? void 0 : _a.type) === 'aggregate' && !widget.dataSource.groupBy) {
236
+ ctx.addIssue({
237
+ code: z.ZodIssueCode.custom,
238
+ path: ['dataSource', 'groupBy'],
239
+ message: 'Pivot table aggregate dataSource must define groupBy',
240
+ });
241
+ }
118
242
  });
119
243
  export const WidgetConfigSchema = z.discriminatedUnion('target', [
120
244
  TableWidgetConfigSchema,
@@ -1,61 +1,38 @@
1
+ import { normalizeChartWidgetConfig } from '../custom/widgets/chart/chart.types.js';
1
2
  export function validateDashboardWidgetApiConfig(adminforth, widget) {
2
- var _a, _b;
3
3
  if (widget.target !== 'chart') {
4
4
  return [];
5
5
  }
6
6
  const errors = [];
7
- if (!widget.query) {
8
- errors.push({
9
- field: 'query',
10
- message: 'Chart widget must have query config',
11
- });
12
- return errors;
13
- }
14
- if (!widget.chart) {
7
+ const chart = normalizeChartWidgetConfig(widget.chart);
8
+ if (!chart) {
15
9
  errors.push({
16
10
  field: 'chart',
17
11
  message: 'Chart widget must have chart config',
18
12
  });
19
13
  return errors;
20
14
  }
21
- const query = widget.query;
22
- const chart = widget.chart;
23
- const resource = adminforth.config.resources.find((item) => item.resourceId === query.resource);
24
- if (!resource) {
25
- errors.push({
26
- field: 'query.resource',
27
- message: `Resource "${query.resource}" is not registered`,
28
- });
29
- return errors;
30
- }
31
- if (!query.select) {
32
- return errors;
33
- }
34
- const resourceFields = resource.columns.map((column) => column.name);
35
- for (const field of query.select) {
36
- if (!resourceFields.includes(field)) {
15
+ const aggregateDataSource = getAggregateDataSource(widget.dataSource);
16
+ if (aggregateDataSource) {
17
+ const resource = adminforth.config.resources.find((item) => item.resourceId === aggregateDataSource.resourceId);
18
+ if (!resource) {
37
19
  errors.push({
38
- field: 'query.select',
39
- message: `Field "${field}" is not in resource "${query.resource}"`,
20
+ field: 'data_source.resource_id',
21
+ message: `Resource "${aggregateDataSource.resourceId}" is not registered`,
40
22
  });
41
23
  }
42
- }
43
- const chartFields = [
44
- chart.x_field,
45
- chart.y_field,
46
- chart.label_field,
47
- chart.value_field,
48
- chart.bucket_field,
49
- ...((_b = (_a = chart.series) === null || _a === void 0 ? void 0 : _a.map((series) => series.field)) !== null && _b !== void 0 ? _b : []),
50
- ].filter((field) => typeof field === 'string');
51
- for (const field of chartFields) {
52
- if (!query.select.includes(field)) {
24
+ if (!aggregateDataSource.groupBy) {
53
25
  errors.push({
54
- field: 'query.select',
55
- message: `Query select must include chart field "${field}"`,
26
+ field: 'data_source.group_by',
27
+ message: 'Chart aggregate dataSource must define groupBy',
56
28
  });
57
29
  }
30
+ return errors;
58
31
  }
32
+ errors.push({
33
+ field: 'data_source',
34
+ message: 'Chart widget must have aggregate dataSource config',
35
+ });
59
36
  return errors;
60
37
  }
61
38
  export function createWidgetConfigValidatorService(adminforth) {
@@ -63,3 +40,12 @@ export function createWidgetConfigValidatorService(adminforth) {
63
40
  validateDashboardWidgetApiConfig: (widget) => validateDashboardWidgetApiConfig(adminforth, widget),
64
41
  };
65
42
  }
43
+ function getAggregateDataSource(dataSource) {
44
+ if (typeof dataSource !== 'object'
45
+ || dataSource === null
46
+ || dataSource.type !== 'aggregate'
47
+ || typeof dataSource.resourceId !== 'string') {
48
+ return null;
49
+ }
50
+ return dataSource;
51
+ }
@@ -1,20 +1,13 @@
1
1
  import type { IAdminForth } from 'adminforth';
2
- import type { DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
3
- export type DashboardWidgetQueryConfig = {
4
- resource: string;
5
- select?: string[];
6
- order?: {
7
- field: string;
8
- direction: 'asc' | 'desc';
2
+ import type { DashboardWidgetConfig, DashboardWidgetData } from '../custom/model/dashboard.types.js';
3
+ export type DashboardWidgetDataOptions = {
4
+ pagination?: {
5
+ page: number;
6
+ pageSize: number;
9
7
  };
10
- limit?: number;
11
- };
12
- export type DashboardWidgetData = {
13
- columns: string[];
14
- rows: Record<string, unknown>[];
15
8
  };
16
9
  export type WidgetDataService = {
17
- getWidgetData: (widget: DashboardWidgetConfig) => Promise<DashboardWidgetData | null>;
10
+ getWidgetData: (widget: DashboardWidgetConfig, options?: DashboardWidgetDataOptions) => Promise<DashboardWidgetData | null>;
18
11
  };
19
- export declare function getWidgetData(adminforth: IAdminForth, widget: DashboardWidgetConfig): Promise<DashboardWidgetData | null>;
12
+ export declare function getWidgetData(adminforth: IAdminForth, widget: DashboardWidgetConfig, options?: DashboardWidgetDataOptions): Promise<DashboardWidgetData | null>;
20
13
  export declare function createWidgetDataService(adminforth: IAdminForth): WidgetDataService;
@@ -7,26 +7,130 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { Sorts } from 'adminforth';
11
- export function getWidgetData(adminforth, widget) {
10
+ import { Aggregates, GroupBy, Sorts } from 'adminforth';
11
+ export function getWidgetData(adminforth_1, widget_1) {
12
+ return __awaiter(this, arguments, void 0, function* (adminforth, widget, options = {}) {
13
+ const dataSource = getWidgetDataSource(widget.dataSource);
14
+ if (!dataSource) {
15
+ return null;
16
+ }
17
+ if (dataSource.type === 'aggregate') {
18
+ return getAggregateWidgetData(adminforth, dataSource);
19
+ }
20
+ return getResourceWidgetData(adminforth, dataSource, options);
21
+ });
22
+ }
23
+ function getResourceWidgetData(adminforth, dataSource, options) {
12
24
  return __awaiter(this, void 0, void 0, function* () {
13
25
  var _a, _b;
14
- if (!widget.query) {
15
- return null;
26
+ const resource = adminforth.resource(dataSource.resourceId);
27
+ const filters = normalizeFilters(dataSource.filters);
28
+ const sort = normalizeSort(dataSource.sort);
29
+ const pagination = options.pagination;
30
+ const offset = pagination ? (pagination.page - 1) * pagination.pageSize : 0;
31
+ const limit = pagination ? pagination.pageSize : undefined;
32
+ const rows = yield resource.list(filters, limit !== null && limit !== void 0 ? limit : undefined, offset, sort);
33
+ const columns = (_a = dataSource.columns) !== null && _a !== void 0 ? _a : Object.keys((_b = rows[0]) !== null && _b !== void 0 ? _b : {});
34
+ const total = pagination ? yield resource.count(filters) : 0;
35
+ return Object.assign({ columns, rows: rows.map((row) => (Object.fromEntries(columns.map((column) => [column, row[column]])))) }, (pagination ? {
36
+ pagination: {
37
+ page: pagination.page,
38
+ pageSize: pagination.pageSize,
39
+ total,
40
+ totalPages: Math.max(Math.ceil(total / pagination.pageSize), 1),
41
+ },
42
+ } : {}));
43
+ });
44
+ }
45
+ function getAggregateWidgetData(adminforth, dataSource) {
46
+ return __awaiter(this, void 0, void 0, function* () {
47
+ var _a, _b;
48
+ const resource = adminforth.resource(dataSource.resourceId);
49
+ const rows = yield resource.aggregate(normalizeFilters(dataSource.filters), Object.fromEntries(Object.entries(dataSource.aggregations).map(([alias, rule]) => [
50
+ alias,
51
+ createAggregationRule(rule),
52
+ ])), dataSource.groupBy ? createGroupByRule(dataSource.groupBy) : undefined);
53
+ const columns = Object.keys((_a = rows[0]) !== null && _a !== void 0 ? _a : {});
54
+ if (!dataSource.groupBy) {
55
+ const values = (_b = rows[0]) !== null && _b !== void 0 ? _b : {};
56
+ return {
57
+ kind: 'aggregate',
58
+ columns: Object.keys(values),
59
+ rows: Object.keys(values).length ? [values] : [],
60
+ values,
61
+ };
16
62
  }
17
- const query = widget.query;
18
- const rows = yield adminforth.resource(query.resource).list([], query.limit, 0, query.order
19
- ? [query.order.direction === 'desc' ? Sorts.DESC(query.order.field) : Sorts.ASC(query.order.field)]
20
- : undefined);
21
- const columns = (_a = query.select) !== null && _a !== void 0 ? _a : Object.keys((_b = rows[0]) !== null && _b !== void 0 ? _b : {});
22
63
  return {
64
+ kind: 'aggregate',
23
65
  columns,
24
- rows: rows.map((row) => (Object.fromEntries(columns.map((column) => [column, row[column]])))),
66
+ rows,
25
67
  };
26
68
  });
27
69
  }
70
+ function getWidgetDataSource(dataSource) {
71
+ if (isWidgetDataSource(dataSource)) {
72
+ return dataSource;
73
+ }
74
+ return undefined;
75
+ }
76
+ function isWidgetDataSource(value) {
77
+ return isRecord(value)
78
+ && (value.type === 'resource' || value.type === 'aggregate')
79
+ && typeof value.resourceId === 'string';
80
+ }
81
+ function normalizeFilters(filters) {
82
+ if (Array.isArray(filters)) {
83
+ return filters;
84
+ }
85
+ if (isRecord(filters)) {
86
+ return filters;
87
+ }
88
+ return [];
89
+ }
90
+ function normalizeSort(sort) {
91
+ if (Array.isArray(sort)) {
92
+ return sort;
93
+ }
94
+ if (!isRecord(sort) || typeof sort.field !== 'string') {
95
+ return undefined;
96
+ }
97
+ if (sort.direction === 'asc') {
98
+ return [Sorts.ASC(sort.field)];
99
+ }
100
+ if (sort.direction === 'desc') {
101
+ return [Sorts.DESC(sort.field)];
102
+ }
103
+ return sort;
104
+ }
105
+ function createAggregationRule(rule) {
106
+ switch (rule.operation) {
107
+ case 'sum':
108
+ return Aggregates.sum(rule.field);
109
+ case 'count':
110
+ return Aggregates.count();
111
+ case 'avg':
112
+ return Aggregates.avg(rule.field);
113
+ case 'min':
114
+ return Aggregates.min(rule.field);
115
+ case 'max':
116
+ return Aggregates.max(rule.field);
117
+ case 'median':
118
+ return Aggregates.median(rule.field);
119
+ default:
120
+ throw new Error(`Unsupported aggregation operation: ${rule.operation}`);
121
+ }
122
+ }
123
+ function createGroupByRule(rule) {
124
+ if (rule.type === 'field') {
125
+ return GroupBy.Field(rule.field);
126
+ }
127
+ return GroupBy.DateTrunc(rule.field, rule.truncation, rule.timezone);
128
+ }
129
+ function isRecord(value) {
130
+ return typeof value === 'object' && value !== null;
131
+ }
28
132
  export function createWidgetDataService(adminforth) {
29
133
  return {
30
- getWidgetData: (widget) => getWidgetData(adminforth, widget),
134
+ getWidgetData: (widget, options) => getWidgetData(adminforth, widget, options),
31
135
  };
32
136
  }
@@ -1,5 +1,8 @@
1
1
  import type { AdminUser, IHttpServer } from 'adminforth';
2
2
  import { randomUUID } from 'crypto';
3
+ import {
4
+ normalizeDashboardWidgetConfig,
5
+ } from '../custom/model/dashboard.types.js';
3
6
  import type { DashboardConfig, DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
4
7
  import {
5
8
  DashboardApiResponseSchema,
@@ -7,8 +10,10 @@ import {
7
10
  GroupIdRequestSchema,
8
11
  MoveWidgetRequestSchema,
9
12
  SetWidgetConfigRequestSchema,
13
+ WidgetDataRequestSchema,
10
14
  WidgetIdRequestSchema,
11
15
  } from '../schema/api.js';
16
+ import { StoredWidgetConfigSchema } from '../schema/widget.js';
12
17
  import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
13
18
  import type { DashboardRecord, PersistedDashboardResponse } from '../services/dashboardConfigService.js';
14
19
 
@@ -24,9 +29,42 @@ type WidgetEndpointsContext = {
24
29
  validateDashboardWidgetApiConfig: (
25
30
  widget: DashboardWidgetConfig,
26
31
  ) => DashboardWidgetConfigValidationError[];
27
- getWidgetData: (widget: DashboardWidgetConfig) => Promise<unknown>;
32
+ getWidgetData: (
33
+ widget: DashboardWidgetConfig,
34
+ options?: { pagination?: { page: number, pageSize: number } },
35
+ ) => Promise<unknown>;
28
36
  };
29
37
 
38
+ function formatWidgetConfigValidationErrors(error: { issues: { path: PropertyKey[], message: string }[] }) {
39
+ return error.issues.map((issue) => ({
40
+ field: issue.path.length ? formatWidgetConfigFieldPath(issue.path.map(String).join('.')) : 'config',
41
+ message: issue.message,
42
+ }));
43
+ }
44
+
45
+ function formatWidgetConfigApiValidationErrors(errors: DashboardWidgetConfigValidationError[]) {
46
+ return errors.map((error) => ({
47
+ ...error,
48
+ field: formatWidgetConfigFieldPath(error.field),
49
+ }));
50
+ }
51
+
52
+ function formatWidgetConfigFieldPath(field: string) {
53
+ const fieldAliases = new Map([
54
+ ['minWidth', 'min_width'],
55
+ ['maxWidth', 'max_width'],
56
+ ['dataSource', 'data_source'],
57
+ ['resourceId', 'resource_id'],
58
+ ['groupBy', 'group_by'],
59
+ ['pageSize', 'page_size'],
60
+ ]);
61
+
62
+ return field
63
+ .split('.')
64
+ .map((segment) => fieldAliases.get(segment) ?? segment)
65
+ .join('.');
66
+ }
67
+
30
68
  export function registerWidgetEndpoints(
31
69
  server: IHttpServer,
32
70
  ctx: WidgetEndpointsContext,
@@ -197,14 +235,24 @@ export function registerWidgetEndpoints(
197
235
  return { error: 'Dashboard widget not found' };
198
236
  }
199
237
 
200
- const typedWidgetConfig = body.config as DashboardWidgetConfig;
238
+ const parsedWidgetConfig = StoredWidgetConfigSchema.safeParse(normalizeDashboardWidgetConfig(body.config));
239
+
240
+ if (!parsedWidgetConfig.success) {
241
+ response.setStatus(422);
242
+ return {
243
+ error: 'Invalid widget config',
244
+ validationErrors: formatWidgetConfigValidationErrors(parsedWidgetConfig.error),
245
+ };
246
+ }
247
+
248
+ const typedWidgetConfig = parsedWidgetConfig.data as DashboardWidgetConfig;
201
249
  const apiValidationErrors = ctx.validateDashboardWidgetApiConfig(typedWidgetConfig);
202
250
 
203
251
  if (apiValidationErrors.length) {
204
252
  response.setStatus(422);
205
253
  return {
206
254
  error: 'Invalid widget config',
207
- validationErrors: apiValidationErrors,
255
+ validationErrors: formatWidgetConfigApiValidationErrors(apiValidationErrors),
208
256
  };
209
257
  }
210
258
 
@@ -225,8 +273,8 @@ export function registerWidgetEndpoints(
225
273
  server.endpoint({
226
274
  method: 'POST',
227
275
  path: '/dashboard/get_dashboard_widget_data',
228
- description: 'Loads query result data for one dashboard widget by dashboard slug and widget id.',
229
- request_schema: WidgetIdRequestSchema,
276
+ description: 'Loads widget data for one dashboard widget by dashboard slug and widget id.',
277
+ request_schema: WidgetDataRequestSchema,
230
278
  response_schema: DashboardWidgetDataResponseSchema,
231
279
  handler: async ({ body, response }) => {
232
280
  const slug = String(body?.slug || 'default');
@@ -248,7 +296,9 @@ export function registerWidgetEndpoints(
248
296
 
249
297
  return {
250
298
  widget,
251
- data: await ctx.getWidgetData(widget),
299
+ data: await ctx.getWidgetData(widget, {
300
+ pagination: body?.pagination,
301
+ }),
252
302
  };
253
303
  },
254
304
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/dashboard",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
package/schema/api.ts CHANGED
@@ -72,6 +72,15 @@ export const WidgetIdRequestZodSchema = z.object({
72
72
  widgetId: z.string(),
73
73
  }).strict()
74
74
 
75
+ export const WidgetDataRequestZodSchema = z.object({
76
+ slug: z.string().optional(),
77
+ widgetId: z.string(),
78
+ pagination: z.object({
79
+ page: z.number().int().positive(),
80
+ pageSize: z.number().int().positive(),
81
+ }).optional(),
82
+ }).strict()
83
+
75
84
  export const MoveWidgetRequestZodSchema = z.object({
76
85
  slug: z.string().optional(),
77
86
  widgetId: z.string(),
@@ -81,7 +90,7 @@ export const MoveWidgetRequestZodSchema = z.object({
81
90
  export const SetWidgetConfigRequestZodSchema = z.object({
82
91
  slug: z.string().optional(),
83
92
  widgetId: z.string(),
84
- config: WidgetConfigSchema,
93
+ config: z.record(z.string(), z.unknown()),
85
94
  }).strict()
86
95
 
87
96
  export const DashboardErrorResponseSchema = toAdminForthJsonSchema(DashboardErrorResponseZodSchema)
@@ -95,5 +104,6 @@ export const GroupIdRequestSchema = toAdminForthJsonSchema(GroupIdRequestZodSche
95
104
  export const MoveGroupRequestSchema = toAdminForthJsonSchema(MoveGroupRequestZodSchema)
96
105
  export const SetGroupConfigRequestSchema = toAdminForthJsonSchema(SetGroupConfigRequestZodSchema)
97
106
  export const WidgetIdRequestSchema = toAdminForthJsonSchema(WidgetIdRequestZodSchema)
107
+ export const WidgetDataRequestSchema = toAdminForthJsonSchema(WidgetDataRequestZodSchema)
98
108
  export const MoveWidgetRequestSchema = toAdminForthJsonSchema(MoveWidgetRequestZodSchema)
99
109
  export const SetWidgetConfigRequestSchema = toAdminForthJsonSchema(SetWidgetConfigRequestZodSchema)