adminforth 1.2.99 → 1.3.2

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.
@@ -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
- let token;
51
- if (this.adminforth.config.rootUser
52
- && username === this.adminforth.config.rootUser.username
53
- && password === this.adminforth.config.rootUser.password) {
54
- this.adminforth.auth.setAuthCookie({ response, username, pk: null });
55
- adminUser = { isRoot: true, dbUser: null, pk: null, username: this.adminforth.config.rootUser.username };
56
- }
57
- else {
58
- // get resource from db
59
- if (!this.adminforth.config.auth) {
60
- throw new Error('No config.auth defined');
61
- }
62
- const userResource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.resourceId);
63
- // if there is no passwordHashField, in columns, add it, with backendOnly and showIn: []
64
- if (!userResource.dataSourceColumns.find((col) => col.name === this.adminforth.config.auth.passwordHashField)) {
65
- userResource.dataSourceColumns.push({
66
- name: this.adminforth.config.auth.passwordHashField,
67
- backendOnly: true,
68
- showIn: [],
69
- type: AdminForthDataTypes.STRING,
70
- });
71
- console.log('Adding passwordHashField to userResource', userResource);
72
- }
73
- const userRecord = (_b = (yield this.adminforth.connectors[userResource.dataSource].getData({
74
- resource: userResource,
75
- filters: [
76
- { field: this.adminforth.config.auth.usernameField, operator: AdminForthFilterOperators.EQ, value: username },
77
- ],
78
- limit: 1,
79
- offset: 0,
80
- sort: [],
81
- })).data) === null || _b === void 0 ? void 0 : _b[0];
82
- if (!userRecord) {
83
- return { error: 'User not found' };
84
- }
85
- const passwordHash = userRecord[this.adminforth.config.auth.passwordHashField];
86
- const valid = yield AdminForthAuth.verifyPassword(password, passwordHash);
87
- if (valid) {
88
- adminUser = {
89
- isRoot: false, dbUser: userRecord,
90
- pk: userRecord[userResource.columns.find((col) => col.primaryKey).name],
91
- username,
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
- else {
108
- return { error: INVALID_MESSAGE };
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.resourceId);
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, _q;
148
+ var _l, _m, _o, _p;
158
149
  let username = '';
159
150
  let userFullName = '';
160
- if (adminUser.isRoot) {
161
- // isRoot can be in JWT token still when rootUser deleted, so we check for rootUser?"
162
- username = ((_l = this.adminforth.config.rootUser) === null || _l === void 0 ? void 0 : _l.username) || 'RootUser';
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 = (_o = (_m = this.adminforth.config.customization).announcementBadge) === null || _o === void 0 ? void 0 : _o.call(_m, adminUser);
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: (_p = this.adminforth.config.customization) === null || _p === void 0 ? void 0 : _p.title,
232
- emptyFieldPlaceholder: (_q = this.adminforth.config.customization) === null || _q === void 0 ? void 0 : _q.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: (_r) => __awaiter(this, [_r], void 0, function* ({ body, adminUser }) {
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: (_s) => __awaiter(this, [_s], void 0, function* ({ body, adminUser }) {
282
- var _t, _u, _v, _w;
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((_u = (_t = resource.hooks) === null || _t === void 0 ? void 0 : _t[source]) === null || _u === void 0 ? void 0 : _u.beforeDatasourceRequest)) {
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((_w = (_v = resource.hooks) === null || _v === void 0 ? void 0 : _v[source]) === null || _w === void 0 ? void 0 : _w.afterDatasourceResponse)) {
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: (_x) => __awaiter(this, [_x], void 0, function* ({ body, adminUser }) {
397
- var _y, _z, _0, _1;
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((_z = (_y = columnConfig.foreignResource.hooks) === null || _y === void 0 ? void 0 : _y.dropdownList) === null || _z === void 0 ? void 0 : _z.beforeDatasourceRequest)) {
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((_1 = (_0 = columnConfig.foreignResource.hooks) === null || _0 === void 0 ? void 0 : _0.dropdownList) === null || _1 === void 0 ? void 0 : _1.afterDatasourceResponse)) {
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: (_2) => __awaiter(this, [_2], void 0, function* ({ body }) {
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: (_3) => __awaiter(this, [_3], void 0, function* ({ body, adminUser }) {
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: (_4) => __awaiter(this, [_4], void 0, function* ({ body, adminUser }) {
516
- var _5, _6, _7, _8;
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((_6 = (_5 = resource.hooks) === null || _5 === void 0 ? void 0 : _5.edit) === null || _6 === void 0 ? void 0 : _6.beforeSave)) {
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((_8 = (_7 = resource.hooks) === null || _7 === void 0 ? void 0 : _7.edit) === null || _8 === void 0 ? void 0 : _8.afterSave)) {
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: (_9) => __awaiter(this, [_9], void 0, function* ({ body, adminUser }) {
580
- var _10, _11, _12, _13;
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((_11 = (_10 = resource.hooks) === null || _10 === void 0 ? void 0 : _10.delete) === null || _11 === void 0 ? void 0 : _11.beforeSave)) {
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((_13 = (_12 = resource.hooks) === null || _12 === void 0 ? void 0 : _12.delete) === null || _13 === void 0 ? void 0 : _13.afterSave)) {
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: (_14) => __awaiter(this, [_14], void 0, function* ({ body, adminUser }) {
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) {
@@ -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,18 @@ 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
+ if (!name) {
313
+ return null;
314
+ }
315
+ const options = {
316
+ includeScore: true, // Includes score in the results to see how close matches are
317
+ threshold: 0.3, // Defines the fuzziness (lower values mean stricter matches)
318
+ };
319
+ const fuse = new Fuse(names.filter((n) => !!n), options);
320
+ // Search for a resource
321
+ const result = fuse.search(name);
322
+ if (result.length > 0) {
323
+ return result[0].item;
324
+ }
325
+ }
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
- resourceInstances: {
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 databaseConnectors in AdminForth config`);
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
- throw new Error(`Resource '${res.table}' refers to unknown dataSource '${res.dataSource}'`);
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(`DataSource ${res.dataSource} was not able to perform field discovery. It will not work properly`);
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
- throw new Error(`Resource '${res.table}' has no column '${col.name}'`);
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.resourceId);
182
+ const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
172
183
  if (!resource) {
173
- throw new Error('No auth resource found');
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
- return this.config.resources.find((res) => res.resourceId === resourceId);
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
- if (!this.config.auth.resourceId) {
78
- throw new Error('No config.auth.resourceId defined');
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.resourceId);
83
+ const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
90
84
  if (!userResource) {
91
- throw new Error(`Resource with id "${this.config.auth.resourceId}" not found`);
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 (typeof filter === 'object' ? [filter] : filter) as IAdminForthFilter[];
5
+ return (Array.isArray(filter) ? filter : [filter]) as IAdminForthFilter[];
31
6
  }
32
7
 
33
8
  function sortsIfSort(sort: IAdminForthSort | IAdminForthSort[]): IAdminForthSort[] {
34
- return (typeof sort === 'object' ? [sort] : sort) as IAdminForthSort[];
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 await this.dataConnector.getData({
48
- resource: this.resourceConfig,
49
- filters: filtersIfFilter(filter),
50
- limit: 1,
51
- offset: 0,
52
- sort: [],
53
- })[0];
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
  }