adminforth 1.3.54-next.23 → 1.3.54-next.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/plugins/audit-log/types.js +2 -0
  2. package/dist/plugins/audit-log/types.js.map +1 -0
  3. package/dist/plugins/chat-gpt/types.js +2 -0
  4. package/dist/plugins/chat-gpt/types.js.map +1 -0
  5. package/dist/plugins/email-password-reset/types.js +2 -0
  6. package/dist/plugins/email-password-reset/types.js.map +1 -0
  7. package/dist/plugins/foreign-inline-list/types.js +2 -0
  8. package/dist/plugins/foreign-inline-list/types.js.map +1 -0
  9. package/dist/plugins/import-export/types.js +2 -0
  10. package/dist/plugins/import-export/types.js.map +1 -0
  11. package/dist/plugins/rich-editor/custom/async-queue.js +29 -0
  12. package/dist/plugins/rich-editor/custom/async-queue.js.map +1 -0
  13. package/dist/plugins/rich-editor/dist/async-queue.js +41 -0
  14. package/dist/plugins/rich-editor/dist/custom/async-queue.js +29 -0
  15. package/dist/plugins/rich-editor/dist/custom/async-queue.js.map +1 -0
  16. package/dist/plugins/rich-editor/types.js +16 -0
  17. package/dist/plugins/rich-editor/types.js.map +1 -0
  18. package/dist/plugins/two-factors-auth/types.js +2 -0
  19. package/dist/plugins/two-factors-auth/types.js.map +1 -0
  20. package/dist/plugins/upload/types.js +2 -0
  21. package/dist/plugins/upload/types.js.map +1 -0
  22. package/package.json +5 -1
  23. package/auth.ts +0 -140
  24. package/basePlugin.ts +0 -70
  25. package/dataConnectors/baseConnector.ts +0 -221
  26. package/dataConnectors/clickhouse.ts +0 -343
  27. package/dataConnectors/mongo.ts +0 -202
  28. package/dataConnectors/postgres.ts +0 -310
  29. package/dataConnectors/sqlite.ts +0 -258
  30. package/index.ts +0 -428
  31. package/modules/codeInjector.ts +0 -747
  32. package/modules/configValidator.ts +0 -588
  33. package/modules/operationalResource.ts +0 -98
  34. package/modules/restApi.ts +0 -718
  35. package/modules/styleGenerator.ts +0 -55
  36. package/modules/styles.ts +0 -126
  37. package/modules/utils.ts +0 -472
  38. package/servers/express.ts +0 -259
  39. package/tsconfig.json +0 -112
  40. package/types/AdminForthConfig.ts +0 -1762
  41. package/types/FrontendAPI.ts +0 -143
