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,408 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { AdminForthResourcePages, AllowedActionsEnum, } from "../types/AdminForthConfig.js";
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import { guessLabelFromName } from './utils.js';
14
+ import { v4 as uuid } from 'uuid';
15
+ export default class ConfigValidator {
16
+ constructor(adminforth, config) {
17
+ this.adminforth = adminforth;
18
+ this.config = config;
19
+ this.adminforth = adminforth;
20
+ this.config = config;
21
+ }
22
+ checkCustomFileExists(filePath) {
23
+ if (filePath.startsWith('@@/')) {
24
+ const checkPath = path.join(this.config.customization.customComponentsDir, filePath.replace('@@/', ''));
25
+ if (!fs.existsSync(checkPath)) {
26
+ return [`File file ${filePath} does not exist in ${this.config.customization.customComponentsDir}`];
27
+ }
28
+ }
29
+ return [];
30
+ }
31
+ validateComponent(component, errors, ignoreExistsCheck = false) {
32
+ if (!component) {
33
+ return component;
34
+ }
35
+ let obj;
36
+ if (typeof component === 'string') {
37
+ obj = { file: component, meta: {} };
38
+ }
39
+ else {
40
+ obj = component;
41
+ }
42
+ if (!ignoreExistsCheck) {
43
+ errors.push(...this.checkCustomFileExists(obj.file));
44
+ }
45
+ return obj;
46
+ }
47
+ validateConfig() {
48
+ var _a;
49
+ const errors = [];
50
+ if (this.config.rootUser) {
51
+ if (!this.config.rootUser.username) {
52
+ throw new Error('rootUser.username is required');
53
+ }
54
+ if (!this.config.rootUser.password) {
55
+ throw new Error('rootUser.password is required');
56
+ }
57
+ console.log('\n ⚠️⚠️⚠️ [INSECURE ALERT] config.rootUser is set, please create a new user and remove config.rootUser from config ASAP when you are in production\n');
58
+ }
59
+ if (!this.config.customization.customComponentsDir) {
60
+ this.config.customization.customComponentsDir = './custom';
61
+ }
62
+ try {
63
+ // check customComponentsDir exists
64
+ fs.accessSync(this.config.customization.customComponentsDir, fs.constants.R_OK);
65
+ }
66
+ catch (e) {
67
+ this.config.customization.customComponentsDir = undefined;
68
+ }
69
+ if (this.config.auth) {
70
+ if (!this.config.auth.resourceId) {
71
+ throw new Error('No config.auth.resourceId defined');
72
+ }
73
+ if (!this.config.auth.passwordHashField) {
74
+ throw new Error('No config.auth.passwordHashField defined');
75
+ }
76
+ if (!this.config.auth.usernameField) {
77
+ throw new Error('No config.auth.usernameField defined');
78
+ }
79
+ if (this.config.auth.loginBackgroundImage) {
80
+ errors.push(...this.checkCustomFileExists(this.config.auth.loginBackgroundImage));
81
+ }
82
+ const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.resourceId);
83
+ if (!userResource) {
84
+ throw new Error(`Resource with id "${this.config.auth.resourceId}" not found`);
85
+ }
86
+ if (!this.config.auth.beforeLoginConfirmation) {
87
+ this.config.auth.beforeLoginConfirmation = [];
88
+ }
89
+ }
90
+ if (!this.config.customization) {
91
+ this.config.customization = {};
92
+ }
93
+ if (!this.config.customization.customComponentsDir) {
94
+ this.config.customization.customComponentsDir = './custom';
95
+ }
96
+ try {
97
+ // check customComponentsDir exists
98
+ fs.accessSync(this.config.customization.customComponentsDir, fs.constants.R_OK);
99
+ }
100
+ catch (e) {
101
+ this.config.customization.customComponentsDir = undefined;
102
+ }
103
+ if (this.config.customization.customPages) {
104
+ this.config.customization.customPages.forEach((page, i) => {
105
+ // validate component if it's not plugin injection
106
+ if (this.adminforth.codeInjector.allComponentNames.hasOwnProperty(page.component)) {
107
+ const validatedPage = this.validateComponent(page.component, errors, true);
108
+ }
109
+ });
110
+ }
111
+ else {
112
+ this.config.customization.customPages = [];
113
+ }
114
+ if (!this.config.baseUrl) {
115
+ this.config.baseUrl = '';
116
+ }
117
+ if (!this.config.baseUrl.endsWith('/')) {
118
+ this.adminforth.baseUrlSlashed = this.config.baseUrl + '/';
119
+ }
120
+ else {
121
+ this.adminforth.baseUrlSlashed = this.config.baseUrl;
122
+ }
123
+ if (((_a = this.config) === null || _a === void 0 ? void 0 : _a.customization.brandName) === undefined) {
124
+ this.config.customization.brandName = 'AdminForth';
125
+ }
126
+ if (this.config.customization.brandLogo) {
127
+ errors.push(...this.checkCustomFileExists(this.config.customization.brandLogo));
128
+ }
129
+ if (this.config.customization.favicon) {
130
+ errors.push(...this.checkCustomFileExists(this.config.customization.favicon));
131
+ }
132
+ if (!this.config.customization.datesFormat) {
133
+ this.config.customization.datesFormat = 'MMM D, YYYY HH:mm:ss';
134
+ }
135
+ if (this.config.resources) {
136
+ this.config.resources.forEach((res) => {
137
+ var _a, _b, _c;
138
+ if (!res.table) {
139
+ errors.push(`Resource "${res.dataSource}" is missing table`);
140
+ }
141
+ // if recordLabel is not callable, throw error
142
+ if (res.recordLabel && typeof res.recordLabel !== 'function') {
143
+ errors.push(`Resource "${res.dataSource}" recordLabel is not a function`);
144
+ }
145
+ if (!res.recordLabel) {
146
+ res.recordLabel = (item) => {
147
+ const pkVal = item[res.columns.find((col) => col.primaryKey).name];
148
+ return `${res.label} ${pkVal}`;
149
+ };
150
+ }
151
+ res.resourceId = res.resourceId || res.table;
152
+ res.label = res.label || res.table.charAt(0).toUpperCase() + res.table.slice(1);
153
+ if (!res.dataSource) {
154
+ errors.push(`Resource "${res.resourceId}" is missing dataSource`);
155
+ }
156
+ if (!res.columns) {
157
+ res.columns = [];
158
+ }
159
+ res.columns.forEach((col) => {
160
+ var _a, _b, _c, _d;
161
+ col.label = col.label || guessLabelFromName(col.name);
162
+ //define default sortable
163
+ if (!Object.keys(col).includes('sortable')) {
164
+ col.sortable = true;
165
+ }
166
+ if (col.showIn && !Array.isArray(col.showIn)) {
167
+ errors.push(`Resource "${res.resourceId}" column "${col.name}" showIn must be an array`);
168
+ }
169
+ // check col.required is string or object
170
+ if (col.required && !((typeof col.required === 'boolean') || (typeof col.required === 'object'))) {
171
+ errors.push(`Resource "${res.resourceId}" column "${col.name}" required must be a string or object`);
172
+ }
173
+ // if it is object check the keys are one of ['create', 'edit']
174
+ if (typeof col.required === 'object') {
175
+ const wrongRequiredOn = Object.keys(col.required).find((c) => !['create', 'edit'].includes(c));
176
+ if (wrongRequiredOn) {
177
+ errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid required value "${wrongRequiredOn}", allowed keys are 'create', 'edit']`);
178
+ }
179
+ }
180
+ // same for editingNote
181
+ if (col.editingNote && !((typeof col.editingNote === 'string') || (typeof col.editingNote === 'object'))) {
182
+ errors.push(`Resource "${res.resourceId}" column "${col.name}" editingNote must be a string or object`);
183
+ }
184
+ if (typeof col.editingNote === 'object') {
185
+ const wrongEditingNoteOn = Object.keys(col.editingNote).find((c) => !['create', 'edit'].includes(c));
186
+ if (wrongEditingNoteOn) {
187
+ errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid editingNote value "${wrongEditingNoteOn}", allowed keys are 'create', 'edit']`);
188
+ }
189
+ }
190
+ const wrongShowIn = col.showIn && col.showIn.find((c) => AdminForthResourcePages[c] === undefined);
191
+ if (wrongShowIn) {
192
+ errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid showIn value "${wrongShowIn}", allowed values are ${Object.keys(AdminForthResourcePages).join(', ')}`);
193
+ }
194
+ col.showIn = col.showIn || Object.values(AdminForthResourcePages);
195
+ if (col.foreignResource) {
196
+ const befHook = (_b = (_a = col.foreignResource.hooks) === null || _a === void 0 ? void 0 : _a.dropdownList) === null || _b === void 0 ? void 0 : _b.beforeDatasourceRequest;
197
+ if (befHook) {
198
+ if (!Array.isArray(befHook)) {
199
+ col.foreignResource.hooks.dropdownList.beforeDatasourceRequest = [befHook];
200
+ }
201
+ }
202
+ const aftHook = (_d = (_c = col.foreignResource.hooks) === null || _c === void 0 ? void 0 : _c.dropdownList) === null || _d === void 0 ? void 0 : _d.afterDatasourceResponse;
203
+ if (aftHook) {
204
+ if (!Array.isArray(aftHook)) {
205
+ col.foreignResource.hooks.dropdownList.afterDatasourceResponse = [aftHook];
206
+ }
207
+ }
208
+ }
209
+ });
210
+ if (!res.options) {
211
+ res.options = { bulkActions: [], allowedActions: {} };
212
+ }
213
+ if (!res.options.allowedActions) {
214
+ res.options.allowedActions = {
215
+ all: true,
216
+ };
217
+ }
218
+ if (Object.keys(res.options.allowedActions).includes('all')) {
219
+ if (Object.keys(res.options.allowedActions).length > 1) {
220
+ errors.push(`Resource "${res.resourceId}" allowedActions cannot have "all" and other keys at same time: ${Object.keys(res.options.allowedActions).join(', ')}`);
221
+ }
222
+ for (const key of Object.keys(AllowedActionsEnum)) {
223
+ if (key !== 'all') {
224
+ res.options.allowedActions[key] = res.options.allowedActions.all;
225
+ }
226
+ }
227
+ delete res.options.allowedActions.all;
228
+ }
229
+ else {
230
+ // by default allow all actions
231
+ for (const key of Object.keys(AllowedActionsEnum)) {
232
+ if (!Object.keys(res.options.allowedActions).includes(key)) {
233
+ res.options.allowedActions[key] = true;
234
+ }
235
+ }
236
+ }
237
+ //check if resource has bulkActions
238
+ let bulkActions = ((_a = res === null || res === void 0 ? void 0 : res.options) === null || _a === void 0 ? void 0 : _a.bulkActions) || [];
239
+ if (!Array.isArray(bulkActions)) {
240
+ errors.push(`Resource "${res.resourceId}" bulkActions must be an array`);
241
+ bulkActions = [];
242
+ }
243
+ if (((_c = (_b = res.options) === null || _b === void 0 ? void 0 : _b.allowedActions) === null || _c === void 0 ? void 0 : _c.delete) && !bulkActions.find((action) => action.label === 'Delete checked')) {
244
+ bulkActions.push({
245
+ label: `Delete checked`,
246
+ state: 'danger',
247
+ icon: 'flowbite:trash-bin-outline',
248
+ action: (_d) => __awaiter(this, [_d], void 0, function* ({ selectedIds }) {
249
+ const connector = this.adminforth.connectors[res.dataSource];
250
+ yield Promise.all(selectedIds.map((recordId) => __awaiter(this, void 0, void 0, function* () {
251
+ yield connector.deleteRecord({ resource: res, recordId });
252
+ })));
253
+ })
254
+ });
255
+ }
256
+ const newBulkActions = bulkActions.map((action) => {
257
+ return Object.assign(action, { id: uuid() });
258
+ });
259
+ res.options.bulkActions = newBulkActions;
260
+ // if pageInjection is a string, make array with one element. Also check file exists
261
+ const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom'];
262
+ if (res.options.pageInjections) {
263
+ Object.entries(res.options.pageInjections).map(([key, value]) => {
264
+ Object.entries(value).map(([injection, target]) => {
265
+ if (possibleInjections.includes(injection)) {
266
+ if (!Array.isArray(res.options.pageInjections[key][injection])) {
267
+ // not array
268
+ res.options.pageInjections[key][injection] = [target];
269
+ }
270
+ res.options.pageInjections[key][injection].forEach((target, i) => {
271
+ res.options.pageInjections[key][injection][i] = this.validateComponent(target, errors);
272
+ });
273
+ }
274
+ else {
275
+ errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')}`);
276
+ }
277
+ });
278
+ });
279
+ }
280
+ // transform all hooks Functions to array of functions
281
+ if (!res.hooks) {
282
+ res.hooks = {};
283
+ }
284
+ for (const hookName of ['show', 'list']) {
285
+ if (!res.hooks[hookName]) {
286
+ res.hooks[hookName] = {};
287
+ }
288
+ if (!res.hooks[hookName].beforeDatasourceRequest) {
289
+ res.hooks[hookName].beforeDatasourceRequest = [];
290
+ }
291
+ if (!Array.isArray(res.hooks[hookName].beforeDatasourceRequest)) {
292
+ res.hooks[hookName].beforeDatasourceRequest = [res.hooks[hookName].beforeDatasourceRequest];
293
+ }
294
+ if (!res.hooks[hookName].afterDatasourceResponse) {
295
+ res.hooks[hookName].afterDatasourceResponse = [];
296
+ }
297
+ if (!Array.isArray(res.hooks[hookName].afterDatasourceResponse)) {
298
+ res.hooks[hookName].afterDatasourceResponse = [res.hooks[hookName].afterDatasourceResponse];
299
+ }
300
+ }
301
+ for (const hookName of ['create', 'edit', 'delete']) {
302
+ if (!res.hooks[hookName]) {
303
+ res.hooks[hookName] = {};
304
+ }
305
+ if (!res.hooks[hookName].beforeSave) {
306
+ res.hooks[hookName].beforeSave = [];
307
+ }
308
+ if (!Array.isArray(res.hooks[hookName].beforeSave)) {
309
+ res.hooks[hookName].beforeSave = [res.hooks[hookName].beforeSave];
310
+ }
311
+ if (!res.hooks[hookName].afterSave) {
312
+ res.hooks[hookName].afterSave = [];
313
+ }
314
+ if (!Array.isArray(res.hooks[hookName].afterSave)) {
315
+ res.hooks[hookName].afterSave = [res.hooks[hookName].afterSave];
316
+ }
317
+ }
318
+ });
319
+ if (!this.config.menu) {
320
+ errors.push('No config.menu defined');
321
+ }
322
+ // check if there is only one homepage: true in menu, recursivly
323
+ let homepages = 0;
324
+ const browseMenu = (menu) => {
325
+ menu.forEach((item) => {
326
+ if (item.component && item.resourceId) {
327
+ errors.push(`Menu item cannot have both component and resourceId: ${JSON.stringify(item)}`);
328
+ }
329
+ if (item.component && !item.path) {
330
+ errors.push(`Menu item with component must have path : ${JSON.stringify(item)}`);
331
+ }
332
+ if (item.type === 'resource' && !item.resourceId) {
333
+ errors.push(`Menu item with type 'resource' must have resourceId : ${JSON.stringify(item)}`);
334
+ }
335
+ if (item.resourceId && !this.config.resources.find((res) => res.resourceId === item.resourceId)) {
336
+ errors.push(`Menu item with type 'resourceId' has resourceId which is not in resources: ${JSON.stringify(item)}`);
337
+ }
338
+ if (item.type === 'component' && !item.component) {
339
+ errors.push(`Menu item with type 'component' must have component : ${JSON.stringify(item)}`);
340
+ }
341
+ // make sure component starts with @@
342
+ if (item.component) {
343
+ if (!item.component.startsWith('@@')) {
344
+ errors.push(`Menu item component must start with @@ : ${JSON.stringify(item)}`);
345
+ }
346
+ const path = item.component.replace('@@', this.config.customization.customComponentsDir);
347
+ if (!fs.existsSync(path)) {
348
+ errors.push(`Menu item component "${item.component.replace('@@', '')}" does not exist in "${this.config.customization.customComponentsDir}"`);
349
+ }
350
+ }
351
+ if (item.homepage) {
352
+ homepages++;
353
+ if (homepages > 1) {
354
+ errors.push('There must be only one homepage: true in menu, found second one in ' + JSON.stringify(item));
355
+ }
356
+ }
357
+ if (item.children) {
358
+ browseMenu(item.children);
359
+ }
360
+ });
361
+ };
362
+ browseMenu(this.config.menu);
363
+ }
364
+ // check for duplicate resourceIds and show which ones are duplicated
365
+ const resourceIds = this.config.resources.map((res) => res.resourceId);
366
+ const uniqueResourceIds = new Set(resourceIds);
367
+ if (uniqueResourceIds.size != resourceIds.length) {
368
+ const duplicates = resourceIds.filter((item, index) => resourceIds.indexOf(item) != index);
369
+ errors.push(`Duplicate fields "resourceId" or "table": ${duplicates.join(', ')}`);
370
+ }
371
+ //add ids for onSelectedAllActions for each resource
372
+ if (errors.length > 0) {
373
+ throw new Error(`Invalid AdminForth config: ${errors.join(', ')}`);
374
+ }
375
+ // check is all custom components files exists
376
+ for (const resource of this.config.resources) {
377
+ for (const column of resource.columns) {
378
+ if (column.components) {
379
+ for (const [key, comp] of Object.entries(column.components)) {
380
+ let ignoreExistsCheck = false;
381
+ if (this.adminforth.codeInjector.allComponentNames[comp.file]) {
382
+ // not obvious, but if we are in this if, it means that this is plugin component
383
+ // and there is no sense to check if it exists in users folder
384
+ ignoreExistsCheck = true;
385
+ }
386
+ column.components[key] = this.validateComponent(comp, errors, ignoreExistsCheck);
387
+ }
388
+ }
389
+ }
390
+ }
391
+ }
392
+ postProcessAfterDiscover(resource) {
393
+ resource.columns.forEach((column) => {
394
+ // if db/user says column is required in boolean, expand
395
+ if (typeof column.required === 'boolean') {
396
+ column.required = { create: column.required, edit: column.required };
397
+ }
398
+ if (!column.required) {
399
+ column.required = { create: false, edit: false };
400
+ }
401
+ // same for editingNote
402
+ if (typeof column.editingNote === 'string') {
403
+ column.editingNote = { create: column.editingNote, edit: column.editingNote };
404
+ }
405
+ });
406
+ resource.dataSourceColumns = resource.columns.filter((col) => !col.virtual);
407
+ }
408
+ }