adminforth 2.27.0-next.6 → 2.27.0-next.61

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 (90) hide show
  1. package/commands/callTsProxy.js +10 -5
  2. package/commands/createApp/templates/api.ts.hbs +28 -9
  3. package/commands/createApp/templates/package.json.hbs +2 -1
  4. package/commands/proxy.ts +18 -10
  5. package/dist/basePlugin.js +1 -1
  6. package/dist/basePlugin.js.map +1 -1
  7. package/dist/commands/proxy.js +14 -10
  8. package/dist/commands/proxy.js.map +1 -1
  9. package/dist/dataConnectors/clickhouse.d.ts +5 -2
  10. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  11. package/dist/dataConnectors/clickhouse.js +73 -9
  12. package/dist/dataConnectors/clickhouse.js.map +1 -1
  13. package/dist/index.d.ts +4 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +17 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/modules/configValidator.d.ts.map +1 -1
  18. package/dist/modules/configValidator.js +16 -9
  19. package/dist/modules/configValidator.js.map +1 -1
  20. package/dist/modules/restApi.d.ts.map +1 -1
  21. package/dist/modules/restApi.js +574 -9
  22. package/dist/modules/restApi.js.map +1 -1
  23. package/dist/modules/styles.js +1 -1
  24. package/dist/modules/utils.d.ts +1 -1
  25. package/dist/modules/utils.d.ts.map +1 -1
  26. package/dist/modules/utils.js +3 -5
  27. package/dist/modules/utils.js.map +1 -1
  28. package/dist/servers/express.d.ts +18 -7
  29. package/dist/servers/express.d.ts.map +1 -1
  30. package/dist/servers/express.js +141 -1
  31. package/dist/servers/express.js.map +1 -1
  32. package/dist/servers/openapi.d.ts +25 -0
  33. package/dist/servers/openapi.d.ts.map +1 -0
  34. package/dist/servers/openapi.js +92 -0
  35. package/dist/servers/openapi.js.map +1 -0
  36. package/dist/servers/openapiDocument.d.ts +12 -0
  37. package/dist/servers/openapiDocument.d.ts.map +1 -0
  38. package/dist/servers/openapiDocument.js +313 -0
  39. package/dist/servers/openapiDocument.js.map +1 -0
  40. package/dist/spa/package-lock.json +41 -0
  41. package/dist/spa/package.json +4 -0
  42. package/dist/spa/pnpm-lock.yaml +384 -310
  43. package/dist/spa/pnpm-workspace.yaml +4 -0
  44. package/dist/spa/src/App.vue +78 -76
  45. package/dist/spa/src/afcl/Button.vue +2 -3
  46. package/dist/spa/src/afcl/Dialog.vue +1 -1
  47. package/dist/spa/src/afcl/Input.vue +1 -1
  48. package/dist/spa/src/afcl/Select.vue +8 -2
  49. package/dist/spa/src/afcl/Skeleton.vue +5 -0
  50. package/dist/spa/src/afcl/Spinner.vue +1 -1
  51. package/dist/spa/src/components/CallActionWrapper.vue +1 -1
  52. package/dist/spa/src/components/ColumnValueInput.vue +16 -3
  53. package/dist/spa/src/components/ColumnValueInputWrapper.vue +25 -2
  54. package/dist/spa/src/components/CustomRangePicker.vue +10 -14
  55. package/dist/spa/src/components/Filters.vue +95 -63
  56. package/dist/spa/src/components/GroupsTable.vue +9 -6
  57. package/dist/spa/src/components/MenuLink.vue +2 -2
  58. package/dist/spa/src/components/ResourceForm.vue +101 -7
  59. package/dist/spa/src/components/ResourceListTable.vue +14 -8
  60. package/dist/spa/src/components/ShowTable.vue +1 -1
  61. package/dist/spa/src/components/Sidebar.vue +29 -8
  62. package/dist/spa/src/components/ThreeDotsMenu.vue +25 -10
  63. package/dist/spa/src/components/ValueRenderer.vue +1 -0
  64. package/dist/spa/src/spa_types/core.ts +32 -0
  65. package/dist/spa/src/stores/core.ts +15 -1
  66. package/dist/spa/src/stores/filters.ts +16 -12
  67. package/dist/spa/src/types/Back.ts +137 -26
  68. package/dist/spa/src/types/Common.ts +24 -5
  69. package/dist/spa/src/types/adapters/CompletionAdapter.ts +27 -5
  70. package/dist/spa/src/types/adapters/index.ts +2 -2
  71. package/dist/spa/src/utils/createEditUtils.ts +65 -0
  72. package/dist/spa/src/utils/index.ts +2 -1
  73. package/dist/spa/src/utils/utils.ts +42 -7
  74. package/dist/spa/src/utils.ts +2 -1
  75. package/dist/spa/src/views/CreateEditSkeleton.vue +74 -0
  76. package/dist/spa/src/views/CreateView.vue +24 -50
  77. package/dist/spa/src/views/EditView.vue +23 -40
  78. package/dist/spa/src/views/ListView.vue +22 -32
  79. package/dist/spa/src/views/ShowView.vue +66 -24
  80. package/dist/types/Back.d.ts +140 -32
  81. package/dist/types/Back.d.ts.map +1 -1
  82. package/dist/types/Back.js.map +1 -1
  83. package/dist/types/Common.d.ts +31 -5
  84. package/dist/types/Common.d.ts.map +1 -1
  85. package/dist/types/Common.js.map +1 -1
  86. package/dist/types/adapters/CompletionAdapter.d.ts +18 -3
  87. package/dist/types/adapters/CompletionAdapter.d.ts.map +1 -1
  88. package/dist/types/adapters/index.d.ts +1 -1
  89. package/dist/types/adapters/index.d.ts.map +1 -1
  90. package/package.json +11 -6