@@ -1,588 +0,0 @@
1
- import {
2
- AdminForthConfig,
3
- AdminForthResource,
4
- IAdminForth, IConfigValidator,
5
- AdminForthComponentDeclaration ,
6
- AdminForthResourcePages, AllowedActionsEnum,
7
- type AdminForthComponentDeclarationFull,
8
- type AfterSaveFunction,
9
- AdminForthBulkAction,
10
- } from "../types/AdminForthConfig.js";
11
-
12
- import fs from 'fs';
13
- import path from 'path';
14
- import { guessLabelFromName, suggestIfTypo } from './utils.js';
15
-
16
- import crypto from 'crypto';
17
- import { AdminForthSortDirections } from "adminforth/index.js";
18
-
19
-
20
-
21
-
22
- export default class ConfigValidator implements IConfigValidator {
23
-
24
- constructor(private adminforth: IAdminForth, private config: AdminForthConfig) {
25
- this.adminforth = adminforth;
26
- this.config = config;
27
- }
28
-
29
- validateAndListifyInjection(obj, key, errors) {
30
- if (!Array.isArray(obj[key])) {
31
- // not array
32
- obj[key] = [obj[key]];
33
- }
34
- obj[key].forEach((target, i) => {
35
- obj[key][i] = this.validateComponent(target, errors);
36
- });
37
- }
38
-
39
- checkCustomFileExists(filePath: string): Array<string> {
40
- if (filePath.startsWith('@@/')) {
41
- const checkPath = path.join(this.config.customization.customComponentsDir, filePath.replace('@@/', ''));
42
- if (!fs.existsSync(checkPath)) {
43
- return [`File file ${filePath} does not exist in ${this.config.customization.customComponentsDir}`];
44
- }
45
- }
46
- return [];
47
- }
48
-
49
- validateComponent(component: AdminForthComponentDeclaration, errors: Array<string>): AdminForthComponentDeclaration {
50
-
51
- if (!component) {
52
- return component;
53
- }
54
- let obj: AdminForthComponentDeclarationFull;
55
- if (typeof component === 'string') {
56
- obj = { file: component, meta: {} };
57
- } else {
58
- obj = component;
59
- }
60
-
61
- let ignoreExistsCheck = false;
62
- if (
63
- this.adminforth.codeInjector.allComponentNames.hasOwnProperty(
64
- (component as AdminForthComponentDeclarationFull).file)
65
- ) {
66
- // not obvious, but if we are in this if, it means that this is plugin component
67
- // if component is plugin component, we don't need to check if it exists in users folder
68
- ignoreExistsCheck = true;
69
- }
70
-
71
-
72
- if (!ignoreExistsCheck) {
73
- errors.push(...this.checkCustomFileExists(obj.file));
74
- }
75
-
76
- return obj;
77
- }
78
-
79
- validateConfig() {
80
- const errors = [];
81
-
82
- if (!this.config.customization.customComponentsDir) {
83
- this.config.customization.customComponentsDir = './custom';
84
- }
85
-
86
- try {
87
- // check customComponentsDir exists
88
- fs.accessSync(this.config.customization.customComponentsDir, fs.constants.R_OK);
89
- } catch (e) {
90
- this.config.customization.customComponentsDir = undefined;
91
- }
92
-
93
- if (!this.config.customization) {
94
- this.config.customization = {};
95
- }
96
-
97
- if (!this.config.customization.customComponentsDir) {
98
- this.config.customization.customComponentsDir = './custom';
99
- }
100
-
101
- try {
102
- // check customComponentsDir exists
103
- fs.accessSync(this.config.customization.customComponentsDir, fs.constants.R_OK);
104
- } catch (e) {
105
- this.config.customization.customComponentsDir = undefined;
106
- }
107
-
108
- if (this.config.customization.customPages) {
109
- this.config.customization.customPages.forEach((page, i) => {
110
- this.validateComponent(page.component, errors);
111
- });
112
- } else {
113
- this.config.customization.customPages = [];
114
- }
115
- if (!this.config.baseUrl) {
116
- this.config.baseUrl = '';
117
- }
118
- if (!this.config.baseUrl.endsWith('/')) {
119
- this.adminforth.baseUrlSlashed = this.config.baseUrl + '/';
120
- } else {
121
- this.adminforth.baseUrlSlashed = this.config.baseUrl;
122
- }
123
- if (this.config?.customization.brandName === undefined) {
124
- this.config.customization.brandName = 'AdminForth';
125
- }
126
- if (this.config.customization.loginPageInjections === undefined) {
127
- this.config.customization.loginPageInjections = {};
128
- }
129
- if (this.config.customization.globalInjections === undefined) {
130
- this.config.customization.globalInjections = {};
131
- }
132
- if (this.config.customization.loginPageInjections.underInputs === undefined) {
133
- this.config.customization.loginPageInjections.underInputs = [];
134
- }
135
- if (this.config.customization.brandLogo) {
136
- errors.push(...this.checkCustomFileExists(this.config.customization.brandLogo));
137
- }
138
- if (this.config.customization.showBrandNameInSidebar === undefined) {
139
- this.config.customization.showBrandNameInSidebar = true;
140
- }
141
- if (this.config.customization.favicon) {
142
- errors.push(...this.checkCustomFileExists(this.config.customization.favicon));
143
- }
144
-
145
- if (!this.config.customization.datesFormat) {
146
- this.config.customization.datesFormat = 'MMM D, YYYY';
147
- }
148
-
149
- if (!this.config.customization.timeFormat) {
150
- this.config.customization.timeFormat = 'HH:mm:ss';
151
- }
152
-
153
- if (this.config.resources) {
154
- this.config.resources.forEach((res: AdminForthResource) => {
155
- if (!res.table) {
156
- errors.push(`Resource "${res.dataSource}" is missing table`);
157
- }
158
- // if recordLabel is not callable, throw error
159
- if (res.recordLabel && typeof res.recordLabel !== 'function') {
160
- errors.push(`Resource "${res.dataSource}" recordLabel is not a function`);
161
- }
162
- if (!res.recordLabel) {
163
- res.recordLabel = (item) => {
164
- const pkVal = item[res.columns.find((col) => col.primaryKey).name];
165
- return `${res.label} ${pkVal}`;
166
- }
167
- }
168
-
169
-
170
- res.resourceId = res.resourceId || res.table;
171
- // as fallback value, capitalize and then replace _ with space
172
- res.label = res.label || res.resourceId.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
173
- if (!res.dataSource) {
174
- errors.push(`Resource "${res.resourceId}" is missing dataSource`);
175
- }
176
- if (!res.columns) {
177
- res.columns = [];
178
- }
179
- res.columns.forEach((col) => {
180
- col.label = col.label || guessLabelFromName(col.name);
181
- //define default sortable
182
- if (!Object.keys(col).includes('sortable')) { col.sortable = true; }
183
- if (col.showIn && !Array.isArray(col.showIn)) {
184
- errors.push(`Resource "${res.resourceId}" column "${col.name}" showIn must be an array`);
185
- }
186
-
187
- // check col.required is string or object
188
- if (col.required && !((typeof col.required === 'boolean') || (typeof col.required === 'object'))) {
189
- errors.push(`Resource "${res.resourceId}" column "${col.name}" required must be a string or object`);
190
- }
191
-
192
- // if it is object check the keys are one of ['create', 'edit']
193
- if (typeof col.required === 'object') {
194
- const wrongRequiredOn = Object.keys(col.required).find((c) => !['create', 'edit'].includes(c));
195
- if (wrongRequiredOn) {
196
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid required value "${wrongRequiredOn}", allowed keys are 'create', 'edit']`);
197
- }
198
- }
199
-
200
- // same for editingNote
201
- if (col.editingNote && !((typeof col.editingNote === 'string') || (typeof col.editingNote === 'object'))) {
202
- errors.push(`Resource "${res.resourceId}" column "${col.name}" editingNote must be a string or object`);
203
- }
204
- if (typeof col.editingNote === 'object') {
205
- const wrongEditingNoteOn = Object.keys(col.editingNote).find((c) => !['create', 'edit'].includes(c));
206
- if (wrongEditingNoteOn) {
207
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid editingNote value "${wrongEditingNoteOn}", allowed keys are 'create', 'edit']`);
208
- }
209
- }
210
-
211
- const wrongShowIn = col.showIn && col.showIn.find((c) => AdminForthResourcePages[c] === undefined);
212
- if (wrongShowIn) {
213
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid showIn value "${wrongShowIn}", allowed values are ${Object.keys(AdminForthResourcePages).join(', ')}`);
214
- }
215
- col.showIn = col.showIn || Object.values(AdminForthResourcePages);
216
-
217
- if (col.foreignResource) {
218
-
219
- if (!col.foreignResource.resourceId) {
220
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource without resourceId`);
221
- }
222
- const resource = this.config.resources.find((r) => r.resourceId === col.foreignResource.resourceId);
223
- if (!resource) {
224
- const similar = suggestIfTypo(this.config.resources.map((r) => r.resourceId), col.foreignResource.resourceId);
225
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource resourceId which is not in resources: "${col.foreignResource.resourceId}".
226
- ${similar ? `Did you mean "${similar}" instead of "${col.foreignResource.resourceId}"?` : ''}`);
227
- }
228
- const befHook = col.foreignResource.hooks?.dropdownList?.beforeDatasourceRequest;
229
- if (befHook) {
230
- if (!Array.isArray(befHook)) {
231
- col.foreignResource.hooks.dropdownList.beforeDatasourceRequest = [befHook];
232
- }
233
- }
234
- const aftHook = col.foreignResource.hooks?.dropdownList?.afterDatasourceResponse;
235
- if (aftHook) {
236
- if (!Array.isArray(aftHook)) {
237
- col.foreignResource.hooks.dropdownList.afterDatasourceResponse = [aftHook];
238
- }
239
- }
240
- }
241
- })
242
-
243
- if (!res.options) {
244
- res.options = { bulkActions: [], allowedActions: {} };
245
- }
246
-
247
- if (!res.options.allowedActions) {
248
- res.options.allowedActions = {
249
- all: true,
250
- };
251
- }
252
-
253
- if (res.options.defaultSort) {
254
- const colName = res.options.defaultSort.columnName;
255
- const col = res.columns.find((c) => c.name === colName);
256
- if (!col) {
257
- const similar = suggestIfTypo(res.columns.map((c) => c.name), colName);
258
- errors.push(`Resource "${res.resourceId}" defaultSort.columnName column "${colName}" not found in columns. ${similar ? `Did you mean "${similar}"?` : ''}`);
259
- }
260
- const dir = res.options.defaultSort.direction;
261
- if (!dir) {
262
- errors.push(`Resource "${res.resourceId}" defaultSort.direction is missing`);
263
- }
264
- // AdminForthSortDirections is enum
265
- if (!(Object.values(AdminForthSortDirections) as string[]).includes(dir)) {
266
- errors.push(`Resource "${res.resourceId}" defaultSort.direction "${dir}" is invalid, allowed values are ${Object.values(AdminForthSortDirections).join(', ')}`);
267
- }
268
- }
269
-
270
- if (Object.keys(res.options.allowedActions).includes('all')) {
271
- if (Object.keys(res.options.allowedActions).length > 1) {
272
- errors.push(`Resource "${res.resourceId}" allowedActions cannot have "all" and other keys at same time: ${Object.keys(res.options.allowedActions).join(', ')}`);
273
- }
274
- for (const key of Object.keys(AllowedActionsEnum)) {
275
- if (key !== 'all') {
276
- res.options.allowedActions[key] = res.options.allowedActions.all;
277
- }
278
- }
279
- delete res.options.allowedActions.all;
280
- } else {
281
- // by default allow all actions
282
- for (const key of Object.keys(AllowedActionsEnum)) {
283
- if (!Object.keys(res.options.allowedActions).includes(key)) {
284
- res.options.allowedActions[key] = true;
285
- }
286
- }
287
- }
288
-
289
-
290
- //check if resource has bulkActions
291
- let bulkActions: AdminForthBulkAction[] = res?.options?.bulkActions || [];
292
-
293
- if (!Array.isArray(bulkActions)) {
294
- errors.push(`Resource "${res.resourceId}" bulkActions must be an array`);
295
- bulkActions = [];
296
- }
297
-
298
- if (!bulkActions.find((action) => action.label === 'Delete checked')) {
299
- bulkActions.push({
300
- label: `Delete checked`,
301
- state: 'danger',
302
- icon: 'flowbite:trash-bin-outline',
303
- confirm: 'Are you sure you want to delete selected items?',
304
- allowed: async ({ resource, adminUser, allowedActions }) => { return allowedActions.delete },
305
- action: async ({ selectedIds, adminUser }) => {
306
- const connector = this.adminforth.connectors[res.dataSource];
307
-
308
- // for now if at least one error, stop and return error
309
- let error = null;
310
-
311
- await Promise.all(
312
- selectedIds.map(async (recordId) => {
313
- const record = await connector.getRecordByPrimaryKey(res, recordId);
314
-
315
- await Promise.all(
316
- (res.hooks.delete.beforeSave as AfterSaveFunction[]).map(
317
- async (hook) => {
318
- const resp = await hook({
319
- recordId: recordId,
320
- resource: res,
321
- record,
322
- adminUser,
323
- });
324
- if (!error && resp.error) {
325
- error = resp.error;
326
- }
327
- }
328
- )
329
- )
330
-
331
- if (error) {
332
- return;
333
- }
334
-
335
- await connector.deleteRecord({ resource: res, recordId });
336
- // call afterDelete hook
337
- await Promise.all(
338
- (res.hooks.delete.afterSave as AfterSaveFunction[]).map(
339
- async (hook) => {
340
- await hook({
341
- resource: res,
342
- record,
343
- adminUser,
344
- recordId: recordId
345
- });
346
- }
347
- )
348
- )
349
-
350
- })
351
- );
352
-
353
- if (error) {
354
- return { error, ok: false };
355
- }
356
- return { ok: true, successMessage: `${selectedIds.length} item${selectedIds.length > 1 ? 's' : ''} deleted` };
357
- }
358
- });
359
- }
360
-
361
- bulkActions.map((action) => {
362
- if (!action.id) {
363
- action.id = crypto.createHash('sha256').update(
364
- action.label,
365
- ).digest('hex');
366
- }
367
- });
368
- res.options.bulkActions = bulkActions;
369
-
370
- // if pageInjection is a string, make array with one element. Also check file exists
371
- const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems', 'customActionIcons'];
372
- const possiblePages = ['list', 'show', 'create', 'edit'];
373
-
374
-
375
-
376
-
377
- if (res.options.pageInjections) {
378
-
379
- Object.entries(res.options.pageInjections).map(([key, value]) => {
380
- if (!possiblePages.includes(key)) {
381
- const similar = suggestIfTypo(possiblePages, key);
382
- errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${key}", allowed keys are ${possiblePages.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
383
- }
384
-
385
- Object.entries(value).map(([injection, target]) => {
386
- if (possibleInjections.includes(injection)) {
387
- this.validateAndListifyInjection(res.options.pageInjections[key], injection, errors);
388
- } else {
389
- const similar = suggestIfTypo(possibleInjections, injection);
390
- errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')} ${similar ? `Did you mean "${similar}"?` : ''}`);
391
- }
392
- });
393
-
394
- })
395
-
396
- }
397
-
398
- // transform all hooks Functions to array of functions
399
- if (!res.hooks) {
400
- res.hooks = {};
401
- }
402
- for (const hookName of ['show', 'list']) {
403
- if (!res.hooks[hookName]) {
404
- res.hooks[hookName] = {};
405
- }
406
- if (!res.hooks[hookName].beforeDatasourceRequest) {
407
- res.hooks[hookName].beforeDatasourceRequest = [];
408
- }
409
-
410
- if (!Array.isArray(res.hooks[hookName].beforeDatasourceRequest)) {
411
- res.hooks[hookName].beforeDatasourceRequest = [res.hooks[hookName].beforeDatasourceRequest];
412
- }
413
-
414
- if (!res.hooks[hookName].afterDatasourceResponse) {
415
- res.hooks[hookName].afterDatasourceResponse = [];
416
- }
417
-
418
- if (!Array.isArray(res.hooks[hookName].afterDatasourceResponse)) {
419
- res.hooks[hookName].afterDatasourceResponse = [res.hooks[hookName].afterDatasourceResponse];
420
- }
421
- }
422
- for (const hookName of ['create', 'edit', 'delete']) {
423
- if (!res.hooks[hookName]) {
424
- res.hooks[hookName] = {};
425
- }
426
- if (!res.hooks[hookName].beforeSave) {
427
- res.hooks[hookName].beforeSave = [];
428
- }
429
- if (!Array.isArray(res.hooks[hookName].beforeSave)) {
430
- res.hooks[hookName].beforeSave = [res.hooks[hookName].beforeSave];
431
- }
432
- if (!res.hooks[hookName].afterSave) {
433
- res.hooks[hookName].afterSave = [];
434
- }
435
- if (!Array.isArray(res.hooks[hookName].afterSave)) {
436
- res.hooks[hookName].afterSave = [res.hooks[hookName].afterSave];
437
- }
438
- }
439
- });
440
-
441
- if (this.config.customization.globalInjections) {
442
- const ALLOWED_GLOBAL_INJECTIONS = ['userMenu', 'header', 'sidebar',]
443
- Object.keys(this.config.customization.globalInjections).forEach((injection) => {
444
- if (ALLOWED_GLOBAL_INJECTIONS.includes(injection)) {
445
- this.validateAndListifyInjection(this.config.customization.globalInjections, injection, errors);
446
- } else {
447
- const similar = suggestIfTypo(ALLOWED_GLOBAL_INJECTIONS, injection);
448
- errors.push(`Global injection key "${injection}" is not allowed. Allowed keys are ${ALLOWED_GLOBAL_INJECTIONS.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
449
- }
450
- });
451
- }
452
-
453
- if (!this.config.menu) {
454
- errors.push('No config.menu defined');
455
- }
456
-
457
- // check if there is only one homepage: true in menu, recursivly
458
- let homepages = 0;
459
- const browseMenu = (menu) => {
460
- menu.forEach((item) => {
461
- if (item.component && item.resourceId) {
462
- errors.push(`Menu item cannot have both component and resourceId: ${JSON.stringify(item)}`);
463
- }
464
- if (item.component && !item.path) {
465
- errors.push(`Menu item with component must have path : ${JSON.stringify(item)}`);
466
- }
467
-
468
- if (item.type === 'resource' && !item.resourceId) {
469
- errors.push(`Menu item with type 'resource' must have resourceId : ${JSON.stringify(item)}`);
470
- }
471
-
472
- if (item.resourceId && !this.config.resources.find((res) => res.resourceId === item.resourceId)) {
473
- const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId), item.resourceId);
474
- errors.push(`Menu item with type 'resourceId' has resourceId which is not in resources: "${JSON.stringify(item)}".
475
- ${similar ? `Did you mean "${similar}" instead of "${item.resourceId}"?` : ''}`);
476
- }
477
-
478
- if (item.type === 'component' && !item.component) {
479
- errors.push(`Menu item with type 'component' must have component : ${JSON.stringify(item)}`);
480
- }
481
-
482
- // make sure component starts with @@
483
- if (item.component) {
484
- if (!item.component.startsWith('@@')) {
485
- errors.push(`Menu item component must start with @@ : ${JSON.stringify(item)}`);
486
- }
487
-
488
- const path = item.component.replace('@@', this.config.customization.customComponentsDir);
489
- if (!fs.existsSync(path)) {
490
- errors.push(`Menu item component "${item.component.replace('@@', '')}" does not exist in "${this.config.customization.customComponentsDir}"`);
491
- }
492
- }
493
-
494
- if (item.homepage) {
495
- homepages++;
496
- if (homepages > 1) {
497
- errors.push('There must be only one homepage: true in menu, found second one in ' + JSON.stringify(item));
498
- }
499
- }
500
- if (item.children) {
501
- browseMenu(item.children);
502
- }
503
- });
504
- };
505
- browseMenu(this.config.menu);
506
-
507
- }
508
-
509
- if (this.config.auth) {
510
- // TODO: remove in future releases
511
- if (!this.config.auth.usersResourceId && this.config.auth.resourceId) {
512
- this.config.auth.usersResourceId = this.config.auth.resourceId;
513
- }
514
-
515
- if (!this.config.auth.usersResourceId) {
516
- throw new Error('No config.auth.usersResourceId defined');
517
- }
518
- if (!this.config.auth.passwordHashField) {
519
- throw new Error('No config.auth.passwordHashField defined');
520
- }
521
- if (!this.config.auth.usernameField) {
522
- throw new Error('No config.auth.usernameField defined');
523
- }
524
- if (this.config.auth.loginBackgroundImage) {
525
- errors.push(...this.checkCustomFileExists(this.config.auth.loginBackgroundImage));
526
- }
527
- const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
528
- if (!userResource) {
529
- const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId ), this.config.auth.usersResourceId);
530
- throw new Error(`Resource with id "${this.config.auth.usersResourceId}" not found. ${similar ? `Did you mean "${similar}"?` : ''}`);
531
- }
532
-
533
- if (!this.config.auth.beforeLoginConfirmation) {
534
- this.config.auth.beforeLoginConfirmation = [];
535
- }
536
- }
537
-
538
- // check for duplicate resourceIds and show which ones are duplicated
539
- const resourceIds = this.config.resources.map((res) => res.resourceId);
540
- const uniqueResourceIds = new Set(resourceIds);
541
- if (uniqueResourceIds.size != resourceIds.length) {
542
- const duplicates = resourceIds.filter((item, index) => resourceIds.indexOf(item) != index);
543
- errors.push(`Duplicate fields "resourceId" or "table": ${duplicates.join(', ')}`);
544
- }
545
-
546
- //add ids for onSelectedAllActions for each resource
547
- if (errors.length > 0) {
548
- throw new Error(`Invalid AdminForth config: ${errors.join(', ')}`);
549
- }
550
-
551
- // check is all custom components files exists
552
- for (const resource of this.config.resources) {
553
- for (const column of resource.columns) {
554
- if (column.components) {
555
-
556
- for (const [key, comp] of Object.entries(column.components as Record<string, AdminForthComponentDeclarationFull>)) {
557
-
558
- column.components[key] = this.validateComponent(comp, errors);
559
- }
560
- }
561
- }
562
- }
563
- }
564
-
565
- postProcessAfterDiscover(resource: AdminForthResource) {
566
- resource.columns.forEach((column) => {
567
- // if db/user says column is required in boolean, expand
568
- if (typeof column.required === 'boolean') {
569
- column.required = { create: column.required, edit: column.required };
570
- }
571
-
572
- if (!column.required) {
573
- column.required = { create: false, edit: false };
574
- }
575
-
576
- // same for editingNote
577
- if (typeof column.editingNote === 'string') {
578
- column.editingNote = { create: column.editingNote, edit: column.editingNote };
579
- }
580
- })
581
- resource.dataSourceColumns = resource.columns.filter((col) => !col.virtual);
582
- (resource.plugins || []).forEach((plugin) => {
583
- if (plugin.validateConfigAfterDiscover) {
584
- plugin.validateConfigAfterDiscover(this.adminforth, resource);
585
- }
586
- });
587
- }
588
- }
@@ -1,98 +0,0 @@
1
- import { IAdminForthFilter, IAdminForthSort, IOperationalResource, IAdminForthDataSourceConnectorBase, AdminForthResource } from '../types/AdminForthConfig.js';
2
-
3
-
4
- function filtersIfFilter(filter: IAdminForthFilter | IAdminForthFilter[] | undefined): IAdminForthFilter[] {
5
- if (!filter) {
6
- return [];
7
- }
8
- return (Array.isArray(filter) ? filter : [filter]) as IAdminForthFilter[];
9
- }
10
-
11
- function sortsIfSort(sort: IAdminForthSort | IAdminForthSort[]): IAdminForthSort[] {
12
- return (Array.isArray(sort) ? sort : [sort]) as IAdminForthSort[];
13
- }
14
-
15
- export default class OperationalResource implements IOperationalResource {
16
- dataConnector: IAdminForthDataSourceConnectorBase;
17
- resourceConfig: AdminForthResource;
18
-
19
- constructor(dataConnector: IAdminForthDataSourceConnectorBase, resourceConfig: AdminForthResource) {
20
- this.dataConnector = dataConnector;
21
- this.resourceConfig = resourceConfig;
22
- }
23
-
24
- async get(filter: IAdminForthFilter | IAdminForthFilter[]): Promise<any | null> {
25
- return (
26
- await this.dataConnector.getData({
27
- resource: this.resourceConfig,
28
- filters: filtersIfFilter(filter),
29
- limit: 1,
30
- offset: 0,
31
- sort: [],
32
- })
33
- ).data[0] || null;
34
- }
35
-
36
- async list(
37
- filter: IAdminForthFilter | IAdminForthFilter[],
38
- limit: number | null = null,
39
- offset: number | null = null,
40
- sort: IAdminForthSort | IAdminForthSort[] = []
41
- ): Promise<any[]> {
42
- // check if type of limit and offset is number
43
- if (limit !== null && typeof limit !== 'number') {
44
- throw new Error('Limit must be a number');
45
- }
46
- if (offset !== null && typeof offset !== 'number') {
47
- throw new Error('Offset must be a number');
48
- }
49
-
50
- let appliedLimit = limit;
51
- if (limit === null) {
52
- appliedLimit = 1000000000;
53
- }
54
- let appliedOffset = offset;
55
- if (offset === null) {
56
- appliedOffset = 0;
57
- }
58
-
59
- const { data } = await this.dataConnector.getData({
60
- resource: this.resourceConfig,
61
- filters: filtersIfFilter(filter),
62
- limit: appliedLimit,
63
- offset: appliedOffset,
64
- sort: sortsIfSort(sort),
65
- getTotals: false,
66
- });
67
- return data;
68
- }
69
-
70
- async count(filter: IAdminForthFilter | IAdminForthFilter[] | undefined): Promise<number> {
71
- return await this.dataConnector.getCount({
72
- resource: this.resourceConfig,
73
- filters: filtersIfFilter(filter),
74
- });
75
- }
76
-
77
- async create(recordValues: any): Promise<{ ok: boolean; createdRecord: any; error?: string; }> {
78
- const { ok, createdRecord, error } = await this.dataConnector.createRecord({
79
- resource: this.resourceConfig,
80
- record: recordValues,
81
- adminUser: null
82
- });
83
- return { ok, createdRecord, error };
84
- }
85
-
86
- async update(primaryKey: any, record: any): Promise<any> {
87
- return await this.dataConnector.updateRecord({
88
- resource: this.resourceConfig,
89
- recordId: primaryKey,
90
- newValues: record
91
- });
92
- }
93
-
94
- async delete(primaryKey: any): Promise<boolean> {
95
- return await this.dataConnector.deleteRecord({ resource: this.resourceConfig, recordId: primaryKey });
96
- }
97
-
98
- }