adminforth 1.1.92 → 1.1.93
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.
- package/dataConnectors/baseConnector.ts +2 -2
- package/dataConnectors/mongo.ts +2 -2
- package/dataConnectors/postgres.ts +2 -2
- package/dataConnectors/sqlite.ts +2 -2
- package/dist/plugins/TwoFactorsAuthPlugin/index.js +2 -3
- package/dist/spa/spa/src/components/Toast.vue +1 -1
- package/index.ts +5 -5
- package/modules/codeInjector.ts +2 -2
- package/package.json +2 -2
- package/plugins/AuditLogPlugin/index.ts +3 -3
- package/plugins/ForeignInlineListPlugin/index.ts +4 -4
- package/plugins/S3UploadPlugin/custom/s3uploader.vue +24 -4
- package/plugins/S3UploadPlugin/index.ts +4 -4
- package/plugins/S3UploadPlugin/package.json +1 -1
- package/plugins/TwoFactorsAuthPlugin/dist/auth.js +108 -0
- package/plugins/TwoFactorsAuthPlugin/dist/dataConnectors/baseConnector.js +90 -0
- package/plugins/TwoFactorsAuthPlugin/dist/dataConnectors/mongo.js +191 -0
- package/plugins/TwoFactorsAuthPlugin/dist/dataConnectors/postgres.js +295 -0
- package/plugins/TwoFactorsAuthPlugin/dist/dataConnectors/sqlite.js +246 -0
- package/plugins/TwoFactorsAuthPlugin/dist/index.js +1186 -0
- package/plugins/TwoFactorsAuthPlugin/dist/modules/codeInjector.js +546 -0
- package/plugins/TwoFactorsAuthPlugin/dist/modules/styleGenerator.js +43 -0
- package/plugins/TwoFactorsAuthPlugin/dist/modules/styles.js +92 -0
- package/plugins/TwoFactorsAuthPlugin/dist/modules/utils.js +301 -0
- package/plugins/TwoFactorsAuthPlugin/dist/plugins/TwoFactorsAuthPlugin/index.js +149 -0
- package/plugins/TwoFactorsAuthPlugin/dist/plugins/TwoFactorsAuthPlugin/types.js +1 -0
- package/plugins/TwoFactorsAuthPlugin/dist/plugins/base.js +34 -0
- package/plugins/TwoFactorsAuthPlugin/dist/servers/express.js +230 -0
- package/plugins/TwoFactorsAuthPlugin/dist/types/AdminForthConfig.js +105 -0
- package/plugins/TwoFactorsAuthPlugin/index.ts +10 -10
- package/plugins/TwoFactorsAuthPlugin/package-lock.json +2 -2
- package/plugins/TwoFactorsAuthPlugin/package.json +8 -6
- package/plugins/TwoFactorsAuthPlugin/tsconfig.json +112 -0
- package/plugins/base.ts +4 -4
- package/servers/express.ts +4 -4
- package/spa/src/components/Toast.vue +1 -1
- package/tsconfig.json +1 -1
- package/types/AdminForthConfig.ts +34 -28
- package/types/FrontendAPI.ts +2 -1
|
@@ -0,0 +1,1186 @@
|
|
|
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
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
11
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
12
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
13
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
14
|
+
};
|
|
15
|
+
var _a, _AdminForth_defaultConfig;
|
|
16
|
+
import AdminForthAuth from './auth.js';
|
|
17
|
+
import MongoConnector from './dataConnectors/mongo.js';
|
|
18
|
+
import PostgresConnector from './dataConnectors/postgres.js';
|
|
19
|
+
import SQLiteConnector from './dataConnectors/sqlite.js';
|
|
20
|
+
import CodeInjector from './modules/codeInjector.js';
|
|
21
|
+
import { guessLabelFromName } from './modules/utils.js';
|
|
22
|
+
import ExpressServer from './servers/express.js';
|
|
23
|
+
import { v1 as uuid } from 'uuid';
|
|
24
|
+
import fs from 'fs';
|
|
25
|
+
import { ADMINFORTH_VERSION, listify } from './modules/utils.js';
|
|
26
|
+
import { AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages, AllowedActionsEnum, ActionCheckSource } from './types/AdminForthConfig.js';
|
|
27
|
+
import path from 'path';
|
|
28
|
+
//get array from enum AdminForthResourcePages
|
|
29
|
+
class AdminForth {
|
|
30
|
+
constructor(config) {
|
|
31
|
+
_AdminForth_defaultConfig.set(this, {
|
|
32
|
+
deleteConfirmation: true,
|
|
33
|
+
});
|
|
34
|
+
this.config = Object.assign(Object.assign({}, __classPrivateFieldGet(this, _AdminForth_defaultConfig, "f")), config);
|
|
35
|
+
this.codeInjector = new CodeInjector(this);
|
|
36
|
+
this.activatedPlugins = [];
|
|
37
|
+
this.validateConfig();
|
|
38
|
+
this.activatePlugins();
|
|
39
|
+
this.validateConfig(); // revalidate after plugins
|
|
40
|
+
this.express = new ExpressServer(this);
|
|
41
|
+
this.auth = new AdminForthAuth(this);
|
|
42
|
+
this.connectors = {};
|
|
43
|
+
this.statuses = {};
|
|
44
|
+
console.log(`🚀 AdminForth v${ADMINFORTH_VERSION} starting up`);
|
|
45
|
+
}
|
|
46
|
+
activatePlugins() {
|
|
47
|
+
for (let resource of this.config.resources) {
|
|
48
|
+
for (let pluginInstance of resource.plugins || []) {
|
|
49
|
+
pluginInstance.modifyResourceConfig(this, resource);
|
|
50
|
+
this.activatedPlugins.push(pluginInstance);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
;
|
|
54
|
+
}
|
|
55
|
+
checkCustomFileExists(filePath) {
|
|
56
|
+
if (filePath.startsWith('@@/')) {
|
|
57
|
+
const checkPath = path.join(this.config.customization.customComponentsDir, filePath.replace('@@/', ''));
|
|
58
|
+
if (!fs.existsSync(checkPath)) {
|
|
59
|
+
return [`File file ${filePath} does not exist in ${this.config.customization.customComponentsDir}`];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
validateComponent(component, errors, ignoreExistsCheck = false) {
|
|
65
|
+
if (!component) {
|
|
66
|
+
return component;
|
|
67
|
+
}
|
|
68
|
+
let obj;
|
|
69
|
+
if (typeof component === 'string') {
|
|
70
|
+
obj = { file: component, meta: {} };
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
obj = component;
|
|
74
|
+
}
|
|
75
|
+
if (!ignoreExistsCheck) {
|
|
76
|
+
errors.push(...this.checkCustomFileExists(obj.file));
|
|
77
|
+
}
|
|
78
|
+
return obj;
|
|
79
|
+
}
|
|
80
|
+
validateConfig() {
|
|
81
|
+
var _b;
|
|
82
|
+
const errors = [];
|
|
83
|
+
if (this.config.rootUser) {
|
|
84
|
+
if (!this.config.rootUser.username) {
|
|
85
|
+
throw new Error('rootUser.username is required');
|
|
86
|
+
}
|
|
87
|
+
if (!this.config.rootUser.password) {
|
|
88
|
+
throw new Error('rootUser.password is required');
|
|
89
|
+
}
|
|
90
|
+
console.log('\n ⚠️⚠️⚠️ [INSECURE ALERT] config.rootUser is set, please create a new user and remove config.rootUser from config before going to production\n');
|
|
91
|
+
}
|
|
92
|
+
if (!this.config.customization.customComponentsDir) {
|
|
93
|
+
this.config.customization.customComponentsDir = './custom';
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
// check customComponentsDir exists
|
|
97
|
+
fs.accessSync(this.config.customization.customComponentsDir, fs.constants.R_OK);
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
this.config.customization.customComponentsDir = undefined;
|
|
101
|
+
}
|
|
102
|
+
if (this.config.auth) {
|
|
103
|
+
if (!this.config.auth.resourceId) {
|
|
104
|
+
throw new Error('No config.auth.resourceId defined');
|
|
105
|
+
}
|
|
106
|
+
if (!this.config.auth.passwordHashField) {
|
|
107
|
+
throw new Error('No config.auth.passwordHashField defined');
|
|
108
|
+
}
|
|
109
|
+
if (!this.config.auth.usernameField) {
|
|
110
|
+
throw new Error('No config.auth.usernameField defined');
|
|
111
|
+
}
|
|
112
|
+
if (this.config.auth.loginBackgroundImage) {
|
|
113
|
+
errors.push(...this.checkCustomFileExists(this.config.auth.loginBackgroundImage));
|
|
114
|
+
}
|
|
115
|
+
const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.resourceId);
|
|
116
|
+
if (!userResource) {
|
|
117
|
+
throw new Error(`Resource with id "${this.config.auth.resourceId}" not found`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!this.config.customization) {
|
|
121
|
+
this.config.customization = {};
|
|
122
|
+
}
|
|
123
|
+
if (!this.config.customization.customComponentsDir) {
|
|
124
|
+
this.config.customization.customComponentsDir = './custom';
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
// check customComponentsDir exists
|
|
128
|
+
fs.accessSync(this.config.customization.customComponentsDir, fs.constants.R_OK);
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
this.config.customization.customComponentsDir = undefined;
|
|
132
|
+
}
|
|
133
|
+
if (this.config.customization.customPages) {
|
|
134
|
+
this.config.customization.customPages.forEach((page, i) => {
|
|
135
|
+
// validate component if it's not plugin injection
|
|
136
|
+
if (this.codeInjector.allComponentNames.hasOwnProperty(page.component)) {
|
|
137
|
+
const validatedPage = this.validateComponent(page.component, errors, true);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (!this.config.baseUrl) {
|
|
142
|
+
this.config.baseUrl = '';
|
|
143
|
+
}
|
|
144
|
+
if (!this.config.baseUrl.endsWith('/')) {
|
|
145
|
+
this.baseUrlSlashed = this.config.baseUrl + '/';
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
this.baseUrlSlashed = this.config.baseUrl;
|
|
149
|
+
}
|
|
150
|
+
if (((_b = this.config) === null || _b === void 0 ? void 0 : _b.customization.brandName) === undefined) {
|
|
151
|
+
this.config.customization.brandName = 'AdminForth';
|
|
152
|
+
}
|
|
153
|
+
if (this.config.customization.brandLogo) {
|
|
154
|
+
errors.push(...this.checkCustomFileExists(this.config.customization.brandLogo));
|
|
155
|
+
}
|
|
156
|
+
if (this.config.customization.favicon) {
|
|
157
|
+
errors.push(...this.checkCustomFileExists(this.config.customization.favicon));
|
|
158
|
+
}
|
|
159
|
+
if (!this.config.customization.datesFormat) {
|
|
160
|
+
this.config.customization.datesFormat = 'MMM D, YYYY HH:mm:ss';
|
|
161
|
+
}
|
|
162
|
+
if (this.config.resources) {
|
|
163
|
+
this.config.resources.forEach((res) => {
|
|
164
|
+
var _b, _c, _d;
|
|
165
|
+
if (!res.table) {
|
|
166
|
+
errors.push(`Resource "${res.dataSource}" is missing table`);
|
|
167
|
+
}
|
|
168
|
+
// if recordLabel is not callable, throw error
|
|
169
|
+
if (res.recordLabel && typeof res.recordLabel !== 'function') {
|
|
170
|
+
errors.push(`Resource "${res.dataSource}" recordLabel is not a function`);
|
|
171
|
+
}
|
|
172
|
+
if (!res.recordLabel) {
|
|
173
|
+
res.recordLabel = (item) => {
|
|
174
|
+
const pkVal = item[res.columns.find((col) => col.primaryKey).name];
|
|
175
|
+
return `${res.label} ${pkVal}`;
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
res.resourceId = res.resourceId || res.table;
|
|
179
|
+
res.label = res.label || res.table.charAt(0).toUpperCase() + res.table.slice(1);
|
|
180
|
+
if (!res.dataSource) {
|
|
181
|
+
errors.push(`Resource "${res.resourceId}" is missing dataSource`);
|
|
182
|
+
}
|
|
183
|
+
if (!res.columns) {
|
|
184
|
+
res.columns = [];
|
|
185
|
+
}
|
|
186
|
+
res.columns.forEach((col) => {
|
|
187
|
+
var _b, _c, _d, _e;
|
|
188
|
+
col.label = col.label || guessLabelFromName(col.name);
|
|
189
|
+
//define default sortable
|
|
190
|
+
if (!Object.keys(col).includes('sortable')) {
|
|
191
|
+
col.sortable = true;
|
|
192
|
+
}
|
|
193
|
+
if (col.showIn && !Array.isArray(col.showIn)) {
|
|
194
|
+
errors.push(`Resource "${res.resourceId}" column "${col.name}" showIn must be an array`);
|
|
195
|
+
}
|
|
196
|
+
// check col.required is string or object
|
|
197
|
+
if (col.required && !((typeof col.required === 'boolean') || (typeof col.required === 'object'))) {
|
|
198
|
+
errors.push(`Resource "${res.resourceId}" column "${col.name}" required must be a string or object`);
|
|
199
|
+
}
|
|
200
|
+
// if it is object check the keys are one of ['create', 'edit']
|
|
201
|
+
if (typeof col.required === 'object') {
|
|
202
|
+
const wrongRequiredOn = Object.keys(col.required).find((c) => !['create', 'edit'].includes(c));
|
|
203
|
+
if (wrongRequiredOn) {
|
|
204
|
+
errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid required value "${wrongRequiredOn}", allowed keys are 'create', 'edit']`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// same for editingNote
|
|
208
|
+
if (col.editingNote && !((typeof col.editingNote === 'string') || (typeof col.editingNote === 'object'))) {
|
|
209
|
+
errors.push(`Resource "${res.resourceId}" column "${col.name}" editingNote must be a string or object`);
|
|
210
|
+
}
|
|
211
|
+
if (typeof col.editingNote === 'object') {
|
|
212
|
+
const wrongEditingNoteOn = Object.keys(col.editingNote).find((c) => !['create', 'edit'].includes(c));
|
|
213
|
+
if (wrongEditingNoteOn) {
|
|
214
|
+
errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid editingNote value "${wrongEditingNoteOn}", allowed keys are 'create', 'edit']`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const wrongShowIn = col.showIn && col.showIn.find((c) => AdminForthResourcePages[c] === undefined);
|
|
218
|
+
if (wrongShowIn) {
|
|
219
|
+
errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid showIn value "${wrongShowIn}", allowed values are ${Object.keys(AdminForthResourcePages).join(', ')}`);
|
|
220
|
+
}
|
|
221
|
+
col.showIn = col.showIn || Object.values(AdminForthResourcePages);
|
|
222
|
+
if (col.foreignResource) {
|
|
223
|
+
const befHook = (_c = (_b = col.foreignResource.hooks) === null || _b === void 0 ? void 0 : _b.dropdownList) === null || _c === void 0 ? void 0 : _c.beforeDatasourceRequest;
|
|
224
|
+
if (befHook) {
|
|
225
|
+
if (!Array.isArray(befHook)) {
|
|
226
|
+
col.foreignResource.hooks.dropdownList.beforeDatasourceRequest = [befHook];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const aftHook = (_e = (_d = col.foreignResource.hooks) === null || _d === void 0 ? void 0 : _d.dropdownList) === null || _e === void 0 ? void 0 : _e.afterDatasourceResponse;
|
|
230
|
+
if (aftHook) {
|
|
231
|
+
if (!Array.isArray(aftHook)) {
|
|
232
|
+
col.foreignResource.hooks.dropdownList.afterDatasourceResponse = [aftHook];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
if (!res.options) {
|
|
238
|
+
res.options = { bulkActions: [], allowedActions: {} };
|
|
239
|
+
}
|
|
240
|
+
if (!res.options.allowedActions) {
|
|
241
|
+
res.options.allowedActions = {
|
|
242
|
+
all: true,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (Object.keys(res.options.allowedActions).includes('all')) {
|
|
246
|
+
if (Object.keys(res.options.allowedActions).length > 1) {
|
|
247
|
+
errors.push(`Resource "${res.resourceId}" allowedActions cannot have "all" and other keys at same time: ${Object.keys(res.options.allowedActions).join(', ')}`);
|
|
248
|
+
}
|
|
249
|
+
for (const key of Object.keys(AllowedActionsEnum)) {
|
|
250
|
+
if (key !== 'all') {
|
|
251
|
+
res.options.allowedActions[key] = res.options.allowedActions.all;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
delete res.options.allowedActions.all;
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
// by default allow all actions
|
|
258
|
+
for (const key of Object.keys(AllowedActionsEnum)) {
|
|
259
|
+
if (!Object.keys(res.options.allowedActions).includes(key)) {
|
|
260
|
+
res.options.allowedActions[key] = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
//check if resource has bulkActions
|
|
265
|
+
let bulkActions = ((_b = res === null || res === void 0 ? void 0 : res.options) === null || _b === void 0 ? void 0 : _b.bulkActions) || [];
|
|
266
|
+
if (!Array.isArray(bulkActions)) {
|
|
267
|
+
errors.push(`Resource "${res.resourceId}" bulkActions must be an array`);
|
|
268
|
+
bulkActions = [];
|
|
269
|
+
}
|
|
270
|
+
if (((_d = (_c = res.options) === null || _c === void 0 ? void 0 : _c.allowedActions) === null || _d === void 0 ? void 0 : _d.delete) && !bulkActions.find((action) => action.label === 'Delete checked')) {
|
|
271
|
+
bulkActions.push({
|
|
272
|
+
label: `Delete checked`,
|
|
273
|
+
state: 'danger',
|
|
274
|
+
icon: 'flowbite:trash-bin-outline',
|
|
275
|
+
action: (_e) => __awaiter(this, [_e], void 0, function* ({ selectedIds }) {
|
|
276
|
+
const connector = this.connectors[res.dataSource];
|
|
277
|
+
yield Promise.all(selectedIds.map((recordId) => __awaiter(this, void 0, void 0, function* () {
|
|
278
|
+
yield connector.deleteRecord({ resource: res, recordId });
|
|
279
|
+
})));
|
|
280
|
+
})
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
const newBulkActions = bulkActions.map((action) => {
|
|
284
|
+
return Object.assign(action, { id: uuid() });
|
|
285
|
+
});
|
|
286
|
+
res.options.bulkActions = newBulkActions;
|
|
287
|
+
// if pageInjection is a string, make array with one element. Also check file exists
|
|
288
|
+
const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom'];
|
|
289
|
+
if (res.options.pageInjections) {
|
|
290
|
+
Object.entries(res.options.pageInjections).map(([key, value]) => {
|
|
291
|
+
Object.entries(value).map(([injection, target]) => {
|
|
292
|
+
if (possibleInjections.includes(injection)) {
|
|
293
|
+
if (!Array.isArray(res.options.pageInjections[key][injection])) {
|
|
294
|
+
// not array
|
|
295
|
+
res.options.pageInjections[key][injection] = [target];
|
|
296
|
+
}
|
|
297
|
+
res.options.pageInjections[key][injection].forEach((target, i) => {
|
|
298
|
+
res.options.pageInjections[key][injection][i] = this.validateComponent(target, errors);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')}`);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
// transform all hooks Functions to array of functions
|
|
308
|
+
if (!res.hooks) {
|
|
309
|
+
res.hooks = {};
|
|
310
|
+
}
|
|
311
|
+
for (const hookName of ['show', 'list']) {
|
|
312
|
+
if (!res.hooks[hookName]) {
|
|
313
|
+
res.hooks[hookName] = {};
|
|
314
|
+
}
|
|
315
|
+
if (!res.hooks[hookName].beforeDatasourceRequest) {
|
|
316
|
+
res.hooks[hookName].beforeDatasourceRequest = [];
|
|
317
|
+
}
|
|
318
|
+
if (!Array.isArray(res.hooks[hookName].beforeDatasourceRequest)) {
|
|
319
|
+
res.hooks[hookName].beforeDatasourceRequest = [res.hooks[hookName].beforeDatasourceRequest];
|
|
320
|
+
}
|
|
321
|
+
if (!res.hooks[hookName].afterDatasourceResponse) {
|
|
322
|
+
res.hooks[hookName].afterDatasourceResponse = [];
|
|
323
|
+
}
|
|
324
|
+
if (!Array.isArray(res.hooks[hookName].afterDatasourceResponse)) {
|
|
325
|
+
res.hooks[hookName].afterDatasourceResponse = [res.hooks[hookName].afterDatasourceResponse];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
for (const hookName of ['create', 'edit', 'delete']) {
|
|
329
|
+
if (!res.hooks[hookName]) {
|
|
330
|
+
res.hooks[hookName] = {};
|
|
331
|
+
}
|
|
332
|
+
if (!res.hooks[hookName].beforeSave) {
|
|
333
|
+
res.hooks[hookName].beforeSave = [];
|
|
334
|
+
}
|
|
335
|
+
if (!Array.isArray(res.hooks[hookName].beforeSave)) {
|
|
336
|
+
res.hooks[hookName].beforeSave = [res.hooks[hookName].beforeSave];
|
|
337
|
+
}
|
|
338
|
+
if (!res.hooks[hookName].afterSave) {
|
|
339
|
+
res.hooks[hookName].afterSave = [];
|
|
340
|
+
}
|
|
341
|
+
if (!Array.isArray(res.hooks[hookName].afterSave)) {
|
|
342
|
+
res.hooks[hookName].afterSave = [res.hooks[hookName].afterSave];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
if (!this.config.menu) {
|
|
347
|
+
errors.push('No config.menu defined');
|
|
348
|
+
}
|
|
349
|
+
// check if there is only one homepage: true in menu, recursivly
|
|
350
|
+
let homepages = 0;
|
|
351
|
+
const browseMenu = (menu) => {
|
|
352
|
+
menu.forEach((item) => {
|
|
353
|
+
if (item.component && item.resourceId) {
|
|
354
|
+
errors.push(`Menu item cannot have both component and resourceId: ${JSON.stringify(item)}`);
|
|
355
|
+
}
|
|
356
|
+
if (item.component && !item.path) {
|
|
357
|
+
errors.push(`Menu item with component must have path : ${JSON.stringify(item)}`);
|
|
358
|
+
}
|
|
359
|
+
if (item.type === 'resource' && !item.resourceId) {
|
|
360
|
+
errors.push(`Menu item with type 'resource' must have resourceId : ${JSON.stringify(item)}`);
|
|
361
|
+
}
|
|
362
|
+
if (item.resourceId && !this.config.resources.find((res) => res.resourceId === item.resourceId)) {
|
|
363
|
+
errors.push(`Menu item with type 'resourceId' has resourceId which is not in resources: ${JSON.stringify(item)}`);
|
|
364
|
+
}
|
|
365
|
+
if (item.type === 'component' && !item.component) {
|
|
366
|
+
errors.push(`Menu item with type 'component' must have component : ${JSON.stringify(item)}`);
|
|
367
|
+
}
|
|
368
|
+
// make sure component starts with @@
|
|
369
|
+
if (item.component) {
|
|
370
|
+
if (!item.component.startsWith('@@')) {
|
|
371
|
+
errors.push(`Menu item component must start with @@ : ${JSON.stringify(item)}`);
|
|
372
|
+
}
|
|
373
|
+
const path = item.component.replace('@@', this.config.customization.customComponentsDir);
|
|
374
|
+
if (!fs.existsSync(path)) {
|
|
375
|
+
errors.push(`Menu item component "${item.component.replace('@@', '')}" does not exist in "${this.config.customization.customComponentsDir}"`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (item.homepage) {
|
|
379
|
+
homepages++;
|
|
380
|
+
if (homepages > 1) {
|
|
381
|
+
errors.push('There must be only one homepage: true in menu, found second one in ' + JSON.stringify(item));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (item.children) {
|
|
385
|
+
browseMenu(item.children);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
browseMenu(this.config.menu);
|
|
390
|
+
}
|
|
391
|
+
// check for duplicate resourceIds and show which ones are duplicated
|
|
392
|
+
const resourceIds = this.config.resources.map((res) => res.resourceId);
|
|
393
|
+
const uniqueResourceIds = new Set(resourceIds);
|
|
394
|
+
if (uniqueResourceIds.size != resourceIds.length) {
|
|
395
|
+
const duplicates = resourceIds.filter((item, index) => resourceIds.indexOf(item) != index);
|
|
396
|
+
errors.push(`Duplicate fields "resourceId" or "table": ${duplicates.join(', ')}`);
|
|
397
|
+
}
|
|
398
|
+
//add ids for onSelectedAllActions for each resource
|
|
399
|
+
if (errors.length > 0) {
|
|
400
|
+
throw new Error(`Invalid AdminForth config: ${errors.join(', ')}`);
|
|
401
|
+
}
|
|
402
|
+
// check is all custom components files exists
|
|
403
|
+
for (const resource of this.config.resources) {
|
|
404
|
+
for (const column of resource.columns) {
|
|
405
|
+
if (column.components) {
|
|
406
|
+
for (const [key, comp] of Object.entries(column.components)) {
|
|
407
|
+
let ignoreExistsCheck = false;
|
|
408
|
+
if (this.codeInjector.allComponentNames[comp.file]) {
|
|
409
|
+
// not obvious, but if we are in this if, it means that this is plugin component
|
|
410
|
+
// and there is no sense to check if it exists in users folder
|
|
411
|
+
ignoreExistsCheck = true;
|
|
412
|
+
}
|
|
413
|
+
column.components[key] = this.validateComponent(comp, errors, ignoreExistsCheck);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
postProcessAfterDiscover(resource) {
|
|
420
|
+
resource.columns.forEach((column) => {
|
|
421
|
+
// if db/user says column is required in boolean, exapd
|
|
422
|
+
if (typeof column.required === 'boolean') {
|
|
423
|
+
column.required = { create: column.required, edit: column.required };
|
|
424
|
+
}
|
|
425
|
+
if (!column.required) {
|
|
426
|
+
column.required = { create: false, edit: false };
|
|
427
|
+
}
|
|
428
|
+
// same for editingNote
|
|
429
|
+
if (typeof column.editingNote === 'string') {
|
|
430
|
+
column.editingNote = { create: column.editingNote, edit: column.editingNote };
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
resource.dataSourceColumns = resource.columns.filter((col) => !col.virtual);
|
|
434
|
+
}
|
|
435
|
+
discoverDatabases() {
|
|
436
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
437
|
+
this.statuses.dbDiscover = 'running';
|
|
438
|
+
this.connectorClasses = {
|
|
439
|
+
'sqlite': SQLiteConnector,
|
|
440
|
+
'postgres': PostgresConnector,
|
|
441
|
+
'mongodb': MongoConnector,
|
|
442
|
+
};
|
|
443
|
+
if (!this.config.databaseConnectors) {
|
|
444
|
+
this.config.databaseConnectors = Object.assign({}, this.connectorClasses);
|
|
445
|
+
}
|
|
446
|
+
this.config.dataSources.forEach((ds) => {
|
|
447
|
+
const dbType = ds.url.split(':')[0];
|
|
448
|
+
if (!this.config.databaseConnectors[dbType]) {
|
|
449
|
+
throw new Error(`Database type ${dbType} is not supported, consider using databaseConnectors in AdminForth config`);
|
|
450
|
+
}
|
|
451
|
+
this.connectors[ds.id] = new this.config.databaseConnectors[dbType]({ url: ds.url });
|
|
452
|
+
});
|
|
453
|
+
yield Promise.all(this.config.resources.map((res) => __awaiter(this, void 0, void 0, function* () {
|
|
454
|
+
if (!this.connectors[res.dataSource]) {
|
|
455
|
+
throw new Error(`Resource '${res.table}' refers to unknown dataSource '${res.dataSource}'`);
|
|
456
|
+
}
|
|
457
|
+
const fieldTypes = yield this.connectors[res.dataSource].discoverFields(res);
|
|
458
|
+
if (!Object.keys(fieldTypes).length) {
|
|
459
|
+
throw new Error(`Table '${res.table}' (In resource '${res.resourceId}') has no fields or does not exist`);
|
|
460
|
+
}
|
|
461
|
+
if (!res.columns) {
|
|
462
|
+
res.columns = Object.keys(fieldTypes).map((name) => ({ name }));
|
|
463
|
+
}
|
|
464
|
+
res.columns.forEach((col, i) => {
|
|
465
|
+
if (!fieldTypes[col.name] && !col.virtual) {
|
|
466
|
+
throw new Error(`Resource '${res.table}' has no column '${col.name}'`);
|
|
467
|
+
}
|
|
468
|
+
// first find discovered values, but allow override
|
|
469
|
+
res.columns[i] = Object.assign(Object.assign({}, fieldTypes[col.name]), col);
|
|
470
|
+
});
|
|
471
|
+
this.postProcessAfterDiscover(res);
|
|
472
|
+
// check if primaryKey column is present
|
|
473
|
+
if (!res.columns.some((col) => col.primaryKey)) {
|
|
474
|
+
throw new Error(`Resource '${res.table}' has no column defined or auto-discovered. Please set 'primaryKey: true' in a columns which has unique value for each record and index`);
|
|
475
|
+
}
|
|
476
|
+
})));
|
|
477
|
+
this.statuses.dbDiscover = 'done';
|
|
478
|
+
// console.log('⚙️⚙️⚙️ Database discovery done', JSON.stringify(this.config.resources, null, 2));
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
bundleNow(_b) {
|
|
482
|
+
return __awaiter(this, arguments, void 0, function* ({ hotReload = false, verbose = false }) {
|
|
483
|
+
yield this.codeInjector.bundleNow({ hotReload, verbose });
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
getUserByPk(pk) {
|
|
487
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
488
|
+
const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.resourceId);
|
|
489
|
+
if (!resource) {
|
|
490
|
+
throw new Error('No auth resource found');
|
|
491
|
+
}
|
|
492
|
+
const users = yield this.connectors[resource.dataSource].getData({
|
|
493
|
+
resource,
|
|
494
|
+
filters: [
|
|
495
|
+
{ field: resource.columns.find((col) => col.primaryKey).name, operator: AdminForthFilterOperators.EQ, value: pk },
|
|
496
|
+
],
|
|
497
|
+
limit: 1,
|
|
498
|
+
offset: 0,
|
|
499
|
+
sort: [],
|
|
500
|
+
});
|
|
501
|
+
return users.data[0] || null;
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
createResourceRecord(_b) {
|
|
505
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, record, adminUser }) {
|
|
506
|
+
var _c, _d, _e, _f, _g;
|
|
507
|
+
for (const column of resource.columns) {
|
|
508
|
+
if (column.fillOnCreate) {
|
|
509
|
+
if (record[column.name] === undefined) {
|
|
510
|
+
record[column.name] = column.fillOnCreate({
|
|
511
|
+
initialRecord: record, adminUser
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (((_c = column.required) === null || _c === void 0 ? void 0 : _c.create) && record[column.name] === undefined) {
|
|
516
|
+
return { error: `Column '${column.name}' is required` };
|
|
517
|
+
}
|
|
518
|
+
if (column.isUnique) {
|
|
519
|
+
const existingRecord = yield this.connectors[resource.dataSource].getData({
|
|
520
|
+
resource,
|
|
521
|
+
filters: [{ field: column.name, operator: AdminForthFilterOperators.EQ, value: record[column.name] }],
|
|
522
|
+
limit: 1,
|
|
523
|
+
sort: [],
|
|
524
|
+
offset: 0
|
|
525
|
+
});
|
|
526
|
+
if (existingRecord.data.length > 0) {
|
|
527
|
+
return { error: `Record with ${column.name} ${record[column.name]} already exists` };
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// execute hook if needed
|
|
532
|
+
for (const hook of listify((_e = (_d = resource.hooks) === null || _d === void 0 ? void 0 : _d.create) === null || _e === void 0 ? void 0 : _e.beforeSave)) {
|
|
533
|
+
const resp = yield hook({ resource, record, adminUser });
|
|
534
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
535
|
+
throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
|
|
536
|
+
}
|
|
537
|
+
if (resp.error) {
|
|
538
|
+
return { error: resp.error };
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// remove virtual columns from record
|
|
542
|
+
for (const column of resource.columns.filter((col) => col.virtual)) {
|
|
543
|
+
if (record[column.name]) {
|
|
544
|
+
delete record[column.name];
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const connector = this.connectors[resource.dataSource];
|
|
548
|
+
yield connector.createRecord({ resource, record });
|
|
549
|
+
// execute hook if needed
|
|
550
|
+
for (const hook of listify((_g = (_f = resource.hooks) === null || _f === void 0 ? void 0 : _f.create) === null || _g === void 0 ? void 0 : _g.afterSave)) {
|
|
551
|
+
console.log('Hook afterSave', hook);
|
|
552
|
+
const resp = yield hook({ resource, record, adminUser });
|
|
553
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
554
|
+
throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
|
|
555
|
+
}
|
|
556
|
+
if (resp.error) {
|
|
557
|
+
return { error: resp.error };
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
setupEndpoints(server) {
|
|
563
|
+
server.endpoint({
|
|
564
|
+
noAuth: true,
|
|
565
|
+
method: 'POST',
|
|
566
|
+
path: '/login',
|
|
567
|
+
handler: (_b) => __awaiter(this, [_b], void 0, function* ({ body, response }) {
|
|
568
|
+
var _c, _d, _e, _f, _g, _h;
|
|
569
|
+
const INVALID_MESSAGE = 'Invalid username or password';
|
|
570
|
+
const { username, password } = body;
|
|
571
|
+
let adminUser;
|
|
572
|
+
let toReturn = { ok: true, allowedLogin: true };
|
|
573
|
+
let token;
|
|
574
|
+
if (username === this.config.rootUser.username && password === this.config.rootUser.password) {
|
|
575
|
+
this.auth.setAuthCookie({ response, username, pk: null });
|
|
576
|
+
adminUser = { isRoot: true, dbUser: null, pk: null, username: this.config.rootUser.username };
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
// get resource from db
|
|
580
|
+
if (!this.config.auth) {
|
|
581
|
+
throw new Error('No config.auth defined');
|
|
582
|
+
}
|
|
583
|
+
const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.resourceId);
|
|
584
|
+
// if there is no passwordHashField, in columns, add it, with backendOnly and showIn: []
|
|
585
|
+
if (!userResource.dataSourceColumns.find((col) => col.name === this.config.auth.passwordHashField)) {
|
|
586
|
+
userResource.dataSourceColumns.push({
|
|
587
|
+
name: this.config.auth.passwordHashField,
|
|
588
|
+
backendOnly: true,
|
|
589
|
+
showIn: [],
|
|
590
|
+
type: _a.Types.STRING,
|
|
591
|
+
});
|
|
592
|
+
console.log('Adding passwordHashField to userResource', userResource);
|
|
593
|
+
}
|
|
594
|
+
const userRecord = (_c = (yield this.connectors[userResource.dataSource].getData({
|
|
595
|
+
resource: userResource,
|
|
596
|
+
filters: [
|
|
597
|
+
{ field: this.config.auth.usernameField, operator: AdminForthFilterOperators.EQ, value: username },
|
|
598
|
+
],
|
|
599
|
+
limit: 1,
|
|
600
|
+
offset: 0,
|
|
601
|
+
sort: [],
|
|
602
|
+
})).data) === null || _c === void 0 ? void 0 : _c[0];
|
|
603
|
+
if (!userRecord) {
|
|
604
|
+
return { error: 'User not found' };
|
|
605
|
+
}
|
|
606
|
+
const passwordHash = userRecord[this.config.auth.passwordHashField];
|
|
607
|
+
const valid = yield AdminForthAuth.verifyPassword(password, passwordHash);
|
|
608
|
+
if (valid) {
|
|
609
|
+
adminUser = {
|
|
610
|
+
isRoot: false, dbUser: userRecord,
|
|
611
|
+
pk: userRecord[userResource.columns.find((col) => col.primaryKey).name],
|
|
612
|
+
username
|
|
613
|
+
};
|
|
614
|
+
const beforeLoginConfirmation = this.config.auth.beforeLoginConfirmation;
|
|
615
|
+
if (beforeLoginConfirmation === null || beforeLoginConfirmation === void 0 ? void 0 : beforeLoginConfirmation.length) {
|
|
616
|
+
for (const hook of beforeLoginConfirmation) {
|
|
617
|
+
const resp = yield hook({ userRecord: adminUser });
|
|
618
|
+
if ((_d = resp === null || resp === void 0 ? void 0 : resp.body) === null || _d === void 0 ? void 0 : _d.setCookie) {
|
|
619
|
+
(_e = resp === null || resp === void 0 ? void 0 : resp.body) === null || _e === void 0 ? void 0 : _e.setCookie.forEach((cookie) => {
|
|
620
|
+
this.auth.setCustomCookie({ response, payload: cookie });
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
if ((_f = resp === null || resp === void 0 ? void 0 : resp.body) === null || _f === void 0 ? void 0 : _f.redirectTo) {
|
|
624
|
+
toReturn = { ok: resp.ok, redirectTo: (_g = resp === null || resp === void 0 ? void 0 : resp.body) === null || _g === void 0 ? void 0 : _g.redirectTo, allowedLogin: (_h = resp === null || resp === void 0 ? void 0 : resp.body) === null || _h === void 0 ? void 0 : _h.allowedLogin };
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (toReturn.allowedLogin) {
|
|
630
|
+
this.auth.setAuthCookie({ response, username, pk: userRecord[userResource.columns.find((col) => col.primaryKey).name] });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
return { error: INVALID_MESSAGE };
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return toReturn;
|
|
638
|
+
})
|
|
639
|
+
});
|
|
640
|
+
server.endpoint({
|
|
641
|
+
method: 'POST',
|
|
642
|
+
path: '/check_auth',
|
|
643
|
+
handler: (_j) => __awaiter(this, [_j], void 0, function* ({ adminUser }) {
|
|
644
|
+
return { ok: true };
|
|
645
|
+
}),
|
|
646
|
+
});
|
|
647
|
+
server.endpoint({
|
|
648
|
+
noAuth: true,
|
|
649
|
+
method: 'POST',
|
|
650
|
+
path: '/logout',
|
|
651
|
+
handler: (_k) => __awaiter(this, [_k], void 0, function* ({ response }) {
|
|
652
|
+
this.auth.removeAuthCookie(response);
|
|
653
|
+
return { ok: true };
|
|
654
|
+
}),
|
|
655
|
+
});
|
|
656
|
+
server.endpoint({
|
|
657
|
+
noAuth: true,
|
|
658
|
+
method: 'GET',
|
|
659
|
+
path: '/get_public_config',
|
|
660
|
+
handler: (_l) => __awaiter(this, [_l], void 0, function* ({ body }) {
|
|
661
|
+
var _m;
|
|
662
|
+
// find resource
|
|
663
|
+
if (!this.config.auth) {
|
|
664
|
+
throw new Error('No config.auth defined');
|
|
665
|
+
}
|
|
666
|
+
const usernameField = this.config.auth.usernameField;
|
|
667
|
+
const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.resourceId);
|
|
668
|
+
const usernameColumn = resource.columns.find((col) => col.name === usernameField);
|
|
669
|
+
return {
|
|
670
|
+
brandName: this.config.customization.brandName,
|
|
671
|
+
usernameFieldName: usernameColumn.label,
|
|
672
|
+
loginBackgroundImage: this.config.auth.loginBackgroundImage,
|
|
673
|
+
title: (_m = this.config.customization) === null || _m === void 0 ? void 0 : _m.title,
|
|
674
|
+
};
|
|
675
|
+
}),
|
|
676
|
+
});
|
|
677
|
+
server.endpoint({
|
|
678
|
+
method: 'GET',
|
|
679
|
+
path: '/get_base_config',
|
|
680
|
+
handler: (_o) => __awaiter(this, [_o], void 0, function* ({ input, adminUser, cookies }) {
|
|
681
|
+
var _p, _q;
|
|
682
|
+
let username = '';
|
|
683
|
+
let userFullName = '';
|
|
684
|
+
if (adminUser.isRoot) {
|
|
685
|
+
username = this.config.rootUser.username;
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
const dbUser = adminUser.dbUser;
|
|
689
|
+
username = dbUser[this.config.auth.usernameField];
|
|
690
|
+
userFullName = dbUser[this.config.auth.userFullNameField];
|
|
691
|
+
}
|
|
692
|
+
const userData = {
|
|
693
|
+
[this.config.auth.usernameField]: username,
|
|
694
|
+
[this.config.auth.userFullNameField]: userFullName
|
|
695
|
+
};
|
|
696
|
+
const checkIsMenuItemVisible = (menuItem) => {
|
|
697
|
+
if (typeof menuItem.visible === 'function') {
|
|
698
|
+
const toReturn = menuItem.visible(adminUser);
|
|
699
|
+
if (typeof toReturn !== 'boolean') {
|
|
700
|
+
throw new Error(`'visible' function of ${menuItem.label || menuItem.type} must return boolean value`);
|
|
701
|
+
}
|
|
702
|
+
return toReturn;
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
let newMenu = [];
|
|
706
|
+
for (let menuItem of this.config.menu) {
|
|
707
|
+
let newMenuItem = Object.assign({}, menuItem);
|
|
708
|
+
if (menuItem.visible) {
|
|
709
|
+
if (!checkIsMenuItemVisible(menuItem)) {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (menuItem.children) {
|
|
714
|
+
let newChildren = [];
|
|
715
|
+
for (let child of menuItem.children) {
|
|
716
|
+
let newChild = Object.assign({}, child);
|
|
717
|
+
if (child.visible) {
|
|
718
|
+
if (!checkIsMenuItemVisible(child)) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
newChildren.push(newChild);
|
|
723
|
+
}
|
|
724
|
+
newMenuItem = Object.assign(Object.assign({}, newMenuItem), { children: newChildren });
|
|
725
|
+
}
|
|
726
|
+
newMenu.push(newMenuItem);
|
|
727
|
+
}
|
|
728
|
+
return {
|
|
729
|
+
user: userData,
|
|
730
|
+
resources: this.config.resources.map((res) => ({
|
|
731
|
+
resourceId: res.resourceId,
|
|
732
|
+
label: res.label,
|
|
733
|
+
})),
|
|
734
|
+
menu: newMenu,
|
|
735
|
+
config: {
|
|
736
|
+
brandName: this.config.customization.brandName,
|
|
737
|
+
brandLogo: this.config.customization.brandLogo,
|
|
738
|
+
datesFormat: this.config.customization.datesFormat,
|
|
739
|
+
deleteConfirmation: this.config.deleteConfirmation,
|
|
740
|
+
auth: this.config.auth,
|
|
741
|
+
usernameField: this.config.auth.usernameField,
|
|
742
|
+
title: (_p = this.config.customization) === null || _p === void 0 ? void 0 : _p.title,
|
|
743
|
+
emptyFieldPlaceholder: (_q = this.config.customization) === null || _q === void 0 ? void 0 : _q.emptyFieldPlaceholder,
|
|
744
|
+
},
|
|
745
|
+
adminUser,
|
|
746
|
+
version: ADMINFORTH_VERSION,
|
|
747
|
+
};
|
|
748
|
+
}),
|
|
749
|
+
});
|
|
750
|
+
function interpretResource(adminUser, resource, meta, source) {
|
|
751
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
752
|
+
var _b;
|
|
753
|
+
if (process.env.HEAVY_DEBUG) {
|
|
754
|
+
console.log('🪲Interpreting resource', resource.resourceId, source);
|
|
755
|
+
}
|
|
756
|
+
const allowedActions = {};
|
|
757
|
+
yield Promise.all(Object.entries(((_b = resource.options) === null || _b === void 0 ? void 0 : _b.allowedActions) || {}).map((_c) => __awaiter(this, [_c], void 0, function* ([key, value]) {
|
|
758
|
+
if (process.env.HEAVY_DEBUG) {
|
|
759
|
+
console.log('🪲checking for allowed call', key, 'value:', value, 'typeof', typeof value);
|
|
760
|
+
}
|
|
761
|
+
// if callable then call
|
|
762
|
+
if (typeof value === 'function') {
|
|
763
|
+
allowedActions[key] = yield value({ adminUser, resource, meta, source });
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
allowedActions[key] = value;
|
|
767
|
+
}
|
|
768
|
+
})));
|
|
769
|
+
return { allowedActions };
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
function checkAccess(action, allowedActions) {
|
|
773
|
+
const allowed = allowedActions[action];
|
|
774
|
+
if (allowed !== true) {
|
|
775
|
+
return { error: typeof allowed === 'string' ? allowed : 'Action is not allowed', allowed: false };
|
|
776
|
+
}
|
|
777
|
+
return { allowed: true };
|
|
778
|
+
}
|
|
779
|
+
server.endpoint({
|
|
780
|
+
method: 'POST',
|
|
781
|
+
path: '/get_resource',
|
|
782
|
+
handler: (_r) => __awaiter(this, [_r], void 0, function* ({ body, adminUser }) {
|
|
783
|
+
const { resourceId } = body;
|
|
784
|
+
if (!this.statuses.dbDiscover) {
|
|
785
|
+
return { error: 'Database discovery not started' };
|
|
786
|
+
}
|
|
787
|
+
if (this.statuses.dbDiscover !== 'done') {
|
|
788
|
+
return { error: 'Database discovery is still in progress, please try later' };
|
|
789
|
+
}
|
|
790
|
+
const resource = this.config.resources.find((res) => res.resourceId == resourceId);
|
|
791
|
+
if (!resource) {
|
|
792
|
+
return { error: `Resource ${resourceId} not found` };
|
|
793
|
+
}
|
|
794
|
+
const { allowedActions } = yield interpretResource(adminUser, resource, {}, ActionCheckSource.DisplayButtons);
|
|
795
|
+
// exclude "plugins" key
|
|
796
|
+
return {
|
|
797
|
+
resource: Object.assign(Object.assign({}, resource), { plugins: undefined, options: Object.assign(Object.assign({}, resource.options), { allowedActions }) })
|
|
798
|
+
};
|
|
799
|
+
}),
|
|
800
|
+
});
|
|
801
|
+
server.endpoint({
|
|
802
|
+
method: 'POST',
|
|
803
|
+
path: '/get_resource_data',
|
|
804
|
+
handler: (_s) => __awaiter(this, [_s], void 0, function* ({ body, adminUser }) {
|
|
805
|
+
var _t, _u, _v, _w;
|
|
806
|
+
const { resourceId, source } = body;
|
|
807
|
+
if (['show', 'list'].includes(source) === false) {
|
|
808
|
+
return { error: 'Invalid source, should be list or show' };
|
|
809
|
+
}
|
|
810
|
+
if (!this.statuses.dbDiscover) {
|
|
811
|
+
return { error: 'Database discovery not started' };
|
|
812
|
+
}
|
|
813
|
+
if (this.statuses.dbDiscover !== 'done') {
|
|
814
|
+
return { error: 'Database discovery is still in progress, please try later' };
|
|
815
|
+
}
|
|
816
|
+
const resource = this.config.resources.find((res) => res.resourceId == resourceId);
|
|
817
|
+
if (!resource) {
|
|
818
|
+
return { error: `Resource ${resourceId} not found` };
|
|
819
|
+
}
|
|
820
|
+
const { allowedActions } = yield interpretResource(adminUser, resource, { requestBody: body }, ActionCheckSource.DisplayButtons);
|
|
821
|
+
const { allowed, error } = checkAccess(source, allowedActions);
|
|
822
|
+
if (!allowed) {
|
|
823
|
+
return { error };
|
|
824
|
+
}
|
|
825
|
+
for (const hook of listify((_u = (_t = resource.hooks) === null || _t === void 0 ? void 0 : _t[source]) === null || _u === void 0 ? void 0 : _u.beforeDatasourceRequest)) {
|
|
826
|
+
const resp = yield hook({ resource, query: body, adminUser });
|
|
827
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
828
|
+
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
829
|
+
}
|
|
830
|
+
if (resp.error) {
|
|
831
|
+
return { error: resp.error };
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
const { limit, offset, filters, sort } = body;
|
|
835
|
+
for (const filter of (filters || [])) {
|
|
836
|
+
if (!Object.values(AdminForthFilterOperators).includes(filter.operator)) {
|
|
837
|
+
throw new Error(`Operator '${filter.operator}' is not allowed`);
|
|
838
|
+
}
|
|
839
|
+
if (!resource.columns.some((col) => col.name === filter.field)) {
|
|
840
|
+
throw new Error(`Field '${filter.field}' is not in resource '${resource.resourceId}'. Available fields: ${resource.columns.map((col) => col.name).join(', ')}`);
|
|
841
|
+
}
|
|
842
|
+
if (filter.operator === AdminForthFilterOperators.IN || filter.operator === AdminForthFilterOperators.NIN) {
|
|
843
|
+
if (!Array.isArray(filter.value)) {
|
|
844
|
+
throw new Error(`Value for operator '${filter.operator}' should be an array`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (filter.operator === AdminForthFilterOperators.IN && filter.value.length === 0) {
|
|
848
|
+
// nonsense
|
|
849
|
+
return { data: [], total: 0 };
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
const data = yield this.connectors[resource.dataSource].getData({
|
|
853
|
+
resource,
|
|
854
|
+
limit,
|
|
855
|
+
offset,
|
|
856
|
+
filters,
|
|
857
|
+
sort,
|
|
858
|
+
});
|
|
859
|
+
// for foreign keys, add references
|
|
860
|
+
yield Promise.all(resource.columns.filter((col) => col.foreignResource).map((col) => __awaiter(this, void 0, void 0, function* () {
|
|
861
|
+
const targetResource = this.config.resources.find((res) => res.resourceId == col.foreignResource.resourceId);
|
|
862
|
+
const targetConnector = this.connectors[targetResource.dataSource];
|
|
863
|
+
const targetResourcePkField = targetResource.columns.find((col) => col.primaryKey).name;
|
|
864
|
+
const pksUnique = [...new Set(data.data.map((item) => item[col.name]))];
|
|
865
|
+
if (pksUnique.length === 0) {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
const targetData = yield targetConnector.getData({
|
|
869
|
+
resource: targetResource,
|
|
870
|
+
limit: limit,
|
|
871
|
+
offset: 0,
|
|
872
|
+
filters: [
|
|
873
|
+
{
|
|
874
|
+
field: targetResourcePkField,
|
|
875
|
+
operator: AdminForthFilterOperators.IN,
|
|
876
|
+
value: pksUnique,
|
|
877
|
+
}
|
|
878
|
+
],
|
|
879
|
+
sort: [],
|
|
880
|
+
});
|
|
881
|
+
const targetDataMap = targetData.data.reduce((acc, item) => {
|
|
882
|
+
acc[item[targetResourcePkField]] = {
|
|
883
|
+
label: targetResource.recordLabel(item),
|
|
884
|
+
pk: item[targetResourcePkField],
|
|
885
|
+
};
|
|
886
|
+
return acc;
|
|
887
|
+
}, {});
|
|
888
|
+
data.data.forEach((item) => {
|
|
889
|
+
item[col.name] = targetDataMap[item[col.name]];
|
|
890
|
+
});
|
|
891
|
+
})));
|
|
892
|
+
for (const hook of listify((_w = (_v = resource.hooks) === null || _v === void 0 ? void 0 : _v[source]) === null || _w === void 0 ? void 0 : _w.afterDatasourceResponse)) {
|
|
893
|
+
const resp = yield hook({ resource, response: data.data, adminUser });
|
|
894
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
895
|
+
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
896
|
+
}
|
|
897
|
+
if (resp.error) {
|
|
898
|
+
return { error: resp.error };
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
// remove all columns which are not defined in resources, or defined but backendOnly
|
|
902
|
+
data.data.forEach((item) => {
|
|
903
|
+
Object.keys(item).forEach((key) => {
|
|
904
|
+
if (!resource.columns.find((col) => col.name === key) || resource.columns.find((col) => col.name === key && col.backendOnly)) {
|
|
905
|
+
delete item[key];
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
data.data.forEach((item) => {
|
|
910
|
+
item._label = resource.recordLabel(item);
|
|
911
|
+
});
|
|
912
|
+
return Object.assign(Object.assign({}, data), { options: resource === null || resource === void 0 ? void 0 : resource.options });
|
|
913
|
+
}),
|
|
914
|
+
});
|
|
915
|
+
server.endpoint({
|
|
916
|
+
method: 'POST',
|
|
917
|
+
path: '/get_resource_foreign_data',
|
|
918
|
+
handler: (_x) => __awaiter(this, [_x], void 0, function* ({ body, adminUser }) {
|
|
919
|
+
var _y, _z, _0, _1;
|
|
920
|
+
const { resourceId, column } = body;
|
|
921
|
+
if (!this.statuses.dbDiscover) {
|
|
922
|
+
return { error: 'Database discovery not started' };
|
|
923
|
+
}
|
|
924
|
+
if (this.statuses.dbDiscover !== 'done') {
|
|
925
|
+
return { error: 'Database discovery is still in progress, please try later' };
|
|
926
|
+
}
|
|
927
|
+
const resource = this.config.resources.find((res) => res.resourceId == resourceId);
|
|
928
|
+
if (!resource) {
|
|
929
|
+
return { error: `Resource '${resourceId}' not found` };
|
|
930
|
+
}
|
|
931
|
+
const columnConfig = resource.columns.find((col) => col.name == column);
|
|
932
|
+
if (!columnConfig) {
|
|
933
|
+
return { error: `Column "${column}' not found in resource with resourceId '${resourceId}'` };
|
|
934
|
+
}
|
|
935
|
+
if (!columnConfig.foreignResource) {
|
|
936
|
+
return { error: `Column '${column}' in resource '${resourceId}' is not a foreign key` };
|
|
937
|
+
}
|
|
938
|
+
const targetResourceId = columnConfig.foreignResource.resourceId;
|
|
939
|
+
const targetResource = this.config.resources.find((res) => res.resourceId == targetResourceId);
|
|
940
|
+
for (const hook of listify((_z = (_y = columnConfig.foreignResource.hooks) === null || _y === void 0 ? void 0 : _y.dropdownList) === null || _z === void 0 ? void 0 : _z.beforeDatasourceRequest)) {
|
|
941
|
+
const resp = yield hook({ query: body, adminUser, resource: targetResource });
|
|
942
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
943
|
+
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
944
|
+
}
|
|
945
|
+
if (resp.error) {
|
|
946
|
+
return { error: resp.error };
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
const { limit, offset, filters, sort } = body;
|
|
950
|
+
const dbDataItems = yield this.connectors[targetResource.dataSource].getData({
|
|
951
|
+
resource: targetResource,
|
|
952
|
+
limit,
|
|
953
|
+
offset,
|
|
954
|
+
filters: filters || [],
|
|
955
|
+
sort: sort || [],
|
|
956
|
+
});
|
|
957
|
+
const items = dbDataItems.data.map((item) => {
|
|
958
|
+
const pk = item[targetResource.columns.find((col) => col.primaryKey).name];
|
|
959
|
+
const labler = targetResource.recordLabel;
|
|
960
|
+
return {
|
|
961
|
+
value: pk,
|
|
962
|
+
label: labler(item),
|
|
963
|
+
_item: item, // user might need it in hook to form new label
|
|
964
|
+
};
|
|
965
|
+
});
|
|
966
|
+
const response = {
|
|
967
|
+
items
|
|
968
|
+
};
|
|
969
|
+
for (const hook of listify((_1 = (_0 = columnConfig.foreignResource.hooks) === null || _0 === void 0 ? void 0 : _0.dropdownList) === null || _1 === void 0 ? void 0 : _1.afterDatasourceResponse)) {
|
|
970
|
+
const resp = yield hook({ response, adminUser, resource: targetResource });
|
|
971
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
972
|
+
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
973
|
+
}
|
|
974
|
+
if (resp.error) {
|
|
975
|
+
return { error: resp.error };
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return response;
|
|
979
|
+
}),
|
|
980
|
+
});
|
|
981
|
+
server.endpoint({
|
|
982
|
+
method: 'POST',
|
|
983
|
+
path: '/get_min_max_for_columns',
|
|
984
|
+
handler: (_2) => __awaiter(this, [_2], void 0, function* ({ body }) {
|
|
985
|
+
const { resourceId } = body;
|
|
986
|
+
if (!this.statuses.dbDiscover) {
|
|
987
|
+
return { error: 'Database discovery not started' };
|
|
988
|
+
}
|
|
989
|
+
if (this.statuses.dbDiscover !== 'done') {
|
|
990
|
+
return { error: 'Database discovery is still in progress, please try later' };
|
|
991
|
+
}
|
|
992
|
+
const resource = this.config.resources.find((res) => res.resourceId == resourceId);
|
|
993
|
+
if (!resource) {
|
|
994
|
+
return { error: `Resource '${resourceId}' not found` };
|
|
995
|
+
}
|
|
996
|
+
const item = yield this.connectors[resource.dataSource].getMinMaxForColumns({
|
|
997
|
+
resource,
|
|
998
|
+
columns: resource.columns.filter((col) => [
|
|
999
|
+
AdminForthDataTypes.INTEGER,
|
|
1000
|
+
AdminForthDataTypes.FLOAT,
|
|
1001
|
+
AdminForthDataTypes.DATE,
|
|
1002
|
+
AdminForthDataTypes.DATETIME,
|
|
1003
|
+
AdminForthDataTypes.TIME,
|
|
1004
|
+
AdminForthDataTypes.DECIMAL,
|
|
1005
|
+
].includes(col.type) && col.allowMinMaxQuery === true),
|
|
1006
|
+
});
|
|
1007
|
+
return item;
|
|
1008
|
+
}),
|
|
1009
|
+
});
|
|
1010
|
+
server.endpoint({
|
|
1011
|
+
method: 'POST',
|
|
1012
|
+
path: '/create_record',
|
|
1013
|
+
handler: (_3) => __awaiter(this, [_3], void 0, function* ({ body, adminUser }) {
|
|
1014
|
+
const resource = this.config.resources.find((res) => res.resourceId == body['resourceId']);
|
|
1015
|
+
if (!resource) {
|
|
1016
|
+
return { error: `Resource '${body['resourceId']}' not found` };
|
|
1017
|
+
}
|
|
1018
|
+
const { allowedActions } = yield interpretResource(adminUser, resource, { requestBody: body }, ActionCheckSource.CreateRequest);
|
|
1019
|
+
const { allowed, error } = checkAccess(AllowedActionsEnum.create, allowedActions);
|
|
1020
|
+
if (!allowed) {
|
|
1021
|
+
return { error };
|
|
1022
|
+
}
|
|
1023
|
+
const { record } = body;
|
|
1024
|
+
// call setFieldValue for each column
|
|
1025
|
+
for (const column of resource.columns) {
|
|
1026
|
+
if (record[column.name] !== undefined) {
|
|
1027
|
+
record[column.name] = this.connectors[resource.dataSource].setFieldValue(column, record[column.name]);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
yield this.createResourceRecord({ resource, record, adminUser });
|
|
1031
|
+
const connector = this.connectors[resource.dataSource];
|
|
1032
|
+
return {
|
|
1033
|
+
newRecordId: body['record'][connector.getPrimaryKey(resource)]
|
|
1034
|
+
};
|
|
1035
|
+
})
|
|
1036
|
+
});
|
|
1037
|
+
server.endpoint({
|
|
1038
|
+
method: 'POST',
|
|
1039
|
+
path: '/update_record',
|
|
1040
|
+
handler: (_4) => __awaiter(this, [_4], void 0, function* ({ body, adminUser }) {
|
|
1041
|
+
var _5, _6, _7, _8;
|
|
1042
|
+
const resource = this.config.resources.find((res) => res.resourceId == body['resourceId']);
|
|
1043
|
+
if (!resource) {
|
|
1044
|
+
return { error: `Resource '${body['resourceId']}' not found` };
|
|
1045
|
+
}
|
|
1046
|
+
const recordId = body['recordId'];
|
|
1047
|
+
const connector = this.connectors[resource.dataSource];
|
|
1048
|
+
const oldRecord = yield connector.getRecordByPrimaryKey(resource, recordId);
|
|
1049
|
+
if (!oldRecord) {
|
|
1050
|
+
const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
|
|
1051
|
+
return { error: `Record with ${primaryKeyColumn.name} ${recordId} not found` };
|
|
1052
|
+
}
|
|
1053
|
+
const record = body['record'];
|
|
1054
|
+
const { allowedActions } = yield interpretResource(adminUser, resource, { requestBody: body, newRecord: record, oldRecord }, ActionCheckSource.EditRequest);
|
|
1055
|
+
const { allowed, error } = checkAccess(AllowedActionsEnum.edit, allowedActions);
|
|
1056
|
+
if (!allowed) {
|
|
1057
|
+
return { error };
|
|
1058
|
+
}
|
|
1059
|
+
// execute hook if needed
|
|
1060
|
+
for (const hook of listify((_6 = (_5 = resource.hooks) === null || _5 === void 0 ? void 0 : _5.edit) === null || _6 === void 0 ? void 0 : _6.beforeSave)) {
|
|
1061
|
+
const resp = yield hook({ resource, record, adminUser });
|
|
1062
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
1063
|
+
throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
|
|
1064
|
+
}
|
|
1065
|
+
if (resp.error) {
|
|
1066
|
+
return { error: resp.error };
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
const newValues = {};
|
|
1070
|
+
for (const recordField in record) {
|
|
1071
|
+
if (record[recordField] !== oldRecord[recordField]) {
|
|
1072
|
+
const column = resource.columns.find((col) => col.name === recordField);
|
|
1073
|
+
if (column) {
|
|
1074
|
+
if (!column.virtual) {
|
|
1075
|
+
newValues[recordField] = connector.setFieldValue(column, record[recordField]);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
newValues[recordField] = record[recordField];
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (Object.keys(newValues).length > 0) {
|
|
1084
|
+
yield connector.updateRecord({ resource, recordId, newValues });
|
|
1085
|
+
}
|
|
1086
|
+
// execute hook if needed
|
|
1087
|
+
for (const hook of listify((_8 = (_7 = resource.hooks) === null || _7 === void 0 ? void 0 : _7.edit) === null || _8 === void 0 ? void 0 : _8.afterSave)) {
|
|
1088
|
+
const resp = yield hook({ resource, record, adminUser, oldRecord });
|
|
1089
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
1090
|
+
throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
|
|
1091
|
+
}
|
|
1092
|
+
if (resp.error) {
|
|
1093
|
+
return { error: resp.error };
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return {
|
|
1097
|
+
newRecordId: recordId
|
|
1098
|
+
};
|
|
1099
|
+
})
|
|
1100
|
+
});
|
|
1101
|
+
server.endpoint({
|
|
1102
|
+
method: 'POST',
|
|
1103
|
+
path: '/delete_record',
|
|
1104
|
+
handler: (_9) => __awaiter(this, [_9], void 0, function* ({ body, adminUser }) {
|
|
1105
|
+
var _10, _11, _12, _13;
|
|
1106
|
+
const resource = this.config.resources.find((res) => res.resourceId == body['resourceId']);
|
|
1107
|
+
const record = yield this.connectors[resource.dataSource].getRecordByPrimaryKey(resource, body['primaryKey']);
|
|
1108
|
+
if (!resource) {
|
|
1109
|
+
return { error: `Resource '${body['resourceId']}' not found` };
|
|
1110
|
+
}
|
|
1111
|
+
if (!record) {
|
|
1112
|
+
return { error: `Record with ${body['primaryKey']} not found` };
|
|
1113
|
+
}
|
|
1114
|
+
if (resource.options.allowedActions.delete === false) {
|
|
1115
|
+
return { error: `Resource '${resource.resourceId}' does not allow delete action` };
|
|
1116
|
+
}
|
|
1117
|
+
const { allowedActions } = yield interpretResource(adminUser, resource, { requestBody: body }, ActionCheckSource.DeleteRequest);
|
|
1118
|
+
const { allowed, error } = checkAccess(AllowedActionsEnum.delete, allowedActions);
|
|
1119
|
+
if (!allowed) {
|
|
1120
|
+
return { error };
|
|
1121
|
+
}
|
|
1122
|
+
// execute hook if needed
|
|
1123
|
+
for (const hook of listify((_11 = (_10 = resource.hooks) === null || _10 === void 0 ? void 0 : _10.delete) === null || _11 === void 0 ? void 0 : _11.beforeSave)) {
|
|
1124
|
+
const resp = yield hook({ resource, record, adminUser });
|
|
1125
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
1126
|
+
throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
|
|
1127
|
+
}
|
|
1128
|
+
if (resp.error) {
|
|
1129
|
+
return { error: resp.error };
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
const connector = this.connectors[resource.dataSource];
|
|
1133
|
+
yield connector.deleteRecord({ resource, recordId: body['primaryKey'] });
|
|
1134
|
+
// execute hook if needed
|
|
1135
|
+
for (const hook of listify((_13 = (_12 = resource.hooks) === null || _12 === void 0 ? void 0 : _12.delete) === null || _13 === void 0 ? void 0 : _13.afterSave)) {
|
|
1136
|
+
const resp = yield hook({ resource, record, adminUser });
|
|
1137
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
1138
|
+
throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
|
|
1139
|
+
}
|
|
1140
|
+
if (resp.error) {
|
|
1141
|
+
return { error: resp.error };
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return {
|
|
1145
|
+
recordId: body['primaryKey']
|
|
1146
|
+
};
|
|
1147
|
+
})
|
|
1148
|
+
});
|
|
1149
|
+
server.endpoint({
|
|
1150
|
+
method: 'POST',
|
|
1151
|
+
path: '/start_bulk_action',
|
|
1152
|
+
handler: (_14) => __awaiter(this, [_14], void 0, function* ({ body }) {
|
|
1153
|
+
const { resourceId, actionId, recordIds } = body;
|
|
1154
|
+
const resource = this.config.resources.find((res) => res.resourceId == resourceId);
|
|
1155
|
+
if (!resource) {
|
|
1156
|
+
return { error: `Resource '${resourceId}' not found` };
|
|
1157
|
+
}
|
|
1158
|
+
const action = resource.options.bulkActions.find((act) => act.id == actionId);
|
|
1159
|
+
if (!action) {
|
|
1160
|
+
return { error: `Action '${actionId}' not found` };
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
yield action.action({ selectedIds: recordIds });
|
|
1164
|
+
}
|
|
1165
|
+
return {
|
|
1166
|
+
actionId,
|
|
1167
|
+
recordIds,
|
|
1168
|
+
resourceId,
|
|
1169
|
+
status: 'success'
|
|
1170
|
+
};
|
|
1171
|
+
})
|
|
1172
|
+
});
|
|
1173
|
+
// setup endpoints for all plugins
|
|
1174
|
+
this.activatedPlugins.forEach((plugin) => {
|
|
1175
|
+
plugin.setupEndpoints(server);
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
_a = AdminForth, _AdminForth_defaultConfig = new WeakMap();
|
|
1180
|
+
AdminForth.Types = AdminForthDataTypes;
|
|
1181
|
+
AdminForth.Utils = {
|
|
1182
|
+
generatePasswordHash: (password) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1183
|
+
return yield AdminForthAuth.generatePasswordHash(password);
|
|
1184
|
+
})
|
|
1185
|
+
};
|
|
1186
|
+
export default AdminForth;
|