@@ -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,465 @@ 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
+ title: 'AdminForthErrorResponse',
38
+ description: 'Standard error response returned by AdminForth endpoints.',
39
+ type: 'object',
40
+ required: ['error'],
41
+ properties: {
42
+ error: { type: 'string' },
43
+ },
44
+ additionalProperties: true,
45
+ };
46
+ const recordIdentifierSchema = {
47
+ title: 'AdminForthRecordIdentifier',
48
+ description: 'Record identifier accepted by AdminForth. Depending on the resource it can be a string or a number.',
49
+ anyOf: [
50
+ { type: 'string' },
51
+ { type: 'number' },
52
+ ],
53
+ };
54
+ const actionIdentifierSchema = {
55
+ title: 'AdminForthActionIdentifier',
56
+ description: 'Action identifier accepted by AdminForth. Depending on configuration it can be a string or a number.',
57
+ anyOf: [
58
+ { type: 'string' },
59
+ { type: 'number' },
60
+ ],
61
+ };
62
+ const namedColumnSchema = {
63
+ title: 'AdminForthNamedColumn',
64
+ type: 'object',
65
+ required: ['name'],
66
+ properties: {
67
+ name: { type: 'string' },
68
+ },
69
+ additionalProperties: true,
70
+ };
71
+ const validationResultSchema = {
72
+ title: 'AdminForthValidationResult',
73
+ type: 'object',
74
+ required: ['isValid'],
75
+ properties: {
76
+ isValid: { type: 'boolean' },
77
+ message: { type: 'string' },
78
+ },
79
+ additionalProperties: true,
80
+ };
81
+ const filterConditionExample = {
82
+ field: 'status',
83
+ operator: AdminForthFilterOperators.EQ,
84
+ value: 'active',
85
+ };
86
+ const filterGroupExample = {
87
+ operator: AdminForthFilterOperators.AND,
88
+ subFilters: [filterConditionExample],
89
+ };
90
+ const sortItemExample = {
91
+ field: 'createdAt',
92
+ direction: AdminForthSortDirections.desc,
93
+ };
94
+ const filterConditionSchema = {
95
+ title: 'AdminForthFilterCondition',
96
+ description: 'Single field comparison used in AdminForth filtering.',
97
+ type: 'object',
98
+ properties: {
99
+ field: { type: 'string' },
100
+ operator: { type: 'string', enum: SIMPLE_FILTER_OPERATORS },
101
+ value: {},
102
+ rightField: { type: 'string' },
103
+ insecureRawSQL: { type: 'string' },
104
+ insecureRawNoSQL: {},
105
+ },
106
+ additionalProperties: true,
107
+ examples: [filterConditionExample],
108
+ };
109
+ const filterGroupSchema = {
110
+ title: 'AdminForthFilterGroup',
111
+ description: 'Nested boolean filter group. Use this for AND or OR combinations of filter nodes.',
112
+ type: 'object',
113
+ required: ['operator', 'subFilters'],
114
+ properties: {
115
+ operator: {
116
+ type: 'string',
117
+ enum: [AdminForthFilterOperators.AND, AdminForthFilterOperators.OR],
118
+ },
119
+ subFilters: {
120
+ type: 'array',
121
+ items: { $ref: '#/$defs/filterNode' },
122
+ description: 'Nested filters evaluated with the selected operator.',
123
+ },
124
+ },
125
+ additionalProperties: true,
126
+ examples: [filterGroupExample],
127
+ };
128
+ const sortItemSchema = {
129
+ title: 'AdminForthSortItem',
130
+ description: 'Single sort instruction applied in order with the rest of the list.',
131
+ type: 'object',
132
+ required: ['field', 'direction'],
133
+ properties: {
134
+ field: { type: 'string' },
135
+ direction: { type: 'string', enum: Object.values(AdminForthSortDirections) },
136
+ },
137
+ additionalProperties: true,
138
+ examples: [sortItemExample],
139
+ };
140
+ const commonFilterSchemaDefs = {
141
+ singleFilter: filterConditionSchema,
142
+ filterGroup: filterGroupSchema,
143
+ filterNode: {
144
+ title: 'AdminForthFilterNode',
145
+ description: 'Either a single filter condition or a nested filter group.',
146
+ anyOf: [
147
+ { $ref: '#/$defs/singleFilter' },
148
+ { $ref: '#/$defs/filterGroup' },
149
+ ],
150
+ examples: [filterConditionExample, filterGroupExample],
151
+ },
152
+ sortItem: sortItemSchema,
153
+ };
154
+ const commonSortSchema = {
155
+ title: 'AdminForthSortList',
156
+ description: 'Ordered list of sort instructions.',
157
+ type: 'array',
158
+ items: { $ref: '#/$defs/sortItem' },
159
+ examples: [[sortItemExample]],
160
+ };
161
+ const commonFiltersSchema = {
162
+ title: 'AdminForthFilterInput',
163
+ description: 'Runtime accepts either a single filter node or an array of filter nodes. The OpenAPI document normalizes this to the array form for readability.',
164
+ oneOf: [
165
+ {
166
+ type: 'array',
167
+ items: { $ref: '#/$defs/filterNode' },
168
+ },
169
+ { $ref: '#/$defs/filterNode' },
170
+ ],
171
+ };
172
+ function createErrorOrSuccessSchema(successSchema) {
173
+ return {
174
+ anyOf: [
175
+ errorResponseSchema,
176
+ successSchema,
177
+ ],
178
+ };
179
+ }
180
+ const getResourceDataRequestSchema = {
181
+ type: 'object',
182
+ $defs: commonFilterSchemaDefs,
183
+ required: ['resourceId', 'source', 'limit', 'offset', 'filters', 'sort'],
184
+ properties: {
185
+ resourceId: { type: 'string' },
186
+ source: {
187
+ type: 'string',
188
+ enum: ['show', 'list', 'edit'],
189
+ description: 'Target UI context. Show and edit requests should use direct field filters that identify a single record.',
190
+ },
191
+ limit: {
192
+ type: 'integer',
193
+ description: 'Maximum number of rows to return for the current page.',
194
+ },
195
+ offset: {
196
+ type: 'integer',
197
+ description: 'Zero-based row offset used for pagination.',
198
+ },
199
+ sort: commonSortSchema,
200
+ filters: commonFiltersSchema,
201
+ },
202
+ additionalProperties: true,
203
+ allOf: [
204
+ {
205
+ if: {
206
+ properties: {
207
+ source: { enum: ['show', 'edit'] },
208
+ },
209
+ required: ['source'],
210
+ },
211
+ then: {
212
+ properties: {
213
+ filters: {
214
+ type: 'array',
215
+ items: {
216
+ allOf: [
217
+ { $ref: '#/$defs/singleFilter' },
218
+ {
219
+ type: 'object',
220
+ required: ['field'],
221
+ },
222
+ ],
223
+ },
224
+ },
225
+ },
226
+ },
227
+ },
228
+ ],
229
+ };
230
+ const getResourceDataResponseSchema = createErrorOrSuccessSchema({
231
+ type: 'object',
232
+ required: ['data'],
233
+ properties: {
234
+ data: {
235
+ type: 'array',
236
+ items: genericObjectSchema,
237
+ },
238
+ total: { type: 'number' },
239
+ options: genericObjectSchema,
240
+ },
241
+ additionalProperties: true,
242
+ });
243
+ const getMenuBadgesResponseSchema = {
244
+ type: 'object',
245
+ additionalProperties: {
246
+ anyOf: [
247
+ { type: 'string' },
248
+ { type: 'number' },
249
+ ],
250
+ },
251
+ };
252
+ const getResourceRequestSchema = {
253
+ type: 'object',
254
+ required: ['resourceId'],
255
+ properties: {
256
+ resourceId: { type: 'string' },
257
+ },
258
+ additionalProperties: true,
259
+ };
260
+ const getResourceResponseSchema = createErrorOrSuccessSchema({
261
+ type: 'object',
262
+ required: ['resource'],
263
+ properties: {
264
+ resource: genericObjectSchema,
265
+ },
266
+ additionalProperties: true,
267
+ });
268
+ const getResourceForeignDataRequestSchema = {
269
+ type: 'object',
270
+ $defs: commonFilterSchemaDefs,
271
+ required: ['resourceId', 'column', 'limit', 'offset'],
272
+ properties: {
273
+ resourceId: { type: 'string' },
274
+ column: { type: 'string' },
275
+ limit: {
276
+ type: 'integer',
277
+ description: 'Maximum number of dropdown options to return.',
278
+ },
279
+ offset: {
280
+ type: 'integer',
281
+ description: 'Zero-based offset used to fetch the next option page.',
282
+ },
283
+ search: { type: 'string' },
284
+ filters: commonFiltersSchema,
285
+ sort: commonSortSchema,
286
+ },
287
+ additionalProperties: true,
288
+ };
289
+ const getResourceForeignDataResponseSchema = createErrorOrSuccessSchema({
290
+ type: 'object',
291
+ required: ['items'],
292
+ properties: {
293
+ items: {
294
+ type: 'array',
295
+ items: {
296
+ type: 'object',
297
+ required: ['value', 'label'],
298
+ properties: {
299
+ value: {},
300
+ label: { type: 'string' },
301
+ },
302
+ additionalProperties: true,
303
+ },
304
+ },
305
+ },
306
+ additionalProperties: true,
307
+ });
308
+ const getMinMaxForColumnsRequestSchema = {
309
+ type: 'object',
310
+ required: ['resourceId'],
311
+ properties: {
312
+ resourceId: { type: 'string' },
313
+ },
314
+ additionalProperties: true,
315
+ };
316
+ const getMinMaxForColumnsResponseSchema = createErrorOrSuccessSchema({
317
+ type: 'object',
318
+ additionalProperties: {
319
+ type: 'object',
320
+ required: ['min', 'max'],
321
+ properties: {
322
+ min: {},
323
+ max: {},
324
+ },
325
+ additionalProperties: true,
326
+ },
327
+ });
328
+ const createRecordRequestSchema = {
329
+ type: 'object',
330
+ required: ['resourceId', 'record', 'requiredColumnsToSkip'],
331
+ properties: {
332
+ resourceId: { type: 'string' },
333
+ record: genericObjectSchema,
334
+ requiredColumnsToSkip: {
335
+ type: 'array',
336
+ items: namedColumnSchema,
337
+ },
338
+ meta: genericObjectSchema,
339
+ },
340
+ additionalProperties: true,
341
+ };
342
+ const createRecordResponseSchema = createErrorOrSuccessSchema({
343
+ type: 'object',
344
+ required: ['ok', 'newRecordId', 'redirectToRecordId'],
345
+ properties: {
346
+ ok: { const: true },
347
+ newRecordId: recordIdentifierSchema,
348
+ redirectToRecordId: recordIdentifierSchema,
349
+ },
350
+ additionalProperties: true,
351
+ });
352
+ const updateRecordRequestSchema = {
353
+ type: 'object',
354
+ required: ['resourceId', 'recordId', 'record'],
355
+ properties: {
356
+ resourceId: { type: 'string' },
357
+ recordId: recordIdentifierSchema,
358
+ record: genericObjectSchema,
359
+ meta: genericObjectSchema,
360
+ },
361
+ additionalProperties: true,
362
+ };
363
+ const updateRecordResponseSchema = createErrorOrSuccessSchema({
364
+ type: 'object',
365
+ required: ['ok'],
366
+ properties: {
367
+ ok: { const: true },
368
+ recordId: recordIdentifierSchema,
369
+ },
370
+ additionalProperties: true,
371
+ });
372
+ const deleteRecordRequestSchema = {
373
+ type: 'object',
374
+ required: ['resourceId', 'primaryKey'],
375
+ properties: {
376
+ resourceId: { type: 'string' },
377
+ primaryKey: recordIdentifierSchema,
378
+ },
379
+ additionalProperties: true,
380
+ };
381
+ const deleteRecordResponseSchema = createErrorOrSuccessSchema({
382
+ type: 'object',
383
+ required: ['ok', 'recordId'],
384
+ properties: {
385
+ ok: { const: true },
386
+ recordId: recordIdentifierSchema,
387
+ },
388
+ additionalProperties: true,
389
+ });
390
+ const startCustomActionRequestSchema = {
391
+ type: 'object',
392
+ required: ['resourceId', 'actionId', 'recordId'],
393
+ properties: {
394
+ resourceId: { type: 'string' },
395
+ actionId: actionIdentifierSchema,
396
+ recordId: recordIdentifierSchema,
397
+ extra: genericObjectSchema,
398
+ },
399
+ additionalProperties: true,
400
+ };
401
+ const startCustomActionResponseSchema = {
402
+ anyOf: [
403
+ errorResponseSchema,
404
+ {
405
+ type: 'object',
406
+ required: ['actionId', 'resourceId', 'recordId', 'redirectUrl'],
407
+ properties: {
408
+ actionId: actionIdentifierSchema,
409
+ resourceId: { type: 'string' },
410
+ recordId: recordIdentifierSchema,
411
+ redirectUrl: { type: 'string' },
412
+ },
413
+ additionalProperties: true,
414
+ },
415
+ {
416
+ type: 'object',
417
+ required: ['actionId', 'resourceId', 'recordId', 'ok'],
418
+ properties: {
419
+ actionId: actionIdentifierSchema,
420
+ resourceId: { type: 'string' },
421
+ recordId: recordIdentifierSchema,
422
+ ok: { const: true },
423
+ },
424
+ additionalProperties: true,
425
+ },
426
+ ],
427
+ };
428
+ const startCustomBulkActionRequestSchema = {
429
+ type: 'object',
430
+ required: ['resourceId', 'actionId', 'recordIds'],
431
+ properties: {
432
+ resourceId: { type: 'string' },
433
+ actionId: actionIdentifierSchema,
434
+ recordIds: {
435
+ type: 'array',
436
+ items: recordIdentifierSchema,
437
+ },
438
+ extra: genericObjectSchema,
439
+ },
440
+ additionalProperties: true,
441
+ };
442
+ const startCustomBulkActionResponseSchema = createErrorOrSuccessSchema({
443
+ type: 'object',
444
+ required: ['actionId', 'resourceId', 'recordIds', 'ok'],
445
+ properties: {
446
+ actionId: actionIdentifierSchema,
447
+ resourceId: { type: 'string' },
448
+ recordIds: {
449
+ type: 'array',
450
+ items: recordIdentifierSchema,
451
+ },
452
+ ok: { const: true },
453
+ },
454
+ additionalProperties: true,
455
+ });
456
+ const validateColumnsRequestSchema = {
457
+ type: 'object',
458
+ required: ['resourceId', 'editableColumns', 'record'],
459
+ properties: {
460
+ resourceId: { type: 'string' },
461
+ editableColumns: {
462
+ type: 'array',
463
+ items: {
464
+ type: 'object',
465
+ required: ['name'],
466
+ properties: {
467
+ name: { type: 'string' },
468
+ value: {},
469
+ },
470
+ additionalProperties: true,
471
+ },
472
+ },
473
+ record: genericObjectSchema,
474
+ },
475
+ additionalProperties: true,
476
+ };
477
+ const validateColumnsResponseSchema = createErrorOrSuccessSchema({
478
+ type: 'object',
479
+ required: ['validationResults'],
480
+ properties: {
481
+ validationResults: {
482
+ type: 'object',
483
+ additionalProperties: validationResultSchema,
484
+ },
485
+ },
486
+ additionalProperties: true,
487
+ });
29
488
  export async function interpretResource(adminUser, resource, meta, source, adminforth) {
30
489
  afLogger.trace(`🪲Interpreting resource, ${resource.resourceId}, ${source}, 'adminUser', ${adminUser}`);
31
490
  const allowedActions = {};
@@ -182,7 +641,7 @@ export default class AdminForthRestAPI {
182
641
  handler: async ({ tr }) => {
183
642
  const loginPromptHTML = await getLoginPromptHTML(this.adminforth.config.auth.loginPromptHTML);
184
643
  return {
185
- loginPromptHTML: await tr(loginPromptHTML, 'system.loginPromptHTML'),
644
+ loginPromptHTML: loginPromptHTML ? await tr(loginPromptHTML, 'system.loginPromptHTML') : null,
186
645
  };
187
646
  }
188
647
  });
