adminforth 1.2.98 → 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.
Files changed (36) hide show
  1. package/auth.ts +6 -5
  2. package/dataConnectors/baseConnector.ts +91 -21
  3. package/dataConnectors/clickhouse.ts +58 -37
  4. package/dataConnectors/mongo.ts +44 -14
  5. package/dataConnectors/postgres.ts +53 -36
  6. package/dataConnectors/sqlite.ts +44 -35
  7. package/dist/auth.js +6 -6
  8. package/dist/dataConnectors/baseConnector.js +75 -17
  9. package/dist/dataConnectors/clickhouse.js +59 -48
  10. package/dist/dataConnectors/mongo.js +28 -12
  11. package/dist/dataConnectors/postgres.js +62 -52
  12. package/dist/dataConnectors/sqlite.js +62 -45
  13. package/dist/index.js +52 -33
  14. package/dist/modules/configValidator.js +25 -16
  15. package/dist/modules/operationalResource.js +88 -0
  16. package/dist/modules/restApi.js +106 -96
  17. package/dist/modules/utils.js +16 -0
  18. package/dist/types/AdminForthConfig.js +37 -0
  19. package/index.ts +76 -43
  20. package/modules/configValidator.ts +26 -21
  21. package/modules/operationalResource.ts +91 -0
  22. package/modules/restApi.ts +91 -80
  23. package/modules/utils.ts +20 -0
  24. package/package.json +3 -2
  25. package/spa/src/App.vue +12 -9
  26. package/spa/src/components/Filters.vue +1 -1
  27. package/spa/src/components/ResourceForm.vue +7 -4
  28. package/spa/src/components/ResourceListTable.vue +124 -112
  29. package/spa/src/components/SkeleteLoader.vue +4 -4
  30. package/spa/src/components/ValueRenderer.vue +1 -1
  31. package/spa/src/router/index.ts +0 -8
  32. package/spa/src/spa_types/core.ts +1 -0
  33. package/spa/src/stores/filters.ts +3 -2
  34. package/spa/src/views/ListView.vue +16 -25
  35. package/spa/src/views/LoginView.vue +26 -9
  36. package/types/AdminForthConfig.ts +140 -38
package/index.ts CHANGED
@@ -5,12 +5,14 @@ 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,
12
12
  type IConfigValidator,
13
- AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages, IHttpServer,
13
+ IOperationalResource,
14
+ AdminForthFilterOperators,
15
+ AdminForthDataTypes, AdminForthResourcePages, IHttpServer,
14
16
  BeforeSaveFunction,
15
17
  AfterSaveFunction,
16
18
  AdminUser,
@@ -20,11 +22,14 @@ import AdminForthPlugin from './basePlugin.js';
20
22
  import ConfigValidator from './modules/configValidator.js';
21
23
  import AdminForthRestAPI, { interpretResource } from './modules/restApi.js';
22
24
  import ClickhouseConnector from './dataConnectors/clickhouse.js';
25
+ import OperationalResource from './modules/operationalResource.js';
26
+ import { error } from 'console';
23
27
 
24
28
  // exports
25
29
  export * from './types/AdminForthConfig.js';
26
30
  export { interpretResource };
27
31
  export { AdminForthPlugin };
32
+ export { suggestIfTypo };
28
33
 
29
34
 
