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