adminforth 1.5.4-next.13 → 1.5.4-next.15

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 (48) hide show
  1. package/dist/auth.js +4 -4
  2. package/dist/auth.js.map +1 -1
  3. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  4. package/dist/dataConnectors/clickhouse.js +0 -1
  5. package/dist/dataConnectors/clickhouse.js.map +1 -1
  6. package/dist/index.d.ts +0 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -13
  9. package/dist/index.js.map +1 -1
  10. package/dist/modules/codeInjector.d.ts.map +1 -1
  11. package/dist/modules/codeInjector.js +7 -5
  12. package/dist/modules/codeInjector.js.map +1 -1
  13. package/dist/modules/configValidator.d.ts +11 -5
  14. package/dist/modules/configValidator.d.ts.map +1 -1
  15. package/dist/modules/configValidator.js +387 -379
  16. package/dist/modules/configValidator.js.map +1 -1
  17. package/dist/modules/restApi.d.ts.map +1 -1
  18. package/dist/modules/restApi.js +10 -9
  19. package/dist/modules/restApi.js.map +1 -1
  20. package/dist/servers/express.js +2 -2
  21. package/dist/servers/express.js.map +1 -1
  22. package/dist/spa/src/App.vue +8 -6
  23. package/dist/spa/src/afcl/Input.vue +1 -1
  24. package/dist/spa/src/afcl/Link.vue +9 -1
  25. package/dist/spa/src/afcl/LinkButton.vue +5 -3
  26. package/dist/spa/src/afcl/VerticalTabs.vue +1 -1
  27. package/dist/spa/src/components/AcceptModal.vue +1 -2
  28. package/dist/spa/src/components/ResourceListTable.vue +4 -3
  29. package/dist/spa/src/components/SkeleteLoader.vue +5 -10
  30. package/dist/spa/src/components/ValueRenderer.vue +13 -14
  31. package/dist/spa/src/spa_types/core.ts +2 -4
  32. package/dist/spa/src/stores/core.ts +15 -16
  33. package/dist/spa/src/types/Back.ts +310 -327
  34. package/dist/spa/src/types/Common.ts +219 -10
  35. package/dist/spa/src/types/FrontendAPI.ts +5 -4
  36. package/dist/spa/src/utils.ts +5 -1
  37. package/dist/types/Back.d.ts +262 -287
  38. package/dist/types/Back.d.ts.map +1 -1
  39. package/dist/types/Back.js +1 -46
  40. package/dist/types/Back.js.map +1 -1
  41. package/dist/types/Common.d.ts +193 -9
  42. package/dist/types/Common.d.ts.map +1 -1
  43. package/dist/types/Common.js +46 -0
  44. package/dist/types/Common.js.map +1 -1
  45. package/dist/types/FrontendAPI.d.ts +4 -4
  46. package/dist/types/FrontendAPI.d.ts.map +1 -1
  47. package/dist/types/FrontendAPI.js.map +1 -1
  48. package/package.json +1 -1
@@ -3,11 +3,11 @@ import path from 'path';
3
3
  import { guessLabelFromName, md5hash, suggestIfTypo } from './utils.js';
4
4
  import { AdminForthSortDirections, AllowedActionsEnum, AdminForthResourcePages, } from "../types/Common.js";