@@ -222,7 +681,7 @@ export default class AdminForthRestAPI {
222
681
  server.endpoint({
223
682
  method: 'GET',
224
683
  path: '/get_base_config',
225
- handler: async ({ input, adminUser, cookies, tr, response }) => {
684
+ handler: async ({ adminUser, cookies, tr, response }) => {
226
685
  var _a, _b, _c, _d, _e;
227
686
  let username = '';
228
687
  let userFullName = '';
@@ -385,14 +844,17 @@ export default class AdminForthRestAPI {
385
844
  server.endpoint({
386
845
  method: 'GET',
387
846
  path: '/get_menu_badges',
847
+ 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.',
848
+ response_schema: getMenuBadgesResponseSchema,
388
849
  handler: async ({ adminUser }) => {
389
850
  const badges = {};
390
851
  const badgeFunctions = [];
852
+ const adminforth = this.adminforth;
391
853
  function processMenuItem(menuItem) {
392
854
  if (menuItem.badge) {
393
855
  if (typeof menuItem.badge === 'function') {
394
856
  badgeFunctions.push(async () => {
395
- badges[menuItem.itemId] = await menuItem.badge(adminUser);
857
+ badges[menuItem.itemId] = await menuItem.badge(adminUser, adminforth);
396
858
  });
397
859
  }
398
860
  else {
@@ -420,8 +882,11 @@ export default class AdminForthRestAPI {
420
882
  server.endpoint({
421
883
  method: 'POST',
422
884
  path: '/get_resource',
885
+ 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.',
886
+ request_schema: getResourceRequestSchema,
887
+ response_schema: getResourceResponseSchema,
423
888
  handler: async ({ body, adminUser, tr }) => {
424
- var _a;
889
+ var _a, _b;
425
890
  const { resourceId } = body;
426
891
  if (!this.adminforth.statuses.dbDiscover) {
427
892
  return { error: 'Database discovery not started' };
@@ -476,8 +941,8 @@ export default class AdminForthRestAPI {
476
941
  const col = JSON.parse(JSON.stringify(inCol));
477
942
  let validation = null;
478
943
  if (col.validation) {
479
- validation = await Promise.all(col.validation.map(async (val) => {
480
- return Object.assign(Object.assign({}, val), { message: await tr(val.message, `resource.${resource.resourceId}`) });
944
+ validation = await Promise.all(col.validation.map(async (val, index) => {
945
+ return Object.assign(Object.assign({}, val), { validator: inCol.validation[index].validator ? true : false, message: await tr(val.message, `resource.${resource.resourceId}`) });
481
946
  }));
482
947
  }
483
948
  let enumItems = undefined;
@@ -507,14 +972,14 @@ export default class AdminForthRestAPI {
507
972
  col.foreignResource.unsetLabel = await tr(col.foreignResource.unsetLabel, `resource.${resource.resourceId}.foreignResource.unsetLabel`);
508
973
  }
509
974
  if (inCol.suggestOnCreate && typeof inCol.suggestOnCreate === 'function') {
510
- col.suggestOnCreate = await inCol.suggestOnCreate(adminUser);
975
+ col.suggestOnCreate = await inCol.suggestOnCreate({ adminUser });
511
976
  }
512
977
  return Object.assign(Object.assign({}, col), { showIn,
513
978
  validation, label: translated[`resCol${i}`], enum: enumItems });
514
979
  })), options: Object.assign(Object.assign({}, resource.options), { fieldGroups: (_a = resource.options.fieldGroups) === null || _a === void 0 ? void 0 : _a.map((group, i) => {
515
980
  var _a;
516
981
  return (Object.assign(Object.assign({}, group), { noTitle: (_a = group.noTitle) !== null && _a !== void 0 ? _a : false, groupName: translated[`fieldGroup${i}`] || group.groupName }));
517
- }), bulkActions: allowedBulkActions.map((action, i) => (Object.assign(Object.assign({}, action), { label: action.label ? translated[`bulkAction${i}`] : action.label, confirm: action.confirm ? translated[`bulkActionConfirm${i}`] : action.confirm }))), allowedActions }) });
982
+ }), bulkActions: allowedBulkActions.map((action, i) => (Object.assign(Object.assign({}, action), { label: action.label ? translated[`bulkAction${i}`] : action.label, confirm: action.confirm ? translated[`bulkActionConfirm${i}`] : action.confirm }))), actions: (_b = resource.options.actions) === null || _b === void 0 ? void 0 : _b.map((action) => (Object.assign(Object.assign({}, action), { hasBulkHandler: !!action.bulkHandler, bulkHandler: undefined }))), allowedActions }) });
518
983
  delete toReturn.hooks;
519
984
  delete toReturn.plugins;
520
985
  return {
@@ -525,6 +990,9 @@ export default class AdminForthRestAPI {
525
990
  server.endpoint({
526
991
  method: 'POST',
527
992
  path: '/get_resource_data',
993
+ 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.',
994
+ request_schema: getResourceDataRequestSchema,
995
+ response_schema: getResourceDataResponseSchema,
528
996
  handler: async ({ body, adminUser, headers, query, cookies, requestUrl, abortSignal }) => {
529
997
  var _a, _b, _c, _d, _e, _f;
530
998
  const { resourceId, source } = body;
@@ -792,6 +1260,9 @@ export default class AdminForthRestAPI {
792
1260
  server.endpoint({
793
1261
  method: 'POST',
794
1262
  path: '/get_resource_foreign_data',
1263
+ 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.',
1264
+ request_schema: getResourceForeignDataRequestSchema,
1265
+ response_schema: getResourceForeignDataResponseSchema,
795
1266
  handler: async ({ body, adminUser, headers, query, cookies, requestUrl }) => {
796
1267
  const { resourceId, column, search } = body;
797
1268
  if (!this.adminforth.statuses.dbDiscover) {
@@ -952,6 +1423,9 @@ export default class AdminForthRestAPI {
952
1423
  server.endpoint({
953
1424
  method: 'POST',
954
1425
  path: '/get_min_max_for_columns',
1426
+ 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.',
1427
+ request_schema: getMinMaxForColumnsRequestSchema,
1428
+ response_schema: getMinMaxForColumnsResponseSchema,
955
1429
  handler: async ({ body }) => {
956
1430
  const { resourceId } = body;
957
1431
  if (!this.adminforth.statuses.dbDiscover) {
@@ -981,6 +1455,9 @@ export default class AdminForthRestAPI {
981
1455
  server.endpoint({
982
1456
  method: 'POST',
983
1457
  path: '/create_record',
1458
+ 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.',
1459
+ request_schema: createRecordRequestSchema,
1460
+ response_schema: createRecordResponseSchema,
984
1461
  handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
985
1462
  var _a, _b, _c, _d;
986
1463
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
@@ -1110,6 +1587,9 @@ export default class AdminForthRestAPI {
1110
1587
  server.endpoint({
1111
1588
  method: 'POST',
1112
1589
  path: '/update_record',
1590
+ 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.',
1591
+ request_schema: updateRecordRequestSchema,
1592
+ response_schema: updateRecordResponseSchema,
1113
1593
  handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
1114
1594
  var _a, _b;
1115
1595
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
@@ -1228,6 +1708,9 @@ export default class AdminForthRestAPI {
1228
1708
  server.endpoint({
1229
1709
  method: 'POST',
1230
1710
  path: '/delete_record',
1711
+ 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.',
1712
+ request_schema: deleteRecordRequestSchema,
1713
+ response_schema: deleteRecordResponseSchema,
1231
1714
  handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
1232
1715
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
1233
1716
  if (!resource) {
@@ -1288,6 +1771,9 @@ export default class AdminForthRestAPI {
1288
1771
  server.endpoint({
1289
1772
  method: 'POST',
1290
1773
  path: '/start_custom_action',
1774
+ 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.',
1775
+ request_schema: startCustomActionRequestSchema,
1776
+ response_schema: startCustomActionResponseSchema,
1291
1777
  handler: async ({ body, adminUser, tr, cookies, response, headers }) => {
1292
1778
  const { resourceId, actionId, recordId, extra } = body;
1293
1779
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
@@ -1319,6 +1805,85 @@ export default class AdminForthRestAPI {
1319
1805
  resourceId }, actionResponse);
1320
1806
  }
1321
1807
  });
1808
+ server.endpoint({
1809
+ method: 'POST',
1810
+ path: '/start_custom_bulk_action',
1811
+ 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.',
1812
+ request_schema: startCustomBulkActionRequestSchema,
1813
+ response_schema: startCustomBulkActionResponseSchema,
1814
+ handler: async ({ body, adminUser, tr, response, cookies, headers }) => {
1815
+ const { resourceId, actionId, recordIds, extra } = body;
1816
+ const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
1817
+ if (!resource) {
1818
+ return { error: await tr(`Resource {resourceId} not found`, 'errors', { resourceId }) };
1819
+ }
1820
+ const { allowedActions } = await interpretResource(adminUser, resource, { requestBody: body }, ActionCheckSource.CustomActionRequest, this.adminforth);
1821
+ const action = resource.options.actions.find((act) => act.id == actionId);
1822
+ if (!action) {
1823
+ return { error: await tr(`Action {actionId} not found`, 'errors', { actionId }) };
1824
+ }
1825
+ if (!action.bulkHandler) {
1826
+ return { error: await tr(`Action "{actionId}" has no bulkHandler`, 'errors', { actionId }) };
1827
+ }
1828
+ if (action.allowed) {
1829
+ const execAllowed = await action.allowed({ adminUser, standardAllowedActions: allowedActions });
1830
+ if (!execAllowed) {
1831
+ return { error: await tr(`Action "{actionId}" not allowed`, 'errors', { actionId: action.name }) };
1832
+ }
1833
+ }
1834
+ const result = await action.bulkHandler({
1835
+ recordIds,
1836
+ adminUser,
1837
+ resource,
1838
+ tr,
1839
+ adminforth: this.adminforth,
1840
+ response,
1841
+ extra: Object.assign(Object.assign({}, extra), { cookies, headers }),
1842
+ });
1843
+ return Object.assign({ actionId, recordIds, resourceId }, result);
1844
+ }
1845
+ });
1846
+ server.endpoint({
1847
+ method: 'POST',
1848
+ path: '/validate_columns',
1849
+ 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.',
1850
+ request_schema: validateColumnsRequestSchema,
1851
+ response_schema: validateColumnsResponseSchema,
1852
+ handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
1853
+ const { resourceId, editableColumns, record } = body;
1854
+ const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
1855
+ if (!resource) {
1856
+ return { error: `Resource '${resourceId}' not found` };
1857
+ }
1858
+ const validationResults = {};
1859
+ const customColumnValidatorsFunctions = [];
1860
+ for (const col of editableColumns) {
1861
+ const columnConfig = resource.columns.find((c) => c.name === col.name);
1862
+ if (columnConfig && columnConfig.validation) {
1863
+ customColumnValidatorsFunctions.push(async () => {
1864
+ for (const val of columnConfig.validation) {
1865
+ if (val.validator) {
1866
+ const result = await val.validator(col.value, record, this.adminforth);
1867
+ if (typeof result === 'object' && result.isValid === false) {
1868
+ validationResults[col.name] = {
1869
+ isValid: result.isValid,
1870
+ message: result.message,
1871
+ };
1872
+ break;
1873
+ }
1874
+ }
1875
+ }
1876
+ });
1877
+ }
1878
+ }
1879
+ if (customColumnValidatorsFunctions.length) {
1880
+ await Promise.all(customColumnValidatorsFunctions.map((fn) => fn()));
1881
+ }
1882
+ return {
1883
+ validationResults
1884
+ };
1885
+ }
1886
+ });
1322
1887
  // setup endpoints for all plugins
1323
1888
  this.adminforth.activatedPlugins.forEach((plugin) => {
1324
1889
  plugin.setupEndpoints(server);