30
35
  class AdminForth implements IAdminForth {
@@ -50,7 +55,9 @@ class AdminForth implements IAdminForth {
50
55
  activatedPlugins: Array<AdminForthPlugin>;
51
56
  configValidator: IConfigValidator;
52
57
  restApi: AdminForthRestAPI;
53
-
58
+ operationalResources: {
59
+ [resourceId: string]: IOperationalResource,
60
+ }
54
61
  baseUrlSlashed: string;
55
62
 
56
63
  statuses: {
@@ -116,21 +123,24 @@ class AdminForth implements IAdminForth {
116
123
  this.config.dataSources.forEach((ds) => {
117
124
  const dbType = ds.url.split(':')[0];
118
125
  if (!this.config.databaseConnectors[dbType]) {
119
- 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`);
120
127
  }
121
128
  this.connectors[ds.id] = new this.config.databaseConnectors[dbType]({url: ds.url});
122
129
  });
123
130
 
124
131
  await Promise.all(this.config.resources.map(async (res) => {
125
132
  if (!this.connectors[res.dataSource]) {
126
- 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
+ );
127
137
  }
128
138
  const fieldTypes = await this.connectors[res.dataSource].discoverFields(res);
129
139
  if (fieldTypes !== null && !Object.keys(fieldTypes).length) {
130
140
  throw new Error(`Table '${res.table}' (In resource '${res.resourceId}') has no fields or does not exist`);
131
141
  }
132
142
  if (fieldTypes === null) {
133
- 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`);
134
144
  return;
135
145
  }
136
146
  if (!res.columns) {
@@ -139,7 +149,8 @@ class AdminForth implements IAdminForth {
139
149
 
140
150
  res.columns.forEach((col, i) => {
141
151
  if (!fieldTypes[col.name] && !col.virtual) {
142
- 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}'?` : ''}`);
143
154
  }
144
155
  // first find discovered values, but allow override
145
156
  res.columns[i] = { ...fieldTypes[col.name], ...col };
@@ -156,6 +167,11 @@ class AdminForth implements IAdminForth {
156
167
 
157
168
  this.statuses.dbDiscover = 'done';
158
169
 
170
+ this.operationalResources = {};
171
+ this.config.resources.forEach((resource) => {
172
+ this.operationalResources[resource.resourceId] = new OperationalResource(this.connectors[resource.dataSource], resource);
173
+ });
174
+
159
175
  // console.log('⚙️⚙️⚙️ Database discovery done', JSON.stringify(this.config.resources, null, 2));
160
176
  }
161
177
 
@@ -164,9 +180,12 @@ class AdminForth implements IAdminForth {
164
180
  }
165
181
 
166
182
  async getUserByPk(pk: string) {
167
- 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);
168
184
  if (!resource) {
169
- 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
+ );
170
189
  }
171
190
  const users = await this.connectors[resource.dataSource].getData({
172
191
  resource,
@@ -180,46 +199,31 @@ class AdminForth implements IAdminForth {
180
199
  return users.data[0] || null;
181
200
  }
182
201
 
183
- 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
+
184
207
  for (const column of resource.columns) {
185
- if (column.fillOnCreate) {
186
- if (record[column.name] === undefined) {
187
- record[column.name] = column.fillOnCreate({
188
- initialRecord: record, adminUser
189
- });
190
- }
191
- }
192
- if (
193
- (column.required as {create?: boolean, edit?: boolean}) ?.create &&
194
- record[column.name] === undefined &&
195
- column.showIn.includes(AdminForthResourcePages.create)
196
- ) {
197
- return { error: `Column '${column.name}' is required` };
198
- }
199
-
200
- if (column.isUnique) {
201
- const existingRecord = await this.connectors[resource.dataSource].getData({
202
- resource,
203
- filters: [{ field: column.name, operator: AdminForthFilterOperators.EQ, value: record[column.name] }],
204
- limit: 1,
205
- sort: [],
206
- offset: 0
207
- });
208
- if (existingRecord.data.length > 0) {
209
- return { error: `Record with ${column.name} ${record[column.name]} already exists` };
210
- }
211
- }
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
+ }
212
216
  }
213
217
 
214
218
  // execute hook if needed
215
219
  for (const hook of listify(resource.hooks?.create?.beforeSave as BeforeSaveFunction[])) {
216
- const resp = await hook({ resource, record, adminUser });
220
+ const resp = await hook({ recordId: undefined, resource, record, adminUser });
217
221
  if (!resp || (!resp.ok && !resp.error)) {
218
222
  throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
219
223
  }
220
224
 
221
225
  if (resp.error) {
222
- return { error: resp.error };
226
+ return { error: resp.error, ok: false };
223
227
  }
224
228
  }
225
229
 
@@ -231,21 +235,50 @@ class AdminForth implements IAdminForth {
231
235
  }
232
236
  const connector = this.connectors[resource.dataSource];
233
237
  process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record createResourceRecord', record);
234
- 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
+
235
245
  // execute hook if needed
236
246
  for (const hook of listify(resource.hooks?.create?.afterSave as AfterSaveFunction[])) {
237
247
  console.log('Hook afterSave', hook);
238
- const resp = await hook({ resource, record, adminUser });
248
+ const resp = await hook({
249
+ recordId: primaryKey,
250
+ resource,
251
+ record: createdRecord,
252
+ adminUser
253
+ });
254
+
239
255
  if (!resp || (!resp.ok && !resp.error)) {
240
256
  throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
241
257
  }
242
258
 
243
259
  if (resp.error) {
244
- return { error: resp.error };
260
+ return { error: resp.error, ok: false };
245
261
  }
246
262
  }
247
263
 
248
- return { ok: true };
264
+ return { ok, error, createdRecord };
265
+ }
266
+
267
+ resource(resourceId: string) {
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];
249
282
  }
250
283
 
251
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
  }
@@ -0,0 +1,91 @@
1
+ import { error } from 'console';
2
+ import { IAdminForthFilter, IAdminForthSort, IOperationalResource, IAdminForthDataSourceConnectorBase, AdminForthResource, IAdminForth } from '../types/AdminForthConfig.js';
3
+
4
+
5
+ function filtersIfFilter(filter: IAdminForthFilter | IAdminForthFilter[] | undefined): IAdminForthFilter[] {
6
+ if (!filter) {
7
+ return [];
8
+ }
9
+ return (Array.isArray(filter) ? filter : [filter]) as IAdminForthFilter[];
10
+ }
11
+
12
+ function sortsIfSort(sort: IAdminForthSort | IAdminForthSort[]): IAdminForthSort[] {
13
+ return (Array.isArray(sort) ? sort : [sort]) as IAdminForthSort[];
14
+ }
15
+
16
+ export default class OperationalResource implements IOperationalResource {
17
+ dataConnector: IAdminForthDataSourceConnectorBase;
18
+ resourceConfig: AdminForthResource;
19
+
20
+ constructor(dataConnector: IAdminForthDataSourceConnectorBase, resourceConfig: AdminForthResource) {
21
+ this.dataConnector = dataConnector;
22
+ this.resourceConfig = resourceConfig;
23
+ }
24
+
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;
35
+ }
36
+
37
+ async list(
38
+ filter: IAdminForthFilter | IAdminForthFilter[],
39
+ limit: number | null,
40
+ offset: number | null,
41
+ sort: IAdminForthSort | IAdminForthSort[]
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
+
52
+ const { data } = await this.dataConnector.getData({
53
+ resource: this.resourceConfig,
54
+ filters: filtersIfFilter(filter),
55
+ limit: appliedLimit,
56
+ offset: appliedOffset,
57
+ sort: sortsIfSort(sort),
58
+ getTotals: false,
59
+ });
60
+ return data;
61
+ }
62
+
63
+ async count(filter: IAdminForthFilter | IAdminForthFilter[] | undefined): Promise<number> {
64
+ return await this.dataConnector.getCount({
65
+ resource: this.resourceConfig,
66
+ filters: filtersIfFilter(filter),
67
+ });
68
+ }
69
+
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 };
77
+ }
78
+
79
+ async update(primaryKey: any, record: any): Promise<any> {
80
+ return await this.dataConnector.updateRecord({
81
+ resource: this.resourceConfig,
82
+ recordId: primaryKey,
83
+ newValues: record
84
+ });
85
+ }
86
+
87
+ async delete(primaryKey: any): Promise<boolean> {
88
+ return await this.dataConnector.deleteRecord({ resource: this.resourceConfig, recordId: primaryKey });
89
+ }
90
+
91
+ }