adminforth 2.27.0-next.49 → 2.27.0-next.50

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.
@@ -3,7 +3,7 @@ import { cascadeChildrenDelete } from './utils.js';
3
3
  import { afLogger } from "./logger.js";
4
4
  import { ADMINFORTH_VERSION, listify, getLoginPromptHTML, hookResponseError } from './utils.js';
5
5
  import AdminForthAuth from "../auth.js";
6
- import { ActionCheckSource, AdminForthDataTypes, AdminForthFilterOperators, AdminForthResourcePages, AllowedActionsEnum } from "../types/Common.js";
6
+ import { ActionCheckSource, AdminForthDataTypes, AdminForthFilterOperators, AdminForthResourcePages, AdminForthSortDirections, AllowedActionsEnum } from "../types/Common.js";
7
7
  import { filtersTools } from "../modules/filtersTools.js";
8
8
  async function resolveBoolOrFn(val, ctx) {
9
9
  if (typeof val === 'function') {
@@ -26,6 +26,406 @@ async function isFilledOnCreate(col) {
26
26
  const fillOnCreate = !!col.fillOnCreate;
27
27
  return fillOnCreate;
28
28
  }
29
+ const SIMPLE_FILTER_OPERATORS = Object.values(AdminForthFilterOperators).filter((operator) => {
30
+ return operator !== AdminForthFilterOperators.AND && operator !== AdminForthFilterOperators.OR;
31
+ });
32
+ const genericObjectSchema = {
33
+ type: 'object',
34
+ additionalProperties: true,
35
+ };
36
+ const errorResponseSchema = {
37
+ type: 'object',
38
+ required: ['error'],
39
+ properties: {
40
+ error: { type: 'string' },
41
+ },
42
+ additionalProperties: true,
43
+ };
44
+ const recordIdentifierSchema = {
45
+ anyOf: [
46
+ { type: 'string' },
47
+ { type: 'number' },
48
+ ],
49
+ };
50
+ const actionIdentifierSchema = {
51
+ anyOf: [
52
+ { type: 'string' },
53
+ { type: 'number' },
54
+ ],
55
+ };
56
+ const namedColumnSchema = {
57
+ type: 'object',
58
+ required: ['name'],
59
+ properties: {
60
+ name: { type: 'string' },
61
+ },
62
+ additionalProperties: true,
63
+ };
64
+ const validationResultSchema = {
65
+ type: 'object',
66
+ required: ['isValid'],
67
+ properties: {
68
+ isValid: { type: 'boolean' },
69
+ message: { type: 'string' },
70
+ },
71
+ additionalProperties: true,
72
+ };
73
+ const commonFilterSchemaDefs = {
74
+ singleFilter: {
75
+ type: 'object',
76
+ properties: {
77
+ field: { type: 'string' },
78
+ operator: { type: 'string', enum: SIMPLE_FILTER_OPERATORS },
79
+ value: {},
80
+ rightField: { type: 'string' },
81
+ insecureRawSQL: { type: 'string' },
82
+ insecureRawNoSQL: {},
83
+ },
84
+ additionalProperties: true,
85
+ },
86
+ filterNode: {
87
+ anyOf: [
88
+ { $ref: '#/$defs/singleFilter' },
89
+ {
90
+ type: 'object',
91
+ required: ['operator', 'subFilters'],
92
+ properties: {
93
+ operator: {
94
+ type: 'string',
95
+ enum: [AdminForthFilterOperators.AND, AdminForthFilterOperators.OR],
96
+ },
97
+ subFilters: {
98
+ type: 'array',
99
+ items: { $ref: '#/$defs/filterNode' },
100
+ },
101
+ },
102
+ additionalProperties: true,
103
+ },
104
+ ],
105
+ },
106
+ sortItem: {
107
+ type: 'object',
108
+ required: ['field', 'direction'],
109
+ properties: {
110
+ field: { type: 'string' },
111
+ direction: { type: 'string', enum: Object.values(AdminForthSortDirections) },
112
+ },
113
+ additionalProperties: true,
114
+ },
115
+ };
116
+ const commonSortSchema = {
117
+ type: 'array',
118
+ items: { $ref: '#/$defs/sortItem' },
119
+ };
120
+ const commonFiltersSchema = {
121
+ oneOf: [
122
+ {
123
+ type: 'array',
124
+ items: { $ref: '#/$defs/filterNode' },
125
+ },
126
+ { $ref: '#/$defs/filterNode' },
127
+ ],
128
+ };
129
+ function createErrorOrSuccessSchema(successSchema) {
130
+ return {
131
+ anyOf: [
132
+ errorResponseSchema,
133
+ successSchema,
134
+ ],
135
+ };
136
+ }
137
+ const getResourceDataRequestSchema = {
138
+ type: 'object',
139
+ $defs: commonFilterSchemaDefs,
140
+ required: ['resourceId', 'source', 'limit', 'offset', 'filters', 'sort'],
141
+ properties: {
142
+ resourceId: { type: 'string' },
143
+ source: { type: 'string', enum: ['show', 'list', 'edit'] },
144
+ limit: { type: 'integer' },
145
+ offset: { type: 'integer' },
146
+ sort: commonSortSchema,
147
+ filters: commonFiltersSchema,
148
+ },
149
+ additionalProperties: true,
150
+ allOf: [
151
+ {
152
+ if: {
153
+ properties: {
154
+ source: { enum: ['show', 'edit'] },
155
+ },
156
+ required: ['source'],
157
+ },
158
+ then: {
159
+ properties: {
160
+ filters: {
161
+ type: 'array',
162
+ items: {
163
+ allOf: [
164
+ { $ref: '#/$defs/singleFilter' },
165
+ {
166
+ type: 'object',
167
+ required: ['field'],
168
+ },
169
+ ],
170
+ },
171
+ },
172
+ },
173
+ },
174
+ },
175
+ ],
176
+ };
177
+ const getResourceDataResponseSchema = createErrorOrSuccessSchema({
178
+ type: 'object',
179
+ required: ['data'],
180
+ properties: {
181
+ data: {
182
+ type: 'array',
183
+ items: genericObjectSchema,
184
+ },
185
+ total: { type: 'number' },
186
+ options: genericObjectSchema,
187
+ },
188
+ additionalProperties: true,
189
+ });
190
+ const getMenuBadgesResponseSchema = {
191
+ type: 'object',
192
+ additionalProperties: {
193
+ anyOf: [
194
+ { type: 'string' },
195
+ { type: 'number' },
196
+ ],
197
+ },
198
+ };
199
+ const getResourceRequestSchema = {
200
+ type: 'object',
201
+ required: ['resourceId'],
202
+ properties: {
203
+ resourceId: { type: 'string' },
204
+ },
205
+ additionalProperties: true,
206
+ };
207
+ const getResourceResponseSchema = createErrorOrSuccessSchema({
208
+ type: 'object',
209
+ required: ['resource'],
210
+ properties: {
211
+ resource: genericObjectSchema,
212
+ },
213
+ additionalProperties: true,
214
+ });
215
+ const getResourceForeignDataRequestSchema = {
216
+ type: 'object',
217
+ $defs: commonFilterSchemaDefs,
218
+ required: ['resourceId', 'column', 'limit', 'offset'],
219
+ properties: {
220
+ resourceId: { type: 'string' },
221
+ column: { type: 'string' },
222
+ limit: { type: 'integer' },
223
+ offset: { type: 'integer' },
224
+ search: { type: 'string' },
225
+ filters: commonFiltersSchema,
226
+ sort: commonSortSchema,
227
+ },
228
+ additionalProperties: true,
229
+ };
230
+ const getResourceForeignDataResponseSchema = createErrorOrSuccessSchema({
231
+ type: 'object',
232
+ required: ['items'],
233
+ properties: {
234
+ items: {
235
+ type: 'array',
236
+ items: {
237
+ type: 'object',
238
+ required: ['value', 'label'],
239
+ properties: {
240
+ value: {},
241
+ label: { type: 'string' },
242
+ },
243
+ additionalProperties: true,
244
+ },
245
+ },
246
+ },
247
+ additionalProperties: true,
248
+ });
249
+ const getMinMaxForColumnsRequestSchema = {
250
+ type: 'object',
251
+ required: ['resourceId'],
252
+ properties: {
253
+ resourceId: { type: 'string' },
254
+ },
255
+ additionalProperties: true,
256
+ };
257
+ const getMinMaxForColumnsResponseSchema = createErrorOrSuccessSchema({
258
+ type: 'object',
259
+ additionalProperties: {
260
+ type: 'object',
261
+ required: ['min', 'max'],
262
+ properties: {
263
+ min: {},
264
+ max: {},
265
+ },
266
+ additionalProperties: true,
267
+ },
268
+ });
269
+ const createRecordRequestSchema = {
270
+ type: 'object',
271
+ required: ['resourceId', 'record', 'requiredColumnsToSkip'],
272
+ properties: {
273
+ resourceId: { type: 'string' },
274
+ record: genericObjectSchema,
275
+ requiredColumnsToSkip: {
276
+ type: 'array',
277
+ items: namedColumnSchema,
278
+ },
279
+ meta: genericObjectSchema,
280
+ },
281
+ additionalProperties: true,
282
+ };
283
+ const createRecordResponseSchema = createErrorOrSuccessSchema({
284
+ type: 'object',
285
+ required: ['ok', 'newRecordId', 'redirectToRecordId'],
286
+ properties: {
287
+ ok: { const: true },
288
+ newRecordId: recordIdentifierSchema,
289
+ redirectToRecordId: recordIdentifierSchema,
290
+ },
291
+ additionalProperties: true,
292
+ });
293
+ const updateRecordRequestSchema = {
294
+ type: 'object',
295
+ required: ['resourceId', 'recordId', 'record'],
296
+ properties: {
297
+ resourceId: { type: 'string' },
298
+ recordId: recordIdentifierSchema,
299
+ record: genericObjectSchema,
300
+ meta: genericObjectSchema,
301
+ },
302
+ additionalProperties: true,
303
+ };
304
+ const updateRecordResponseSchema = createErrorOrSuccessSchema({
305
+ type: 'object',
306
+ required: ['ok'],
307
+ properties: {
308
+ ok: { const: true },
309
+ recordId: recordIdentifierSchema,
310
+ },
311
+ additionalProperties: true,
312
+ });
313
+ const deleteRecordRequestSchema = {
314
+ type: 'object',
315
+ required: ['resourceId', 'primaryKey'],
316
+ properties: {
317
+ resourceId: { type: 'string' },
318
+ primaryKey: recordIdentifierSchema,
319
+ },
320
+ additionalProperties: true,
321
+ };
322
+ const deleteRecordResponseSchema = createErrorOrSuccessSchema({
323
+ type: 'object',
324
+ required: ['ok', 'recordId'],
325
+ properties: {
326
+ ok: { const: true },
327
+ recordId: recordIdentifierSchema,
328
+ },
329
+ additionalProperties: true,
330
+ });
331
+ const startCustomActionRequestSchema = {
332
+ type: 'object',
333
+ required: ['resourceId', 'actionId', 'recordId'],
334
+ properties: {
335
+ resourceId: { type: 'string' },
336
+ actionId: actionIdentifierSchema,
337
+ recordId: recordIdentifierSchema,
338
+ extra: genericObjectSchema,
339
+ },
340
+ additionalProperties: true,
341
+ };
342
+ const startCustomActionResponseSchema = {
343
+ anyOf: [
344
+ errorResponseSchema,
345
+ {
346
+ type: 'object',
347
+ required: ['actionId', 'resourceId', 'recordId', 'redirectUrl'],
348
+ properties: {
349
+ actionId: actionIdentifierSchema,
350
+ resourceId: { type: 'string' },
351
+ recordId: recordIdentifierSchema,
352
+ redirectUrl: { type: 'string' },
353
+ },
354
+ additionalProperties: true,
355
+ },
356
+ {
357
+ type: 'object',
358
+ required: ['actionId', 'resourceId', 'recordId', 'ok'],
359
+ properties: {
360
+ actionId: actionIdentifierSchema,
361
+ resourceId: { type: 'string' },
362
+ recordId: recordIdentifierSchema,
363
+ ok: { const: true },
364
+ },
365
+ additionalProperties: true,
366
+ },
367
+ ],
368
+ };
369
+ const startCustomBulkActionRequestSchema = {
370
+ type: 'object',
371
+ required: ['resourceId', 'actionId', 'recordIds'],
372
+ properties: {
373
+ resourceId: { type: 'string' },
374
+ actionId: actionIdentifierSchema,
375
+ recordIds: {
376
+ type: 'array',
377
+ items: recordIdentifierSchema,
378
+ },
379
+ extra: genericObjectSchema,
380
+ },
381
+ additionalProperties: true,
382
+ };
383
+ const startCustomBulkActionResponseSchema = createErrorOrSuccessSchema({
384
+ type: 'object',
385
+ required: ['actionId', 'resourceId', 'recordIds', 'ok'],
386
+ properties: {
387
+ actionId: actionIdentifierSchema,
388
+ resourceId: { type: 'string' },
389
+ recordIds: {
390
+ type: 'array',
391
+ items: recordIdentifierSchema,
392
+ },
393
+ ok: { const: true },
394
+ },
395
+ additionalProperties: true,
396
+ });
397
+ const validateColumnsRequestSchema = {
398
+ type: 'object',
399
+ required: ['resourceId', 'editableColumns', 'record'],
400
+ properties: {
401
+ resourceId: { type: 'string' },
402
+ editableColumns: {
403
+ type: 'array',
404
+ items: {
405
+ type: 'object',
406
+ required: ['name'],
407
+ properties: {
408
+ name: { type: 'string' },
409
+ value: {},
410
+ },
411
+ additionalProperties: true,
412
+ },
413
+ },
414
+ record: genericObjectSchema,
415
+ },
416
+ additionalProperties: true,
417
+ };
418
+ const validateColumnsResponseSchema = createErrorOrSuccessSchema({
419
+ type: 'object',
420
+ required: ['validationResults'],
421
+ properties: {
422
+ validationResults: {
423
+ type: 'object',
424
+ additionalProperties: validationResultSchema,
425
+ },
426
+ },
427
+ additionalProperties: true,
428
+ });
29
429
  export async function interpretResource(adminUser, resource, meta, source, adminforth) {
30
430
  afLogger.trace(`🪲Interpreting resource, ${resource.resourceId}, ${source}, 'adminUser', ${adminUser}`);
31
431
  const allowedActions = {};
@@ -182,7 +582,7 @@ export default class AdminForthRestAPI {
182
582
  handler: async ({ tr }) => {
183
583
  const loginPromptHTML = await getLoginPromptHTML(this.adminforth.config.auth.loginPromptHTML);
184
584
  return {
185
- loginPromptHTML: await tr(loginPromptHTML, 'system.loginPromptHTML'),
585
+ loginPromptHTML: loginPromptHTML ? await tr(loginPromptHTML, 'system.loginPromptHTML') : null,
186
586
  };
187
587
  }
188
588
  });
@@ -222,7 +622,7 @@ export default class AdminForthRestAPI {
222
622
  server.endpoint({
223
623
  method: 'GET',
224
624
  path: '/get_base_config',
225
- handler: async ({ input, adminUser, cookies, tr, response }) => {
625
+ handler: async ({ adminUser, cookies, tr, response }) => {
226
626
  var _a, _b, _c, _d, _e;
227
627
  let username = '';
228
628
  let userFullName = '';
@@ -385,6 +785,8 @@ export default class AdminForthRestAPI {
385
785
  server.endpoint({
386
786
  method: 'GET',
387
787
  path: '/get_menu_badges',
788
+ description: 'Computes the current menu badge values for the authenticated admin user. Static badges are returned directly, and dynamic badge callbacks are resolved for all configured menu items, including nested items.',
789
+ response_schema: getMenuBadgesResponseSchema,
388
790
  handler: async ({ adminUser }) => {
389
791
  const badges = {};
390
792
  const badgeFunctions = [];
@@ -421,6 +823,9 @@ export default class AdminForthRestAPI {
421
823
  server.endpoint({
422
824
  method: 'POST',
423
825
  path: '/get_resource',
826
+ description: 'Returns the definition of a single resource. The response includes translated labels, column metadata, allowed actions, visible bulk actions, frontend action metadata, and resource options after permission checks and removal of backend-only internals.',
827
+ request_schema: getResourceRequestSchema,
828
+ response_schema: getResourceResponseSchema,
424
829
  handler: async ({ body, adminUser, tr }) => {
425
830
  var _a, _b;
426
831
  const { resourceId } = body;
@@ -508,7 +913,7 @@ export default class AdminForthRestAPI {
508
913
  col.foreignResource.unsetLabel = await tr(col.foreignResource.unsetLabel, `resource.${resource.resourceId}.foreignResource.unsetLabel`);
509
914
  }
510
915
  if (inCol.suggestOnCreate && typeof inCol.suggestOnCreate === 'function') {
511
- col.suggestOnCreate = await inCol.suggestOnCreate(adminUser);
916
+ col.suggestOnCreate = await inCol.suggestOnCreate({ adminUser });
512
917
  }
513
918
  return Object.assign(Object.assign({}, col), { showIn,
514
919
  validation, label: translated[`resCol${i}`], enum: enumItems });
@@ -526,6 +931,9 @@ export default class AdminForthRestAPI {
526
931
  server.endpoint({
527
932
  method: 'POST',
528
933
  path: '/get_resource_data',
934
+ description: 'Loads resource rows for list, show, or edit views. The endpoint validates access, applies request hooks, filters, sorting, pagination, record labels, and row click URLs, then returns the final dataset with resource options.',
935
+ request_schema: getResourceDataRequestSchema,
936
+ response_schema: getResourceDataResponseSchema,
529
937
  handler: async ({ body, adminUser, headers, query, cookies, requestUrl, abortSignal }) => {
530
938
  var _a, _b, _c, _d, _e, _f;
531
939
  const { resourceId, source } = body;
@@ -793,6 +1201,9 @@ export default class AdminForthRestAPI {
793
1201
  server.endpoint({
794
1202
  method: 'POST',
795
1203
  path: '/get_resource_foreign_data',
1204
+ description: 'Loads dropdown options for a foreign-key column. It resolves the referenced resource or polymorphic resources, applies optional search text, hook-injected filters, pagination, and per-record labels, then returns sanitized option items.',
1205
+ request_schema: getResourceForeignDataRequestSchema,
1206
+ response_schema: getResourceForeignDataResponseSchema,
796
1207
  handler: async ({ body, adminUser, headers, query, cookies, requestUrl }) => {
797
1208
  const { resourceId, column, search } = body;
798
1209
  if (!this.adminforth.statuses.dbDiscover) {
@@ -953,6 +1364,9 @@ export default class AdminForthRestAPI {
953
1364
  server.endpoint({
954
1365
  method: 'POST',
955
1366
  path: '/get_min_max_for_columns',
1367
+ description: 'Returns min and max values for resource columns that explicitly opt in to min/max queries. This is used to build range-based filter controls without exposing columns that do not allow the query.',
1368
+ request_schema: getMinMaxForColumnsRequestSchema,
1369
+ response_schema: getMinMaxForColumnsResponseSchema,
956
1370
  handler: async ({ body }) => {
957
1371
  const { resourceId } = body;
958
1372
  if (!this.adminforth.statuses.dbDiscover) {
@@ -982,6 +1396,9 @@ export default class AdminForthRestAPI {
982
1396
  server.endpoint({
983
1397
  method: 'POST',
984
1398
  path: '/create_record',
1399
+ description: 'Creates a new record in the specified resource. The endpoint validates create permissions, required fields, hidden or backend-only field rules, polymorphic foreign keys, and resource hooks before persisting and returning the created primary key.',
1400
+ request_schema: createRecordRequestSchema,
1401
+ response_schema: createRecordResponseSchema,
985
1402
  handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
986
1403
  var _a, _b, _c, _d;
987
1404
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
@@ -1111,6 +1528,9 @@ export default class AdminForthRestAPI {
1111
1528
  server.endpoint({
1112
1529
  method: 'POST',
1113
1530
  path: '/update_record',
1531
+ description: 'Updates an existing record by primary key. The endpoint validates edit permissions, current record existence, hidden, backend-only, and read-only field rules, polymorphic foreign keys, and resource hooks before saving changes.',
1532
+ request_schema: updateRecordRequestSchema,
1533
+ response_schema: updateRecordResponseSchema,
1114
1534
  handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
1115
1535
  var _a, _b;
1116
1536
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
@@ -1229,6 +1649,9 @@ export default class AdminForthRestAPI {
1229
1649
  server.endpoint({
1230
1650
  method: 'POST',
1231
1651
  path: '/delete_record',
1652
+ description: 'Deletes an existing record by primary key. The endpoint validates delete permissions, loads the current record, executes configured cascade child deletion, and then removes the record.',
1653
+ request_schema: deleteRecordRequestSchema,
1654
+ response_schema: deleteRecordResponseSchema,
1232
1655
  handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
1233
1656
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
1234
1657
  if (!resource) {
@@ -1289,6 +1712,9 @@ export default class AdminForthRestAPI {
1289
1712
  server.endpoint({
1290
1713
  method: 'POST',
1291
1714
  path: '/start_custom_action',
1715
+ description: 'Executes a custom resource action for a single record. The endpoint validates the resource, action existence, and action permissions, then either returns a redirect URL or executes the action handler and returns its result together with action context.',
1716
+ request_schema: startCustomActionRequestSchema,
1717
+ response_schema: startCustomActionResponseSchema,
1292
1718
  handler: async ({ body, adminUser, tr, cookies, response, headers }) => {
1293
1719
  const { resourceId, actionId, recordId, extra } = body;
1294
1720
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
@@ -1323,6 +1749,9 @@ export default class AdminForthRestAPI {
1323
1749
  server.endpoint({
1324
1750
  method: 'POST',
1325
1751
  path: '/start_custom_bulk_action',
1752
+ description: 'Executes a custom resource action in bulk mode for multiple records. The endpoint validates the resource, action existence, bulk handler availability, and permissions, then runs the bulk handler and returns its result together with action context.',
1753
+ request_schema: startCustomBulkActionRequestSchema,
1754
+ response_schema: startCustomBulkActionResponseSchema,
1326
1755
  handler: async ({ body, adminUser, tr, response, cookies, headers }) => {
1327
1756
  const { resourceId, actionId, recordIds, extra } = body;
1328
1757
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
@@ -1358,6 +1787,9 @@ export default class AdminForthRestAPI {
1358
1787
  server.endpoint({
1359
1788
  method: 'POST',
1360
1789
  path: '/validate_columns',
1790
+ description: 'Runs server-side custom validators for editable columns in a resource form. Only validators defined on submitted columns are executed, and the response maps each invalid column to its validation result.',
1791
+ request_schema: validateColumnsRequestSchema,
1792
+ response_schema: validateColumnsResponseSchema,
1361
1793
  handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
1362
1794
  const { resourceId, editableColumns, record } = body;
1363
1795
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);