adminforth 1.2.1 → 1.2.3

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.
@@ -0,0 +1,709 @@
1
+ import {
2
+ type IAdminForth,
3
+ type IHttpServer, type AdminUser,
4
+ AdminForthFilterOperators,
5
+ AdminForthDataTypes,
6
+ BeforeLoginConfirmationFunction,
7
+ AdminForthResource,
8
+ AllowedActionValue,
9
+ AllowedActionsEnum,
10
+ AllowedActions,
11
+ ActionCheckSource,
12
+ BeforeSaveFunction,
13
+ AfterDataSourceResponseFunction,
14
+ BeforeDataSourceRequestFunction,
15
+ AfterSaveFunction
16
+
17
+ } from "../types/AdminForthConfig.js";
18
+
19
+ import { ADMINFORTH_VERSION, listify } from './utils.js';
20
+
21
+ import AdminForthAuth from "../auth.js";
22
+
23
+ export default class AdminForthRestAPI {
24
+
25
+ adminforth: IAdminForth;
26
+
27
+ constructor(adminforth: IAdminForth) {
28
+ this.adminforth = adminforth;
29
+ }
30
+
31
+ registerEndpoints(server: IHttpServer) {
32
+ server.endpoint({
33
+ noAuth: true,
34
+ method: 'POST',
35
+ path: '/login',
36
+ handler: async ({ body, response }) => {
37
+
38
+ const INVALID_MESSAGE = 'Invalid username or password';
39
+ const { username, password } = body;
40
+ let adminUser: AdminUser;
41
+ let toReturn: { ok: boolean, redirectTo?: string, allowedLogin:boolean } = { ok: true, allowedLogin:true};
42
+
43
+ let token;
44
+ if (username === this.adminforth.config.rootUser.username && password === this.adminforth.config.rootUser.password) {
45
+ this.adminforth.auth.setAuthCookie({ response, username, pk: null });
46
+ adminUser = { isRoot: true, dbUser: null, pk: null, username: this.adminforth.config.rootUser.username};
47
+ } else {
48
+ // get resource from db
49
+ if (!this.adminforth.config.auth) {
50
+ throw new Error('No config.auth defined');
51
+ }
52
+ const userResource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.resourceId);
53
+ // if there is no passwordHashField, in columns, add it, with backendOnly and showIn: []
54
+ if (!userResource.dataSourceColumns.find((col) => col.name === this.adminforth.config.auth.passwordHashField)) {
55
+ userResource.dataSourceColumns.push({
56
+ name: this.adminforth.config.auth.passwordHashField,
57
+ backendOnly: true,
58
+ showIn: [],
59
+ type: AdminForthDataTypes.STRING,
60
+ });
61
+ console.log('Adding passwordHashField to userResource', userResource)
62
+ }
63
+
64
+ const userRecord = (
65
+ await this.adminforth.connectors[userResource.dataSource].getData({
66
+ resource: userResource,
67
+ filters: [
68
+ { field: this.adminforth.config.auth.usernameField, operator: AdminForthFilterOperators.EQ, value: username },
69
+ ],
70
+ limit: 1,
71
+ offset: 0,
72
+ sort: [],
73
+ })
74
+ ).data?.[0];
75
+
76
+ if (!userRecord) {
77
+ return { error: 'User not found' };
78
+ }
79
+
80
+ const passwordHash = userRecord[this.adminforth.config.auth.passwordHashField];
81
+ const valid = await AdminForthAuth.verifyPassword(password, passwordHash);
82
+ if (valid) {
83
+ adminUser = {
84
+ isRoot: false, dbUser: userRecord,
85
+ pk: userRecord[userResource.columns.find((col) => col.primaryKey).name],
86
+ username,
87
+ };
88
+ const beforeLoginConfirmation = this.adminforth.config.auth.beforeLoginConfirmation as (BeforeLoginConfirmationFunction[] | undefined);
89
+ if (beforeLoginConfirmation?.length){
90
+ for (const hook of beforeLoginConfirmation) {
91
+ const resp = await hook({ adminUser, response });
92
+
93
+ if (resp?.body?.redirectTo) {
94
+ toReturn = {ok:resp.ok, redirectTo:resp?.body?.redirectTo, allowedLogin:resp?.body?.allowedLogin};
95
+ break;
96
+ }
97
+ }
98
+ }
99
+ if (toReturn.allowedLogin){
100
+ this.adminforth.auth.setAuthCookie({ response, username, pk: userRecord[userResource.columns.find((col) => col.primaryKey).name] });
101
+ }
102
+ } else {
103
+ return { error: INVALID_MESSAGE };
104
+ }
105
+
106
+ }
107
+
108
+ return toReturn;
109
+ }
110
+ });
111
+
112
+ server.endpoint({
113
+ method: 'POST',
114
+ path: '/check_auth',
115
+ handler: async ({ adminUser }) => {
116
+ return { ok: true };
117
+ },
118
+ });
119
+
120
+ server.endpoint({
121
+ noAuth: true,
122
+ method: 'POST',
123
+ path: '/logout',
124
+ handler: async ({ response }) => {
125
+ this.adminforth.auth.removeAuthCookie( response );
126
+ return { ok: true };
127
+ },
128
+ })
129
+
130
+ server.endpoint({
131
+ noAuth: true,
132
+ method: 'GET',
133
+ path: '/get_public_config',
134
+ handler: async ({ body }) => {
135
+
136
+ // find resource
137
+ if (!this.adminforth.config.auth) {
138
+ throw new Error('No config.auth defined');
139
+ }
140
+ const usernameField = this.adminforth.config.auth.usernameField;
141
+ const resource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.resourceId);
142
+ const usernameColumn = resource.columns.find((col) => col.name === usernameField);
143
+
144
+ return {
145
+ brandName: this.adminforth.config.customization.brandName,
146
+ usernameFieldName: usernameColumn.label,
147
+ loginBackgroundImage: this.adminforth.config.auth.loginBackgroundImage,
148
+ title: this.adminforth.config.customization?.title,
149
+ };
150
+ },
151
+ });
152
+
153
+ server.endpoint({
154
+ method: 'GET',
155
+ path: '/get_base_config',
156
+ handler: async ({input, adminUser, cookies}) => {
157
+ let username = ''
158
+ let userFullName = ''
159
+ if (adminUser.isRoot) {
160
+ username = this.adminforth.config.rootUser.username;
161
+ } else {
162
+ const dbUser = adminUser.dbUser;
163
+ username = dbUser[this.adminforth.config.auth.usernameField];
164
+ userFullName =dbUser[this.adminforth.config.auth.userFullNameField];
165
+ }
166
+
167
+ const userData = {
168
+ [this.adminforth.config.auth.usernameField]: username,
169
+ [this.adminforth.config.auth.userFullNameField]: userFullName
170
+ };
171
+ const checkIsMenuItemVisible = (menuItem) => {
172
+ if (typeof menuItem.visible === 'function') {
173
+ const toReturn = menuItem.visible( adminUser );
174
+ if (typeof toReturn !== 'boolean') {
175
+ throw new Error(`'visible' function of ${menuItem.label || menuItem.type } must return boolean value`);
176
+ }
177
+ return toReturn;
178
+ }}
179
+ let newMenu = []
180
+ for (let menuItem of this.adminforth.config.menu) {
181
+ let newMenuItem = {...menuItem,}
182
+ if (menuItem.visible){
183
+ if (!checkIsMenuItemVisible(menuItem)){
184
+ continue
185
+ }
186
+ }
187
+ if (menuItem.children){
188
+ let newChildren = []
189
+ for (let child of menuItem.children){
190
+ let newChild = {...child,}
191
+ if (child.visible){
192
+ if (!checkIsMenuItemVisible(child)){
193
+ continue
194
+ }
195
+ }
196
+ newChildren.push(newChild)
197
+ }
198
+ newMenuItem = {...newMenuItem, children: newChildren}
199
+ }
200
+ newMenu.push(newMenuItem)
201
+ }
202
+
203
+ return {
204
+ user: userData,
205
+ resources: this.adminforth.config.resources.map((res) => ({
206
+ resourceId: res.resourceId,
207
+ label: res.label,
208
+ })),
209
+ menu: newMenu,
210
+ config: {
211
+ brandName: this.adminforth.config.customization.brandName,
212
+ brandLogo: this.adminforth.config.customization.brandLogo,
213
+ datesFormat: this.adminforth.config.customization.datesFormat,
214
+ deleteConfirmation: this.adminforth.config.deleteConfirmation,
215
+ auth: this.adminforth.config.auth,
216
+ usernameField: this.adminforth.config.auth.usernameField,
217
+ title: this.adminforth.config.customization?.title,
218
+ emptyFieldPlaceholder: this.adminforth.config.customization?.emptyFieldPlaceholder,
219
+ },
220
+ adminUser,
221
+ version: ADMINFORTH_VERSION,
222
+ };
223
+ },
224
+ });
225
+
226
+ async function interpretResource(adminUser: AdminUser, resource: AdminForthResource, meta: any, source: ActionCheckSource): Promise<{allowedActions: AllowedActions}> {
227
+ if (process.env.HEAVY_DEBUG) {
228
+ console.log('🪲Interpreting resource', resource.resourceId, source);
229
+ }
230
+ const allowedActions = {};
231
+
232
+ await Promise.all(
233
+ Object.entries(resource.options?.allowedActions || {}).map(
234
+ async ([key, value]: [string, AllowedActionValue]) => {
235
+ if (process.env.HEAVY_DEBUG) {
236
+ console.log('🪲checking for allowed call', key, 'value:', value, 'typeof', typeof value);
237
+ }
238
+
239
+ // if callable then call
240
+ if (typeof value === 'function') {
241
+ allowedActions[key] = await value({ adminUser, resource, meta, source });
242
+ } else {
243
+ allowedActions[key] = value;
244
+ }
245
+ })
246
+ );
247
+
248
+ return { allowedActions };
249
+ }
250
+
251
+ function checkAccess(action: AllowedActionsEnum, allowedActions: AllowedActions): { allowed: boolean, error?: string } {
252
+ const allowed = (allowedActions[action] as boolean | string | undefined);
253
+ if (allowed !== true) {
254
+ return { error: typeof allowed === 'string' ? allowed : 'Action is not allowed', allowed: false };
255
+ }
256
+ return { allowed: true };
257
+ }
258
+
259
+ server.endpoint({
260
+ method: 'POST',
261
+ path: '/get_resource',
262
+ handler: async ({ body, adminUser }) => {
263
+ const { resourceId } = body;
264
+ if (!this.adminforth.statuses.dbDiscover) {
265
+ return { error: 'Database discovery not started' };
266
+ }
267
+ if (this.adminforth.statuses.dbDiscover !== 'done') {
268
+ return { error : 'Database discovery is still in progress, please try later' };
269
+ }
270
+ const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
271
+ if (!resource) {
272
+ return { error: `Resource ${resourceId} not found` };
273
+ }
274
+
275
+ const { allowedActions } = await interpretResource(adminUser, resource, {}, ActionCheckSource.DisplayButtons);
276
+
277
+ // exclude "plugins" key
278
+ return {
279
+ resource: {
280
+ ...resource,
281
+ plugins: undefined,
282
+ options: {
283
+ ...resource.options,
284
+ allowedActions,
285
+ }
286
+ }
287
+ };
288
+ },
289
+ });
290
+ server.endpoint({
291
+ method: 'POST',
292
+ path: '/get_resource_data',
293
+ handler: async ({ body, adminUser }) => {
294
+ const { resourceId, source } = body;
295
+ if (['show', 'list'].includes(source) === false) {
296
+ return { error: 'Invalid source, should be list or show' };
297
+ }
298
+ if (!this.adminforth.statuses.dbDiscover) {
299
+ return { error: 'Database discovery not started' };
300
+ }
301
+ if (this.adminforth.statuses.dbDiscover !== 'done') {
302
+ return { error : 'Database discovery is still in progress, please try later' };
303
+ }
304
+ const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
305
+ if (!resource) {
306
+ return { error: `Resource ${resourceId} not found` };
307
+ }
308
+
309
+ const { allowedActions } = await interpretResource(adminUser, resource, { requestBody: body }, ActionCheckSource.DisplayButtons);
310
+
311
+ const { allowed, error } = checkAccess(source as AllowedActionsEnum, allowedActions);
312
+ if (!allowed) {
313
+ return { error };
314
+ }
315
+
316
+ for (const hook of listify(resource.hooks?.[source]?.beforeDatasourceRequest)) {
317
+ const resp = await hook({ resource, query: body, adminUser });
318
+ if (!resp || (!resp.ok && !resp.error)) {
319
+ throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
320
+ }
321
+
322
+ if (resp.error) {
323
+ return { error: resp.error };
324
+ }
325
+ }
326
+ const { limit, offset, filters, sort } = body;
327
+
328
+
329
+
330
+ for (const filter of (filters || [])) {
331
+ if (!Object.values(AdminForthFilterOperators).includes(filter.operator)) {
332
+ throw new Error(`Operator '${filter.operator}' is not allowed`);
333
+ }
334
+
335
+ if (!resource.columns.some((col) => col.name === filter.field)) {
336
+ throw new Error(`Field '${filter.field}' is not in resource '${resource.resourceId}'. Available fields: ${resource.columns.map((col) => col.name).join(', ')}`);
337
+ }
338
+
339
+ if (filter.operator === AdminForthFilterOperators.IN || filter.operator === AdminForthFilterOperators.NIN) {
340
+ if (!Array.isArray(filter.value)) {
341
+ throw new Error(`Value for operator '${filter.operator}' should be an array`);
342
+ }
343
+ }
344
+
345
+ if (filter.operator === AdminForthFilterOperators.IN && filter.value.length === 0) {
346
+ // nonsense
347
+ return { data: [], total: 0 };
348
+ }
349
+ }
350
+
351
+ const data = await this.adminforth.connectors[resource.dataSource].getData({
352
+ resource,
353
+ limit,
354
+ offset,
355
+ filters,
356
+ sort,
357
+ });
358
+ // for foreign keys, add references
359
+ await Promise.all(
360
+ resource.columns.filter((col) => col.foreignResource).map(async (col) => {
361
+ const targetResource = this.adminforth.config.resources.find((res) => res.resourceId == col.foreignResource.resourceId);
362
+ const targetConnector = this.adminforth.connectors[targetResource.dataSource];
363
+ const targetResourcePkField = targetResource.columns.find((col) => col.primaryKey).name;
364
+ const pksUnique = [...new Set(data.data.map((item) => item[col.name]))];
365
+ if (pksUnique.length === 0) {
366
+ return;
367
+ }
368
+ const targetData = await targetConnector.getData({
369
+ resource: targetResource,
370
+ limit: limit,
371
+ offset: 0,
372
+ filters: [
373
+ {
374
+ field: targetResourcePkField,
375
+ operator: AdminForthFilterOperators.IN,
376
+ value: pksUnique,
377
+ }
378
+ ],
379
+ sort: [],
380
+ });
381
+ const targetDataMap = targetData.data.reduce((acc, item) => {
382
+ acc[item[targetResourcePkField]] = {
383
+ label: targetResource.recordLabel(item),
384
+ pk: item[targetResourcePkField],
385
+ }
386
+ return acc;
387
+ }, {});
388
+ data.data.forEach((item) => {
389
+ item[col.name] = targetDataMap[item[col.name]];
390
+ });
391
+ })
392
+ );
393
+
394
+ for (const hook of listify(resource.hooks?.[source]?.afterDatasourceResponse)) {
395
+ const resp = await hook({ resource, response: data.data, adminUser });
396
+ if (!resp || (!resp.ok && !resp.error)) {
397
+ throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
398
+ }
399
+
400
+ if (resp.error) {
401
+ return { error: resp.error };
402
+ }
403
+ }
404
+
405
+ // remove all columns which are not defined in resources, or defined but backendOnly
406
+ data.data.forEach((item) => {
407
+ Object.keys(item).forEach((key) => {
408
+ if (!resource.columns.find((col) => col.name === key) || resource.columns.find((col) => col.name === key && col.backendOnly)) {
409
+ delete item[key];
410
+ }
411
+ })
412
+ });
413
+
414
+ data.data.forEach((item) => {
415
+ item._label = resource.recordLabel(item);
416
+ });
417
+
418
+ return {
419
+ ...data,
420
+ options: resource?.options,
421
+ };
422
+ },
423
+ });
424
+ server.endpoint({
425
+ method: 'POST',
426
+ path: '/get_resource_foreign_data',
427
+ handler: async ({ body, adminUser }) => {
428
+ const { resourceId, column } = body;
429
+ if (!this.adminforth.statuses.dbDiscover) {
430
+ return { error: 'Database discovery not started' };
431
+ }
432
+ if (this.adminforth.statuses.dbDiscover !== 'done') {
433
+ return { error : 'Database discovery is still in progress, please try later' };
434
+ }
435
+ const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
436
+ if (!resource) {
437
+ return { error: `Resource '${resourceId}' not found` };
438
+ }
439
+ const columnConfig = resource.columns.find((col) => col.name == column);
440
+ if (!columnConfig) {
441
+ return { error: `Column "${column}' not found in resource with resourceId '${resourceId}'` };
442
+ }
443
+ if (!columnConfig.foreignResource) {
444
+ return { error: `Column '${column}' in resource '${resourceId}' is not a foreign key` };
445
+ }
446
+ const targetResourceId = columnConfig.foreignResource.resourceId;
447
+ const targetResource = this.adminforth.config.resources.find((res) => res.resourceId == targetResourceId);
448
+
449
+ for (const hook of listify(columnConfig.foreignResource.hooks?.dropdownList?.beforeDatasourceRequest as BeforeDataSourceRequestFunction[])) {
450
+ const resp = await hook({ query: body, adminUser, resource: targetResource });
451
+ if (!resp || (!resp.ok && !resp.error)) {
452
+ throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
453
+ }
454
+
455
+ if (resp.error) {
456
+ return { error: resp.error };
457
+ }
458
+ }
459
+ const { limit, offset, filters, sort } = body;
460
+ const dbDataItems = await this.adminforth.connectors[targetResource.dataSource].getData({
461
+ resource: targetResource,
462
+ limit,
463
+ offset,
464
+ filters: filters || [],
465
+ sort: sort || [],
466
+ });
467
+ const items = dbDataItems.data.map((item) => {
468
+ const pk = item[targetResource.columns.find((col) => col.primaryKey).name];
469
+ const labler = targetResource.recordLabel;
470
+ return {
471
+ value: pk,
472
+ label: labler(item),
473
+ _item: item, // user might need it in hook to form new label
474
+ }
475
+ });
476
+ const response = {
477
+ items
478
+ };
479
+
480
+ for (const hook of listify(columnConfig.foreignResource.hooks?.dropdownList?.afterDatasourceResponse as AfterDataSourceResponseFunction[])) {
481
+ const resp = await hook({ response, adminUser, resource: targetResource });
482
+ if (!resp || (!resp.ok && !resp.error)) {
483
+ throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
484
+ }
485
+
486
+ if (resp.error) {
487
+ return { error: resp.error };
488
+ }
489
+ }
490
+
491
+ return response;
492
+ },
493
+ });
494
+
495
+ server.endpoint({
496
+ method: 'POST',
497
+ path: '/get_min_max_for_columns',
498
+ handler: async ({ body }) => {
499
+ const { resourceId } = body;
500
+ if (!this.adminforth.statuses.dbDiscover) {
501
+ return { error: 'Database discovery not started' };
502
+ }
503
+ if (this.adminforth.statuses.dbDiscover !== 'done') {
504
+ return { error : 'Database discovery is still in progress, please try later' };
505
+ }
506
+ const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
507
+ if (!resource) {
508
+ return { error: `Resource '${resourceId}' not found` };
509
+ }
510
+ const item = await this.adminforth.connectors[resource.dataSource].getMinMaxForColumns({
511
+ resource,
512
+ columns: resource.columns.filter((col) => [
513
+ AdminForthDataTypes.INTEGER,
514
+ AdminForthDataTypes.FLOAT,
515
+ AdminForthDataTypes.DATE,
516
+ AdminForthDataTypes.DATETIME,
517
+ AdminForthDataTypes.TIME,
518
+ AdminForthDataTypes.DECIMAL,
519
+ ].includes(col.type) && col.allowMinMaxQuery === true),
520
+ });
521
+ return item;
522
+ },
523
+ });
524
+ server.endpoint({
525
+ method: 'POST',
526
+ path: '/create_record',
527
+ handler: async ({ body, adminUser }) => {
528
+ const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
529
+ if (!resource) {
530
+ return { error: `Resource '${body['resourceId']}' not found` };
531
+ }
532
+ const { allowedActions } = await interpretResource(adminUser, resource, { requestBody: body}, ActionCheckSource.CreateRequest);
533
+
534
+ const { allowed, error } = checkAccess(AllowedActionsEnum.create, allowedActions);
535
+ if (!allowed) {
536
+ return { error };
537
+ }
538
+
539
+ const { record } = body;
540
+
541
+ const response = await this.adminforth.createResourceRecord({ resource, record, adminUser });
542
+ if (response.error) {
543
+ return { error: response.error };
544
+ }
545
+ const connector = this.adminforth.connectors[resource.dataSource];
546
+
547
+ return {
548
+ newRecordId: record[connector.getPrimaryKey(resource)]
549
+ }
550
+ }
551
+ });
552
+ server.endpoint({
553
+ method: 'POST',
554
+ path: '/update_record',
555
+ handler: async ({ body, adminUser }) => {
556
+ const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
557
+ if (!resource) {
558
+ return { error: `Resource '${body['resourceId']}' not found` };
559
+ }
560
+
561
+ const recordId = body['recordId'];
562
+ const connector = this.adminforth.connectors[resource.dataSource];
563
+ const oldRecord = await connector.getRecordByPrimaryKey(resource, recordId)
564
+ if (!oldRecord) {
565
+ const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
566
+ return { error: `Record with ${primaryKeyColumn.name} ${recordId} not found` };
567
+ }
568
+ const record = body['record'];
569
+
570
+ const { allowedActions } = await interpretResource(adminUser, resource, { requestBody: body, newRecord: record, oldRecord}, ActionCheckSource.EditRequest);
571
+
572
+ const { allowed, error } = checkAccess(AllowedActionsEnum.edit, allowedActions);
573
+ if (!allowed) {
574
+ return { error };
575
+ }
576
+
577
+ // execute hook if needed
578
+ for (const hook of listify(resource.hooks?.edit?.beforeSave as BeforeSaveFunction[])) {
579
+ const resp = await hook({ resource, record, adminUser });
580
+ if (!resp || (!resp.ok && !resp.error)) {
581
+ throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
582
+ }
583
+
584
+ if (resp.error) {
585
+ return { error: resp.error };
586
+ }
587
+ }
588
+ const newValues = {};
589
+
590
+ for (const recordField in record) {
591
+ if (record[recordField] !== oldRecord[recordField]) {
592
+ const column = resource.columns.find((col) => col.name === recordField);
593
+ if (column) {
594
+ if (!column.virtual) {
595
+ newValues[recordField] = connector.setFieldValue(column, record[recordField]);
596
+ }
597
+ } else {
598
+ newValues[recordField] = record[recordField];
599
+ }
600
+ }
601
+ }
602
+
603
+ if (Object.keys(newValues).length > 0) {
604
+ await connector.updateRecord({ resource, recordId, newValues});
605
+ }
606
+
607
+ // execute hook if needed
608
+ for (const hook of listify(resource.hooks?.edit?.afterSave as AfterSaveFunction[])) {
609
+ const resp = await hook({ resource, record, adminUser, oldRecord });
610
+ if (!resp || (!resp.ok && !resp.error)) {
611
+ throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
612
+ }
613
+
614
+ if (resp.error) {
615
+ return { error: resp.error };
616
+ }
617
+ }
618
+
619
+ return {
620
+ newRecordId: recordId
621
+ }
622
+ }
623
+ });
624
+ server.endpoint({
625
+ method: 'POST',
626
+ path: '/delete_record',
627
+ handler: async ({ body, adminUser }) => {
628
+ const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
629
+ const record = await this.adminforth.connectors[resource.dataSource].getRecordByPrimaryKey(resource, body['primaryKey']);
630
+ if (!resource) {
631
+ return { error: `Resource '${body['resourceId']}' not found` };
632
+ }
633
+ if (!record){
634
+ return { error: `Record with ${body['primaryKey']} not found` };
635
+ }
636
+ if (resource.options.allowedActions.delete === false) {
637
+ return { error: `Resource '${resource.resourceId}' does not allow delete action` };
638
+ }
639
+
640
+ const { allowedActions } = await interpretResource(adminUser, resource, { requestBody: body }, ActionCheckSource.DeleteRequest);
641
+
642
+ const { allowed, error } = checkAccess(AllowedActionsEnum.delete, allowedActions);
643
+ if (!allowed) {
644
+ return { error };
645
+ }
646
+
647
+ // execute hook if needed
648
+ for (const hook of listify(resource.hooks?.delete?.beforeSave as BeforeSaveFunction[])) {
649
+ const resp = await hook({ resource, record, adminUser });
650
+ if (!resp || (!resp.ok && !resp.error)) {
651
+ throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
652
+ }
653
+
654
+ if (resp.error) {
655
+ return { error: resp.error };
656
+ }
657
+ }
658
+
659
+ const connector = this.adminforth.connectors[resource.dataSource];
660
+ await connector.deleteRecord({ resource, recordId: body['primaryKey']});
661
+
662
+ // execute hook if needed
663
+ for (const hook of listify(resource.hooks?.delete?.afterSave as BeforeSaveFunction[])) {
664
+ const resp = await hook({ resource, record, adminUser });
665
+ if (!resp || (!resp.ok && !resp.error)) {
666
+ throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
667
+ }
668
+
669
+ if (resp.error) {
670
+ return { error: resp.error };
671
+ }
672
+ }
673
+ return {
674
+ recordId: body['primaryKey']
675
+ }
676
+ }
677
+ });
678
+ server.endpoint({
679
+ method: 'POST',
680
+ path: '/start_bulk_action',
681
+ handler: async ({ body }) => {
682
+ const { resourceId, actionId, recordIds } = body;
683
+ const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
684
+ if (!resource) {
685
+ return { error: `Resource '${resourceId}' not found` };
686
+ }
687
+ const action = resource.options.bulkActions.find((act) => act.id == actionId);
688
+ if (!action) {
689
+ return { error: `Action '${actionId}' not found` };
690
+ } else{
691
+ await action.action({selectedIds:recordIds})
692
+
693
+ }
694
+ return {
695
+ actionId,
696
+ recordIds,
697
+ resourceId,
698
+ status:'success'
699
+
700
+ }
701
+ }
702
+ })
703
+
704
+ // setup endpoints for all plugins
705
+ this.adminforth.activatedPlugins.forEach((plugin) => {
706
+ plugin.setupEndpoints(server);
707
+ });
708
+ }
709
+ }