adminforth 1.2.99 → 1.3.1
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/auth.ts +6 -5
- package/dataConnectors/baseConnector.ts +20 -3
- package/dataConnectors/clickhouse.ts +1 -0
- package/dist/auth.js +6 -6
- package/dist/dataConnectors/baseConnector.js +16 -3
- package/dist/dataConnectors/clickhouse.js +1 -0
- package/dist/index.js +34 -16
- package/dist/modules/configValidator.js +10 -14
- package/dist/modules/operationalResource.js +6 -22
- package/dist/modules/restApi.js +75 -90
- package/dist/modules/utils.js +18 -0
- package/index.ts +37 -17
- package/modules/configValidator.ts +11 -16
- package/modules/operationalResource.ts +14 -38
- package/modules/restApi.ts +59 -72
- package/modules/utils.ts +22 -0
- package/package.json +2 -1
- package/types/AdminForthConfig.ts +27 -29
package/dist/modules/restApi.js
CHANGED
|
@@ -47,67 +47,58 @@ export default class AdminForthRestAPI {
|
|
|
47
47
|
const { username, password } = body;
|
|
48
48
|
let adminUser;
|
|
49
49
|
let toReturn = { ok: true, allowedLogin: true };
|
|
50
|
-
|
|
51
|
-
if (this.adminforth.config.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
};
|
|
93
|
-
const beforeLoginConfirmation = this.adminforth.config.auth.beforeLoginConfirmation;
|
|
94
|
-
if (beforeLoginConfirmation === null || beforeLoginConfirmation === void 0 ? void 0 : beforeLoginConfirmation.length) {
|
|
95
|
-
for (const hook of beforeLoginConfirmation) {
|
|
96
|
-
const resp = yield hook({ adminUser, response });
|
|
97
|
-
if ((_c = resp === null || resp === void 0 ? void 0 : resp.body) === null || _c === void 0 ? void 0 : _c.redirectTo) {
|
|
98
|
-
toReturn = { ok: resp.ok, redirectTo: (_d = resp === null || resp === void 0 ? void 0 : resp.body) === null || _d === void 0 ? void 0 : _d.redirectTo, allowedLogin: (_e = resp === null || resp === void 0 ? void 0 : resp.body) === null || _e === void 0 ? void 0 : _e.allowedLogin };
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
50
|
+
// get resource from db
|
|
51
|
+
if (!this.adminforth.config.auth) {
|
|
52
|
+
throw new Error('No config.auth defined we need it to find user, please follow the docs');
|
|
53
|
+
}
|
|
54
|
+
const userResource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.usersResourceId);
|
|
55
|
+
// if there is no passwordHashField, in columns, add it, with backendOnly and showIn: []
|
|
56
|
+
if (!userResource.dataSourceColumns.find((col) => col.name === this.adminforth.config.auth.passwordHashField)) {
|
|
57
|
+
userResource.dataSourceColumns.push({
|
|
58
|
+
name: this.adminforth.config.auth.passwordHashField,
|
|
59
|
+
backendOnly: true,
|
|
60
|
+
showIn: [],
|
|
61
|
+
type: AdminForthDataTypes.STRING,
|
|
62
|
+
});
|
|
63
|
+
console.log('Adding passwordHashField to userResource', userResource);
|
|
64
|
+
}
|
|
65
|
+
const userRecord = (_b = (yield this.adminforth.connectors[userResource.dataSource].getData({
|
|
66
|
+
resource: userResource,
|
|
67
|
+
filters: [
|
|
68
|
+
{ field: this.adminforth.config.auth.usernameField, operator: AdminForthFilterOperators.EQ, value: username },
|
|
69
|
+
],
|
|
70
|
+
limit: 1,
|
|
71
|
+
offset: 0,
|
|
72
|
+
sort: [],
|
|
73
|
+
})).data) === null || _b === void 0 ? void 0 : _b[0];
|
|
74
|
+
if (!userRecord) {
|
|
75
|
+
return { error: 'User not found' };
|
|
76
|
+
}
|
|
77
|
+
const passwordHash = userRecord[this.adminforth.config.auth.passwordHashField];
|
|
78
|
+
const valid = yield AdminForthAuth.verifyPassword(password, passwordHash);
|
|
79
|
+
if (valid) {
|
|
80
|
+
adminUser = {
|
|
81
|
+
dbUser: userRecord,
|
|
82
|
+
pk: userRecord[userResource.columns.find((col) => col.primaryKey).name],
|
|
83
|
+
username,
|
|
84
|
+
};
|
|
85
|
+
const beforeLoginConfirmation = this.adminforth.config.auth.beforeLoginConfirmation;
|
|
86
|
+
if (beforeLoginConfirmation === null || beforeLoginConfirmation === void 0 ? void 0 : beforeLoginConfirmation.length) {
|
|
87
|
+
for (const hook of beforeLoginConfirmation) {
|
|
88
|
+
const resp = yield hook({ adminUser, response });
|
|
89
|
+
if ((_c = resp === null || resp === void 0 ? void 0 : resp.body) === null || _c === void 0 ? void 0 : _c.redirectTo) {
|
|
90
|
+
toReturn = { ok: resp.ok, redirectTo: (_d = resp === null || resp === void 0 ? void 0 : resp.body) === null || _d === void 0 ? void 0 : _d.redirectTo, allowedLogin: (_e = resp === null || resp === void 0 ? void 0 : resp.body) === null || _e === void 0 ? void 0 : _e.allowedLogin };
|
|
91
|
+
break;
|
|
101
92
|
}
|
|
102
93
|
}
|
|
103
|
-
if (toReturn.allowedLogin) {
|
|
104
|
-
this.adminforth.auth.setAuthCookie({ response, username, pk: userRecord[userResource.columns.find((col) => col.primaryKey).name] });
|
|
105
|
-
}
|
|
106
94
|
}
|
|
107
|
-
|
|
108
|
-
|
|
95
|
+
if (toReturn.allowedLogin) {
|
|
96
|
+
this.adminforth.auth.setAuthCookie({ response, username, pk: userRecord[userResource.columns.find((col) => col.primaryKey).name] });
|
|
109
97
|
}
|
|
110
98
|
}
|
|
99
|
+
else {
|
|
100
|
+
return { error: INVALID_MESSAGE };
|
|
101
|
+
}
|
|
111
102
|
return toReturn;
|
|
112
103
|
})
|
|
113
104
|
});
|
|
@@ -138,7 +129,7 @@ export default class AdminForthRestAPI {
|
|
|
138
129
|
throw new Error('No config.auth defined');
|
|
139
130
|
}
|
|
140
131
|
const usernameField = this.adminforth.config.auth.usernameField;
|
|
141
|
-
const resource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.
|
|
132
|
+
const resource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.usersResourceId);
|
|
142
133
|
const usernameColumn = resource.columns.find((col) => col.name === usernameField);
|
|
143
134
|
return {
|
|
144
135
|
brandName: this.adminforth.config.customization.brandName,
|
|
@@ -154,18 +145,12 @@ export default class AdminForthRestAPI {
|
|
|
154
145
|
method: 'GET',
|
|
155
146
|
path: '/get_base_config',
|
|
156
147
|
handler: (_k) => __awaiter(this, [_k], void 0, function* ({ input, adminUser, cookies }) {
|
|
157
|
-
var _l, _m, _o, _p
|
|
148
|
+
var _l, _m, _o, _p;
|
|
158
149
|
let username = '';
|
|
159
150
|
let userFullName = '';
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
const dbUser = adminUser.dbUser;
|
|
166
|
-
username = dbUser[this.adminforth.config.auth.usernameField];
|
|
167
|
-
userFullName = dbUser[this.adminforth.config.auth.userFullNameField];
|
|
168
|
-
}
|
|
151
|
+
const dbUser = adminUser.dbUser;
|
|
152
|
+
username = dbUser[this.adminforth.config.auth.usernameField];
|
|
153
|
+
userFullName = dbUser[this.adminforth.config.auth.userFullNameField];
|
|
169
154
|
const userData = {
|
|
170
155
|
[this.adminforth.config.auth.usernameField]: username,
|
|
171
156
|
[this.adminforth.config.auth.userFullNameField]: userFullName
|
|
@@ -213,7 +198,7 @@ export default class AdminForthRestAPI {
|
|
|
213
198
|
yield processMenuItem(newMenuItem);
|
|
214
199
|
newMenu.push(newMenuItem);
|
|
215
200
|
}
|
|
216
|
-
const announcementBadge = (
|
|
201
|
+
const announcementBadge = (_m = (_l = this.adminforth.config.customization).announcementBadge) === null || _m === void 0 ? void 0 : _m.call(_l, adminUser);
|
|
217
202
|
return {
|
|
218
203
|
user: userData,
|
|
219
204
|
resources: this.adminforth.config.resources.map((res) => ({
|
|
@@ -228,8 +213,8 @@ export default class AdminForthRestAPI {
|
|
|
228
213
|
deleteConfirmation: this.adminforth.config.deleteConfirmation,
|
|
229
214
|
auth: this.adminforth.config.auth,
|
|
230
215
|
usernameField: this.adminforth.config.auth.usernameField,
|
|
231
|
-
title: (
|
|
232
|
-
emptyFieldPlaceholder: (
|
|
216
|
+
title: (_o = this.adminforth.config.customization) === null || _o === void 0 ? void 0 : _o.title,
|
|
217
|
+
emptyFieldPlaceholder: (_p = this.adminforth.config.customization) === null || _p === void 0 ? void 0 : _p.emptyFieldPlaceholder,
|
|
233
218
|
announcementBadge,
|
|
234
219
|
},
|
|
235
220
|
adminUser,
|
|
@@ -247,7 +232,7 @@ export default class AdminForthRestAPI {
|
|
|
247
232
|
server.endpoint({
|
|
248
233
|
method: 'POST',
|
|
249
234
|
path: '/get_resource',
|
|
250
|
-
handler: (
|
|
235
|
+
handler: (_q) => __awaiter(this, [_q], void 0, function* ({ body, adminUser }) {
|
|
251
236
|
const { resourceId } = body;
|
|
252
237
|
if (!this.adminforth.statuses.dbDiscover) {
|
|
253
238
|
return { error: 'Database discovery not started' };
|
|
@@ -278,8 +263,8 @@ export default class AdminForthRestAPI {
|
|
|
278
263
|
server.endpoint({
|
|
279
264
|
method: 'POST',
|
|
280
265
|
path: '/get_resource_data',
|
|
281
|
-
handler: (
|
|
282
|
-
var _t, _u, _v
|
|
266
|
+
handler: (_r) => __awaiter(this, [_r], void 0, function* ({ body, adminUser }) {
|
|
267
|
+
var _s, _t, _u, _v;
|
|
283
268
|
const { resourceId, source } = body;
|
|
284
269
|
if (['show', 'list'].includes(source) === false) {
|
|
285
270
|
return { error: 'Invalid source, should be list or show' };
|
|
@@ -299,7 +284,7 @@ export default class AdminForthRestAPI {
|
|
|
299
284
|
if (!allowed) {
|
|
300
285
|
return { error };
|
|
301
286
|
}
|
|
302
|
-
for (const hook of listify((
|
|
287
|
+
for (const hook of listify((_t = (_s = resource.hooks) === null || _s === void 0 ? void 0 : _s[source]) === null || _t === void 0 ? void 0 : _t.beforeDatasourceRequest)) {
|
|
303
288
|
const resp = yield hook({ resource, query: body, adminUser });
|
|
304
289
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
305
290
|
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -378,7 +363,7 @@ export default class AdminForthRestAPI {
|
|
|
378
363
|
item._label = resource.recordLabel(item);
|
|
379
364
|
});
|
|
380
365
|
// only after adminforth made all post processing, give user ability to edit it
|
|
381
|
-
for (const hook of listify((
|
|
366
|
+
for (const hook of listify((_v = (_u = resource.hooks) === null || _u === void 0 ? void 0 : _u[source]) === null || _v === void 0 ? void 0 : _v.afterDatasourceResponse)) {
|
|
382
367
|
const resp = yield hook({ resource, response: data.data, adminUser });
|
|
383
368
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
384
369
|
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -393,8 +378,8 @@ export default class AdminForthRestAPI {
|
|
|
393
378
|
server.endpoint({
|
|
394
379
|
method: 'POST',
|
|
395
380
|
path: '/get_resource_foreign_data',
|
|
396
|
-
handler: (
|
|
397
|
-
var _y, _z, _0
|
|
381
|
+
handler: (_w) => __awaiter(this, [_w], void 0, function* ({ body, adminUser }) {
|
|
382
|
+
var _x, _y, _z, _0;
|
|
398
383
|
const { resourceId, column } = body;
|
|
399
384
|
if (!this.adminforth.statuses.dbDiscover) {
|
|
400
385
|
return { error: 'Database discovery not started' };
|
|
@@ -415,7 +400,7 @@ export default class AdminForthRestAPI {
|
|
|
415
400
|
}
|
|
416
401
|
const targetResourceId = columnConfig.foreignResource.resourceId;
|
|
417
402
|
const targetResource = this.adminforth.config.resources.find((res) => res.resourceId == targetResourceId);
|
|
418
|
-
for (const hook of listify((
|
|
403
|
+
for (const hook of listify((_y = (_x = columnConfig.foreignResource.hooks) === null || _x === void 0 ? void 0 : _x.dropdownList) === null || _y === void 0 ? void 0 : _y.beforeDatasourceRequest)) {
|
|
419
404
|
const resp = yield hook({ query: body, adminUser, resource: targetResource });
|
|
420
405
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
421
406
|
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -444,7 +429,7 @@ export default class AdminForthRestAPI {
|
|
|
444
429
|
const response = {
|
|
445
430
|
items
|
|
446
431
|
};
|
|
447
|
-
for (const hook of listify((
|
|
432
|
+
for (const hook of listify((_0 = (_z = columnConfig.foreignResource.hooks) === null || _z === void 0 ? void 0 : _z.dropdownList) === null || _0 === void 0 ? void 0 : _0.afterDatasourceResponse)) {
|
|
448
433
|
const resp = yield hook({ response, adminUser, resource: targetResource });
|
|
449
434
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
450
435
|
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -459,7 +444,7 @@ export default class AdminForthRestAPI {
|
|
|
459
444
|
server.endpoint({
|
|
460
445
|
method: 'POST',
|
|
461
446
|
path: '/get_min_max_for_columns',
|
|
462
|
-
handler: (
|
|
447
|
+
handler: (_1) => __awaiter(this, [_1], void 0, function* ({ body }) {
|
|
463
448
|
const { resourceId } = body;
|
|
464
449
|
if (!this.adminforth.statuses.dbDiscover) {
|
|
465
450
|
return { error: 'Database discovery not started' };
|
|
@@ -488,7 +473,7 @@ export default class AdminForthRestAPI {
|
|
|
488
473
|
server.endpoint({
|
|
489
474
|
method: 'POST',
|
|
490
475
|
path: '/create_record',
|
|
491
|
-
handler: (
|
|
476
|
+
handler: (_2) => __awaiter(this, [_2], void 0, function* ({ body, adminUser }) {
|
|
492
477
|
const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
|
|
493
478
|
if (!resource) {
|
|
494
479
|
return { error: `Resource '${body['resourceId']}' not found` };
|
|
@@ -512,8 +497,8 @@ export default class AdminForthRestAPI {
|
|
|
512
497
|
server.endpoint({
|
|
513
498
|
method: 'POST',
|
|
514
499
|
path: '/update_record',
|
|
515
|
-
handler: (
|
|
516
|
-
var _5, _6, _7
|
|
500
|
+
handler: (_3) => __awaiter(this, [_3], void 0, function* ({ body, adminUser }) {
|
|
501
|
+
var _4, _5, _6, _7;
|
|
517
502
|
const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
|
|
518
503
|
if (!resource) {
|
|
519
504
|
return { error: `Resource '${body['resourceId']}' not found` };
|
|
@@ -532,7 +517,7 @@ export default class AdminForthRestAPI {
|
|
|
532
517
|
return { error };
|
|
533
518
|
}
|
|
534
519
|
// execute hook if needed
|
|
535
|
-
for (const hook of listify((
|
|
520
|
+
for (const hook of listify((_5 = (_4 = resource.hooks) === null || _4 === void 0 ? void 0 : _4.edit) === null || _5 === void 0 ? void 0 : _5.beforeSave)) {
|
|
536
521
|
const resp = yield hook({ resource, record, adminUser });
|
|
537
522
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
538
523
|
throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -559,7 +544,7 @@ export default class AdminForthRestAPI {
|
|
|
559
544
|
yield connector.updateRecord({ resource, recordId, newValues });
|
|
560
545
|
}
|
|
561
546
|
// execute hook if needed
|
|
562
|
-
for (const hook of listify((
|
|
547
|
+
for (const hook of listify((_7 = (_6 = resource.hooks) === null || _6 === void 0 ? void 0 : _6.edit) === null || _7 === void 0 ? void 0 : _7.afterSave)) {
|
|
563
548
|
const resp = yield hook({ resource, record, adminUser, oldRecord });
|
|
564
549
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
565
550
|
throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -576,8 +561,8 @@ export default class AdminForthRestAPI {
|
|
|
576
561
|
server.endpoint({
|
|
577
562
|
method: 'POST',
|
|
578
563
|
path: '/delete_record',
|
|
579
|
-
handler: (
|
|
580
|
-
var _10, _11, _12
|
|
564
|
+
handler: (_8) => __awaiter(this, [_8], void 0, function* ({ body, adminUser }) {
|
|
565
|
+
var _9, _10, _11, _12;
|
|
581
566
|
const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
|
|
582
567
|
const record = yield this.adminforth.connectors[resource.dataSource].getRecordByPrimaryKey(resource, body['primaryKey']);
|
|
583
568
|
if (!resource) {
|
|
@@ -595,7 +580,7 @@ export default class AdminForthRestAPI {
|
|
|
595
580
|
return { error };
|
|
596
581
|
}
|
|
597
582
|
// execute hook if needed
|
|
598
|
-
for (const hook of listify((
|
|
583
|
+
for (const hook of listify((_10 = (_9 = resource.hooks) === null || _9 === void 0 ? void 0 : _9.delete) === null || _10 === void 0 ? void 0 : _10.beforeSave)) {
|
|
599
584
|
const resp = yield hook({ resource, record, adminUser });
|
|
600
585
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
601
586
|
throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -607,7 +592,7 @@ export default class AdminForthRestAPI {
|
|
|
607
592
|
const connector = this.adminforth.connectors[resource.dataSource];
|
|
608
593
|
yield connector.deleteRecord({ resource, recordId: body['primaryKey'] });
|
|
609
594
|
// execute hook if needed
|
|
610
|
-
for (const hook of listify((
|
|
595
|
+
for (const hook of listify((_12 = (_11 = resource.hooks) === null || _11 === void 0 ? void 0 : _11.delete) === null || _12 === void 0 ? void 0 : _12.afterSave)) {
|
|
611
596
|
const resp = yield hook({ resource, record, adminUser });
|
|
612
597
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
613
598
|
throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -624,7 +609,7 @@ export default class AdminForthRestAPI {
|
|
|
624
609
|
server.endpoint({
|
|
625
610
|
method: 'POST',
|
|
626
611
|
path: '/start_bulk_action',
|
|
627
|
-
handler: (
|
|
612
|
+
handler: (_13) => __awaiter(this, [_13], void 0, function* ({ body, adminUser }) {
|
|
628
613
|
const { resourceId, actionId, recordIds } = body;
|
|
629
614
|
const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
|
|
630
615
|
if (!resource) {
|
package/dist/modules/utils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import fs from 'fs';
|
|
4
|
+
import Fuse from 'fuse.js';
|
|
4
5
|
// @ts-ignore-next-line
|
|
5
6
|
const csscolors = {
|
|
6
7
|
"aliceblue": "#f0f8ff",
|
|
@@ -307,3 +308,20 @@ export function inverseRGBA(rgba) {
|
|
|
307
308
|
let brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
|
308
309
|
return brightness > 128 ? 'rgba(0,0,0,1)' : 'rgba(255,255,255,1)';
|
|
309
310
|
}
|
|
311
|
+
export function suggestIfTypo(names, name) {
|
|
312
|
+
console.log('names', names);
|
|
313
|
+
console.log('name', name);
|
|
314
|
+
if (!name) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
const options = {
|
|
318
|
+
includeScore: true, // Includes score in the results to see how close matches are
|
|
319
|
+
threshold: 0.3, // Defines the fuzziness (lower values mean stricter matches)
|
|
320
|
+
};
|
|
321
|
+
const fuse = new Fuse(names.filter((n) => !!n), options);
|
|
322
|
+
// Search for a resource
|
|
323
|
+
const result = fuse.search(name);
|
|
324
|
+
if (result.length > 0) {
|
|
325
|
+
return result[0].item;
|
|
326
|
+
}
|
|
327
|
+
}
|
package/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import PostgresConnector from './dataConnectors/postgres.js';
|
|
|
5
5
|
import SQLiteConnector from './dataConnectors/sqlite.js';
|
|
6
6
|
import CodeInjector from './modules/codeInjector.js';
|
|
7
7
|
import ExpressServer from './servers/express.js';
|
|
8
|
-
import { ADMINFORTH_VERSION, listify } from './modules/utils.js';
|
|
8
|
+
import { ADMINFORTH_VERSION, listify, suggestIfTypo } from './modules/utils.js';
|
|
9
9
|
import {
|
|
10
10
|
type AdminForthConfig,
|
|
11
11
|
type IAdminForth,
|
|
@@ -22,11 +22,13 @@ import AdminForthPlugin from './basePlugin.js';
|
|
|
22
22
|
import ConfigValidator from './modules/configValidator.js';
|
|
23
23
|
import AdminForthRestAPI, { interpretResource } from './modules/restApi.js';
|
|
24
24
|
import ClickhouseConnector from './dataConnectors/clickhouse.js';
|
|
25
|
+
import OperationalResource from './modules/operationalResource.js';
|
|
25
26
|
|
|
26
27
|
// exports
|
|
27
28
|
export * from './types/AdminForthConfig.js';
|
|
28
29
|
export { interpretResource };
|
|
29
30
|
export { AdminForthPlugin };
|
|
31
|
+
export { suggestIfTypo };
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
class AdminForth implements IAdminForth {
|
|
@@ -52,7 +54,7 @@ class AdminForth implements IAdminForth {
|
|
|
52
54
|
activatedPlugins: Array<AdminForthPlugin>;
|
|
53
55
|
configValidator: IConfigValidator;
|
|
54
56
|
restApi: AdminForthRestAPI;
|
|
55
|
-
|
|
57
|
+
operationalResources: {
|
|
56
58
|
[resourceId: string]: IOperationalResource,
|
|
57
59
|
}
|
|
58
60
|
baseUrlSlashed: string;
|
|
@@ -120,21 +122,24 @@ class AdminForth implements IAdminForth {
|
|
|
120
122
|
this.config.dataSources.forEach((ds) => {
|
|
121
123
|
const dbType = ds.url.split(':')[0];
|
|
122
124
|
if (!this.config.databaseConnectors[dbType]) {
|
|
123
|
-
throw new Error(`Database type ${dbType} is not supported, consider using
|
|
125
|
+
throw new Error(`Database type '${dbType}' is not supported, consider using one of ${Object.keys(this.connectorClasses).join(', ')} or create your own data-source connector`);
|
|
124
126
|
}
|
|
125
127
|
this.connectors[ds.id] = new this.config.databaseConnectors[dbType]({url: ds.url});
|
|
126
128
|
});
|
|
127
129
|
|
|
128
130
|
await Promise.all(this.config.resources.map(async (res) => {
|
|
129
131
|
if (!this.connectors[res.dataSource]) {
|
|
130
|
-
|
|
132
|
+
const similar = suggestIfTypo(Object.keys(this.connectors), res.dataSource);
|
|
133
|
+
throw new Error(`Resource '${res.table}' refers to unknown dataSource '${res.dataSource}' ${similar
|
|
134
|
+
? `. Did you mean '${similar}'?` : 'Available dataSources: '+Object.keys(this.connectors).join(', ')}`
|
|
135
|
+
);
|
|
131
136
|
}
|
|
132
137
|
const fieldTypes = await this.connectors[res.dataSource].discoverFields(res);
|
|
133
138
|
if (fieldTypes !== null && !Object.keys(fieldTypes).length) {
|
|
134
139
|
throw new Error(`Table '${res.table}' (In resource '${res.resourceId}') has no fields or does not exist`);
|
|
135
140
|
}
|
|
136
141
|
if (fieldTypes === null) {
|
|
137
|
-
console.error(
|
|
142
|
+
console.error(`⛔ DataSource ${res.dataSource} was not able to perform field discovery. It will not work properly`);
|
|
138
143
|
return;
|
|
139
144
|
}
|
|
140
145
|
if (!res.columns) {
|
|
@@ -143,7 +148,8 @@ class AdminForth implements IAdminForth {
|
|
|
143
148
|
|
|
144
149
|
res.columns.forEach((col, i) => {
|
|
145
150
|
if (!fieldTypes[col.name] && !col.virtual) {
|
|
146
|
-
|
|
151
|
+
const similar = suggestIfTypo(Object.keys(fieldTypes), col.name);
|
|
152
|
+
throw new Error(`Resource '${res.table}' has no column '${col.name}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
|
|
147
153
|
}
|
|
148
154
|
// first find discovered values, but allow override
|
|
149
155
|
res.columns[i] = { ...fieldTypes[col.name], ...col };
|
|
@@ -160,6 +166,11 @@ class AdminForth implements IAdminForth {
|
|
|
160
166
|
|
|
161
167
|
this.statuses.dbDiscover = 'done';
|
|
162
168
|
|
|
169
|
+
this.operationalResources = {};
|
|
170
|
+
this.config.resources.forEach((resource) => {
|
|
171
|
+
this.operationalResources[resource.resourceId] = new OperationalResource(this.connectors[resource.dataSource], resource);
|
|
172
|
+
});
|
|
173
|
+
|
|
163
174
|
// console.log('⚙️⚙️⚙️ Database discovery done', JSON.stringify(this.config.resources, null, 2));
|
|
164
175
|
}
|
|
165
176
|
|
|
@@ -168,9 +179,12 @@ class AdminForth implements IAdminForth {
|
|
|
168
179
|
}
|
|
169
180
|
|
|
170
181
|
async getUserByPk(pk: string) {
|
|
171
|
-
const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.
|
|
182
|
+
const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
|
|
172
183
|
if (!resource) {
|
|
173
|
-
|
|
184
|
+
const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId), this.config.auth.usersResourceId);
|
|
185
|
+
throw new Error(`No resource with ${this.config.auth.usersResourceId} found. ${similar ?
|
|
186
|
+
`Did you mean '${similar}' in config.auth.usersResourceId?` : 'Please set correct resource in config.auth.usersResourceId'}`
|
|
187
|
+
);
|
|
174
188
|
}
|
|
175
189
|
const users = await this.connectors[resource.dataSource].getData({
|
|
176
190
|
resource,
|
|
@@ -186,13 +200,6 @@ class AdminForth implements IAdminForth {
|
|
|
186
200
|
|
|
187
201
|
async createResourceRecord({ resource, record, adminUser }: { resource: AdminForthResource, record: any, adminUser: AdminUser }) {
|
|
188
202
|
for (const column of resource.columns) {
|
|
189
|
-
if (column.fillOnCreate) {
|
|
190
|
-
if (record[column.name] === undefined) {
|
|
191
|
-
record[column.name] = column.fillOnCreate({
|
|
192
|
-
initialRecord: record, adminUser
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
203
|
if (
|
|
197
204
|
(column.required as {create?: boolean, edit?: boolean}) ?.create &&
|
|
198
205
|
record[column.name] === undefined &&
|
|
@@ -235,7 +242,7 @@ class AdminForth implements IAdminForth {
|
|
|
235
242
|
}
|
|
236
243
|
const connector = this.connectors[resource.dataSource];
|
|
237
244
|
process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record createResourceRecord', record);
|
|
238
|
-
await connector.createRecord({ resource, record });
|
|
245
|
+
await connector.createRecord({ resource, record, adminUser });
|
|
239
246
|
// execute hook if needed
|
|
240
247
|
for (const hook of listify(resource.hooks?.create?.afterSave as AfterSaveFunction[])) {
|
|
241
248
|
console.log('Hook afterSave', hook);
|
|
@@ -253,7 +260,20 @@ class AdminForth implements IAdminForth {
|
|
|
253
260
|
}
|
|
254
261
|
|
|
255
262
|
resource(resourceId: string) {
|
|
256
|
-
|
|
263
|
+
if (this.statuses.dbDiscover !== 'done') {
|
|
264
|
+
if (this.statuses.dbDiscover === 'running') {
|
|
265
|
+
throw new Error('Database discovery is running. You can\'t use data API while database discovery is not finished.\n'+
|
|
266
|
+
'Consider moving your code to a place where it will be executed after database discovery is already done (after await admin.discoverDatabases())');
|
|
267
|
+
} else {
|
|
268
|
+
throw new Error('Database discovery is not yet started. You can\'t use data API before database discovery is done. \n'+
|
|
269
|
+
'Call admin.discoverDatabases() first and await it before using data API');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (!this.operationalResources[resourceId]) {
|
|
273
|
+
const closeName = suggestIfTypo(Object.keys(this.operationalResources), resourceId);
|
|
274
|
+
throw new Error(`Resource with id '${resourceId}' not found${closeName ? `. Did you mean '${closeName}'?` : ''}`);
|
|
275
|
+
}
|
|
276
|
+
return this.operationalResources[resourceId];
|
|
257
277
|
}
|
|
258
278
|
|
|
259
279
|
setupEndpoints(server: IHttpServer) {
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import path from 'path';
|
|
13
|
-
import { guessLabelFromName } from './utils.js';
|
|
13
|
+
import { guessLabelFromName, suggestIfTypo } from './utils.js';
|
|
14
14
|
|
|
15
15
|
import crypto from 'crypto';
|
|
16
16
|
|
|
@@ -51,17 +51,6 @@ export default class ConfigValidator implements IConfigValidator {
|
|
|
51
51
|
validateConfig() {
|
|
52
52
|
const errors = [];
|
|
53
53
|
|
|
54
|
-
if (this.config.rootUser) {
|
|
55
|
-
if (!this.config.rootUser.username) {
|
|
56
|
-
throw new Error('rootUser.username is required');
|
|
57
|
-
}
|
|
58
|
-
if (!this.config.rootUser.password) {
|
|
59
|
-
throw new Error('rootUser.password is required');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
console.log('\n ☝️☝️☝️ [INSECURE ALERT] config.rootUser is set, please create a new user to login in backoffice and remove config.rootUser from config ASAP when you are in production\n');
|
|
63
|
-
}
|
|
64
|
-
|
|
65
54
|
if (!this.config.customization.customComponentsDir) {
|
|
66
55
|
this.config.customization.customComponentsDir = './custom';
|
|
67
56
|
}
|
|
@@ -74,8 +63,13 @@ export default class ConfigValidator implements IConfigValidator {
|
|
|
74
63
|
}
|
|
75
64
|
|
|
76
65
|
if (this.config.auth) {
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
// TODO: remove in future releases
|
|
67
|
+
if (!this.config.auth.usersResourceId && this.config.auth.resourceId) {
|
|
68
|
+
this.config.auth.usersResourceId = this.config.auth.resourceId;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!this.config.auth.usersResourceId) {
|
|
72
|
+
throw new Error('No config.auth.usersResourceId defined');
|
|
79
73
|
}
|
|
80
74
|
if (!this.config.auth.passwordHashField) {
|
|
81
75
|
throw new Error('No config.auth.passwordHashField defined');
|
|
@@ -86,9 +80,10 @@ export default class ConfigValidator implements IConfigValidator {
|
|
|
86
80
|
if (this.config.auth.loginBackgroundImage) {
|
|
87
81
|
errors.push(...this.checkCustomFileExists(this.config.auth.loginBackgroundImage));
|
|
88
82
|
}
|
|
89
|
-
const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.
|
|
83
|
+
const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
|
|
90
84
|
if (!userResource) {
|
|
91
|
-
|
|
85
|
+
const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId || res.table), this.config.auth.usersResourceId);
|
|
86
|
+
throw new Error(`Resource with id "${this.config.auth.usersResourceId}" not found. ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
92
87
|
}
|
|
93
88
|
|
|
94
89
|
if (!this.config.auth.beforeLoginConfirmation) {
|
|
@@ -1,40 +1,15 @@
|
|
|
1
1
|
import { IAdminForthFilter, IAdminForthSort, IOperationalResource, IAdminForthDataSourceConnectorBase, AdminForthResource, IAdminForth } from '../types/AdminForthConfig.js';
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
// export interface IOperationalResource {
|
|
5
|
-
// get: (filters: IAdminForthFilter | IAdminForthFilter[]) => Promise<any[]>;
|
|
6
|
-
|
|
7
|
-
// list: (filters: IAdminForthFilter | IAdminForthFilter[], limit: number, offset: number, sort: IAdminForthSort | IAdminForthSort[]) => Promise<any[]>;
|
|
8
|
-
|
|
9
|
-
// count: (filters: IAdminForthFilter | IAdminForthFilter[]) => Promise<number>;
|
|
10
|
-
|
|
11
|
-
// create: (record: any) => Promise<any>;
|
|
12
|
-
|
|
13
|
-
// update: (primaryKey: any, record: any) => Promise<any>;
|
|
14
|
-
|
|
15
|
-
// delete: (primaryKey: any) => Promise<boolean>;
|
|
16
|
-
|
|
17
|
-
// deleteMany: (primaryKeys: any[]) => Promise<boolean>;
|
|
18
|
-
// }
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// async getData({ resource, limit, offset, sort, filters }: {
|
|
22
|
-
// resource: AdminForthResource,
|
|
23
|
-
// limit: number,
|
|
24
|
-
// offset: number,
|
|
25
|
-
// sort: { field: string, direction: AdminForthSortDirections }[],
|
|
26
|
-
// filters: { field: string, operator: AdminForthFilterOperators, value: any }[]
|
|
27
|
-
// }): Promise<{ data: any[], total: number }> {
|
|
28
|
-
|
|
29
4
|
function filtersIfFilter(filter: IAdminForthFilter | IAdminForthFilter[]): IAdminForthFilter[] {
|
|
30
|
-
return (
|
|
5
|
+
return (Array.isArray(filter) ? filter : [filter]) as IAdminForthFilter[];
|
|
31
6
|
}
|
|
32
7
|
|
|
33
8
|
function sortsIfSort(sort: IAdminForthSort | IAdminForthSort[]): IAdminForthSort[] {
|
|
34
|
-
return (
|
|
9
|
+
return (Array.isArray(sort) ? sort : [sort]) as IAdminForthSort[];
|
|
35
10
|
}
|
|
36
11
|
|
|
37
|
-
export class OperationalResource implements IOperationalResource {
|
|
12
|
+
export default class OperationalResource implements IOperationalResource {
|
|
38
13
|
dataConnector: IAdminForthDataSourceConnectorBase;
|
|
39
14
|
resourceConfig: AdminForthResource;
|
|
40
15
|
|
|
@@ -43,14 +18,16 @@ export class OperationalResource implements IOperationalResource {
|
|
|
43
18
|
this.resourceConfig = resourceConfig;
|
|
44
19
|
}
|
|
45
20
|
|
|
46
|
-
async get(filter: IAdminForthFilter | IAdminForthFilter[]): Promise<any
|
|
47
|
-
return
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
21
|
+
async get(filter: IAdminForthFilter | IAdminForthFilter[]): Promise<any | null> {
|
|
22
|
+
return (
|
|
23
|
+
await this.dataConnector.getData({
|
|
24
|
+
resource: this.resourceConfig,
|
|
25
|
+
filters: filtersIfFilter(filter),
|
|
26
|
+
limit: 1,
|
|
27
|
+
offset: 0,
|
|
28
|
+
sort: [],
|
|
29
|
+
})
|
|
30
|
+
).data[0] || null;
|
|
54
31
|
}
|
|
55
32
|
|
|
56
33
|
async list(
|
|
@@ -78,7 +55,7 @@ export class OperationalResource implements IOperationalResource {
|
|
|
78
55
|
}
|
|
79
56
|
|
|
80
57
|
async create(record: any): Promise<any> {
|
|
81
|
-
return await this.dataConnector.createRecord({ resource: this.resourceConfig, record });
|
|
58
|
+
return await this.dataConnector.createRecord({ resource: this.resourceConfig, record, adminUser: null });
|
|
82
59
|
}
|
|
83
60
|
|
|
84
61
|
async update(primaryKey: any, record: any): Promise<any> {
|
|
@@ -92,6 +69,5 @@ export class OperationalResource implements IOperationalResource {
|
|
|
92
69
|
async delete(primaryKey: any): Promise<boolean> {
|
|
93
70
|
return await this.dataConnector.deleteRecord({ resource: this.resourceConfig, recordId: primaryKey });
|
|
94
71
|
}
|
|
95
|
-
|
|
96
72
|
|
|
97
73
|
}
|