5
5
  export default class ConfigValidator {
6
- constructor(adminforth, config) {
6
+ constructor(adminforth, inputConfig) {
7
7
  this.adminforth = adminforth;
8
- this.config = config;
8
+ this.inputConfig = inputConfig;
9
9
  this.adminforth = adminforth;
10
- this.config = config;
10
+ this.inputConfig = inputConfig;
11
11
  }
12
12
  validateAndListifyInjection(obj, key, errors) {
13
13
  if (!Array.isArray(obj[key])) {
@@ -18,18 +18,29 @@ export default class ConfigValidator {
18
18
  obj[key][i] = this.validateComponent(target, errors);
19
19
  });
20
20
  }
21
+ validateAndListifyInjectionNew(obj, key, errors) {
22
+ let injections = obj[key];
23
+ if (!Array.isArray(injections)) {
24
+ // not array
25
+ injections = [injections];
26
+ }
27
+ injections.forEach((target, i) => {
28
+ injections[i] = this.validateComponent(target, errors);
29
+ });
30
+ return injections;
31
+ }
21
32
  checkCustomFileExists(filePath) {
22
33
  if (filePath.startsWith('@@/')) {
23
- const checkPath = path.join(this.config.customization.customComponentsDir, filePath.replace('@@/', ''));
34
+ const checkPath = path.join(this.customComponentsDir, filePath.replace('@@/', ''));
24
35
  if (!fs.existsSync(checkPath)) {
25
- return [`File file ${filePath} does not exist in ${this.config.customization.customComponentsDir}`];
36
+ return [`File file ${filePath} does not exist in ${this.customComponentsDir}`];
26
37
  }
27
38
  }
28
39
  return [];
29
40
  }
30
41
  validateComponent(component, errors) {
31
42
  if (!component) {
32
- return component;
43
+ throw new Error('Component is missing during validation');
33
44
  }
34
45
  let obj;
35
46
  if (typeof component === 'string') {
@@ -49,422 +60,428 @@ export default class ConfigValidator {
49
60
  }
50
61
  return obj;
51
62
  }
52
- validateConfig() {
53
- const errors = [];
54
- if (!this.config.customization.customComponentsDir) {
55
- this.config.customization.customComponentsDir = './custom';
56
- }
57
- try {
58
- // check customComponentsDir exists
59
- fs.accessSync(this.config.customization.customComponentsDir, fs.constants.R_OK);
60
- }
61
- catch (e) {
62
- this.config.customization.customComponentsDir = undefined;
63
- }
64
- if (!this.config.customization) {
65
- this.config.customization = {};
66
- }
67
- if (!this.config.customization.customComponentsDir) {
68
- this.config.customization.customComponentsDir = './custom';
63
+ validateAndNormalizeCustomization(errors) {
64
+ var _a, _b, _c;
65
+ this.customComponentsDir = (_a = this.inputConfig.customization) === null || _a === void 0 ? void 0 : _a.customComponentsDir;
66
+ if (!this.customComponentsDir) {
67
+ this.customComponentsDir = './custom';
69
68
  }
70
69
  try {
71
70
  // check customComponentsDir exists
72
- fs.accessSync(this.config.customization.customComponentsDir, fs.constants.R_OK);
71
+ fs.accessSync(this.customComponentsDir, fs.constants.R_OK);
73
72
  }
74
73
  catch (e) {
75
- this.config.customization.customComponentsDir = undefined;
76
- }
77
- if (this.config.customization.customPages) {
78
- this.config.customization.customPages.forEach((page, i) => {
79
- this.validateComponent(page.component, errors);
74
+ this.customComponentsDir = undefined;
75
+ }
76
+ const loginPageInjections = {
77
+ underInputs: [],
78
+ };
79
+ if ((_b = this.inputConfig.customization) === null || _b === void 0 ? void 0 : _b.loginPageInjections) {
80
+ const ALLOWED_LOGIN_INJECTIONS = ['underInputs'];
81
+ Object.keys(this.inputConfig.customization.loginPageInjections).forEach((injection) => {
82
+ if (ALLOWED_LOGIN_INJECTIONS.includes(injection)) {
83
+ loginPageInjections[injection] = this.validateAndListifyInjectionNew(this.inputConfig.customization.loginPageInjections, injection, errors);
84
+ console.log('🐛🐛 loginPageInjections', JSON.stringify(loginPageInjections, null, 2));
85
+ }
86
+ else {
87
+ const similar = suggestIfTypo(ALLOWED_LOGIN_INJECTIONS, injection);
88
+ errors.push(`Login page injection key "${injection}" is not allowed. Allowed keys are ${ALLOWED_LOGIN_INJECTIONS.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
89
+ }
80
90
  });
81
91
  }
82
- else {
83
- this.config.customization.customPages = [];
84
- }
85
- if (!this.config.baseUrl) {
86
- this.config.baseUrl = '';
87
- }
88
- if (!this.config.baseUrl.endsWith('/')) {
89
- this.adminforth.baseUrlSlashed = this.config.baseUrl + '/';
92
+ const globalInjections = {
93
+ userMenu: [],
94
+ header: [],
95
+ sidebar: [],
96
+ };
97
+ if ((_c = this.inputConfig.customization) === null || _c === void 0 ? void 0 : _c.globalInjections) {
98
+ const ALLOWED_GLOBAL_INJECTIONS = ['userMenu', 'header', 'sidebar'];
99
+ Object.keys(this.inputConfig.customization.globalInjections).forEach((injection) => {
100
+ if (ALLOWED_GLOBAL_INJECTIONS.includes(injection)) {
101
+ globalInjections[injection] = this.validateAndListifyInjectionNew(this.inputConfig.customization.globalInjections, injection, errors);
102
+ }
103
+ else {
104
+ const similar = suggestIfTypo(ALLOWED_GLOBAL_INJECTIONS, injection);
105
+ errors.push(`Global injection key "${injection}" is not allowed. Allowed keys are ${ALLOWED_GLOBAL_INJECTIONS.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
106
+ }
107
+ });
90
108
  }
91
- else {
92
- this.adminforth.baseUrlSlashed = this.config.baseUrl;
109
+ const customization = Object.assign(Object.assign({}, (this.inputConfig.customization || {})), { customComponentsDir: this.customComponentsDir, loginPageInjections,
110
+ globalInjections });
111
+ if (!customization.customPages) {
112
+ customization.customPages = [];
93
113
  }
94
- if (this.config.customization.brandName === undefined) {
95
- this.config.customization.brandName = 'AdminForth';
114
+ customization.customPages.forEach((page, i) => {
115
+ this.validateComponent(page.component, errors);
116
+ });
117
+ if (!customization.brandName) { //} === undefined) {
118
+ customization.brandName = 'AdminForth';
96
119
  }
97
120
  // slug should have only lowercase letters, dashes and numbers
98
- this.config.customization._brandNameSlug = this.config.customization.brandName.toLowerCase().replace(/[^a-z0-9-]/g, '');
99
- if (this.config.customization.loginPageInjections === undefined) {
100
- this.config.customization.loginPageInjections = {};
101
- }
102
- if (this.config.customization.globalInjections === undefined) {
103
- this.config.customization.globalInjections = {};
121
+ customization.brandNameSlug = customization.brandName.toLowerCase().replace(/[^a-z0-9-]/g, '');
122
+ if (customization.brandLogo) {
123
+ errors.push(...this.checkCustomFileExists(customization.brandLogo));
104
124
  }
105
- if (this.config.customization.loginPageInjections.underInputs === undefined) {
106
- this.config.customization.loginPageInjections.underInputs = [];
125
+ if (customization.showBrandNameInSidebar === undefined) {
126
+ customization.showBrandNameInSidebar = true;
107
127
  }
108
- if (this.config.customization.brandLogo) {
109
- errors.push(...this.checkCustomFileExists(this.config.customization.brandLogo));
128
+ if (customization.favicon) {
129
+ errors.push(...this.checkCustomFileExists(customization.favicon));
110
130
  }
111
- if (this.config.customization.showBrandNameInSidebar === undefined) {
112
- this.config.customization.showBrandNameInSidebar = true;
131
+ if (!customization.datesFormat) {
132
+ customization.datesFormat = 'MMM D, YYYY';
113
133
  }
114
- if (this.config.customization.favicon) {
115
- errors.push(...this.checkCustomFileExists(this.config.customization.favicon));
134
+ if (!customization.timeFormat) {
135
+ customization.timeFormat = 'HH:mm:ss';
116
136
  }
117
- if (!this.config.customization.datesFormat) {
118
- this.config.customization.datesFormat = 'MMM D, YYYY';
137
+ return customization;
138
+ }
139
+ validateAndNormalizeAllowedActions(resInput, errors) {
140
+ var _a;
141
+ const allowedActions = ((_a = resInput.options) === null || _a === void 0 ? void 0 : _a.allowedActions) || { all: true };
142
+ if (Object.keys(allowedActions).includes('all')) {
143
+ if (Object.keys(allowedActions).length > 1) {
144
+ errors.push(`Resource "${resInput.resourceId || resInput.table}" allowedActions cannot have "all" and other keys at same time: ${Object.keys(allowedActions).join(', ')}`);
145
+ }
146
+ for (const key of Object.keys(AllowedActionsEnum)) {
147
+ if (key !== 'all') {
148
+ allowedActions[key] = allowedActions.all;
149
+ }
150
+ }
151
+ delete allowedActions.all;
119
152
  }
120
- if (!this.config.customization.timeFormat) {
121
- this.config.customization.timeFormat = 'HH:mm:ss';
153
+ else {
154
+ // by default allow all actions
155
+ for (const key of Object.keys(AllowedActionsEnum)) {
156
+ if (!Object.keys(allowedActions).includes(key)) {
157
+ allowedActions[key] = true;
158
+ }
159
+ }
122
160
  }
123
- if (this.config.resources) {
124
- this.config.resources.forEach((res) => {
125
- var _a;
126
- if (!res.table) {
127
- errors.push(`Resource "${res.dataSource}" is missing table`);
128
- }
129
- // if recordLabel is not callable, throw error
130
- if (res.recordLabel && typeof res.recordLabel !== 'function') {
131
- errors.push(`Resource "${res.dataSource}" recordLabel is not a function`);
132
- }
133
- if (!res.recordLabel) {
134
- res.recordLabel = (item) => {
135
- const pkVal = item[res.columns.find((col) => col.primaryKey).name];
136
- return `${res.label} ${pkVal}`;
137
- };
138
- }
139
- res.resourceId = res.resourceId || res.table;
140
- // as fallback value, capitalize and then replace _ with space
141
- res.label = res.label || res.resourceId.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
142
- if (!res.dataSource) {
143
- errors.push(`Resource "${res.resourceId}" is missing dataSource`);
144
- }
145
- if (!res.columns) {
146
- res.columns = [];
147
- }
148
- res.columns.forEach((col) => {
149
- var _a, _b, _c, _d;
150
- col.label = col.label || guessLabelFromName(col.name);
151
- //define default sortable
152
- if (!Object.keys(col).includes('sortable')) {
153
- col.sortable = true;
154
- }
155
- if (col.showIn && !Array.isArray(col.showIn)) {
156
- errors.push(`Resource "${res.resourceId}" column "${col.name}" showIn must be an array`);
157
- }
158
- // check col.required is string or object
159
- if (col.required && !((typeof col.required === 'boolean') || (typeof col.required === 'object'))) {
160
- errors.push(`Resource "${res.resourceId}" column "${col.name}" required must be a string or object`);
161
- }
162
- // if it is object check the keys are one of ['create', 'edit']
163
- if (typeof col.required === 'object') {
164
- const wrongRequiredOn = Object.keys(col.required).find((c) => !['create', 'edit'].includes(c));
165
- if (wrongRequiredOn) {
166
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid required value "${wrongRequiredOn}", allowed keys are 'create', 'edit']`);
161
+ return allowedActions;
162
+ }
163
+ validateAndNormalizeBulkActions(res, errors) {
164
+ var _a;
165
+ //check if resource has bulkActions
166
+ let bulkActions = ((_a = res === null || res === void 0 ? void 0 : res.options) === null || _a === void 0 ? void 0 : _a.bulkActions) || [];
167
+ if (!Array.isArray(bulkActions)) {
168
+ errors.push(`Resource "${res.resourceId}" bulkActions must be an array`);
169
+ bulkActions = [];
170
+ }
171
+ bulkActions.push({
172
+ label: `Delete checked`,
173
+ state: 'danger',
174
+ icon: 'flowbite:trash-bin-outline',
175
+ confirm: 'Are you sure you want to delete selected items?',
176
+ allowed: async ({ resource, adminUser, allowedActions }) => { return allowedActions.delete; },
177
+ action: async ({ selectedIds, adminUser }) => {
178
+ const connector = this.adminforth.connectors[res.dataSource];
179
+ // for now if at least one error, stop and return error
180
+ let error = null;
181
+ await Promise.all(selectedIds.map(async (recordId) => {
182
+ const record = await connector.getRecordByPrimaryKey(res, recordId);
183
+ await Promise.all(res.hooks.delete.beforeSave.map(async (hook) => {
184
+ const resp = await hook({
185
+ recordId: recordId,
186
+ resource: res,
187
+ record,
188
+ adminUser,
189
+ adminforth: this.adminforth
190
+ });
191
+ if (!error && resp.error) {
192
+ error = resp.error;
167
193
  }
194
+ }));
195
+ if (error) {
196
+ return;
197
+ }
198
+ await connector.deleteRecord({ resource: res, recordId });
199
+ // call afterDelete hook
200
+ await Promise.all(res.hooks.delete.afterSave.map(async (hook) => {
201
+ await hook({
202
+ resource: res,
203
+ record,
204
+ adminUser,
205
+ recordId: recordId,
206
+ adminforth: this.adminforth,
207
+ });
208
+ }));
209
+ }));
210
+ if (error) {
211
+ return { error, ok: false };
212
+ }
213
+ return { ok: true, successMessage: `${selectedIds.length} item${selectedIds.length > 1 ? 's' : ''} deleted` };
214
+ }
215
+ });
216
+ bulkActions.map((action) => {
217
+ if (!action.id) {
218
+ action.id = md5hash(action.label);
219
+ }
220
+ });
221
+ return bulkActions;
222
+ }
223
+ validateAndNormalizeResources(errors) {
224
+ if (!this.inputConfig.resources) {
225
+ errors.push('No resources defined, at least one resource must be defined');
226
+ return [];
227
+ }
228
+ return this.inputConfig.resources.map((resInput) => {
229
+ const res = Object.assign(Object.assign({}, resInput), { options: undefined });
230
+ if (!res.table) {
231
+ errors.push(`Resource in "${res.dataSource}" is missing table`);
232
+ }
233
+ res.resourceId = res.resourceId || res.table;
234
+ // if recordLabel is not callable, throw error
235
+ if (res.recordLabel && typeof res.recordLabel !== 'function') {
236
+ errors.push(`Resource "${res.resourceId}" recordLabel is not a function`);
237
+ }
238
+ if (!res.recordLabel) {
239
+ res.recordLabel = (item) => {
240
+ const pkVal = item[res.columns.find((col) => col.primaryKey).name];
241
+ return `${res.label} ${pkVal}`;
242
+ };
243
+ }
244
+ // as fallback value, capitalize and then replace _ with space
245
+ res.label = res.label || res.resourceId.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
246
+ if (!res.dataSource) {
247
+ errors.push(`Resource "${res.resourceId}" is missing dataSource`);
248
+ }
249
+ if (!res.columns) {
250
+ res.columns = [];
251
+ }
252
+ res.columns = res.columns.map((inCol) => {
253
+ var _a, _b, _c, _d;
254
+ const col = Object.assign({}, inCol);
255
+ col.label = col.label || guessLabelFromName(col.name);
256
+ //define default sortable
257
+ if (!Object.keys(col).includes('sortable')) {
258
+ col.sortable = true;
259
+ }
260
+ if (col.showIn && !Array.isArray(col.showIn)) {
261
+ errors.push(`Resource "${res.resourceId}" column "${col.name}" showIn must be an array`);
262
+ }
263
+ // check col.required is string or object
264
+ if (col.required && !((typeof col.required === 'boolean') || (typeof col.required === 'object'))) {
265
+ errors.push(`Resource "${res.resourceId}" column "${col.name}" required must be a string or object`);
266
+ }
267
+ // if it is object check the keys are one of ['create', 'edit']
268
+ if (typeof col.required === 'object') {
269
+ const wrongRequiredOn = Object.keys(col.required).find((c) => !['create', 'edit'].includes(c));
270
+ if (wrongRequiredOn) {
271
+ errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid required value "${wrongRequiredOn}", allowed keys are 'create', 'edit']`);
168
272
  }
169
- // same for editingNote
170
- if (col.editingNote && !((typeof col.editingNote === 'string') || (typeof col.editingNote === 'object'))) {
171
- errors.push(`Resource "${res.resourceId}" column "${col.name}" editingNote must be a string or object`);
273
+ }
274
+ // same for editingNote
275
+ if (col.editingNote && !((typeof col.editingNote === 'string') || (typeof col.editingNote === 'object'))) {
276
+ errors.push(`Resource "${res.resourceId}" column "${col.name}" editingNote must be a string or object`);
277
+ }
278
+ if (typeof col.editingNote === 'object') {
279
+ const wrongEditingNoteOn = Object.keys(col.editingNote).find((c) => !['create', 'edit'].includes(c));
280
+ if (wrongEditingNoteOn) {
281
+ errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid editingNote value "${wrongEditingNoteOn}", allowed keys are 'create', 'edit']`);
172
282
  }
173
- if (typeof col.editingNote === 'object') {
174
- const wrongEditingNoteOn = Object.keys(col.editingNote).find((c) => !['create', 'edit'].includes(c));
175
- if (wrongEditingNoteOn) {
176
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid editingNote value "${wrongEditingNoteOn}", allowed keys are 'create', 'edit']`);
283
+ }
284
+ const wrongShowIn = col.showIn && col.showIn.find((c) => AdminForthResourcePages[c] === undefined);
285
+ if (wrongShowIn) {
286
+ errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid showIn value "${wrongShowIn}", allowed values are ${Object.keys(AdminForthResourcePages).join(', ')}`);
287
+ }
288
+ col.showIn = col.showIn || Object.values(AdminForthResourcePages);
289
+ if (col.foreignResource) {
290
+ if (!col.foreignResource.resourceId) {
291
+ errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource without resourceId`);
292
+ }
293
+ // we do || here because 'resourceId' might yet not be assigned from 'table'
294
+ const resource = this.inputConfig.resources.find((r) => r.resourceId === col.foreignResource.resourceId || r.table === col.foreignResource.resourceId);
295
+ if (!resource) {
296
+ const similar = suggestIfTypo(this.inputConfig.resources.map((r) => r.resourceId || r.table), col.foreignResource.resourceId);
297
+ errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource resourceId which is not in resources: "${col.foreignResource.resourceId}".
298
+ ${similar ? `Did you mean "${similar}" instead of "${col.foreignResource.resourceId}"?` : ''}`);
299
+ }
300
+ const befHook = (_b = (_a = col.foreignResource.hooks) === null || _a === void 0 ? void 0 : _a.dropdownList) === null || _b === void 0 ? void 0 : _b.beforeDatasourceRequest;
301
+ if (befHook) {
302
+ if (!Array.isArray(befHook)) {
303
+ col.foreignResource.hooks.dropdownList.beforeDatasourceRequest = [befHook];
177
304
  }
178
305
  }
179
- const wrongShowIn = col.showIn && col.showIn.find((c) => AdminForthResourcePages[c] === undefined);
180
- if (wrongShowIn) {
181
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid showIn value "${wrongShowIn}", allowed values are ${Object.keys(AdminForthResourcePages).join(', ')}`);
182
- }
183
- col.showIn = col.showIn || Object.values(AdminForthResourcePages);
184
- if (col.foreignResource) {
185
- if (!col.foreignResource.resourceId) {
186
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource without resourceId`);
187
- }
188
- const resource = this.config.resources.find((r) => r.resourceId === col.foreignResource.resourceId);
189
- if (!resource) {
190
- const similar = suggestIfTypo(this.config.resources.map((r) => r.resourceId), col.foreignResource.resourceId);
191
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource resourceId which is not in resources: "${col.foreignResource.resourceId}".
192
- ${similar ? `Did you mean "${similar}" instead of "${col.foreignResource.resourceId}"?` : ''}`);
193
- }
194
- const befHook = (_b = (_a = col.foreignResource.hooks) === null || _a === void 0 ? void 0 : _a.dropdownList) === null || _b === void 0 ? void 0 : _b.beforeDatasourceRequest;
195
- if (befHook) {
196
- if (!Array.isArray(befHook)) {
197
- col.foreignResource.hooks.dropdownList.beforeDatasourceRequest = [befHook];
198
- }
199
- }
200
- const aftHook = (_d = (_c = col.foreignResource.hooks) === null || _c === void 0 ? void 0 : _c.dropdownList) === null || _d === void 0 ? void 0 : _d.afterDatasourceResponse;
201
- if (aftHook) {
202
- if (!Array.isArray(aftHook)) {
203
- col.foreignResource.hooks.dropdownList.afterDatasourceResponse = [aftHook];
204
- }
306
+ const aftHook = (_d = (_c = col.foreignResource.hooks) === null || _c === void 0 ? void 0 : _c.dropdownList) === null || _d === void 0 ? void 0 : _d.afterDatasourceResponse;
307
+ if (aftHook) {
308
+ if (!Array.isArray(aftHook)) {
309
+ col.foreignResource.hooks.dropdownList.afterDatasourceResponse = [aftHook];
205
310
  }
206
311
  }
207
- });
208
- if (!res.options) {
209
- res.options = { bulkActions: [], allowedActions: {} };
210
- }
211
- if (!res.options.allowedActions) {
212
- res.options.allowedActions = {
213
- all: true,
214
- };
215
- }
216
- if (res.options.defaultSort) {
217
- const colName = res.options.defaultSort.columnName;
218
- const col = res.columns.find((c) => c.name === colName);
219
- if (!col) {
220
- const similar = suggestIfTypo(res.columns.map((c) => c.name), colName);
221
- errors.push(`Resource "${res.resourceId}" defaultSort.columnName column "${colName}" not found in columns. ${similar ? `Did you mean "${similar}"?` : ''}`);
222
- }
223
- const dir = res.options.defaultSort.direction;
224
- if (!dir) {
225
- errors.push(`Resource "${res.resourceId}" defaultSort.direction is missing`);
226
- }
227
- // AdminForthSortDirections is enum
228
- if (!Object.values(AdminForthSortDirections).includes(dir)) {
229
- errors.push(`Resource "${res.resourceId}" defaultSort.direction "${dir}" is invalid, allowed values are ${Object.values(AdminForthSortDirections).join(', ')}`);
230
- }
231
312
  }
232
- if (Object.keys(res.options.allowedActions).includes('all')) {
233
- if (Object.keys(res.options.allowedActions).length > 1) {
234
- errors.push(`Resource "${res.resourceId}" allowedActions cannot have "all" and other keys at same time: ${Object.keys(res.options.allowedActions).join(', ')}`);
313
+ // check is all custom components files exists
314
+ if (col.components) {
315
+ for (const [key, comp] of Object.entries(col.components)) {
316
+ col.components[key] = this.validateComponent(comp, errors);
235
317
  }
236
- for (const key of Object.keys(AllowedActionsEnum)) {
237
- if (key !== 'all') {
238
- res.options.allowedActions[key] = res.options.allowedActions.all;
239
- }
240
- }
241
- delete res.options.allowedActions.all;
242
318
  }
243
- else {
244
- // by default allow all actions
245
- for (const key of Object.keys(AllowedActionsEnum)) {
246
- if (!Object.keys(res.options.allowedActions).includes(key)) {
247
- res.options.allowedActions[key] = true;
248
- }
249
- }
319
+ return col;
320
+ });
321
+ const options = Object.assign(Object.assign({}, resInput.options), { bulkActions: undefined, allowedActions: undefined });
322
+ options.allowedActions = this.validateAndNormalizeAllowedActions(resInput, errors);
323
+ if (options.defaultSort) {
324
+ const colName = options.defaultSort.columnName;
325
+ const col = res.columns.find((c) => c.name === colName);
326
+ if (!col) {
327
+ const similar = suggestIfTypo(res.columns.map((c) => c.name), colName);
328
+ errors.push(`Resource "${res.resourceId}" defaultSort.columnName column "${colName}" not found in columns. ${similar ? `Did you mean "${similar}"?` : ''}`);
250
329
  }
251
- //check if resource has bulkActions
252
- let bulkActions = ((_a = res === null || res === void 0 ? void 0 : res.options) === null || _a === void 0 ? void 0 : _a.bulkActions) || [];
253
- if (!Array.isArray(bulkActions)) {
254
- errors.push(`Resource "${res.resourceId}" bulkActions must be an array`);
255
- bulkActions = [];
256
- }
257
- if (!bulkActions.find((action) => action.label === 'Delete checked')) {
258
- bulkActions.push({
259
- label: `Delete checked`,
260
- state: 'danger',
261
- icon: 'flowbite:trash-bin-outline',
262
- confirm: 'Are you sure you want to delete selected items?',
263
- allowed: async ({ resource, adminUser, allowedActions }) => { return allowedActions.delete; },
264
- action: async ({ selectedIds, adminUser }) => {
265
- const connector = this.adminforth.connectors[res.dataSource];
266
- // for now if at least one error, stop and return error
267
- let error = null;
268
- await Promise.all(selectedIds.map(async (recordId) => {
269
- const record = await connector.getRecordByPrimaryKey(res, recordId);
270
- await Promise.all(res.hooks.delete.beforeSave.map(async (hook) => {
271
- const resp = await hook({
272
- recordId: recordId,
273
- resource: res,
274
- record,
275
- adminUser,
276
- adminforth: this.adminforth
277
- });
278
- if (!error && resp.error) {
279
- error = resp.error;
280
- }
281
- }));
282
- if (error) {
283
- return;
284
- }
285
- await connector.deleteRecord({ resource: res, recordId });
286
- // call afterDelete hook
287
- await Promise.all(res.hooks.delete.afterSave.map(async (hook) => {
288
- await hook({
289
- resource: res,
290
- record,
291
- adminUser,
292
- recordId: recordId,
293
- adminforth: this.adminforth,
294
- });
295
- }));
296
- }));
297
- if (error) {
298
- return { error, ok: false };
299
- }
300
- return { ok: true, successMessage: `${selectedIds.length} item${selectedIds.length > 1 ? 's' : ''} deleted` };
301
- }
302
- });
330
+ const dir = options.defaultSort.direction;
331
+ if (!dir) {
332
+ errors.push(`Resource "${res.resourceId}" defaultSort.direction is missing`);
303
333
  }
304
- bulkActions.map((action) => {
305
- if (!action.id) {
306
- action.id = md5hash(action.label);
307
- }
308
- });
309
- res.options.bulkActions = bulkActions;
310
- // if pageInjection is a string, make array with one element. Also check file exists
311
- const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems', 'customActionIcons'];
312
- const possiblePages = ['list', 'show', 'create', 'edit'];
313
- if (res.options.pageInjections) {
314
- Object.entries(res.options.pageInjections).map(([key, value]) => {
315
- if (!possiblePages.includes(key)) {
316
- const similar = suggestIfTypo(possiblePages, key);
317
- errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${key}", allowed keys are ${possiblePages.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
334
+ // AdminForthSortDirections is enum
335
+ if (!Object.values(AdminForthSortDirections).includes(dir)) {
336
+ errors.push(`Resource "${res.resourceId}" defaultSort.direction "${dir}" is invalid, allowed values are ${Object.values(AdminForthSortDirections).join(', ')}`);
337
+ }
338
+ }
339
+ options.bulkActions = this.validateAndNormalizeBulkActions(res, errors);
340
+ // if pageInjection is a string, make array with one element. Also check file exists
341
+ const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems', 'customActionIcons'];
342
+ const possiblePages = ['list', 'show', 'create', 'edit'];
343
+ if (options.pageInjections) {
344
+ Object.entries(options.pageInjections).map(([key, value]) => {
345
+ if (!possiblePages.includes(key)) {
346
+ const similar = suggestIfTypo(possiblePages, key);
347
+ errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${key}", allowed keys are ${possiblePages.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
348
+ }
349
+ Object.entries(value).map(([injection, target]) => {
350
+ if (possibleInjections.includes(injection)) {
351
+ this.validateAndListifyInjection(options.pageInjections[key], injection, errors);
352
+ }
353
+ else {
354
+ const similar = suggestIfTypo(possibleInjections, injection);
355
+ errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')} ${similar ? `Did you mean "${similar}"?` : ''}`);
318
356
  }
319
- Object.entries(value).map(([injection, target]) => {
320
- if (possibleInjections.includes(injection)) {
321
- this.validateAndListifyInjection(res.options.pageInjections[key], injection, errors);
322
- }
323
- else {
324
- const similar = suggestIfTypo(possibleInjections, injection);
325
- errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')} ${similar ? `Did you mean "${similar}"?` : ''}`);
326
- }
327
- });
328
357
  });
358
+ });
359
+ }
360
+ res.options = options;
361
+ // transform all hooks Functions to array of functions
362
+ if (!res.hooks) {
363
+ res.hooks = {};
364
+ }
365
+ for (const hookName of ['show', 'list']) {
366
+ if (!res.hooks[hookName]) {
367
+ res.hooks[hookName] = {};
329
368
  }
330
- // transform all hooks Functions to array of functions
331
- if (!res.hooks) {
332
- res.hooks = {};
369
+ if (!res.hooks[hookName].beforeDatasourceRequest) {
370
+ res.hooks[hookName].beforeDatasourceRequest = [];
333
371
  }
334
- for (const hookName of ['show', 'list']) {
335
- if (!res.hooks[hookName]) {
336
- res.hooks[hookName] = {};
337
- }
338
- if (!res.hooks[hookName].beforeDatasourceRequest) {
339
- res.hooks[hookName].beforeDatasourceRequest = [];
340
- }
341
- if (!Array.isArray(res.hooks[hookName].beforeDatasourceRequest)) {
342
- res.hooks[hookName].beforeDatasourceRequest = [res.hooks[hookName].beforeDatasourceRequest];
343
- }
344
- if (!res.hooks[hookName].afterDatasourceResponse) {
345
- res.hooks[hookName].afterDatasourceResponse = [];
346
- }
347
- if (!Array.isArray(res.hooks[hookName].afterDatasourceResponse)) {
348
- res.hooks[hookName].afterDatasourceResponse = [res.hooks[hookName].afterDatasourceResponse];
349
- }
372
+ if (!Array.isArray(res.hooks[hookName].beforeDatasourceRequest)) {
373
+ res.hooks[hookName].beforeDatasourceRequest = [res.hooks[hookName].beforeDatasourceRequest];
350
374
  }
351
- for (const hookName of ['create', 'edit', 'delete']) {
352
- if (!res.hooks[hookName]) {
353
- res.hooks[hookName] = {};
354
- }
355
- if (!res.hooks[hookName].beforeSave) {
356
- res.hooks[hookName].beforeSave = [];
357
- }
358
- if (!Array.isArray(res.hooks[hookName].beforeSave)) {
359
- res.hooks[hookName].beforeSave = [res.hooks[hookName].beforeSave];
360
- }
361
- if (!res.hooks[hookName].afterSave) {
362
- res.hooks[hookName].afterSave = [];
363
- }
364
- if (!Array.isArray(res.hooks[hookName].afterSave)) {
365
- res.hooks[hookName].afterSave = [res.hooks[hookName].afterSave];
366
- }
375
+ if (!res.hooks[hookName].afterDatasourceResponse) {
376
+ res.hooks[hookName].afterDatasourceResponse = [];
377
+ }
378
+ if (!Array.isArray(res.hooks[hookName].afterDatasourceResponse)) {
379
+ res.hooks[hookName].afterDatasourceResponse = [res.hooks[hookName].afterDatasourceResponse];
367
380
  }
368
- });
369
- if (this.config.customization.globalInjections) {
370
- const ALLOWED_GLOBAL_INJECTIONS = ['userMenu', 'header', 'sidebar',];
371
- Object.keys(this.config.customization.globalInjections).forEach((injection) => {
372
- if (ALLOWED_GLOBAL_INJECTIONS.includes(injection)) {
373
- this.validateAndListifyInjection(this.config.customization.globalInjections, injection, errors);
374
- }
375
- else {
376
- const similar = suggestIfTypo(ALLOWED_GLOBAL_INJECTIONS, injection);
377
- errors.push(`Global injection key "${injection}" is not allowed. Allowed keys are ${ALLOWED_GLOBAL_INJECTIONS.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
378
- }
379
- });
380
- }
381
- if (this.config.customization.loginPageInjections) {
382
- const ALLOWED_GLOBAL_INJECTIONS = ['underInputs'];
383
- Object.keys(this.config.customization.loginPageInjections).forEach((injection) => {
384
- if (ALLOWED_GLOBAL_INJECTIONS.includes(injection)) {
385
- this.validateAndListifyInjection(this.config.customization.loginPageInjections, injection, errors);
386
- }
387
- else {
388
- const similar = suggestIfTypo(ALLOWED_GLOBAL_INJECTIONS, injection);
389
- errors.push(`Login page injection key "${injection}" is not allowed. Allowed keys are ${ALLOWED_GLOBAL_INJECTIONS.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
390
- }
391
- });
392
381
  }
393
- if (!this.config.menu) {
394
- errors.push('No config.menu defined');
382
+ for (const hookName of ['create', 'edit', 'delete']) {
383
+ if (!res.hooks[hookName]) {
384
+ res.hooks[hookName] = {};
385
+ }
386
+ if (!res.hooks[hookName].beforeSave) {
387
+ res.hooks[hookName].beforeSave = [];
388
+ }
389
+ if (!Array.isArray(res.hooks[hookName].beforeSave)) {
390
+ res.hooks[hookName].beforeSave = [res.hooks[hookName].beforeSave];
391
+ }
392
+ if (!res.hooks[hookName].afterSave) {
393
+ res.hooks[hookName].afterSave = [];
394
+ }
395
+ if (!Array.isArray(res.hooks[hookName].afterSave)) {
396
+ res.hooks[hookName].afterSave = [res.hooks[hookName].afterSave];
397
+ }
395
398
  }
396
- // check if there is only one homepage: true in menu, recursivly
397
- let homepages = 0;
398
- const browseMenu = (menu) => {
399
- menu.forEach((item) => {
400
- if (item.component && item.resourceId) {
401
- errors.push(`Menu item cannot have both component and resourceId: ${JSON.stringify(item)}`);
402
- }
403
- if (item.component && !item.path) {
404
- errors.push(`Menu item with component must have path : ${JSON.stringify(item)}`);
405
- }
406
- if (item.type === 'resource' && !item.resourceId) {
407
- errors.push(`Menu item with type 'resource' must have resourceId : ${JSON.stringify(item)}`);
408
- }
409
- if (item.resourceId && !this.config.resources.find((res) => res.resourceId === item.resourceId)) {
410
- const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId), item.resourceId);
411
- errors.push(`Menu item with type 'resourceId' has resourceId which is not in resources: "${JSON.stringify(item)}".
412
- ${similar ? `Did you mean "${similar}" instead of "${item.resourceId}"?` : ''}`);
413
- }
414
- if (item.type === 'component' && !item.component) {
415
- errors.push(`Menu item with type 'component' must have component : ${JSON.stringify(item)}`);
416
- }
417
- // make sure component starts with @@
418
- if (item.component) {
419
- if (!item.component.startsWith('@@')) {
420
- errors.push(`Menu item component must start with @@ : ${JSON.stringify(item)}`);
421
- }
422
- const path = item.component.replace('@@', this.config.customization.customComponentsDir);
423
- if (!fs.existsSync(path)) {
424
- errors.push(`Menu item component "${item.component.replace('@@', '')}" does not exist in "${this.config.customization.customComponentsDir}"`);
425
- }
399
+ return res;
400
+ });
401
+ }
402
+ validateConfig() {
403
+ const errors = [];
404
+ const newConfig = Object.assign(Object.assign({}, this.inputConfig), { customization: this.validateAndNormalizeCustomization(errors), resources: this.validateAndNormalizeResources(errors) });
405
+ if (!newConfig.baseUrl) {
406
+ newConfig.baseUrl = '';
407
+ }
408
+ if (!newConfig.baseUrl.endsWith('/')) {
409
+ newConfig.baseUrlSlashed = `${newConfig.baseUrl}/`;
410
+ }
411
+ else {
412
+ newConfig.baseUrlSlashed = newConfig.baseUrl;
413
+ }
414
+ if (!newConfig.menu) {
415
+ errors.push('No config.menu defined');
416
+ }
417
+ // check if there is only one homepage: true in menu, recursivly
418
+ let homepages = 0;
419
+ const browseMenu = (menu) => {
420
+ menu.forEach((item) => {
421
+ if (item.component && item.resourceId) {
422
+ errors.push(`Menu item cannot have both component and resourceId: ${JSON.stringify(item)}`);
423
+ }
424
+ if (item.component && !item.path) {
425
+ errors.push(`Menu item with component must have path : ${JSON.stringify(item)}`);
426
+ }
427
+ if (item.type === 'resource' && !item.resourceId) {
428
+ errors.push(`Menu item with type 'resource' must have resourceId : ${JSON.stringify(item)}`);
429
+ }
430
+ if (item.resourceId && !newConfig.resources.find((res) => res.resourceId === item.resourceId)) {
431
+ const similar = suggestIfTypo(newConfig.resources.map((res) => res.resourceId), item.resourceId);
432
+ errors.push(`Menu item with type 'resourceId' has resourceId which is not in resources: "${JSON.stringify(item)}".
433
+ ${similar ? `Did you mean "${similar}" instead of "${item.resourceId}"?` : ''}`);
434
+ }
435
+ if (item.type === 'page' && !item.component) {
436
+ errors.push(`Menu item with type 'component' must have component : ${JSON.stringify(item)}`);
437
+ }
438
+ // make sure component starts with @@
439
+ if (item.component) {
440
+ if (!item.component.startsWith('@@')) {
441
+ errors.push(`Menu item component must start with @@ : ${JSON.stringify(item)}`);
426
442
  }
427
- if (item.homepage) {
428
- homepages++;
429
- if (homepages > 1) {
430
- errors.push('There must be only one homepage: true in menu, found second one in ' + JSON.stringify(item));
431
- }
443
+ const path = item.component.replace('@@', newConfig.customization.customComponentsDir);
444
+ if (!fs.existsSync(path)) {
445
+ errors.push(`Menu item component "${item.component.replace('@@', '')}" does not exist in "${newConfig.customization.customComponentsDir}"`);
432
446
  }
433
- if (item.children) {
434
- browseMenu(item.children);
447
+ }
448
+ if (item.homepage) {
449
+ homepages++;
450
+ if (homepages > 1) {
451
+ errors.push('There must be only one homepage: true in menu, found second one in ' + JSON.stringify(item));
435
452
  }
436
- });
437
- };
438
- browseMenu(this.config.menu);
439
- }
440
- if (this.config.auth) {
441
- // TODO: remove in future releases
442
- if (!this.config.auth.usersResourceId && this.config.auth.resourceId) {
443
- this.config.auth.usersResourceId = this.config.auth.resourceId;
444
- }
445
- if (!this.config.auth.usersResourceId) {
453
+ }
454
+ if (item.children) {
455
+ browseMenu(item.children);
456
+ }
457
+ });
458
+ };
459
+ browseMenu(newConfig.menu);
460
+ // AUTH checks
461
+ if (newConfig.auth) {
462
+ if (!newConfig.auth.usersResourceId) {
446
463
  throw new Error('No config.auth.usersResourceId defined');
447
464
  }
448
- if (!this.config.auth.passwordHashField) {
465
+ if (!newConfig.auth.passwordHashField) {
449
466
  throw new Error('No config.auth.passwordHashField defined');
450
467
  }
451
- if (!this.config.auth.usernameField) {
468
+ if (!newConfig.auth.usernameField) {
452
469
  throw new Error('No config.auth.usernameField defined');
453
470
  }
454
- if (this.config.auth.loginBackgroundImage) {
455
- errors.push(...this.checkCustomFileExists(this.config.auth.loginBackgroundImage));
471
+ if (newConfig.auth.loginBackgroundImage) {
472
+ errors.push(...this.checkCustomFileExists(newConfig.auth.loginBackgroundImage));
456
473
  }
457
- const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
474
+ const userResource = newConfig.resources.find((res) => res.resourceId === newConfig.auth.usersResourceId);
458
475
  if (!userResource) {
459
- const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId), this.config.auth.usersResourceId);
460
- throw new Error(`Resource with id "${this.config.auth.usersResourceId}" not found. ${similar ? `Did you mean "${similar}"?` : ''}`);
476
+ const similar = suggestIfTypo(newConfig.resources.map((res) => res.resourceId), newConfig.auth.usersResourceId);
477
+ throw new Error(`Resource with id "${newConfig.auth.usersResourceId}" not found. ${similar ? `Did you mean "${similar}"?` : ''}`);
461
478
  }
462
- if (!this.config.auth.beforeLoginConfirmation) {
463
- this.config.auth.beforeLoginConfirmation = [];
479
+ if (!newConfig.auth.beforeLoginConfirmation) {
480
+ newConfig.auth.beforeLoginConfirmation = [];
464
481
  }
465
482
  }
466
483
  // check for duplicate resourceIds and show which ones are duplicated
467
- const resourceIds = this.config.resources.map((res) => res.resourceId);
484
+ const resourceIds = newConfig.resources.map((res) => res.resourceId);
468
485
  const uniqueResourceIds = new Set(resourceIds);
469
486
  if (uniqueResourceIds.size != resourceIds.length) {
470
487
  const duplicates = resourceIds.filter((item, index) => resourceIds.indexOf(item) != index);
@@ -474,16 +491,7 @@ export default class ConfigValidator {
474
491
  if (errors.length > 0) {
475
492
  throw new Error(`Invalid AdminForth config: ${errors.join(', ')}`);
476
493
  }
477
- // check is all custom components files exists
478
- for (const resource of this.config.resources) {
479
- for (const column of resource.columns) {
480
- if (column.components) {
481
- for (const [key, comp] of Object.entries(column.components)) {
482
- column.components[key] = this.validateComponent(comp, errors);
483
- }
484
- }
485
- }
486
- }
494
+ this.adminforth.config = newConfig;
487
495
  }
488
496
  postProcessAfterDiscover(resource) {
489
497
  resource.columns.forEach((column) => {