adminforth 1.2.99 → 1.2.100

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/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,14 @@ 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';
26
+ import { error } from 'console';
25
27
 
26
28
  // exports
27
29
  export * from './types/AdminForthConfig.js';
28
30
  export { interpretResource };
29
31
  export { AdminForthPlugin };
32
+ export { suggestIfTypo };
30
33
 
31
34
 
32
35
  class AdminForth implements IAdminForth {
@@ -52,7 +55,7 @@ class AdminForth implements IAdminForth {
52
55
  activatedPlugins: Array<AdminForthPlugin>;
53
56
  configValidator: IConfigValidator;
54
57
  restApi: AdminForthRestAPI;
55
- resourceInstances: {
58
+ operationalResources: {
56
59
  [resourceId: string]: IOperationalResource,
57
60
  }
58
61
  baseUrlSlashed: string;
@@ -120,21 +123,24 @@ class AdminForth implements IAdminForth {
120
123
  this.config.dataSources.forEach((ds) => {
121
124
  const dbType = ds.url.split(':')[0];
122
125
  if (!this.config.databaseConnectors[dbType]) {
123
- throw new Error(`Database type ${dbType} is not supported, consider using databaseConnectors in AdminForth config`);
126
+ 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
127
  }
125
128
  this.connectors[ds.id] = new this.config.databaseConnectors[dbType]({url: ds.url});
126
129
  });
127
130
 
128
131
  await Promise.all(this.config.resources.map(async (res) => {
129
132
  if (!this.connectors[res.dataSource]) {
130
- throw new Error(`Resource '${res.table}' refers to unknown dataSource '${res.dataSource}'`);
133
+ const similar = suggestIfTypo(Object.keys(this.connectors), res.dataSource);
134
+ throw new Error(`Resource '${res.table}' refers to unknown dataSource '${res.dataSource}' ${similar
135
+ ? `. Did you mean '${similar}'?` : 'Available dataSources: '+Object.keys(this.connectors).join(', ')}`
136
+ );
131
137
  }
132
138
  const fieldTypes = await this.connectors[res.dataSource].discoverFields(res);
133
139
  if (fieldTypes !== null && !Object.keys(fieldTypes).length) {
134
140
  throw new Error(`Table '${res.table}' (In resource '${res.resourceId}') has no fields or does not exist`);
135
141
  }
136
142
  if (fieldTypes === null) {
137
- console.error(`DataSource ${res.dataSource} was not able to perform field discovery. It will not work properly`);
143
+ console.error(`⛔ DataSource ${res.dataSource} was not able to perform field discovery. It will not work properly`);
138
144
  return;
139
145
  }
140
146
  if (!res.columns) {
@@ -143,7 +149,8 @@ class AdminForth implements IAdminForth {
143
149
 
144
150
  res.columns.forEach((col, i) => {
145
151
  if (!fieldTypes[col.name] && !col.virtual) {
146
- throw new Error(`Resource '${res.table}' has no column '${col.name}'`);
152
+ const similar = suggestIfTypo(Object.keys(fieldTypes), col.name);
153
+ throw new Error(`Resource '${res.table}' has no column '${col.name}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
147
154
  }
148
155
  // first find discovered values, but allow override
149
156
  res.columns[i] = { ...fieldTypes[col.name], ...col };
@@ -160,6 +167,11 @@ class AdminForth implements IAdminForth {
160
167
 
161
168
  this.statuses.dbDiscover = 'done';
162
169
 
170
+ this.operationalResources = {};
171
+ this.config.resources.forEach((resource) => {
172
+ this.operationalResources[resource.resourceId] = new OperationalResource(this.connectors[resource.dataSource], resource);
173
+ });
174
+
163
175
  // console.log('⚙️⚙️⚙️ Database discovery done', JSON.stringify(this.config.resources, null, 2));
164
176
  }
165
177
 
@@ -168,9 +180,12 @@ class AdminForth implements IAdminForth {
168
180
  }
169
181
 
170
182
  async getUserByPk(pk: string) {
171
- const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.resourceId);
183
+ const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
172
184
  if (!resource) {
173
- throw new Error('No auth resource found');
185
+ const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId), this.config.auth.usersResourceId);
186
+ throw new Error(`No resource with ${this.config.auth.usersResourceId} found. ${similar ?
187
+ `Did you mean '${similar}' in config.auth.usersResourceId?` : 'Please set correct resource in config.auth.usersResourceId'}`
188
+ );
174
189
  }
175
190
  const users = await this.connectors[resource.dataSource].getData({
176
191
  resource,
@@ -184,46 +199,31 @@ class AdminForth implements IAdminForth {
184
199
  return users.data[0] || null;
185
200
  }
186
201
 
187
- async createResourceRecord({ resource, record, adminUser }: { resource: AdminForthResource, record: any, adminUser: AdminUser }) {
202
+ async createResourceRecord(
203
+ { resource, record, adminUser }:
204
+ { resource: AdminForthResource, record: any, adminUser: AdminUser }
205
+ ): Promise<{ ok: boolean, error?: string, createdRecord?: any }> {
206
+
188
207
  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
- if (
197
- (column.required as {create?: boolean, edit?: boolean}) ?.create &&
198
- record[column.name] === undefined &&
199
- column.showIn.includes(AdminForthResourcePages.create)
200
- ) {
201
- return { error: `Column '${column.name}' is required` };
202
- }
203
-
204
- if (column.isUnique) {
205
- const existingRecord = await this.connectors[resource.dataSource].getData({
206
- resource,
207
- filters: [{ field: column.name, operator: AdminForthFilterOperators.EQ, value: record[column.name] }],
208
- limit: 1,
209
- sort: [],
210
- offset: 0
211
- });
212
- if (existingRecord.data.length > 0) {
213
- return { error: `Record with ${column.name} ${record[column.name]} already exists` };
214
- }
215
- }
208
+ // TODO: assuming specifity for AdminForthResourcePages.create better to move it to api for this button
209
+ if (
210
+ (column.required as {create?: boolean, edit?: boolean}) ?.create &&
211
+ record[column.name] === undefined &&
212
+ column.showIn.includes(AdminForthResourcePages.create)
213
+ ) {
214
+ return { error: `Column '${column.name}' is required`, ok: false };
215
+ }
216
216
  }
217
217
 
218
218
  // execute hook if needed
219
219
  for (const hook of listify(resource.hooks?.create?.beforeSave as BeforeSaveFunction[])) {
220
- const resp = await hook({ resource, record, adminUser });
220
+ const resp = await hook({ recordId: undefined, resource, record, adminUser });
221
221
  if (!resp || (!resp.ok && !resp.error)) {
222
222
  throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
223
223
  }
224
224
 
225
225
  if (resp.error) {
226
- return { error: resp.error };
226
+ return { error: resp.error, ok: false };
227
227
  }
228
228
  }
229
229
 
@@ -235,25 +235,50 @@ class AdminForth implements IAdminForth {
235
235
  }
236
236
  const connector = this.connectors[resource.dataSource];
237
237
  process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record createResourceRecord', record);
238
- await connector.createRecord({ resource, record });
238
+ const { ok, error, createdRecord } = await connector.createRecord({ resource, record, adminUser });
239
+ if (!ok) {
240
+ return { ok, error };
241
+ }
242
+
243
+ const primaryKey = record[resource.columns.find((col) => col.primaryKey).name];
244
+
239
245
  // execute hook if needed
240
246
  for (const hook of listify(resource.hooks?.create?.afterSave as AfterSaveFunction[])) {
241
247
  console.log('Hook afterSave', hook);
242
- const resp = await hook({ resource, record, adminUser });
248
+ const resp = await hook({
249
+ recordId: primaryKey,
250
+ resource,
251
+ record: createdRecord,
252
+ adminUser
253
+ });
254
+
243
255
  if (!resp || (!resp.ok && !resp.error)) {
244
256
  throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
245
257
  }
246
258
 
247
259
  if (resp.error) {
248
- return { error: resp.error };
260
+ return { error: resp.error, ok: false };
249
261
  }
250
262
  }
251
263
 
252
- return { ok: true };
264
+ return { ok, error, createdRecord };
253
265
  }
254
266
 
255
267
  resource(resourceId: string) {
256
- return this.config.resources.find((res) => res.resourceId === resourceId);
268
+ if (this.statuses.dbDiscover !== 'done') {
269
+ if (this.statuses.dbDiscover === 'running') {
270
+ throw new Error('Database discovery is running. You can\'t use data API while database discovery is not finished.\n'+
271
+ 'Consider moving your code to a place where it will be executed after database discovery is already done (after await admin.discoverDatabases())');
272
+ } else {
273
+ throw new Error('Database discovery is not yet started. You can\'t use data API before database discovery is done. \n'+
274
+ 'Call admin.discoverDatabases() first and await it before using data API');
275
+ }
276
+ }
277
+ if (!this.operationalResources[resourceId]) {
278
+ const closeName = suggestIfTypo(Object.keys(this.operationalResources), resourceId);
279
+ throw new Error(`Resource with id '${resourceId}' not found${closeName ? `. Did you mean '${closeName}'?` : ''}`);
280
+ }
281
+ return this.operationalResources[resourceId];
257
282
  }
258
283
 
259
284
  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) {
@@ -135,7 +130,9 @@ export default class ConfigValidator implements IConfigValidator {
135
130
  if (this.config.customization.brandLogo) {
136
131
  errors.push(...this.checkCustomFileExists(this.config.customization.brandLogo));
137
132
  }
138
-
133
+ if (this.config.customization.showBrandNameInSidebar === undefined) {
134
+ this.config.customization.showBrandNameInSidebar = true;
135
+ }
139
136
  if (this.config.customization.favicon) {
140
137
  errors.push(...this.checkCustomFileExists(this.config.customization.favicon));
141
138
  }
@@ -282,7 +279,12 @@ export default class ConfigValidator implements IConfigValidator {
282
279
  await Promise.all(
283
280
  (res.hooks.delete.beforeSave as AfterSaveFunction[]).map(
284
281
  async (hook) => {
285
- const resp = await hook({ resource: res, record, adminUser });
282
+ const resp = await hook({
283
+ recordId: recordId,
284
+ resource: res,
285
+ record,
286
+ adminUser,
287
+ });
286
288
  if (!error && resp.error) {
287
289
  error = resp.error;
288
290
  }
@@ -299,7 +301,12 @@ export default class ConfigValidator implements IConfigValidator {
299
301
  await Promise.all(
300
302
  (res.hooks.delete.afterSave as AfterSaveFunction[]).map(
301
303
  async (hook) => {
302
- await hook({ resource: res, record, adminUser });
304
+ await hook({
305
+ resource: res,
306
+ record,
307
+ adminUser,
308
+ recordId: recordId
309
+ });
303
310
  }
304
311
  )
305
312
  )
@@ -388,8 +395,6 @@ export default class ConfigValidator implements IConfigValidator {
388
395
  }
389
396
  });
390
397
 
391
-
392
-
393
398
  if (!this.config.menu) {
394
399
  errors.push('No config.menu defined');
395
400
  }
@@ -1,40 +1,19 @@
1
+ import { error } from 'console';
1
2
  import { IAdminForthFilter, IAdminForthSort, IOperationalResource, IAdminForthDataSourceConnectorBase, AdminForthResource, IAdminForth } from '../types/AdminForthConfig.js';
2
3
 
3
4
 
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
- function filtersIfFilter(filter: IAdminForthFilter | IAdminForthFilter[]): IAdminForthFilter[] {
30
- return (typeof filter === 'object' ? [filter] : filter) as IAdminForthFilter[];
5
+ function filtersIfFilter(filter: IAdminForthFilter | IAdminForthFilter[] | undefined): IAdminForthFilter[] {
6
+ if (!filter) {
7
+ return [];
8
+ }
9
+ return (Array.isArray(filter) ? filter : [filter]) as IAdminForthFilter[];
31
10
  }
32
11
 
33
12
  function sortsIfSort(sort: IAdminForthSort | IAdminForthSort[]): IAdminForthSort[] {
34
- return (typeof sort === 'object' ? [sort] : sort) as IAdminForthSort[];
13
+ return (Array.isArray(sort) ? sort : [sort]) as IAdminForthSort[];
35
14
  }
36
15
 
37
- export class OperationalResource implements IOperationalResource {
16
+ export default class OperationalResource implements IOperationalResource {
38
17
  dataConnector: IAdminForthDataSourceConnectorBase;
39
18
  resourceConfig: AdminForthResource;
40
19
 
@@ -43,42 +22,58 @@ export class OperationalResource implements IOperationalResource {
43
22
  this.resourceConfig = resourceConfig;
44
23
  }
45
24
 
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];
25
+ async get(filter: IAdminForthFilter | IAdminForthFilter[]): Promise<any | null> {
26
+ return (
27
+ await this.dataConnector.getData({
28
+ resource: this.resourceConfig,
29
+ filters: filtersIfFilter(filter),
30
+ limit: 1,
31
+ offset: 0,
32
+ sort: [],
33
+ })
34
+ ).data[0] || null;
54
35
  }
55
36
 
56
37
  async list(
57
38
  filter: IAdminForthFilter | IAdminForthFilter[],
58
- limit: number,
59
- offset: number,
39
+ limit: number | null,
40
+ offset: number | null,
60
41
  sort: IAdminForthSort | IAdminForthSort[]
61
42
  ): Promise<any[]> {
43
+ let appliedLimit = limit;
44
+ if (limit === null) {
45
+ appliedLimit = 1000000000;
46
+ }
47
+ let appliedOffset = offset;
48
+ if (offset === null) {
49
+ appliedOffset = 0;
50
+ }
51
+
62
52
  const { data } = await this.dataConnector.getData({
63
53
  resource: this.resourceConfig,
64
54
  filters: filtersIfFilter(filter),
65
- limit,
66
- offset,
55
+ limit: appliedLimit,
56
+ offset: appliedOffset,
67
57
  sort: sortsIfSort(sort),
68
58
  getTotals: false,
69
59
  });
70
60
  return data;
71
61
  }
72
62
 
73
- async count(filter: IAdminForthFilter | IAdminForthFilter[]): Promise<number> {
63
+ async count(filter: IAdminForthFilter | IAdminForthFilter[] | undefined): Promise<number> {
74
64
  return await this.dataConnector.getCount({
75
65
  resource: this.resourceConfig,
76
66
  filters: filtersIfFilter(filter),
77
67
  });
78
68
  }
79
69
 
80
- async create(record: any): Promise<any> {
81
- return await this.dataConnector.createRecord({ resource: this.resourceConfig, record });
70
+ async create(recordValues: any): Promise<{ ok: boolean; createdRecord: any; error?: string; }> {
71
+ const { ok, createdRecord, error } = await this.dataConnector.createRecord({
72
+ resource: this.resourceConfig,
73
+ record: recordValues,
74
+ adminUser: null
75
+ });
76
+ return { ok, createdRecord, error };
82
77
  }
83
78
 
84
79
  async update(primaryKey: any, record: any): Promise<any> {
@@ -92,6 +87,5 @@ export class OperationalResource implements IOperationalResource {
92
87
  async delete(primaryKey: any): Promise<boolean> {
93
88
  return await this.dataConnector.deleteRecord({ resource: this.resourceConfig, recordId: primaryKey });
94
89
  }
95
-
96
90
 
97
91
  }