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/auth.ts CHANGED
@@ -88,12 +88,13 @@ class AdminForthAuth {
88
88
  console.error(`Invalid token type during verification: ${t}, must be ${mustHaveType}`);
89
89
  return null;
90
90
  }
91
- if (pk === null) {
92
- decoded.isRoot = true;
93
- } else {
94
- const dbUser = await this.adminforth.getUserByPk(pk);
95
- decoded.dbUser = dbUser;
91
+ const dbUser = await this.adminforth.getUserByPk(pk);
92
+ if (!dbUser) {
93
+ console.error(`User with pk ${pk} not found in database`);
94
+ // will logout user which was deleted
95
+ return null;
96
96
  }
97
+ decoded.dbUser = dbUser;
97
98
  return decoded;
98
99
  }
99
100
 
@@ -1,5 +1,6 @@
1
1
  import { get } from "http";
2
2
  import { AdminForthResource, IAdminForthDataSourceConnectorBase, AdminForthSortDirections, AdminForthFilterOperators, AdminForthResourceColumn, IAdminForthSort, IAdminForthFilter } from "../types/AdminForthConfig.js";
3
+ import { suggestIfTypo } from "../modules/utils.js";
3
4
 
4
5
 
5
6
  export default class AdminForthBaseConnector implements IAdminForthDataSourceConnectorBase {
@@ -56,14 +57,66 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
56
57
  throw new Error('Method not implemented.');
57
58
  }
58
59
 
