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