@adminforth/dashboard 1.9.0 → 1.11.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.
@@ -38,6 +38,10 @@ export const DashboardWidgetDataResponseZodSchema = z.union([
38
38
  export const SlugRequestZodSchema = z.object({
39
39
  slug: z.string(),
40
40
  }).strict();
41
+ export const GetSlugsResponseZodSchema = z.array(z.object({
42
+ slug: z.string(),
43
+ label: z.string(),
44
+ }));
41
45
  export const GroupIdRequestZodSchema = z.object({
42
46
  slug: z.string(),
43
47
  groupId: z.string(),
@@ -159,20 +163,16 @@ export const ConfigurePieChartWidgetRequestZodSchema = configureWidgetRequestSch
159
163
  export const ConfigureHistogramChartWidgetRequestZodSchema = configureWidgetRequestSchema(ConfigurableHistogramChartWidgetConfigSchema);
160
164
  export const ConfigureFunnelChartWidgetRequestZodSchema = configureWidgetRequestSchema(ConfigurableFunnelChartWidgetConfigSchema);
161
165
  export const ConfigurePivotTableWidgetRequestZodSchema = configureWidgetRequestSchema(ConfigurablePivotTableWidgetConfigSchema);
162
- export const DashboardMutationResponseZodSchema = z.union([
163
- z.object({
164
- ok: z.literal(true),
165
- slug: z.string(),
166
- widgetId: z.string().optional(),
167
- groupId: z.string().optional(),
168
- target: z.string().optional(),
169
- revision: z.number().optional(),
170
- }).strict(),
171
- DashboardErrorResponseZodSchema,
172
- ]);
173
- export const DashboardApiResponseSchema = toJSONSchema(DashboardApiResponseZodSchema, { target: 'draft-07' });
166
+ export const DashboardMutationResponseZodSchema = z.object({
167
+ ok: z.boolean(),
168
+ error: z.string().optional(),
169
+ groupId: z.string().optional(),
170
+ widgetId: z.string().optional(),
171
+ }).strict();
172
+ export const DashboardApiResponseSchema = toJSONSchema(z.unknown(), { target: 'draft-07' });
174
173
  export const DashboardWidgetDataResponseSchema = toJSONSchema(DashboardWidgetDataResponseZodSchema, { target: 'draft-07' });
175
174
  export const SlugRequestSchema = toJSONSchema(SlugRequestZodSchema, { target: 'draft-07' });
175
+ export const GetSlugsResponseSchema = toJSONSchema(GetSlugsResponseZodSchema, { target: 'draft-07' });
176
176
  export const GroupIdRequestSchema = toJSONSchema(GroupIdRequestZodSchema, { target: 'draft-07' });
177
177
  export const MoveGroupRequestSchema = toJSONSchema(MoveGroupRequestZodSchema, { target: 'draft-07' });
178
178
  export const SetGroupConfigRequestSchema = toJSONSchema(SetGroupConfigRequestZodSchema, { target: 'draft-07' });
@@ -1 +1,2 @@
1
- export declare function evaluateCalc(calc: string, values: Record<string, unknown>): any;
1
+ import type { DashboardVariables } from '../custom/model/dashboard.types.js';
2
+ export declare function evaluateCalc(calc: string, values: Record<string, unknown>, variables?: DashboardVariables): any;
@@ -1,4 +1,5 @@
1
1
  import { Parser } from 'expr-eval-fork';
2
+ const LOOKUP_VARIABLE_PATH_RE = /lookup\(\s*\$variables((?:\.[a-zA-Z_][a-zA-Z0-9_]*)+)\s*,/g;
2
3
  const CALC_PARSER_OPTIONS = {
3
4
  allowMemberAccess: false,
4
5
  operators: {
@@ -12,9 +13,25 @@ const CALC_PARSER_OPTIONS = {
12
13
  random: false,
13
14
  },
14
15
  };
15
- const CALC_PARSER = new Parser(CALC_PARSER_OPTIONS);
16
- export function evaluateCalc(calc, values) {
17
- return CALC_PARSER.parse(calc).evaluate(normalizeCalcValues(values));
16
+ export function evaluateCalc(calc, values, variables = {}) {
17
+ const parser = createCalcParser(variables);
18
+ return parser.parse(normalizeLookupPaths(calc)).evaluate(normalizeCalcValues(values));
19
+ }
20
+ function createCalcParser(variables) {
21
+ const parser = new Parser(CALC_PARSER_OPTIONS);
22
+ parser.functions.lookup = (path, key, defaultValue = 0) => {
23
+ const map = resolveVariablePath(variables, String(path));
24
+ const value = isRecord(map) && Object.prototype.hasOwnProperty.call(map, String(key))
25
+ ? map[String(key)]
26
+ : defaultValue;
27
+ return toFiniteNumber(value);
28
+ };
29
+ return parser;
30
+ }
31
+ function normalizeLookupPaths(calc) {
32
+ return calc.replace(LOOKUP_VARIABLE_PATH_RE, (_match, path) => {
33
+ return `lookup("${path.replace(/^\./, '')}",`;
34
+ });
18
35
  }
19
36
  function normalizeCalcValues(values) {
20
37
  return Object.fromEntries(Object.entries(values).map(([key, value]) => [
@@ -26,3 +43,12 @@ function toFiniteNumber(value) {
26
43
  const numberValue = typeof value === 'number' ? value : Number(value);
27
44
  return Number.isFinite(numberValue) ? numberValue : 0;
28
45
  }
46
+ function resolveVariablePath(variables, path) {
47
+ return path
48
+ .split('.')
49
+ .filter(Boolean)
50
+ .reduce((current, segment) => isRecord(current) ? current[segment] : undefined, variables);
51
+ }
52
+ function isRecord(value) {
53
+ return typeof value === 'object' && value !== null;
54
+ }
@@ -17,10 +17,12 @@ export type PersistedDashboardResponse = {
17
17
  };
18
18
  type DashboardConfigMutator = (config: DashboardConfig, dashboard: DashboardRecord) => DashboardConfig | null | Promise<DashboardConfig | null>;
19
19
  export declare function getDashboardRecord(adminforth: IAdminForth, dashboardConfigsResourceId: string, slug: string): Promise<DashboardRecord | null>;
20
+ export declare function getAllDashboardRecords(adminforth: IAdminForth, dashboardConfigsResourceId: string): Promise<DashboardRecord[]>;
20
21
  export declare function persistDashboardConfig(adminforth: IAdminForth, dashboardConfigsResourceId: string, dashboard: DashboardRecord, config: DashboardConfig): Promise<PersistedDashboardResponse>;
21
22
  export declare function updateDashboardConfig(adminforth: IAdminForth, dashboardConfigsResourceId: string, slug: string, mutateConfig: DashboardConfigMutator): Promise<PersistedDashboardResponse | null>;
22
23
  export type DashboardConfigService = {
23
24
  getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
25
+ getAllDashboardRecords: () => Promise<DashboardRecord[]>;
24
26
  parseStoredDashboardConfig: typeof parseStoredDashboardConfig;
25
27
  persistDashboardConfig: (dashboard: DashboardRecord, config: DashboardConfig) => Promise<PersistedDashboardResponse>;
26
28
  updateDashboardConfig: (slug: string, mutateConfig: DashboardConfigMutator) => Promise<PersistedDashboardResponse | null>;
@@ -57,6 +57,11 @@ export function getDashboardRecord(adminforth, dashboardConfigsResourceId, slug)
57
57
  return dashboard || null;
58
58
  });
59
59
  }
60
+ export function getAllDashboardRecords(adminforth, dashboardConfigsResourceId) {
61
+ return __awaiter(this, void 0, void 0, function* () {
62
+ return yield adminforth.resource(dashboardConfigsResourceId).list([]);
63
+ });
64
+ }
60
65
  export function persistDashboardConfig(adminforth, dashboardConfigsResourceId, dashboard, config) {
61
66
  return __awaiter(this, void 0, void 0, function* () {
62
67
  const normalizedConfig = normalizeDashboardOrder(config);
@@ -103,6 +108,7 @@ export function updateDashboardConfig(adminforth, dashboardConfigsResourceId, sl
103
108
  export function createDashboardConfigService(adminforth, dashboardConfigsResourceId) {
104
109
  return {
105
110
  getDashboardRecord: (slug) => getDashboardRecord(adminforth, dashboardConfigsResourceId, slug),
111
+ getAllDashboardRecords: () => getAllDashboardRecords(adminforth, dashboardConfigsResourceId),
106
112
  parseStoredDashboardConfig,
107
113
  persistDashboardConfig: (dashboard, config) => persistDashboardConfig(adminforth, dashboardConfigsResourceId, dashboard, config),
108
114
  updateDashboardConfig: (slug, mutateConfig) => updateDashboardConfig(adminforth, dashboardConfigsResourceId, slug, mutateConfig),
@@ -175,7 +175,7 @@ function getAggregateRows(adminforth, resourceId, baseFilters, select, groupBy)
175
175
  function buildCalculatedRow(baseValues, select, calcs = [], variables) {
176
176
  const values = Object.assign({}, baseValues);
177
177
  for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
178
- values[item.as] = evaluateCalc(item.calc, values);
178
+ values[item.as] = evaluateCalc(item.calc, values, variables);
179
179
  }
180
180
  return values;
181
181
  }
@@ -190,7 +190,7 @@ function buildPlainRow(row, select, calcs = [], variables) {
190
190
  }
191
191
  }
192
192
  for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
193
- values[item.as] = evaluateCalc(item.calc, values);
193
+ values[item.as] = evaluateCalc(item.calc, values, variables);
194
194
  }
195
195
  return values;
196
196
  }
@@ -2,12 +2,14 @@ import type { IHttpServer } from 'adminforth';
2
2
  import type { DashboardConfig } from '../custom/model/dashboard.types.js';
3
3
  import {
4
4
  DashboardApiResponseSchema,
5
+ GetSlugsResponseSchema,
5
6
  SlugRequestSchema,
6
7
  } from '../schema/api.js';
7
8
  import type { DashboardRecord } from '../services/dashboardConfigService.js';
8
9
 
9
10
  type DashboardEndpointsContext = {
10
11
  getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
12
+ getAllDashboardRecords: () => Promise<DashboardRecord[]>;
11
13
  parseStoredDashboardConfig: (config: unknown) => DashboardConfig;
12
14
  };
13
15
 
@@ -38,4 +40,19 @@ export function registerDashboardEndpoints(
38
40
  };
39
41
  },
40
42
  });
43
+
44
+ server.endpoint({
45
+ method: 'GET',
46
+ path: '/dashboard/get-slugs',
47
+ description: 'Returns a list of all dashboard slugs and labels for listing purposes.',
48
+ request_schema: undefined,
49
+ response_schema: GetSlugsResponseSchema,
50
+ handler: async () => {
51
+ const dashboards = await ctx.getAllDashboardRecords();
52
+ return dashboards.map((dashboard) => ({
53
+ slug: dashboard.slug,
54
+ label: dashboard.label,
55
+ }));
56
+ },
57
+ });
41
58
  }
@@ -6,7 +6,7 @@ import type {
6
6
  DashboardGroupConfig,
7
7
  } from '../custom/model/dashboard.types.js';
8
8
  import {
9
- DashboardApiResponseSchema,
9
+ DashboardMutationResponseSchema,
10
10
  GroupIdRequestSchema,
11
11
  MoveGroupRequestSchema,
12
12
  SetGroupConfigRequestSchema,
@@ -37,13 +37,14 @@ export function registerGroupEndpoints(
37
37
  path: '/dashboard/add_dashboard_group',
38
38
  description: 'Adds a new group to a dashboard configuration. Superadmin only.',
39
39
  request_schema: SlugRequestSchema,
40
- response_schema: DashboardApiResponseSchema,
40
+ response_schema: DashboardMutationResponseSchema,
41
41
  handler: async ({ body, adminUser, response }) => {
42
42
  if (!ctx.canEditDashboard(adminUser)) {
43
43
  response.setStatus(403);
44
- return { error: 'Dashboard edit is not allowed' };
44
+ return { ok: false, error: 'Dashboard edit is not allowed' };
45
45
  }
46
46
 
47
+ let groupId: string | null = null;
47
48
  const updatedDashboard = await ctx.updateDashboardConfig(body.slug, (config) => {
48
49
  const nextOrder = config.groups.length + 1;
49
50
  const group: DashboardGroupConfig = {
@@ -51,6 +52,7 @@ export function registerGroupEndpoints(
51
52
  label: 'New group',
52
53
  order: nextOrder,
53
54
  };
55
+ groupId = group.id;
54
56
 
55
57
  return {
56
58
  ...config,
@@ -60,10 +62,10 @@ export function registerGroupEndpoints(
60
62
 
61
63
  if (!updatedDashboard) {
62
64
  response.setStatus(404);
63
- return { error: 'Dashboard not found' };
65
+ return { ok: false, error: 'Dashboard not found' };
64
66
  }
65
67
 
66
- return updatedDashboard;
68
+ return { ok: true, groupId };
67
69
  },
68
70
  });
69
71
 
@@ -72,11 +74,11 @@ export function registerGroupEndpoints(
72
74
  path: '/dashboard/set_dashboard_group_config',
73
75
  description: 'Replaces editable JSON configuration for a dashboard group while preserving group id and order. Superadmin only.',
74
76
  request_schema: SetGroupConfigRequestSchema,
75
- response_schema: DashboardApiResponseSchema,
77
+ response_schema: DashboardMutationResponseSchema,
76
78
  handler: async ({ body, adminUser, response }) => {
77
79
  if (!ctx.canEditDashboard(adminUser)) {
78
80
  response.setStatus(403);
79
- return { error: 'Dashboard edit is not allowed' };
81
+ return { ok: false, error: 'Dashboard edit is not allowed' };
80
82
  }
81
83
 
82
84
  const groupId = body.groupId;
@@ -105,15 +107,15 @@ export function registerGroupEndpoints(
105
107
 
106
108
  if (!updatedDashboard) {
107
109
  response.setStatus(404);
108
- return { error: 'Dashboard not found' };
110
+ return { ok: false, error: 'Dashboard not found' };
109
111
  }
110
112
 
111
113
  if (mutationError) {
112
114
  response.setStatus(404);
113
- return { error: mutationError };
115
+ return { ok: false, error: mutationError };
114
116
  }
115
117
 
116
- return updatedDashboard;
118
+ return { ok: true };
117
119
  },
118
120
  });
119
121
 
@@ -122,11 +124,11 @@ export function registerGroupEndpoints(
122
124
  path: '/dashboard/move_dashboard_group',
123
125
  description: 'Moves a dashboard group up or down in its dashboard. Superadmin only.',
124
126
  request_schema: MoveGroupRequestSchema,
125
- response_schema: DashboardApiResponseSchema,
127
+ response_schema: DashboardMutationResponseSchema,
126
128
  handler: async ({ body, adminUser, response }) => {
127
129
  if (!ctx.canEditDashboard(adminUser)) {
128
130
  response.setStatus(403);
129
- return { error: 'Dashboard edit is not allowed' };
131
+ return { ok: false, error: 'Dashboard edit is not allowed' };
130
132
  }
131
133
 
132
134
  let mutationError: string | null = null;
@@ -157,15 +159,15 @@ export function registerGroupEndpoints(
157
159
 
158
160
  if (!updatedDashboard) {
159
161
  response.setStatus(404);
160
- return { error: 'Dashboard not found' };
162
+ return { ok: false, error: 'Dashboard not found' };
161
163
  }
162
164
 
163
165
  if (mutationError) {
164
166
  response.setStatus(404);
165
- return { error: mutationError };
167
+ return { ok: false, error: mutationError };
166
168
  }
167
169
 
168
- return updatedDashboard;
170
+ return { ok: true };
169
171
  },
170
172
  });
171
173
 
@@ -174,11 +176,11 @@ export function registerGroupEndpoints(
174
176
  path: '/dashboard/remove_dashboard_group',
175
177
  description: 'Removes a dashboard group and all widgets inside it. Superadmin only.',
176
178
  request_schema: GroupIdRequestSchema,
177
- response_schema: DashboardApiResponseSchema,
179
+ response_schema: DashboardMutationResponseSchema,
178
180
  handler: async ({ body, adminUser, response }) => {
179
181
  if (!ctx.canEditDashboard(adminUser)) {
180
182
  response.setStatus(403);
181
- return { error: 'Dashboard edit is not allowed' };
183
+ return { ok: false, error: 'Dashboard edit is not allowed' };
182
184
  }
183
185
 
184
186
  const groupId = body.groupId;
@@ -200,15 +202,15 @@ export function registerGroupEndpoints(
200
202
 
201
203
  if (!updatedDashboard) {
202
204
  response.setStatus(404);
203
- return { error: 'Dashboard not found' };
205
+ return { ok: false, error: 'Dashboard not found' };
204
206
  }
205
207
 
206
208
  if (mutationError) {
207
209
  response.setStatus(404);
208
- return { error: mutationError };
210
+ return { ok: false, error: mutationError };
209
211
  }
210
212
 
211
- return updatedDashboard;
213
+ return { ok: true };
212
214
  },
213
215
  });
214
216
 
@@ -21,7 +21,7 @@ import {
21
21
  ConfigurePivotTableWidgetRequestSchema,
22
22
  ConfigureStackedBarChartWidgetRequestSchema,
23
23
  ConfigureTableWidgetRequestSchema,
24
- DashboardApiResponseSchema,
24
+ DashboardMutationResponseSchema,
25
25
  DashboardWidgetDataResponseSchema,
26
26
  GroupIdRequestSchema,
27
27
  MoveWidgetRequestSchema,
@@ -117,11 +117,11 @@ function registerConfigureWidgetEndpoint(
117
117
  path: options.path,
118
118
  description: options.description,
119
119
  request_schema: options.requestSchema,
120
- response_schema: DashboardApiResponseSchema,
120
+ response_schema: DashboardMutationResponseSchema,
121
121
  handler: async ({ body, adminUser, response }) => {
122
122
  if (!ctx.canEditDashboard(adminUser)) {
123
123
  response.setStatus(403);
124
- return { error: 'Dashboard edit is not allowed' };
124
+ return { ok: false, error: 'Dashboard edit is not allowed' };
125
125
  }
126
126
 
127
127
  const request = body as ConfigureWidgetRequest;
@@ -134,15 +134,15 @@ function registerConfigureWidgetEndpoint(
134
134
 
135
135
  if (!updatedDashboard) {
136
136
  response.setStatus(404);
137
- return { error: 'Dashboard not found' };
137
+ return { ok: false, error: 'Dashboard not found' };
138
138
  }
139
139
 
140
140
  if (mutationError) {
141
141
  response.setStatus(404);
142
- return { error: mutationError };
142
+ return { ok: false, error: mutationError };
143
143
  }
144
144
 
145
- return updatedDashboard;
145
+ return { ok: true };
146
146
  },
147
147
  });
148
148
  }
@@ -156,14 +156,15 @@ export function registerWidgetEndpoints(
156
156
  path: '/dashboard/add_dashboard_widget',
157
157
  description: 'Adds a new empty widget to a dashboard group. Superadmin only.',
158
158
  request_schema: GroupIdRequestSchema,
159
- response_schema: DashboardApiResponseSchema,
159
+ response_schema: DashboardMutationResponseSchema,
160
160
  handler: async ({ body, adminUser, response }) => {
161
161
  if (!ctx.canEditDashboard(adminUser)) {
162
162
  response.setStatus(403);
163
- return { error: 'Dashboard edit is not allowed' };
163
+ return { ok: false, error: 'Dashboard edit is not allowed' };
164
164
  }
165
165
 
166
166
  let mutationError: string | null = null;
167
+ let widgetId: string | null = null;
167
168
  const updatedDashboard = await ctx.updateDashboardConfig(body.slug, (config) => {
168
169
  const group = config.groups.find((item) => item.id === body.groupId);
169
170
 
@@ -181,6 +182,7 @@ export function registerWidgetEndpoints(
181
182
  order: nextOrder,
182
183
  target: 'empty',
183
184
  };
185
+ widgetId = widget.id;
184
186
 
185
187
  return {
186
188
  ...config,
@@ -190,15 +192,15 @@ export function registerWidgetEndpoints(
190
192
 
191
193
  if (!updatedDashboard) {
192
194
  response.setStatus(404);
193
- return { error: 'Dashboard not found' };
195
+ return { ok: false, error: 'Dashboard not found' };
194
196
  }
195
197
 
196
198
  if (mutationError) {
197
199
  response.setStatus(404);
198
- return { error: mutationError };
200
+ return { ok: false, error: mutationError };
199
201
  }
200
202
 
201
- return updatedDashboard;
203
+ return { ok: true, widgetId };
202
204
  },
203
205
  });
204
206
 
@@ -207,11 +209,11 @@ export function registerWidgetEndpoints(
207
209
  path: '/dashboard/move_dashboard_widget',
208
210
  description: 'Moves a dashboard widget up or down inside its group. Superadmin only.',
209
211
  request_schema: MoveWidgetRequestSchema,
210
- response_schema: DashboardApiResponseSchema,
212
+ response_schema: DashboardMutationResponseSchema,
211
213
  handler: async ({ body, adminUser, response }) => {
212
214
  if (!ctx.canEditDashboard(adminUser)) {
213
215
  response.setStatus(403);
214
- return { error: 'Dashboard edit is not allowed' };
216
+ return { ok: false, error: 'Dashboard edit is not allowed' };
215
217
  }
216
218
 
217
219
  let mutationError: string | null = null;
@@ -249,15 +251,15 @@ export function registerWidgetEndpoints(
249
251
 
250
252
  if (!updatedDashboard) {
251
253
  response.setStatus(404);
252
- return { error: 'Dashboard not found' };
254
+ return { ok: false, error: 'Dashboard not found' };
253
255
  }
254
256
 
255
257
  if (mutationError) {
256
258
  response.setStatus(404);
257
- return { error: mutationError };
259
+ return { ok: false, error: mutationError };
258
260
  }
259
261
 
260
- return updatedDashboard;
262
+ return { ok: true };
261
263
  },
262
264
  });
263
265
 
@@ -266,11 +268,11 @@ export function registerWidgetEndpoints(
266
268
  path: '/dashboard/remove_dashboard_widget',
267
269
  description: 'Removes one dashboard widget by id. Superadmin only.',
268
270
  request_schema: WidgetIdRequestSchema,
269
- response_schema: DashboardApiResponseSchema,
271
+ response_schema: DashboardMutationResponseSchema,
270
272
  handler: async ({ body, adminUser, response }) => {
271
273
  if (!ctx.canEditDashboard(adminUser)) {
272
274
  response.setStatus(403);
273
- return { error: 'Dashboard edit is not allowed' };
275
+ return { ok: false, error: 'Dashboard edit is not allowed' };
274
276
  }
275
277
 
276
278
  let mutationError: string | null = null;
@@ -290,15 +292,15 @@ export function registerWidgetEndpoints(
290
292
 
291
293
  if (!updatedDashboard) {
292
294
  response.setStatus(404);
293
- return { error: 'Dashboard not found' };
295
+ return { ok: false, error: 'Dashboard not found' };
294
296
  }
295
297
 
296
298
  if (mutationError) {
297
299
  response.setStatus(404);
298
- return { error: mutationError };
300
+ return { ok: false, error: mutationError };
299
301
  }
300
302
 
301
- return updatedDashboard;
303
+ return { ok: true };
302
304
  },
303
305
  });
304
306
 
@@ -308,11 +310,11 @@ export function registerWidgetEndpoints(
308
310
  path: '/dashboard/set_widget_config',
309
311
  description: 'Replaces editable JSON configuration for a dashboard widget while preserving widget id, group id, and order. Superadmin only.',
310
312
  request_schema: SetWidgetConfigRequestSchema,
311
- response_schema: DashboardApiResponseSchema,
313
+ response_schema: DashboardMutationResponseSchema,
312
314
  handler: async ({ body, adminUser, response }) => {
313
315
  if (!ctx.canEditDashboard(adminUser)) {
314
316
  response.setStatus(403);
315
- return { error: 'Dashboard edit is not allowed' };
317
+ return { ok: false, error: 'Dashboard edit is not allowed' };
316
318
  }
317
319
 
318
320
  const request = body as ConfigureWidgetRequest;
@@ -320,15 +322,15 @@ export function registerWidgetEndpoints(
320
322
 
321
323
  if (!updatedDashboard) {
322
324
  response.setStatus(404);
323
- return { error: 'Dashboard not found' };
325
+ return { ok: false, error: 'Dashboard not found' };
324
326
  }
325
327
 
326
328
  if (mutationError) {
327
329
  response.setStatus(404);
328
- return { error: mutationError };
330
+ return { ok: false, error: mutationError };
329
331
  }
330
332
 
331
- return updatedDashboard;
333
+ return { ok: true };
332
334
  },
333
335
  });
334
336
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/dashboard",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
package/schema/api.ts CHANGED
@@ -60,6 +60,11 @@ export const SlugRequestZodSchema = z.object({
60
60
  slug: z.string(),
61
61
  }).strict()
62
62
 
63
+ export const GetSlugsResponseZodSchema = z.array(z.object({
64
+ slug: z.string(),
65
+ label: z.string(),
66
+ }))
67
+
63
68
  export const GroupIdRequestZodSchema = z.object({
64
69
  slug: z.string(),
65
70
  groupId: z.string(),
@@ -203,21 +208,17 @@ export const ConfigureHistogramChartWidgetRequestZodSchema = configureWidgetRequ
203
208
  export const ConfigureFunnelChartWidgetRequestZodSchema = configureWidgetRequestSchema(ConfigurableFunnelChartWidgetConfigSchema)
204
209
  export const ConfigurePivotTableWidgetRequestZodSchema = configureWidgetRequestSchema(ConfigurablePivotTableWidgetConfigSchema)
205
210
 
206
- export const DashboardMutationResponseZodSchema = z.union([
207
- z.object({
208
- ok: z.literal(true),
209
- slug: z.string(),
210
- widgetId: z.string().optional(),
211
- groupId: z.string().optional(),
212
- target: z.string().optional(),
213
- revision: z.number().optional(),
214
- }).strict(),
215
- DashboardErrorResponseZodSchema,
216
- ])
211
+ export const DashboardMutationResponseZodSchema = z.object({
212
+ ok: z.boolean(),
213
+ error: z.string().optional(),
214
+ groupId: z.string().optional(),
215
+ widgetId: z.string().optional(),
216
+ }).strict()
217
217
 
218
- export const DashboardApiResponseSchema = toJSONSchema(DashboardApiResponseZodSchema, { target: 'draft-07' })
218
+ export const DashboardApiResponseSchema = toJSONSchema(z.unknown(), { target: 'draft-07' })
219
219
  export const DashboardWidgetDataResponseSchema = toJSONSchema(DashboardWidgetDataResponseZodSchema, { target: 'draft-07' })
220
220
  export const SlugRequestSchema = toJSONSchema(SlugRequestZodSchema, { target: 'draft-07' })
221
+ export const GetSlugsResponseSchema = toJSONSchema(GetSlugsResponseZodSchema, { target: 'draft-07' })
221
222
  export const GroupIdRequestSchema = toJSONSchema(GroupIdRequestZodSchema, { target: 'draft-07' })
222
223
  export const MoveGroupRequestSchema = toJSONSchema(MoveGroupRequestZodSchema, { target: 'draft-07' })
223
224
  export const SetGroupConfigRequestSchema = toJSONSchema(SetGroupConfigRequestZodSchema, { target: 'draft-07' })
@@ -1,5 +1,7 @@
1
1
  import { Parser } from 'expr-eval-fork';
2
+ import type { DashboardVariables } from '../custom/model/dashboard.types.js';
2
3
 
4
+ const LOOKUP_VARIABLE_PATH_RE = /lookup\(\s*\$variables((?:\.[a-zA-Z_][a-zA-Z0-9_]*)+)\s*,/g;
3
5
  const CALC_PARSER_OPTIONS = {
4
6
  allowMemberAccess: false,
5
7
  operators: {
@@ -14,10 +16,35 @@ const CALC_PARSER_OPTIONS = {
14
16
  },
15
17
  } as const;
16
18
 
17
- const CALC_PARSER = new Parser(CALC_PARSER_OPTIONS);
19
+ export function evaluateCalc(
20
+ calc: string,
21
+ values: Record<string, unknown>,
22
+ variables: DashboardVariables = {},
23
+ ) {
24
+ const parser = createCalcParser(variables);
18
25
 
19
- export function evaluateCalc(calc: string, values: Record<string, unknown>) {
20
- return CALC_PARSER.parse(calc).evaluate(normalizeCalcValues(values));
26
+ return parser.parse(normalizeLookupPaths(calc)).evaluate(normalizeCalcValues(values));
27
+ }
28
+
29
+ function createCalcParser(variables: DashboardVariables) {
30
+ const parser = new Parser(CALC_PARSER_OPTIONS);
31
+
32
+ parser.functions.lookup = (path: string | number, key: string | number, defaultValue = 0) => {
33
+ const map = resolveVariablePath(variables, String(path));
34
+ const value = isRecord(map) && Object.prototype.hasOwnProperty.call(map, String(key))
35
+ ? map[String(key)]
36
+ : defaultValue;
37
+
38
+ return toFiniteNumber(value);
39
+ };
40
+
41
+ return parser;
42
+ }
43
+
44
+ function normalizeLookupPaths(calc: string) {
45
+ return calc.replace(LOOKUP_VARIABLE_PATH_RE, (_match, path: string) => {
46
+ return `lookup("${path.replace(/^\./, '')}",`;
47
+ });
21
48
  }
22
49
 
23
50
  function normalizeCalcValues(values: Record<string, unknown>) {
@@ -31,3 +58,14 @@ function toFiniteNumber(value: unknown) {
31
58
  const numberValue = typeof value === 'number' ? value : Number(value);
32
59
  return Number.isFinite(numberValue) ? numberValue : 0;
33
60
  }
61
+
62
+ function resolveVariablePath(variables: DashboardVariables, path: string) {
63
+ return path
64
+ .split('.')
65
+ .filter(Boolean)
66
+ .reduce<unknown>((current, segment) => isRecord(current) ? current[segment] : undefined, variables);
67
+ }
68
+
69
+ function isRecord(value: unknown): value is Record<string, any> {
70
+ return typeof value === 'object' && value !== null;
71
+ }
@@ -96,6 +96,13 @@ export async function getDashboardRecord(
96
96
  return dashboard || null;
97
97
  }
98
98
 
99
+ export async function getAllDashboardRecords(
100
+ adminforth: IAdminForth,
101
+ dashboardConfigsResourceId: string,
102
+ ): Promise<DashboardRecord[]> {
103
+ return await adminforth.resource(dashboardConfigsResourceId).list([]);
104
+ }
105
+
99
106
  export async function persistDashboardConfig(
100
107
  adminforth: IAdminForth,
101
108
  dashboardConfigsResourceId: string,
@@ -156,6 +163,7 @@ export async function updateDashboardConfig(
156
163
 
157
164
  export type DashboardConfigService = {
158
165
  getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
166
+ getAllDashboardRecords: () => Promise<DashboardRecord[]>;
159
167
  parseStoredDashboardConfig: typeof parseStoredDashboardConfig;
160
168
  persistDashboardConfig: (
161
169
  dashboard: DashboardRecord,
@@ -173,6 +181,7 @@ export function createDashboardConfigService(
173
181
  ): DashboardConfigService {
174
182
  return {
175
183
  getDashboardRecord: (slug) => getDashboardRecord(adminforth, dashboardConfigsResourceId, slug),
184
+ getAllDashboardRecords: () => getAllDashboardRecords(adminforth, dashboardConfigsResourceId),
176
185
  parseStoredDashboardConfig,
177
186
  persistDashboardConfig: (dashboard, config) => persistDashboardConfig(
178
187
  adminforth,
@@ -341,7 +341,7 @@ function buildCalculatedRow(
341
341
  const values: Record<string, unknown> = { ...baseValues };
342
342
 
343
343
  for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
344
- values[item.as] = evaluateCalc(item.calc, values);
344
+ values[item.as] = evaluateCalc(item.calc, values, variables);
345
345
  }
346
346
 
347
347
  return values;
@@ -364,7 +364,7 @@ function buildPlainRow(
364
364
  }
365
365
 
366
366
  for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
367
- values[item.as] = evaluateCalc(item.calc, values);
367
+ values[item.as] = evaluateCalc(item.calc, values, variables);
368
368
  }
369
369
 
370
370
  return values;