59
- createRecord({ resource, record }: { resource: AdminForthResource; record: any; }): Promise<void> {
60
+ async checkUnique(resource: AdminForthResource, column: AdminForthResourceColumn, value: any) {
61
+ process.env.HEAVY_DEBUG && console.log('☝️🪲🪲🪲🪲 checkUnique|||', column, value);
62
+ const existingRecord = await this.getData({
63
+ resource,
64
+ filters: [{ field: column.name, operator: AdminForthFilterOperators.EQ, value }],
65
+ limit: 1,
66
+ sort: [],
67
+ offset: 0,
68
+ getTotals: false
69
+ });
70
+ process.env.HEAVY_DEBUG && console.log('☝️🪲🪲🪲🪲 existingRecord|||', existingRecord);
71
+
72
+ return existingRecord.data.length > 0;
73
+ }
74
+
75
+ async createRecord({ resource, record, adminUser }: {
76
+ resource: AdminForthResource; record: any; adminUser: any;
77
+ }): Promise<{ error?: string; ok: boolean; createdRecord?: any; }> {
60
78
  // transform value using setFieldValue and call createRecordOriginalValues
61
- const newRecord = {...record};
79
+ const filledRecord = {...record};
80
+ const recordWithOriginalValues = {...record};
81
+
62
82
  for (const col of resource.dataSourceColumns) {
63
- newRecord[col.name] = this.setFieldValue(col, record[col.name]);
83
+ if (col.fillOnCreate) {
84
+ if (filledRecord[col.name] === undefined) {
85
+ filledRecord[col.name] = col.fillOnCreate({
86
+ initialRecord: record,
87
+ adminUser
88
+ });
89
+ }
90
+ }
91
+ recordWithOriginalValues[col.name] = this.setFieldValue(col, filledRecord[col.name]);
92
+ }
93
+
94
+
95
+ let error: string | null = null;
96
+ await Promise.all(
97
+ resource.dataSourceColumns.map(async (col) => {
98
+
99
+ if (col.isUnique && !col.virtual && !error) {
100
+
101
+ const exists = await this.checkUnique(resource, col, recordWithOriginalValues[col.name]);
102
+ if (exists) {
103
+ error = `Record with ${col.name} ${recordWithOriginalValues[col.name]} already exists`;
104
+ }
105
+ }
106
+ })
107
+ );
108
+ process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 error', error);
109
+ if (error) {
110
+ return { error, ok: false };
111
+ }
112
+
113
+ process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record', recordWithOriginalValues);
114
+ await this.createRecordOriginalValues({ resource, record: recordWithOriginalValues });
115
+
116
+ return {
117
+ ok: true,
118
+ createdRecord: recordWithOriginalValues,
64
119
  }
65
- process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record', newRecord);
66
- return this.createRecordOriginalValues({ resource, record: newRecord });
67
120
  }
68
121
 
69
122
  updateRecord({ resource, recordId, newValues }: { resource: AdminForthResource; recordId: string; newValues: any; }): Promise<void> {
@@ -85,10 +138,15 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
85
138
  }): Promise<{ data: any[], total: number }> {
86
139
  if (filters) {
87
140
  filters.map((f) => {
141
+ const fieldObj = resource.dataSourceColumns.find((col) => col.name == f.field);
142
+ if (!fieldObj) {
143
+ const similar = suggestIfTypo(resource.dataSourceColumns.map((col) => col.name), f.field);
144
+ throw new Error(`Field '${f.field}' not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
145
+ }
88
146
  if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
89
- f.value = f.value.map((val) => this.setFieldValue(resource.dataSourceColumns.find((col) => col.name == f.field), val));
147
+ f.value = f.value.map((val) => this.setFieldValue(fieldObj, val));
90
148
  } else {
91
- f.value = this.setFieldValue(resource.dataSourceColumns.find((col) => col.name == f.field), f.value);
149
+ f.value = this.setFieldValue(fieldObj, f.value);
92
150
  }
93
151
  });
94
152
  }
@@ -220,6 +220,7 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
220
220
  sort: { field: string, direction: AdminForthSortDirections }[],
221
221
  filters: { field: string, operator: AdminForthFilterOperators, value: any }[],
222
222
  }): Promise<any[]> {
223
+ console.log('getDataWithOriginalTypes', resource, limit, offset, sort, filters);
223
224
  const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
224
225
  const tableName = resource.table;
225
226
 
@@ -107,13 +107,12 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
107
107
  return value;
108
108
  }
109
109
 
110
- async genQuery({ resource, limit, offset, sort, filters }) {
111
- const collection = this.db.db().collection(resource.table);
110
+ async genQuery({ filters }) {
112
111
  const query = {};
113
112
  for (const filter of filters) {
114
113
  query[filter.field] = this.OperatorsMap[filter.operator](filter.value);
115
114
  }
116
- return { collection, query };
115
+ return query;
117
116
  }
118
117
 
119
118
  async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }:
@@ -130,7 +129,7 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
130
129
  const tableName = resource.table;
131
130
 
132
131
  const collection = this.db.db().collection(tableName);
133
- const query = this.genQuery({ resource, limit, offset, sort, filters });
132
+ const query = await this.genQuery({ filters });
134
133
 
135
134
  const sortArray: any[] = sort.map((s) => {
136
135
  return [s.field, this.SortDirectionsMap[s.direction]];
@@ -47,6 +47,9 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
47
47
  } else if (baseType == 'boolean') {
48
48
  field.type = AdminForthDataTypes.BOOLEAN;
49
49
  field._underlineType = 'boolean';
50
+ } else if (baseType == 'datetime') {
51
+ field.type = AdminForthDataTypes.DATETIME;
52
+ field._underlineType = 'datetime';
50
53
  } else {
51
54
  field.type = 'unknown'
52
55
  }
@@ -68,6 +71,8 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
68
71
  return dayjs.unix(+value).toISOString();
69
72
  } else if (field._underlineType == 'varchar') {
70
73
  return dayjs(value).toISOString();
74
+ } else if (field._underlineType == 'datetime') {
75
+ return dayjs(value).toISOString();
71
76
  } else {
72
77
  throw new Error(`AdminForth does not support row type: ${field._underlineType} for timestamps, use VARCHAR (with iso strings) or TIMESTAMP/INT (with unix timestamps). Issue in field "${field.name}"`);
73
78
  }
@@ -102,6 +107,8 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
102
107
  } else if (field._underlineType == 'varchar') {
103
108
  // value is iso string now, convert to unix timestamp
104
109
  return dayjs(value).toISOString();
110
+ } else {
111
+ return value;
105
112
  }
106
113
  } else if (field.type == AdminForthDataTypes.BOOLEAN) {
107
114
  return value ? 1 : 0;
package/dist/auth.js CHANGED
@@ -80,13 +80,13 @@ class AdminForthAuth {
80
80
  console.error(`Invalid token type during verification: ${t}, must be ${mustHaveType}`);
81
81
  return null;
82
82
  }
83
- if (pk === null) {
84
- decoded.isRoot = true;
85
- }
86
- else {
87
- const dbUser = yield this.adminforth.getUserByPk(pk);
88
- decoded.dbUser = dbUser;
83
+ const dbUser = yield this.adminforth.getUserByPk(pk);
84
+ if (!dbUser) {
85
+ console.error(`User with pk ${pk} not found in database`);
86
+ // will logout user which was deleted
87
+ return null;
89
88
  }
89
+ decoded.dbUser = dbUser;
90
90
  return decoded;
91
91
  });
92
92
  }
@@ -8,6 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { AdminForthFilterOperators } from "../types/AdminForthConfig.js";
11
+ import { suggestIfTypo } from "../modules/utils.js";
11
12
  export default class AdminForthBaseConnector {
12
13
  getPrimaryKey(resource) {
13
14
  for (const col of resource.dataSourceColumns) {
@@ -49,14 +50,57 @@ export default class AdminForthBaseConnector {
49
50
  createRecordOriginalValues({ resource, record }) {
50
51
  throw new Error('Method not implemented.');
51
52
  }
52
- createRecord({ resource, record }) {
53
- // transform value using setFieldValue and call createRecordOriginalValues
54
- const newRecord = Object.assign({}, record);
55
- for (const col of resource.dataSourceColumns) {
56
- newRecord[col.name] = this.setFieldValue(col, record[col.name]);
57
- }
58
- process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record', newRecord);
59
- return this.createRecordOriginalValues({ resource, record: newRecord });
53
+ checkUnique(resource, column, value) {
54
+ return __awaiter(this, void 0, void 0, function* () {
55
+ process.env.HEAVY_DEBUG && console.log('☝️🪲🪲🪲🪲 checkUnique|||', column, value);
56
+ const existingRecord = yield this.getData({
57
+ resource,
58
+ filters: [{ field: column.name, operator: AdminForthFilterOperators.EQ, value }],
59
+ limit: 1,
60
+ sort: [],
61
+ offset: 0,
62
+ getTotals: false
63
+ });
64
+ process.env.HEAVY_DEBUG && console.log('☝️🪲🪲🪲🪲 existingRecord|||', existingRecord);
65
+ return existingRecord.data.length > 0;
66
+ });
67
+ }
68
+ createRecord(_a) {
69
+ return __awaiter(this, arguments, void 0, function* ({ resource, record, adminUser }) {
70
+ // transform value using setFieldValue and call createRecordOriginalValues
71
+ const filledRecord = Object.assign({}, record);
72
+ const recordWithOriginalValues = Object.assign({}, record);
73
+ for (const col of resource.dataSourceColumns) {
74
+ if (col.fillOnCreate) {
75
+ if (filledRecord[col.name] === undefined) {
76
+ filledRecord[col.name] = col.fillOnCreate({
77
+ initialRecord: record,
78
+ adminUser
79
+ });
80
+ }
81
+ }
82
+ recordWithOriginalValues[col.name] = this.setFieldValue(col, filledRecord[col.name]);
83
+ }
84
+ let error = null;
85
+ yield Promise.all(resource.dataSourceColumns.map((col) => __awaiter(this, void 0, void 0, function* () {
86
+ if (col.isUnique && !col.virtual && !error) {
87
+ const exists = yield this.checkUnique(resource, col, recordWithOriginalValues[col.name]);
88
+ if (exists) {
89
+ error = `Record with ${col.name} ${recordWithOriginalValues[col.name]} already exists`;
90
+ }
91
+ }
92
+ })));
93
+ process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 error', error);
94
+ if (error) {
95
+ return { error, ok: false };
96
+ }
97
+ process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record', recordWithOriginalValues);
98
+ yield this.createRecordOriginalValues({ resource, record: recordWithOriginalValues });
99
+ return {
100
+ ok: true,
101
+ createdRecord: recordWithOriginalValues,
102
+ };
103
+ });
60
104
  }
61
105
  updateRecord({ resource, recordId, newValues }) {
62
106
  throw new Error('Method not implemented.');
@@ -68,11 +112,16 @@ export default class AdminForthBaseConnector {
68
112
  return __awaiter(this, arguments, void 0, function* ({ resource, limit, offset, sort, filters, getTotals }) {
69
113
  if (filters) {
70
114
  filters.map((f) => {
115
+ const fieldObj = resource.dataSourceColumns.find((col) => col.name == f.field);
116
+ if (!fieldObj) {
117
+ const similar = suggestIfTypo(resource.dataSourceColumns.map((col) => col.name), f.field);
118
+ throw new Error(`Field '${f.field}' not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
119
+ }
71
120
  if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
72
- f.value = f.value.map((val) => this.setFieldValue(resource.dataSourceColumns.find((col) => col.name == f.field), val));
121
+ f.value = f.value.map((val) => this.setFieldValue(fieldObj, val));
73
122
  }
74
123
  else {
75
- f.value = this.setFieldValue(resource.dataSourceColumns.find((col) => col.name == f.field), f.value);
124
+ f.value = this.setFieldValue(fieldObj, f.value);
76
125
  }
77
126
  });
78
127
  }
@@ -217,6 +217,7 @@ class ClickhouseConnector extends AdminForthBaseConnector {
217
217
  }
218
218
  getDataWithOriginalTypes(_a) {
219
219
  return __awaiter(this, arguments, void 0, function* ({ resource, limit, offset, sort, filters }) {
220
+ console.log('getDataWithOriginalTypes', resource, limit, offset, sort, filters);
220
221
  const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
221
222
  const tableName = resource.table;
222
223
  const where = this.whereClause(resource, filters);
@@ -109,13 +109,12 @@ class MongoConnector extends AdminForthBaseConnector {
109
109
  return value;
110
110
  }
111
111
  genQuery(_a) {
112
- return __awaiter(this, arguments, void 0, function* ({ resource, limit, offset, sort, filters }) {
113
- const collection = this.db.db().collection(resource.table);
112
+ return __awaiter(this, arguments, void 0, function* ({ filters }) {
114
113
  const query = {};
115
114
  for (const filter of filters) {
116
115
  query[filter.field] = this.OperatorsMap[filter.operator](filter.value);
117
116
  }
118
- return { collection, query };
117
+ return query;
119
118
  });
120
119
  }
121
120
  getDataWithOriginalTypes(_a) {
@@ -123,7 +122,7 @@ class MongoConnector extends AdminForthBaseConnector {
123
122
  // const columns = resource.dataSourceColumns.filter(c=> !c.virtual).map((col) => col.name).join(', ');
124
123
  const tableName = resource.table;
125
124
  const collection = this.db.db().collection(tableName);
126
- const query = this.genQuery({ resource, limit, offset, sort, filters });
125
+ const query = yield this.genQuery({ filters });
127
126
  const sortArray = sort.map((s) => {
128
127
  return [s.field, this.SortDirectionsMap[s.direction]];
129
128
  });
@@ -75,6 +75,10 @@ class SQLiteConnector extends AdminForthBaseConnector {
75
75
  field.type = AdminForthDataTypes.BOOLEAN;
76
76
  field._underlineType = 'boolean';
77
77
  }
78
+ else if (baseType == 'datetime') {
79
+ field.type = AdminForthDataTypes.DATETIME;
80
+ field._underlineType = 'datetime';
81
+ }
78
82
  else {
79
83
  field.type = 'unknown';
80
84
  }
@@ -98,6 +102,9 @@ class SQLiteConnector extends AdminForthBaseConnector {
98
102
  else if (field._underlineType == 'varchar') {
99
103
  return dayjs(value).toISOString();
100
104
  }
105
+ else if (field._underlineType == 'datetime') {
106
+ return dayjs(value).toISOString();
107
+ }
101
108
  else {
102
109
  throw new Error(`AdminForth does not support row type: ${field._underlineType} for timestamps, use VARCHAR (with iso strings) or TIMESTAMP/INT (with unix timestamps). Issue in field "${field.name}"`);
103
110
  }
@@ -134,6 +141,9 @@ class SQLiteConnector extends AdminForthBaseConnector {
134
141
  // value is iso string now, convert to unix timestamp
135
142
  return dayjs(value).toISOString();
136
143
  }
144
+ else {
145
+ return value;
146
+ }
137
147
  }
138
148
  else if (field.type == AdminForthDataTypes.BOOLEAN) {
139
149
  return value ? 1 : 0;
package/dist/index.js CHANGED
@@ -19,16 +19,18 @@ import PostgresConnector from './dataConnectors/postgres.js';
19
19
  import SQLiteConnector from './dataConnectors/sqlite.js';
20
20
  import CodeInjector from './modules/codeInjector.js';
21
21
  import ExpressServer from './servers/express.js';
22
- import { ADMINFORTH_VERSION, listify } from './modules/utils.js';
22
+ import { ADMINFORTH_VERSION, listify, suggestIfTypo } from './modules/utils.js';
23
23
  import { AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages, } from './types/AdminForthConfig.js';
24
24
  import AdminForthPlugin from './basePlugin.js';
25
25
  import ConfigValidator from './modules/configValidator.js';
26
26
  import AdminForthRestAPI, { interpretResource } from './modules/restApi.js';
27
27
  import ClickhouseConnector from './dataConnectors/clickhouse.js';
28
+ import OperationalResource from './modules/operationalResource.js';
28
29
  // exports
29
30
  export * from './types/AdminForthConfig.js';
30
31
  export { interpretResource };
31
32
  export { AdminForthPlugin };
33
+ export { suggestIfTypo };
32
34
  class AdminForth {
33
35
  constructor(config) {
34
36
  _AdminForth_defaultConfig.set(this, {
@@ -85,20 +87,22 @@ class AdminForth {
85
87
  this.config.dataSources.forEach((ds) => {
86
88
  const dbType = ds.url.split(':')[0];
87
89
  if (!this.config.databaseConnectors[dbType]) {
88
- throw new Error(`Database type ${dbType} is not supported, consider using databaseConnectors in AdminForth config`);
90
+ 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`);
89
91
  }
90
92
  this.connectors[ds.id] = new this.config.databaseConnectors[dbType]({ url: ds.url });
91
93
  });
92
94
  yield Promise.all(this.config.resources.map((res) => __awaiter(this, void 0, void 0, function* () {
93
95
  if (!this.connectors[res.dataSource]) {
94
- throw new Error(`Resource '${res.table}' refers to unknown dataSource '${res.dataSource}'`);
96
+ const similar = suggestIfTypo(Object.keys(this.connectors), res.dataSource);
97
+ throw new Error(`Resource '${res.table}' refers to unknown dataSource '${res.dataSource}' ${similar
98
+ ? `. Did you mean '${similar}'?` : 'Available dataSources: ' + Object.keys(this.connectors).join(', ')}`);
95
99
  }
96
100
  const fieldTypes = yield this.connectors[res.dataSource].discoverFields(res);
97
101
  if (fieldTypes !== null && !Object.keys(fieldTypes).length) {
98
102
  throw new Error(`Table '${res.table}' (In resource '${res.resourceId}') has no fields or does not exist`);
99
103
  }
100
104
  if (fieldTypes === null) {
101
- console.error(`DataSource ${res.dataSource} was not able to perform field discovery. It will not work properly`);
105
+ console.error(`⛔ DataSource ${res.dataSource} was not able to perform field discovery. It will not work properly`);
102
106
  return;
103
107
  }
104
108
  if (!res.columns) {
@@ -106,7 +110,8 @@ class AdminForth {
106
110
  }
107
111
  res.columns.forEach((col, i) => {
108
112
  if (!fieldTypes[col.name] && !col.virtual) {
109
- throw new Error(`Resource '${res.table}' has no column '${col.name}'`);
113
+ const similar = suggestIfTypo(Object.keys(fieldTypes), col.name);
114
+ throw new Error(`Resource '${res.table}' has no column '${col.name}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
110
115
  }
111
116
  // first find discovered values, but allow override
112
117
  res.columns[i] = Object.assign(Object.assign({}, fieldTypes[col.name]), col);
@@ -118,6 +123,10 @@ class AdminForth {
118
123
  }
119
124
  })));
120
125
  this.statuses.dbDiscover = 'done';
126
+ this.operationalResources = {};
127
+ this.config.resources.forEach((resource) => {
128
+ this.operationalResources[resource.resourceId] = new OperationalResource(this.connectors[resource.dataSource], resource);
129
+ });
121
130
  // console.log('⚙️⚙️⚙️ Database discovery done', JSON.stringify(this.config.resources, null, 2));
122
131
  });
123
132
  }
@@ -128,9 +137,11 @@ class AdminForth {
128
137
  }
129
138
  getUserByPk(pk) {
130
139
  return __awaiter(this, void 0, void 0, function* () {
131
- const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.resourceId);
140
+ const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
132
141
  if (!resource) {
133
- throw new Error('No auth resource found');
142
+ const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId), this.config.auth.usersResourceId);
143
+ throw new Error(`No resource with ${this.config.auth.usersResourceId} found. ${similar ?
144
+ `Did you mean '${similar}' in config.auth.usersResourceId?` : 'Please set correct resource in config.auth.usersResourceId'}`);
134
145
  }
135
146
  const users = yield this.connectors[resource.dataSource].getData({
136
147
  resource,
@@ -148,39 +159,21 @@ class AdminForth {
148
159
  return __awaiter(this, arguments, void 0, function* ({ resource, record, adminUser }) {
149
160
  var _c, _d, _e, _f, _g;
150
161
  for (const column of resource.columns) {
151
- if (column.fillOnCreate) {
152
- if (record[column.name] === undefined) {
153
- record[column.name] = column.fillOnCreate({
154
- initialRecord: record, adminUser
155
- });
156
- }
157
- }
162
+ // TODO: assuming specifity for AdminForthResourcePages.create better to move it to api for this button
158
163
  if (((_c = column.required) === null || _c === void 0 ? void 0 : _c.create) &&
159
164
  record[column.name] === undefined &&
160
165
  column.showIn.includes(AdminForthResourcePages.create)) {
161
- return { error: `Column '${column.name}' is required` };
162
- }
163
- if (column.isUnique) {
164
- const existingRecord = yield this.connectors[resource.dataSource].getData({
165
- resource,
166
- filters: [{ field: column.name, operator: AdminForthFilterOperators.EQ, value: record[column.name] }],
167
- limit: 1,
168
- sort: [],
169
- offset: 0
170
- });
171
- if (existingRecord.data.length > 0) {
172
- return { error: `Record with ${column.name} ${record[column.name]} already exists` };
173
- }
166
+ return { error: `Column '${column.name}' is required`, ok: false };
174
167
  }
175
168
  }
176
169
  // execute hook if needed
177
170
  for (const hook of listify((_e = (_d = resource.hooks) === null || _d === void 0 ? void 0 : _d.create) === null || _e === void 0 ? void 0 : _e.beforeSave)) {
178
- const resp = yield hook({ resource, record, adminUser });
171
+ const resp = yield hook({ recordId: undefined, resource, record, adminUser });
179
172
  if (!resp || (!resp.ok && !resp.error)) {
180
173
  throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
181
174
  }
182
175
  if (resp.error) {
183
- return { error: resp.error };
176
+ return { error: resp.error, ok: false };
184
177
  }
185
178
  }
186
179
  // remove virtual columns from record
@@ -191,23 +184,46 @@ class AdminForth {
191
184
  }
192
185
  const connector = this.connectors[resource.dataSource];
193
186
  process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record createResourceRecord', record);
194
- yield connector.createRecord({ resource, record });
187
+ const { ok, error, createdRecord } = yield connector.createRecord({ resource, record, adminUser });
188
+ if (!ok) {
189
+ return { ok, error };
190
+ }
191
+ const primaryKey = record[resource.columns.find((col) => col.primaryKey).name];
195
192
  // execute hook if needed
196
193
  for (const hook of listify((_g = (_f = resource.hooks) === null || _f === void 0 ? void 0 : _f.create) === null || _g === void 0 ? void 0 : _g.afterSave)) {
197
194
  console.log('Hook afterSave', hook);
198
- const resp = yield hook({ resource, record, adminUser });
195
+ const resp = yield hook({
196
+ recordId: primaryKey,
197
+ resource,
198
+ record: createdRecord,
199
+ adminUser
200
+ });
199
201
  if (!resp || (!resp.ok && !resp.error)) {
200
202
  throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
201
203
  }
202
204
  if (resp.error) {
203
- return { error: resp.error };
205
+ return { error: resp.error, ok: false };
204
206
  }
205
207
  }
206
- return { ok: true };
208
+ return { ok, error, createdRecord };
207
209
  });
208
210
  }
209
211
  resource(resourceId) {
210
- return this.config.resources.find((res) => res.resourceId === resourceId);
212
+ if (this.statuses.dbDiscover !== 'done') {
213
+ if (this.statuses.dbDiscover === 'running') {
214
+ throw new Error('Database discovery is running. You can\'t use data API while database discovery is not finished.\n' +
215
+ 'Consider moving your code to a place where it will be executed after database discovery is already done (after await admin.discoverDatabases())');
216
+ }
217
+ else {
218
+ throw new Error('Database discovery is not yet started. You can\'t use data API before database discovery is done. \n' +
219
+ 'Call admin.discoverDatabases() first and await it before using data API');
220
+ }
221
+ }
222
+ if (!this.operationalResources[resourceId]) {
223
+ const closeName = suggestIfTypo(Object.keys(this.operationalResources), resourceId);
224
+ throw new Error(`Resource with id '${resourceId}' not found${closeName ? `. Did you mean '${closeName}'?` : ''}`);
225
+ }
226
+ return this.operationalResources[resourceId];
211
227
  }
212
228
  setupEndpoints(server) {
213
229
  this.restApi.registerEndpoints(server);
@@ -10,7 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { AdminForthResourcePages, AllowedActionsEnum, } from "../types/AdminForthConfig.js";
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
  import crypto from 'crypto';
15
15
  export default class ConfigValidator {
16
16
  constructor(adminforth, config) {
@@ -47,15 +47,6 @@ export default class ConfigValidator {
47
47
  validateConfig() {
48
48
  var _a;
49
49
  const errors = [];
50
- if (this.config.rootUser) {
51
- if (!this.config.rootUser.username) {
52
- throw new Error('rootUser.username is required');
53
- }
54
- if (!this.config.rootUser.password) {
55
- throw new Error('rootUser.password is required');
56
- }
57
- 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');
58
- }
59
50
  if (!this.config.customization.customComponentsDir) {
60
51
  this.config.customization.customComponentsDir = './custom';
61
52
  }
@@ -67,8 +58,12 @@ export default class ConfigValidator {
67
58
  this.config.customization.customComponentsDir = undefined;
68
59
  }
69
60
  if (this.config.auth) {
70
- if (!this.config.auth.resourceId) {
71
- throw new Error('No config.auth.resourceId defined');
61
+ // TODO: remove in future releases
62
+ if (!this.config.auth.usersResourceId && this.config.auth.resourceId) {
63
+ this.config.auth.usersResourceId = this.config.auth.resourceId;
64
+ }
65
+ if (!this.config.auth.usersResourceId) {
66
+ throw new Error('No config.auth.usersResourceId defined');
72
67
  }
73
68
  if (!this.config.auth.passwordHashField) {
74
69
  throw new Error('No config.auth.passwordHashField defined');
@@ -79,9 +74,10 @@ export default class ConfigValidator {
79
74
  if (this.config.auth.loginBackgroundImage) {
80
75
  errors.push(...this.checkCustomFileExists(this.config.auth.loginBackgroundImage));
81
76
  }
82
- const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.resourceId);
77
+ const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
83
78
  if (!userResource) {
84
- throw new Error(`Resource with id "${this.config.auth.resourceId}" not found`);
79
+ const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId || res.table), this.config.auth.usersResourceId);
80
+ throw new Error(`Resource with id "${this.config.auth.usersResourceId}" not found. ${similar ? `Did you mean "${similar}"?` : ''}`);
85
81
  }
86
82
  if (!this.config.auth.beforeLoginConfirmation) {
87
83
  this.config.auth.beforeLoginConfirmation = [];
@@ -126,6 +122,9 @@ export default class ConfigValidator {
126
122
  if (this.config.customization.brandLogo) {
127
123
  errors.push(...this.checkCustomFileExists(this.config.customization.brandLogo));
128
124
  }
125
+ if (this.config.customization.showBrandNameInSidebar === undefined) {
126
+ this.config.customization.showBrandNameInSidebar = true;
127
+ }
129
128
  if (this.config.customization.favicon) {
130
129
  errors.push(...this.checkCustomFileExists(this.config.customization.favicon));
131
130
  }
@@ -254,7 +253,12 @@ export default class ConfigValidator {
254
253
  yield Promise.all(selectedIds.map((recordId) => __awaiter(this, void 0, void 0, function* () {
255
254
  const record = yield connector.getRecordByPrimaryKey(res, recordId);
256
255
  yield Promise.all(res.hooks.delete.beforeSave.map((hook) => __awaiter(this, void 0, void 0, function* () {
257
- const resp = yield hook({ resource: res, record, adminUser });
256
+ const resp = yield hook({
257
+ recordId: recordId,
258
+ resource: res,
259
+ record,
260
+ adminUser,
261
+ });
258
262
  if (!error && resp.error) {
259
263
  error = resp.error;
260
264
  }
@@ -265,7 +269,12 @@ export default class ConfigValidator {
265
269
  yield connector.deleteRecord({ resource: res, recordId });
266
270
  // call afterDelete hook
267
271
  yield Promise.all(res.hooks.delete.afterSave.map((hook) => __awaiter(this, void 0, void 0, function* () {
268
- yield hook({ resource: res, record, adminUser });
272
+ yield hook({
273
+ resource: res,
274
+ record,
275
+ adminUser,
276
+ recordId: recordId
277
+ });
269
278
  })));
270
279
  })));
271
280
  if (error) {