adminforth 1.3.27 → 1.3.28

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.
@@ -67,7 +67,6 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
67
67
  offset: 0,
68
68
  getTotals: false
69
69
  });
70
- process.env.HEAVY_DEBUG && console.log('☝️🪲🪲🪲🪲 existingRecord|||', existingRecord);
71
70
 
72
71
  return existingRecord.data.length > 0;
73
72
  }
@@ -90,14 +89,11 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
90
89
  }
91
90
  recordWithOriginalValues[col.name] = this.setFieldValue(col, filledRecord[col.name]);
92
91
  }
93
-
94
92
 
95
93
  let error: string | null = null;
96
94
  await Promise.all(
97
95
  resource.dataSourceColumns.map(async (col) => {
98
-
99
96
  if (col.isUnique && !col.virtual && !error) {
100
-
101
97
  const exists = await this.checkUnique(resource, col, recordWithOriginalValues[col.name]);
102
98
  if (exists) {
103
99
  error = `Record with ${col.name} ${recordWithOriginalValues[col.name]} already exists`;
@@ -105,12 +101,12 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
105
101
  }
106
102
  })
107
103
  );
108
- process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 error', error);
109
104
  if (error) {
105
+ process.env.HEAVY_DEBUG && console.log('🪲🆕 check unique error', error);
110
106
  return { error, ok: false };
111
107
  }
112
108
 
113
- process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record', recordWithOriginalValues);
109
+ process.env.HEAVY_DEBUG && console.log('🪲🆕 creating record', recordWithOriginalValues);
114
110
  await this.createRecordOriginalValues({ resource, record: recordWithOriginalValues });
115
111
 
116
112
  return {
@@ -119,10 +115,26 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
119
115
  }
120
116
  }
121
117
 
122
- updateRecord({ resource, recordId, newValues }: { resource: AdminForthResource; recordId: string; newValues: any; }): Promise<void> {
118
+ updateRecordOriginalValues({ resource, recordId, newValues }: { resource: AdminForthResource; recordId: string; newValues: any; }): Promise<void> {
123
119
  throw new Error('Method not implemented.');
124
120
  }
125
121
 
122
+ async updateRecord({ resource, recordId, newValues }: { resource: AdminForthResource; recordId: string; newValues: any; }): Promise<{ error?: string; ok: boolean; }> {
123
+ // transform value using setFieldValue and call updateRecordOriginalValues
124
+ const recordWithOriginalValues = {...newValues};
125
+
126
+ for (const field of Object.keys(newValues)) {
127
+ const col = resource.dataSourceColumns.find((col) => col.name == field);
128
+ recordWithOriginalValues[col.name] = this.setFieldValue(col, newValues[col.name]);
129
+ }
130
+
131
+ process.env.HEAVY_DEBUG && console.log(`🪲✏️ updating record id:${recordId}, values: ${JSON.stringify(recordWithOriginalValues)}`);
132
+
133
+ await this.updateRecordOriginalValues({ resource, recordId, newValues: recordWithOriginalValues });
134
+
135
+ return { ok: true };
136
+ }
137
+
126
138
  deleteRecord({ resource, recordId }: { resource: AdminForthResource; recordId: string; }): Promise<boolean> {
127
139
  throw new Error('Method not implemented.');
128
140
  }
@@ -303,11 +303,10 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
303
303
  });
304
304
  }
305
305
 
306
- async updateRecord({ resource, recordId, newValues }: { resource: AdminForthResource, recordId: any, newValues: any }) {
306
+ async updateRecordOriginalValues({ resource, recordId, newValues }: { resource: AdminForthResource, recordId: any, newValues: any }) {
307
307
  const columnsWithPlaceholders = Object.keys(newValues).map((col) => {
308
308
  return `${col} = {${col}:${resource.dataSourceColumns.find((c) => c.name == col)._underlineType}}`
309
309
  });
310
- const values = [...Object.values(newValues), recordId];
311
310
 
312
311
  await this.client.command(
313
312
  {
@@ -182,7 +182,7 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
182
182
  await collection.insertOne(newRecord);
183
183
  }
184
184
 
185
- async updateRecord({ resource, recordId, newValues }) {
185
+ async updateRecordOriginalValues({ resource, recordId, newValues }) {
186
186
  const collection = this.db.db().collection(resource.table);
187
187
  await collection.updateOne({ [this.getPrimaryKey(resource)]: recordId }, { $set: newValues });
188
188
  }
@@ -287,7 +287,7 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
287
287
  await this.db.query(`INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`, values);
288
288
  }
289
289
 
290
- async updateRecord({ resource, recordId, newValues }) {
290
+ async updateRecordOriginalValues({ resource, recordId, newValues }) {
291
291
  const values = [...Object.values(newValues), recordId];
292
292
  const columnsWithPlaceholders = Object.keys(newValues).map((col, i) => `"${col}" = $${i + 1}`).join(', ');
293
293
  await this.db.query(`UPDATE ${resource.table} SET ${columnsWithPlaceholders} WHERE "${this.getPrimaryKey(resource)}" = $${values.length}`, values);
@@ -181,13 +181,12 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
181
181
 
182
182
  const orderBy = sort.length ? `ORDER BY ${sort.map((s) => `${s.field} ${this.SortDirectionsMap[s.direction]}`).join(', ')}` : '';
183
183
 
184
- console.log('🪲 SQLITE Query', `SELECT ${columns} FROM ${tableName} ${where} ${orderBy} LIMIT ? OFFSET ?`, 'params:', [...filterValues, limit, offset]);
185
184
  const q = `SELECT ${columns} FROM ${tableName} ${where} ${orderBy} LIMIT ? OFFSET ?`;
186
185
  const stmt = this.db.prepare(q);
187
186
  const d = [...filterValues, limit, offset];
188
187
 
189
188
  if (process.env.HEAVY_DEBUG) {
190
- console.log('🪲 SQLITE Query', q, 'params:', d);
189
+ console.log('🪲📜 SQLITE Q', q, 'params:', d);
191
190
  }
192
191
  const rows = await stmt.all(d);
193
192
 
@@ -230,13 +229,14 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
230
229
  await q.run(values);
231
230
  }
232
231
 
233
- async updateRecord({ resource, recordId, newValues }: { resource: AdminForthResource, recordId: any, newValues: any }) {
232
+ async updateRecordOriginalValues({ resource, recordId, newValues }: { resource: AdminForthResource, recordId: any, newValues: any }) {
234
233
  const columnsWithPlaceholders = Object.keys(newValues).map((col) => `${col} = ?`);
235
234
  const values = [...Object.values(newValues), recordId];
236
235
 
237
236
  const q = this.db.prepare(
238
237
  `UPDATE ${resource.table} SET ${columnsWithPlaceholders} WHERE ${this.getPrimaryKey(resource)} = ?`
239
238
  )
239
+ process.env.HEAVY_DEBUG && console.log('🪲 SQLITE Query', `UPDATE ${resource.table} SET ${columnsWithPlaceholders} WHERE ${this.getPrimaryKey(resource)} = ?`, 'params:', values);
240
240
  await q.run(values);
241
241
  }
242
242
 
@@ -61,7 +61,6 @@ export default class AdminForthBaseConnector {
61
61
  offset: 0,
62
62
  getTotals: false
63
63
  });
64
- process.env.HEAVY_DEBUG && console.log('☝️🪲🪲🪲🪲 existingRecord|||', existingRecord);
65
64
  return existingRecord.data.length > 0;
66
65
  });
67
66
  }
@@ -90,11 +89,11 @@ export default class AdminForthBaseConnector {
90
89
  }
91
90
  }
92
91
  })));
93
- process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 error', error);
94
92
  if (error) {
93
+ process.env.HEAVY_DEBUG && console.log('🪲🆕 check unique error', error);
95
94
  return { error, ok: false };
96
95
  }
97
- process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record', recordWithOriginalValues);
96
+ process.env.HEAVY_DEBUG && console.log('🪲🆕 creating record', recordWithOriginalValues);
98
97
  yield this.createRecordOriginalValues({ resource, record: recordWithOriginalValues });
99
98
  return {
100
99
  ok: true,
@@ -102,9 +101,22 @@ export default class AdminForthBaseConnector {
102
101
  };
103
102
  });
104
103
  }
105
- updateRecord({ resource, recordId, newValues }) {
104
+ updateRecordOriginalValues({ resource, recordId, newValues }) {
106
105
  throw new Error('Method not implemented.');
107
106
  }
107
+ updateRecord(_a) {
108
+ return __awaiter(this, arguments, void 0, function* ({ resource, recordId, newValues }) {
109
+ // transform value using setFieldValue and call updateRecordOriginalValues
110
+ const recordWithOriginalValues = Object.assign({}, newValues);
111
+ for (const field of Object.keys(newValues)) {
112
+ const col = resource.dataSourceColumns.find((col) => col.name == field);
113
+ recordWithOriginalValues[col.name] = this.setFieldValue(col, newValues[col.name]);
114
+ }
115
+ process.env.HEAVY_DEBUG && console.log(`🪲✏️ updating record id:${recordId}, values: ${JSON.stringify(recordWithOriginalValues)}`);
116
+ yield this.updateRecordOriginalValues({ resource, recordId, newValues: recordWithOriginalValues });
117
+ return { ok: true };
118
+ });
119
+ }
108
120
  deleteRecord({ resource, recordId }) {
109
121
  throw new Error('Method not implemented.');
110
122
  }
@@ -285,12 +285,11 @@ class ClickhouseConnector extends AdminForthBaseConnector {
285
285
  });
286
286
  });
287
287
  }
288
- updateRecord(_a) {
288
+ updateRecordOriginalValues(_a) {
289
289
  return __awaiter(this, arguments, void 0, function* ({ resource, recordId, newValues }) {
290
290
  const columnsWithPlaceholders = Object.keys(newValues).map((col) => {
291
291
  return `${col} = {${col}:${resource.dataSourceColumns.find((c) => c.name == col)._underlineType}}`;
292
292
  });
293
- const values = [...Object.values(newValues), recordId];
294
293
  yield this.client.command({
295
294
  query: `ALTER TABLE ${this.dbName}.${resource.table} UPDATE ${columnsWithPlaceholders.join(', ')} WHERE ${this.getPrimaryKey(resource)} = {recordId:${resource.dataSourceColumns.find((c) => c.primaryKey)._underlineType}}`,
296
295
  query_params: Object.assign(Object.assign({}, newValues), { recordId }),
@@ -171,7 +171,7 @@ class MongoConnector extends AdminForthBaseConnector {
171
171
  yield collection.insertOne(newRecord);
172
172
  });
173
173
  }
174
- updateRecord(_a) {
174
+ updateRecordOriginalValues(_a) {
175
175
  return __awaiter(this, arguments, void 0, function* ({ resource, recordId, newValues }) {
176
176
  const collection = this.db.db().collection(resource.table);
177
177
  yield collection.updateOne({ [this.getPrimaryKey(resource)]: recordId }, { $set: newValues });
@@ -283,7 +283,7 @@ class PostgresConnector extends AdminForthBaseConnector {
283
283
  yield this.db.query(`INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`, values);
284
284
  });
285
285
  }
286
- updateRecord(_a) {
286
+ updateRecordOriginalValues(_a) {
287
287
  return __awaiter(this, arguments, void 0, function* ({ resource, recordId, newValues }) {
288
288
  const values = [...Object.values(newValues), recordId];
289
289
  const columnsWithPlaceholders = Object.keys(newValues).map((col, i) => `"${col}" = $${i + 1}`).join(', ');
@@ -196,12 +196,11 @@ class SQLiteConnector extends AdminForthBaseConnector {
196
196
  const where = this.whereClause(filters);
197
197
  const filterValues = this.whereParams(filters);
198
198
  const orderBy = sort.length ? `ORDER BY ${sort.map((s) => `${s.field} ${this.SortDirectionsMap[s.direction]}`).join(', ')}` : '';
199
- console.log('🪲 SQLITE Query', `SELECT ${columns} FROM ${tableName} ${where} ${orderBy} LIMIT ? OFFSET ?`, 'params:', [...filterValues, limit, offset]);
200
199
  const q = `SELECT ${columns} FROM ${tableName} ${where} ${orderBy} LIMIT ? OFFSET ?`;
201
200
  const stmt = this.db.prepare(q);
202
201
  const d = [...filterValues, limit, offset];
203
202
  if (process.env.HEAVY_DEBUG) {
204
- console.log('🪲 SQLITE Query', q, 'params:', d);
203
+ console.log('🪲📜 SQLITE Q', q, 'params:', d);
205
204
  }
206
205
  const rows = yield stmt.all(d);
207
206
  return rows.map((row) => {
@@ -246,11 +245,12 @@ class SQLiteConnector extends AdminForthBaseConnector {
246
245
  yield q.run(values);
247
246
  });
248
247
  }
249
- updateRecord(_a) {
248
+ updateRecordOriginalValues(_a) {
250
249
  return __awaiter(this, arguments, void 0, function* ({ resource, recordId, newValues }) {
251
250
  const columnsWithPlaceholders = Object.keys(newValues).map((col) => `${col} = ?`);
252
251
  const values = [...Object.values(newValues), recordId];
253
252
  const q = this.db.prepare(`UPDATE ${resource.table} SET ${columnsWithPlaceholders} WHERE ${this.getPrimaryKey(resource)} = ?`);
253
+ process.env.HEAVY_DEBUG && console.log('🪲 SQLITE Query', `UPDATE ${resource.table} SET ${columnsWithPlaceholders} WHERE ${this.getPrimaryKey(resource)} = ?`, 'params:', values);
254
254
  yield q.run(values);
255
255
  });
256
256
  }
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import SQLiteConnector from './dataConnectors/sqlite.js';
20
20
  import CodeInjector from './modules/codeInjector.js';
21
21
  import ExpressServer from './servers/express.js';
22
22
  import { ADMINFORTH_VERSION, listify, suggestIfTypo } from './modules/utils.js';
23
- import { AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages, } from './types/AdminForthConfig.js';
23
+ import { AdminForthFilterOperators, AdminForthDataTypes, } 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';
@@ -157,23 +157,15 @@ class AdminForth {
157
157
  }
158
158
  createResourceRecord(_b) {
159
159
  return __awaiter(this, arguments, void 0, function* ({ resource, record, adminUser }) {
160
- var _c, _d, _e, _f, _g;
161
- for (const column of resource.columns) {
162
- // TODO: assuming specifity for AdminForthResourcePages.create better to move it to api for this button
163
- if (((_c = column.required) === null || _c === void 0 ? void 0 : _c.create) &&
164
- record[column.name] === undefined &&
165
- column.showIn.includes(AdminForthResourcePages.create)) {
166
- return { error: `Column '${column.name}' is required`, ok: false };
167
- }
168
- }
160
+ var _c, _d, _e, _f;
169
161
  // execute hook if needed
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)) {
162
+ for (const hook of listify((_d = (_c = resource.hooks) === null || _c === void 0 ? void 0 : _c.create) === null || _d === void 0 ? void 0 : _d.beforeSave)) {
171
163
  const resp = yield hook({ recordId: undefined, resource, record, adminUser });
172
164
  if (!resp || (!resp.ok && !resp.error)) {
173
165
  throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
174
166
  }
175
167
  if (resp.error) {
176
- return { error: resp.error, ok: false };
168
+ return { error: resp.error };
177
169
  }
178
170
  }
179
171
  // remove virtual columns from record
@@ -183,15 +175,15 @@ class AdminForth {
183
175
  }
184
176
  }
185
177
  const connector = this.connectors[resource.dataSource];
186
- process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record createResourceRecord', record);
187
- const { ok, error, createdRecord } = yield connector.createRecord({ resource, record, adminUser });
188
- if (!ok) {
189
- return { ok, error };
178
+ process.env.HEAVY_DEBUG && console.log('🪲🆕 creating record createResourceRecord', record);
179
+ const { error, createdRecord } = yield connector.createRecord({ resource, record, adminUser });
180
+ if (error) {
181
+ return { error };
190
182
  }
191
183
  const primaryKey = record[resource.columns.find((col) => col.primaryKey).name];
192
184
  // execute hook if needed
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)) {
194
- console.log('Hook afterSave', hook);
185
+ for (const hook of listify((_f = (_e = resource.hooks) === null || _e === void 0 ? void 0 : _e.create) === null || _f === void 0 ? void 0 : _f.afterSave)) {
186
+ process.env.HEAVY_DEBUG && console.log('🪲 Hook afterSave', hook);
195
187
  const resp = yield hook({
196
188
  recordId: primaryKey,
197
189
  resource,
@@ -202,10 +194,100 @@ class AdminForth {
202
194
  throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
203
195
  }
204
196
  if (resp.error) {
205
- return { error: resp.error, ok: false };
197
+ return { error: resp.error };
198
+ }
199
+ }
200
+ return { error, createdRecord };
201
+ });
202
+ }
203
+ /**
204
+ * record is partial record with only changed fields
205
+ */
206
+ updateResourceRecord(_b) {
207
+ return __awaiter(this, arguments, void 0, function* ({ resource, recordId, record, oldRecord, adminUser }) {
208
+ var _c, _d, _e, _f;
209
+ // execute hook if needed
210
+ for (const hook of listify((_d = (_c = resource.hooks) === null || _c === void 0 ? void 0 : _c.edit) === null || _d === void 0 ? void 0 : _d.beforeSave)) {
211
+ const resp = yield hook({
212
+ recordId,
213
+ resource,
214
+ record,
215
+ oldRecord,
216
+ adminUser
217
+ });
218
+ if (!resp || (!resp.ok && !resp.error)) {
219
+ throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
220
+ }
221
+ if (resp.error) {
222
+ return { error: resp.error };
223
+ }
224
+ }
225
+ const newValues = {};
226
+ const connector = this.connectors[resource.dataSource];
227
+ for (const recordField in record) {
228
+ if (record[recordField] !== oldRecord[recordField]) {
229
+ // leave only changed fields to reduce data transfer/modifications in db
230
+ const column = resource.columns.find((col) => col.name === recordField);
231
+ if (!column || !column.virtual) {
232
+ // exclude virtual columns
233
+ newValues[recordField] = record[recordField];
234
+ }
235
+ }
236
+ }
237
+ if (Object.keys(newValues).length > 0) {
238
+ yield connector.updateRecord({ resource, recordId, newValues });
239
+ }
240
+ // execute hook if needed
241
+ for (const hook of listify((_f = (_e = resource.hooks) === null || _e === void 0 ? void 0 : _e.edit) === null || _f === void 0 ? void 0 : _f.afterSave)) {
242
+ const resp = yield hook({
243
+ resource,
244
+ record,
245
+ adminUser,
246
+ oldRecord,
247
+ recordId,
248
+ });
249
+ if (!resp || (!resp.ok && !resp.error)) {
250
+ throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
251
+ }
252
+ return { error: resp.error };
253
+ }
254
+ });
255
+ }
256
+ deleteResourceRecord(_b) {
257
+ return __awaiter(this, arguments, void 0, function* ({ resource, recordId, adminUser, record }) {
258
+ var _c, _d, _e, _f;
259
+ // execute hook if needed
260
+ for (const hook of listify((_d = (_c = resource.hooks) === null || _c === void 0 ? void 0 : _c.delete) === null || _d === void 0 ? void 0 : _d.beforeSave)) {
261
+ const resp = yield hook({
262
+ resource,
263
+ record,
264
+ adminUser,
265
+ recordId,
266
+ });
267
+ if (!resp || (!resp.ok && !resp.error)) {
268
+ throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
269
+ }
270
+ if (resp.error) {
271
+ return { error: resp.error };
272
+ }
273
+ }
274
+ const connector = this.connectors[resource.dataSource];
275
+ yield connector.deleteRecord({ resource, recordId });
276
+ // execute hook if needed
277
+ for (const hook of listify((_f = (_e = resource.hooks) === null || _e === void 0 ? void 0 : _e.delete) === null || _f === void 0 ? void 0 : _f.afterSave)) {
278
+ const resp = yield hook({
279
+ resource,
280
+ record,
281
+ adminUser,
282
+ recordId,
283
+ });
284
+ if (!resp || (!resp.ok && !resp.error)) {
285
+ throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
286
+ }
287
+ if (resp.error) {
288
+ return { error: resp.error };
206
289
  }
207
290
  }
208
- return { ok, error, createdRecord };
209
291
  });
210
292
  }
211
293
  resource(resourceId) {
@@ -234,6 +316,16 @@ AdminForth.Types = AdminForthDataTypes;
234
316
  AdminForth.Utils = {
235
317
  generatePasswordHash: (password) => __awaiter(void 0, void 0, void 0, function* () {
236
318
  return yield AdminForthAuth.generatePasswordHash(password);
237
- })
319
+ }),
320
+ PASSWORD_VALIDATORS: {
321
+ UP_LOW_NUM_SPECIAL: {
322
+ regExp: '^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[!@#\\$%\\^&\\*\\(\\)\\-_=\\+\\[\\]\\{\\}\\|;:\',\\.<>\\/\\?]).+$',
323
+ message: 'Password must include at least one uppercase letter, one lowercase letter, one number, and one special character'
324
+ },
325
+ UP_LOW_NUM: {
326
+ regExp: '^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).+$',
327
+ message: 'Password must include at least one uppercase letter, one lowercase letter, and one number'
328
+ },
329
+ }
238
330
  };
239
331
  export default AdminForth;
@@ -217,13 +217,13 @@ class CodeInjector {
217
217
  dereference: true, // needed to dereference types
218
218
  });
219
219
  if (process.env.HEAVY_DEBUG) {
220
- console.log('🪲 await fsExtra.copy copy single file', src, dest);
220
+ console.log('🪲⚙️ fsExtra.copy copy single file', src, dest);
221
221
  }
222
222
  })));
223
223
  }
224
224
  else {
225
225
  if (process.env.HEAVY_DEBUG) {
226
- console.log(`🪲 await fsExtra.copy from ${path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'spa')}, -> ${CodeInjector.SPA_TMP_PATH}`);
226
+ console.log(`🪲⚙️ fsExtra.copy from ${path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'spa')}, -> ${CodeInjector.SPA_TMP_PATH}`);
227
227
  }
228
228
  // try to rm SPA_TMP_PATH/src/types directory
229
229
  try {
@@ -237,7 +237,7 @@ class CodeInjector {
237
237
  filter: (src) => {
238
238
  const filterPasses = !src.includes('/adminforth/spa/node_modules') && !src.includes('/adminforth/spa/dist');
239
239
  if (process.env.HEAVY_DEBUG && !filterPasses) {
240
- console.log('🪲 await fsExtra.copy filtered out', src);
240
+ console.log('🪲⚙️ fsExtra.copy filtered out', src);
241
241
  }
242
242
  return filterPasses;
243
243
  },
@@ -261,7 +261,7 @@ class CodeInjector {
261
261
  for (const [src, dest] of Object.entries(this.srcFoldersToSync)) {
262
262
  const to = path.join(CodeInjector.SPA_TMP_PATH, 'src', 'custom', dest);
263
263
  if (process.env.HEAVY_DEBUG) {
264
- console.log(`🪲 await fsExtra.copy from ${src}, ${to}`);
264
+ console.log(`🪲⚙️ fsExtra.copy from ${src}, ${to}`);
265
265
  }
266
266
  yield fsExtra.copy(src, to, {
267
267
  recursive: true,
@@ -465,7 +465,7 @@ class CodeInjector {
465
465
  });
466
466
  yield collectDirectories(spaPath);
467
467
  if (process.env.HEAVY_DEBUG) {
468
- console.log('🔎 Watching for changes in:', directories.join(','));
468
+ console.log('🪲🔎 Watch for:', directories.join(','));
469
469
  }
470
470
  const watcher = filewatcher();
471
471
  directories.forEach((dir) => {
@@ -514,11 +514,11 @@ class CodeInjector {
514
514
  yield collectDirectories(customComponentsDir);
515
515
  const watcher = filewatcher();
516
516
  files.forEach((file) => {
517
- process.env.HEAVY_DEBUG && console.log(`🔎 Watching for changes in file ${file}`);
517
+ process.env.HEAVY_DEBUG && console.log(`🪲🔎 Watch for file ${file}`);
518
518
  watcher.add(file);
519
519
  });
520
520
  if (process.env.HEAVY_DEBUG) {
521
- console.log('🔎 Watching for changes in:', directories.join(','));
521
+ console.log('🪲🔎 Watch for:', directories.join(','));
522
522
  }
523
523
  watcher.on('change', (fileOrDir) => __awaiter(this, void 0, void 0, function* () {
524
524
  // copy one file
@@ -568,7 +568,7 @@ class CodeInjector {
568
568
  }
569
569
  else {
570
570
  const command = 'run dev';
571
- console.log(`⚙️ spawn: npm ${command}...`);
571
+ console.log(`🪲⚙️ spawn: npm ${command}...`);
572
572
  const nodeBinary = process.execPath;
573
573
  const npmPath = path.join(path.dirname(nodeBinary), 'npm');
574
574
  const env = Object.assign({ VITE_ADMINFORTH_PUBLIC_PATH: this.adminforth.config.baseUrl, FORCE_COLOR: '1' }, process.env);
@@ -7,7 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { AdminForthFilterOperators, AdminForthDataTypes, AllowedActionsEnum, ActionCheckSource } from "../types/AdminForthConfig.js";
10
+ import { AdminForthFilterOperators, AdminForthDataTypes, AllowedActionsEnum, ActionCheckSource, AdminForthResourcePages } from "../types/AdminForthConfig.js";
11
11
  import { ADMINFORTH_VERSION, listify } from './utils.js';
12
12
  import AdminForthAuth from "../auth.js";
13
13
  export function interpretResource(adminUser, resource, meta, source) {
@@ -19,7 +19,7 @@ export function interpretResource(adminUser, resource, meta, source) {
19
19
  const allowedActions = {};
20
20
  yield Promise.all(Object.entries(((_a = resource.options) === null || _a === void 0 ? void 0 : _a.allowedActions) || {}).map((_b) => __awaiter(this, [_b], void 0, function* ([key, value]) {
21
21
  if (process.env.HEAVY_DEBUG) {
22
- console.log('🪲checking for allowed call', key, 'value:', value, 'typeof', typeof value);
22
+ console.log(`🪲🚥check allowed ${key}, ${value}`);
23
23
  }
24
24
  // if callable then call
25
25
  if (typeof value === 'function') {
@@ -485,6 +485,7 @@ export default class AdminForthRestAPI {
485
485
  method: 'POST',
486
486
  path: '/create_record',
487
487
  handler: (_2) => __awaiter(this, [_2], void 0, function* ({ body, adminUser }) {
488
+ var _3;
488
489
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
489
490
  if (!resource) {
490
491
  return { error: `Resource '${body['resourceId']}' not found` };
@@ -495,6 +496,13 @@ export default class AdminForthRestAPI {
495
496
  return { error };
496
497
  }
497
498
  const { record } = body;
499
+ for (const column of resource.columns) {
500
+ if (((_3 = column.required) === null || _3 === void 0 ? void 0 : _3.create) &&
501
+ record[column.name] === undefined &&
502
+ column.showIn.includes(AdminForthResourcePages.create)) {
503
+ return { error: `Column '${column.name}' is required`, ok: false };
504
+ }
505
+ }
498
506
  const response = yield this.adminforth.createResourceRecord({ resource, record, adminUser });
499
507
  if (response.error) {
500
508
  return { error: response.error, ok: false };
@@ -509,8 +517,7 @@ export default class AdminForthRestAPI {
509
517
  server.endpoint({
510
518
  method: 'POST',
511
519
  path: '/update_record',
512
- handler: (_3) => __awaiter(this, [_3], void 0, function* ({ body, adminUser }) {
513
- var _4, _5, _6, _7;
520
+ handler: (_4) => __awaiter(this, [_4], void 0, function* ({ body, adminUser }) {
514
521
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
515
522
  if (!resource) {
516
523
  return { error: `Resource '${body['resourceId']}' not found` };
@@ -524,68 +531,23 @@ export default class AdminForthRestAPI {
524
531
  }
525
532
  const record = body['record'];
526
533
  const { allowedActions } = yield interpretResource(adminUser, resource, { requestBody: body, newRecord: record, oldRecord }, ActionCheckSource.EditRequest);
527
- const { allowed, error } = checkAccess(AllowedActionsEnum.edit, allowedActions);
534
+ const { allowed, error: allowedError } = checkAccess(AllowedActionsEnum.edit, allowedActions);
528
535
  if (!allowed) {
529
- return { error };
530
- }
531
- // execute hook if needed
532
- 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)) {
533
- const resp = yield hook({
534
- recordId,
535
- resource,
536
- record,
537
- adminUser
538
- });
539
- if (!resp || (!resp.ok && !resp.error)) {
540
- throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
541
- }
542
- if (resp.error) {
543
- return { error: resp.error };
544
- }
536
+ return { allowedError };
545
537
  }
546
- const newValues = {};
547
- for (const recordField in record) {
548
- if (record[recordField] !== oldRecord[recordField]) {
549
- const column = resource.columns.find((col) => col.name === recordField);
550
- if (column) {
551
- if (!column.virtual) {
552
- newValues[recordField] = connector.setFieldValue(column, record[recordField]);
553
- }
554
- }
555
- else {
556
- newValues[recordField] = record[recordField];
557
- }
558
- }
559
- }
560
- if (Object.keys(newValues).length > 0) {
561
- yield connector.updateRecord({ resource, recordId, newValues });
562
- }
563
- // execute hook if needed
564
- 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)) {
565
- const resp = yield hook({
566
- resource,
567
- record,
568
- adminUser,
569
- oldRecord,
570
- recordId,
571
- });
572
- if (!resp || (!resp.ok && !resp.error)) {
573
- throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
574
- }
575
- if (resp.error) {
576
- return { error: resp.error };
577
- }
538
+ const { error } = yield this.adminforth.updateResourceRecord({ resource, record, adminUser, oldRecord, recordId });
539
+ if (error) {
540
+ return { error };
578
541
  }
579
542
  return {
580
- newRecordId: recordId
543
+ ok: true
581
544
  };
582
545
  })
583
546
  });
584
547
  server.endpoint({
585
548
  method: 'POST',
586
549
  path: '/delete_record',
587
- handler: (_8) => __awaiter(this, [_8], void 0, function* ({ body, adminUser }) {
588
- var _9, _10, _11, _12;
550
+ handler: (_5) => __awaiter(this, [_5], void 0, function* ({ body, adminUser }) {
589
551
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
590
552
  const record = yield this.adminforth.connectors[resource.dataSource].getRecordByPrimaryKey(resource, body['primaryKey']);
591
553
  if (!resource) {
@@ -602,39 +564,12 @@ export default class AdminForthRestAPI {
602
564
  if (!allowed) {
603
565
  return { error };
604
566
  }
605
- // execute hook if needed
606
- 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)) {
607
- const resp = yield hook({
608
- resource,
609
- record,
610
- adminUser,
611
- recordId: body['primaryKey']
612
- });
613
- if (!resp || (!resp.ok && !resp.error)) {
614
- throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
615
- }
616
- if (resp.error) {
617
- return { error: resp.error };
618
- }
619
- }
620
- const connector = this.adminforth.connectors[resource.dataSource];
621
- yield connector.deleteRecord({ resource, recordId: body['primaryKey'] });
622
- // execute hook if needed
623
- 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)) {
624
- const resp = yield hook({
625
- resource,
626
- record,
627
- adminUser,
628
- recordId: body['primaryKey']
629
- });
630
- if (!resp || (!resp.ok && !resp.error)) {
631
- throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
632
- }
633
- if (resp.error) {
634
- return { error: resp.error };
635
- }
567
+ const { error: deleteError } = yield this.adminforth.deleteResourceRecord({ resource, record, adminUser, recordId: body['primaryKey'] });
568
+ if (deleteError) {
569
+ return { error: deleteError };
636
570
  }
637
571
  return {
572
+ ok: true,
638
573
  recordId: body['primaryKey']
639
574
  };
640
575
  })
@@ -642,7 +577,7 @@ export default class AdminForthRestAPI {
642
577
  server.endpoint({
643
578
  method: 'POST',
644
579
  path: '/start_bulk_action',
645
- handler: (_13) => __awaiter(this, [_13], void 0, function* ({ body, adminUser }) {
580
+ handler: (_6) => __awaiter(this, [_6], void 0, function* ({ body, adminUser }) {
646
581
  const { resourceId, actionId, recordIds } = body;
647
582
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
648
583
  if (!resource) {
package/index.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  AfterSaveFunction,
18
18
  AdminUser,
19
19
  AdminForthResource,
20
+ IAdminForthDataSourceConnectorBase,
20
21
  } from './types/AdminForthConfig.js';
21
22
  import AdminForthPlugin from './basePlugin.js';
22
23
  import ConfigValidator from './modules/configValidator.js';
@@ -38,6 +39,17 @@ class AdminForth implements IAdminForth {
38
39
  static Utils = {
39
40
  generatePasswordHash: async (password) => {
40
41
  return await AdminForthAuth.generatePasswordHash(password);
42
+ },
43
+
44
+ PASSWORD_VALIDATORS: {
45
+ UP_LOW_NUM_SPECIAL: {
46
+ regExp: '^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[!@#\\$%\\^&\\*\\(\\)\\-_=\\+\\[\\]\\{\\}\\|;:\',\\.<>\\/\\?]).+$',
47
+ message: 'Password must include at least one uppercase letter, one lowercase letter, one number, and one special character'
48
+ },
49
+ UP_LOW_NUM: {
50
+ regExp: '^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).+$',
51
+ message: 'Password must include at least one uppercase letter, one lowercase letter, and one number'
52
+ },
41
53
  }
42
54
  }
43
55
 
@@ -49,7 +61,9 @@ class AdminForth implements IAdminForth {
49
61
  express: ExpressServer;
50
62
  auth: AdminForthAuth;
51
63
  codeInjector: CodeInjector;
52
- connectors;
64
+ connectors: {
65
+ [dataSourceId: string]: IAdminForthDataSourceConnectorBase,
66
+ };
53
67
  connectorClasses: any;
54
68
  runningHotReload: boolean;
55
69
  activatedPlugins: Array<AdminForthPlugin>;
@@ -202,18 +216,7 @@ class AdminForth implements IAdminForth {
202
216
  async createResourceRecord(
203
217
  { resource, record, adminUser }:
204
218
  { resource: AdminForthResource, record: any, adminUser: AdminUser }
205
- ): Promise<{ ok: boolean, error?: string, createdRecord?: any }> {
206
-
207
- for (const column of resource.columns) {
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
- }
219
+ ): Promise<{ error?: string, createdRecord?: any }> {
217
220
 
218
221
  // execute hook if needed
219
222
  for (const hook of listify(resource.hooks?.create?.beforeSave as BeforeSaveFunction[])) {
@@ -223,28 +226,28 @@ class AdminForth implements IAdminForth {
223
226
  }
224
227
 
225
228
  if (resp.error) {
226
- return { error: resp.error, ok: false };
229
+ return { error: resp.error };
227
230
  }
228
231
  }
229
232
 
230
233
  // remove virtual columns from record
231
234
  for (const column of resource.columns.filter((col) => col.virtual)) {
232
- if (record[column.name]) {
233
- delete record[column.name];
234
- }
235
+ if (record[column.name]) {
236
+ delete record[column.name];
237
+ }
235
238
  }
236
239
  const connector = this.connectors[resource.dataSource];
237
- process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record createResourceRecord', record);
238
- const { ok, error, createdRecord } = await connector.createRecord({ resource, record, adminUser });
239
- if (!ok) {
240
- return { ok, error };
240
+ process.env.HEAVY_DEBUG && console.log('🪲🆕 creating record createResourceRecord', record);
241
+ const { error, createdRecord } = await connector.createRecord({ resource, record, adminUser });
242
+ if ( error ) {
243
+ return { error };
241
244
  }
242
245
 
243
246
  const primaryKey = record[resource.columns.find((col) => col.primaryKey).name];
244
247
 
245
248
  // execute hook if needed
246
249
  for (const hook of listify(resource.hooks?.create?.afterSave as AfterSaveFunction[])) {
247
- console.log('Hook afterSave', hook);
250
+ process.env.HEAVY_DEBUG && console.log('🪲 Hook afterSave', hook);
248
251
  const resp = await hook({
249
252
  recordId: primaryKey,
250
253
  resource,
@@ -257,11 +260,112 @@ class AdminForth implements IAdminForth {
257
260
  }
258
261
 
259
262
  if (resp.error) {
260
- return { error: resp.error, ok: false };
263
+ return { error: resp.error };
264
+ }
265
+ }
266
+
267
+ return { error, createdRecord };
268
+ }
269
+
270
+ /**
271
+ * record is partial record with only changed fields
272
+ */
273
+ async updateResourceRecord(
274
+ { resource, recordId, record, oldRecord, adminUser }:
275
+ { resource: AdminForthResource, recordId: any, record: any, oldRecord: any, adminUser: AdminUser }
276
+ ): Promise<{ error?: string }> {
277
+
278
+ // execute hook if needed
279
+ for (const hook of listify(resource.hooks?.edit?.beforeSave as BeforeSaveFunction[])) {
280
+ const resp = await hook({
281
+ recordId,
282
+ resource,
283
+ record,
284
+ oldRecord,
285
+ adminUser
286
+ });
287
+ if (!resp || (!resp.ok && !resp.error)) {
288
+ throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
289
+ }
290
+ if (resp.error) {
291
+ return { error: resp.error };
261
292
  }
262
293
  }
294
+ const newValues = {};
295
+ const connector = this.connectors[resource.dataSource];
296
+
297
+ for (const recordField in record) {
298
+ if (record[recordField] !== oldRecord[recordField]) {
299
+ // leave only changed fields to reduce data transfer/modifications in db
300
+ const column = resource.columns.find((col) => col.name === recordField);
301
+ if (!column || !column.virtual) {
302
+ // exclude virtual columns
303
+ newValues[recordField] = record[recordField];
304
+ }
305
+ }
306
+ }
263
307
 
264
- return { ok, error, createdRecord };
308
+ if (Object.keys(newValues).length > 0) {
309
+ await connector.updateRecord({ resource, recordId, newValues });
310
+ }
311
+
312
+ // execute hook if needed
313
+ for (const hook of listify(resource.hooks?.edit?.afterSave as AfterSaveFunction[])) {
314
+ const resp = await hook({
315
+ resource,
316
+ record,
317
+ adminUser,
318
+ oldRecord,
319
+ recordId,
320
+ });
321
+ if (!resp || (!resp.ok && !resp.error)) {
322
+ throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
323
+ }
324
+
325
+ return { error: resp.error };
326
+ }
327
+ }
328
+
329
+ async deleteResourceRecord(
330
+ { resource, recordId, adminUser, record }:
331
+ { resource: AdminForthResource, recordId: any, adminUser: AdminUser, record: any }
332
+ ): Promise<{ error?: string }> {
333
+ // execute hook if needed
334
+ for (const hook of listify(resource.hooks?.delete?.beforeSave as BeforeSaveFunction[])) {
335
+ const resp = await hook({
336
+ resource,
337
+ record,
338
+ adminUser,
339
+ recordId,
340
+ });
341
+ if (!resp || (!resp.ok && !resp.error)) {
342
+ throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
343
+ }
344
+
345
+ if (resp.error) {
346
+ return { error: resp.error };
347
+ }
348
+ }
349
+
350
+ const connector = this.connectors[resource.dataSource];
351
+ await connector.deleteRecord({ resource, recordId});
352
+
353
+ // execute hook if needed
354
+ for (const hook of listify(resource.hooks?.delete?.afterSave as BeforeSaveFunction[])) {
355
+ const resp = await hook({
356
+ resource,
357
+ record,
358
+ adminUser,
359
+ recordId,
360
+ });
361
+ if (!resp || (!resp.ok && !resp.error)) {
362
+ throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
363
+ }
364
+
365
+ if (resp.error) {
366
+ return { error: resp.error };
367
+ }
368
+ }
265
369
  }
266
370
 
267
371
  resource(resourceId: string): IOperationalResource {
@@ -236,13 +236,13 @@ class CodeInjector implements ICodeInjector {
236
236
  dereference: true, // needed to dereference types
237
237
  });
238
238
  if (process.env.HEAVY_DEBUG) {
239
- console.log('🪲 await fsExtra.copy copy single file', src, dest);
239
+ console.log('🪲⚙️ fsExtra.copy copy single file', src, dest);
240
240
  }
241
241
 
242
242
  }));
243
243
  } else {
244
244
  if (process.env.HEAVY_DEBUG) {
245
- console.log(`🪲 await fsExtra.copy from ${path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'spa')}, -> ${CodeInjector.SPA_TMP_PATH}`);
245
+ console.log(`🪲⚙️ fsExtra.copy from ${path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'spa')}, -> ${CodeInjector.SPA_TMP_PATH}`);
246
246
  }
247
247
 
248
248
  // try to rm SPA_TMP_PATH/src/types directory
@@ -257,7 +257,7 @@ class CodeInjector implements ICodeInjector {
257
257
  filter: (src) => {
258
258
  const filterPasses = !src.includes('/adminforth/spa/node_modules') && !src.includes('/adminforth/spa/dist')
259
259
  if (process.env.HEAVY_DEBUG && !filterPasses) {
260
- console.log('🪲 await fsExtra.copy filtered out', src);
260
+ console.log('🪲⚙️ fsExtra.copy filtered out', src);
261
261
  }
262
262
 
263
263
  return filterPasses
@@ -287,7 +287,7 @@ class CodeInjector implements ICodeInjector {
287
287
  for (const [src, dest] of Object.entries(this.srcFoldersToSync)) {
288
288
  const to = path.join(CodeInjector.SPA_TMP_PATH, 'src', 'custom', dest);
289
289
  if (process.env.HEAVY_DEBUG) {
290
- console.log(`🪲 await fsExtra.copy from ${src}, ${to}`);
290
+ console.log(`🪲⚙️ fsExtra.copy from ${src}, ${to}`);
291
291
  }
292
292
 
293
293
  await fsExtra.copy(src, to, {
@@ -534,7 +534,7 @@ class CodeInjector implements ICodeInjector {
534
534
  await collectDirectories(spaPath);
535
535
 
536
536
  if (process.env.HEAVY_DEBUG) {
537
- console.log('🔎 Watching for changes in:', directories.join(','));
537
+ console.log('🪲🔎 Watch for:', directories.join(','));
538
538
  }
539
539
 
540
540
  const watcher = filewatcher();
@@ -593,12 +593,12 @@ class CodeInjector implements ICodeInjector {
593
593
 
594
594
  const watcher = filewatcher();
595
595
  files.forEach((file) => {
596
- process.env.HEAVY_DEBUG && console.log(`🔎 Watching for changes in file ${file}`);
596
+ process.env.HEAVY_DEBUG && console.log(`🪲🔎 Watch for file ${file}`);
597
597
  watcher.add(file);
598
598
  });
599
599
 
600
600
  if (process.env.HEAVY_DEBUG) {
601
- console.log('🔎 Watching for changes in:', directories.join(','));
601
+ console.log('🪲🔎 Watch for:', directories.join(','));
602
602
  }
603
603
 
604
604
  watcher.on(
@@ -657,7 +657,7 @@ class CodeInjector implements ICodeInjector {
657
657
  await this.runNpmShell({command: 'run build-only', cwd});
658
658
  } else {
659
659
  const command = 'run dev';
660
- console.log(`⚙️ spawn: npm ${command}...`);
660
+ console.log(`🪲⚙️ spawn: npm ${command}...`);
661
661
  const nodeBinary = process.execPath;
662
662
  const npmPath = path.join(path.dirname(nodeBinary), 'npm');
663
663
  const env = {
@@ -87,7 +87,7 @@ export default class OperationalResource implements IOperationalResource {
87
87
  return await this.dataConnector.updateRecord({
88
88
  resource: this.resourceConfig,
89
89
  recordId: primaryKey,
90
- newValues: record
90
+ newValues: record
91
91
  });
92
92
  }
93
93
 
@@ -13,7 +13,8 @@ import {
13
13
  AfterDataSourceResponseFunction,
14
14
  BeforeDataSourceRequestFunction,
15
15
  AfterSaveFunction,
16
- AllowedActionsResolved
16
+ AllowedActionsResolved,
17
+ AdminForthResourcePages
17
18
 
18
19
  } from "../types/AdminForthConfig.js";
19
20
 
@@ -32,7 +33,7 @@ export async function interpretResource(adminUser: AdminUser, resource: AdminFor
32
33
  Object.entries(resource.options?.allowedActions || {}).map(
33
34
  async ([key, value]: [string, AllowedActionValue]) => {
34
35
  if (process.env.HEAVY_DEBUG) {
35
- console.log('🪲checking for allowed call', key, 'value:', value, 'typeof', typeof value);
36
+ console.log(`🪲🚥check allowed ${key}, ${value}`)
36
37
  }
37
38
 
38
39
  // if callable then call
@@ -575,6 +576,16 @@ export default class AdminForthRestAPI {
575
576
 
576
577
  const { record } = body;
577
578
 
579
+ for (const column of resource.columns) {
580
+ if (
581
+ (column.required as {create?: boolean, edit?: boolean})?.create &&
582
+ record[column.name] === undefined &&
583
+ column.showIn.includes(AdminForthResourcePages.create)
584
+ ) {
585
+ return { error: `Column '${column.name}' is required`, ok: false };
586
+ }
587
+ }
588
+
578
589
  const response = await this.adminforth.createResourceRecord({ resource, record, adminUser });
579
590
  if (response.error) {
580
591
  return { error: response.error, ok: false };
@@ -607,66 +618,17 @@ export default class AdminForthRestAPI {
607
618
 
608
619
  const { allowedActions } = await interpretResource(adminUser, resource, { requestBody: body, newRecord: record, oldRecord}, ActionCheckSource.EditRequest);
609
620
 
610
- const { allowed, error } = checkAccess(AllowedActionsEnum.edit, allowedActions);
621
+ const { allowed, error: allowedError } = checkAccess(AllowedActionsEnum.edit, allowedActions);
611
622
  if (!allowed) {
612
- return { error };
623
+ return { allowedError };
613
624
  }
614
625
 
615
- // execute hook if needed
616
- for (const hook of listify(resource.hooks?.edit?.beforeSave as BeforeSaveFunction[])) {
617
- const resp = await hook({
618
- recordId,
619
- resource,
620
- record,
621
- adminUser
622
- });
623
- if (!resp || (!resp.ok && !resp.error)) {
624
- throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
625
- }
626
-
627
- if (resp.error) {
628
- return { error: resp.error };
629
- }
630
- }
631
- const newValues = {};
632
-
633
- for (const recordField in record) {
634
- if (record[recordField] !== oldRecord[recordField]) {
635
- const column = resource.columns.find((col) => col.name === recordField);
636
- if (column) {
637
- if (!column.virtual) {
638
- newValues[recordField] = connector.setFieldValue(column, record[recordField]);
639
- }
640
- } else {
641
- newValues[recordField] = record[recordField];
642
- }
643
- }
644
- }
645
-
646
- if (Object.keys(newValues).length > 0) {
647
- await connector.updateRecord({ resource, recordId, newValues});
648
- }
649
-
650
- // execute hook if needed
651
- for (const hook of listify(resource.hooks?.edit?.afterSave as AfterSaveFunction[])) {
652
- const resp = await hook({
653
- resource,
654
- record,
655
- adminUser,
656
- oldRecord,
657
- recordId,
658
- });
659
- if (!resp || (!resp.ok && !resp.error)) {
660
- throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
661
- }
662
-
663
- if (resp.error) {
664
- return { error: resp.error };
665
- }
626
+ const { error } = await this.adminforth.updateResourceRecord({ resource, record, adminUser, oldRecord, recordId });
627
+ if (error) {
628
+ return { error };
666
629
  }
667
-
668
630
  return {
669
- newRecordId: recordId
631
+ ok: true
670
632
  }
671
633
  }
672
634
  });
@@ -693,43 +655,12 @@ export default class AdminForthRestAPI {
693
655
  return { error };
694
656
  }
695
657
 
696
- // execute hook if needed
697
- for (const hook of listify(resource.hooks?.delete?.beforeSave as BeforeSaveFunction[])) {
698
- const resp = await hook({
699
- resource,
700
- record,
701
- adminUser,
702
- recordId: body['primaryKey']
703
- });
704
- if (!resp || (!resp.ok && !resp.error)) {
705
- throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
706
- }
707
-
708
- if (resp.error) {
709
- return { error: resp.error };
710
- }
711
- }
712
-
713
- const connector = this.adminforth.connectors[resource.dataSource];
714
- await connector.deleteRecord({ resource, recordId: body['primaryKey']});
715
-
716
- // execute hook if needed
717
- for (const hook of listify(resource.hooks?.delete?.afterSave as BeforeSaveFunction[])) {
718
- const resp = await hook({
719
- resource,
720
- record,
721
- adminUser,
722
- recordId: body['primaryKey']
723
- });
724
- if (!resp || (!resp.ok && !resp.error)) {
725
- throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
726
- }
727
-
728
- if (resp.error) {
729
- return { error: resp.error };
730
- }
658
+ const { error: deleteError } = await this.adminforth.deleteResourceRecord({ resource, record, adminUser, recordId: body['primaryKey'] });
659
+ if (deleteError) {
660
+ return { error: deleteError };
731
661
  }
732
662
  return {
663
+ ok: true,
733
664
  recordId: body['primaryKey']
734
665
  }
735
666
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.3.27",
3
+ "version": "1.3.28",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -145,7 +145,7 @@
145
145
 
146
146
  import CustomDatePicker from "@/components/CustomDatePicker.vue";
147
147
  import Dropdown from '@/components/Dropdown.vue';
148
- import { callAdminForthApi, getCustomComponent } from '@/utils';
148
+ import { applyRegexValidation, callAdminForthApi, getCustomComponent } from '@/utils';
149
149
  import { IconExclamationCircleSolid, IconEyeSlashSolid, IconEyeSolid } from '@iconify-prerendered/vue-flowbite';
150
150
  import { computedAsync } from '@vueuse/core';
151
151
  import { initFlowbite } from 'flowbite';
@@ -172,7 +172,7 @@ const currentValues = ref(null);
172
172
 
173
173
  const customComponentsInValidity = ref({});
174
174
  const customComponentsEmptiness = ref({});
175
-
175
+
176
176
 
177
177
  const columnError = (column) => {
178
178
  const val = computed(() => {
@@ -212,22 +212,12 @@ const columnError = (column) => {
212
212
  return `This field must be less than ${column.maxValue}`;
213
213
  }
214
214
  }
215
- if ( column.validation && column.validation.length ) {
216
- const validationArray = column.validation;
217
- for (let i = 0; i < validationArray.length; i++) {
218
- if (validationArray[i].regExp) {
219
- const regExp = new RegExp(validationArray[i].regExp);
220
- let value = currentValues.value[column.name];
221
- if (value === undefined || value === null) {
222
- value = '';
223
- }
224
- if (!regExp.test(value)) {
225
- return validationArray[i].message;
226
- }
227
- }
228
- }
229
215
 
216
+ const error = applyRegexValidation(currentValues.value[column.name], column.validation);
217
+ if (error) {
218
+ return error;
230
219
  }
220
+
231
221
  return null;
232
222
  });
233
223
  return val.value;
package/spa/src/utils.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { onMounted, ref, resolveComponent } from 'vue';
2
- import type { CoreConfig } from './spa_types/core';
2
+ import type { CoreConfig } from './spa_types/core';
3
+ import type { ValidationObject } from './types/AdminForthConfig';
4
+
3
5
 
4
6
  import router from "./router";
5
7
  import { useCoreStore } from './stores/core';
@@ -113,4 +115,35 @@ export function initThreeDotsDropdown() {
113
115
  dd.hide();
114
116
  }
115
117
  }
118
+ }
119
+
120
+ export function applyRegexValidation(value: any, validation: ValidationObject[] | undefined) {
121
+
122
+ if ( validation?.length ) {
123
+ const validationArray = validation;
124
+ for (let i = 0; i < validationArray.length; i++) {
125
+ if (validationArray[i].regExp) {
126
+ let flags = '';
127
+ if (validationArray[i].caseSensitive) {
128
+ flags += 'i';
129
+ }
130
+ if (validationArray[i].multiline) {
131
+ flags += 'm';
132
+ }
133
+ if (validationArray[i].global) {
134
+ flags += 'g';
135
+ }
136
+
137
+ const regExp = new RegExp(validationArray[i].regExp, flags);
138
+ if (value === undefined || value === null) {
139
+ value = '';
140
+ }
141
+ let valueS = `${value}`;
142
+
143
+ if (!regExp.test(valueS)) {
144
+ return validationArray[i].message;
145
+ }
146
+ }
147
+ }
148
+ }
116
149
  }
@@ -189,14 +189,12 @@ export interface IAdminForthDataSourceConnector {
189
189
  createRecordOriginalValues({ resource, record }: { resource: AdminForthResource, record: any }): Promise<void>;
190
190
 
191
191
  /**
192
- * Used to update record in database.
192
+ * Update record in database. newValues might have not all fields in record, but only changed ones.
193
193
  * recordId is value of field which is marked as {@link AdminForthResourceColumn.primaryKey}
194
- * newValues is array of fields which should be updated (might be not all fields in record, but only changed fields).
195
194
  */
196
- updateRecord({ resource, recordId, newValues }:
197
- { resource: AdminForthResource, recordId: string, newValues: any }
198
- ): Promise<void>;
199
-
195
+ updateRecordOriginalValues({ resource, recordId, newValues }: { resource: AdminForthResource; recordId: string; newValues: any; }): Promise<void>;
196
+
197
+
200
198
  /**
201
199
  * Used to delete record in database.
202
200
  */
@@ -228,12 +226,18 @@ export interface IAdminForthDataSourceConnectorBase extends IAdminForthDataSourc
228
226
  adminUser: AdminUser
229
227
  }): Promise<{ok: boolean, error?: string, createdRecord?: any}>;
230
228
 
229
+ updateRecord({ resource, recordId, newValues }: {
230
+ resource: AdminForthResource,
231
+ recordId: string,
232
+ newValues: any,
233
+ }): Promise<{ok: boolean, error?: string}>;
234
+
231
235
  getMinMaxForColumns({ resource, columns }: { resource: AdminForthResource, columns: AdminForthResourceColumn[] }): Promise<{ [key: string]: { min: any, max: any } }>;
232
236
  }
233
237
 
234
238
 
235
239
  export interface IAdminForthDataSourceConnectorConstructor {
236
- new ({ url }: { url: string }): IAdminForthDataSourceConnector;
240
+ new ({ url }: { url: string }): IAdminForthDataSourceConnectorBase;
237
241
  }
238
242
 
239
243
  export interface IAdminForthAuth {
@@ -266,7 +270,15 @@ export interface IAdminForth {
266
270
 
267
271
  createResourceRecord(
268
272
  params: { resource: AdminForthResource, record: any, adminUser: AdminUser }
269
- ): Promise<{ ok: boolean, error?: string, createdRecord?: any }>;
273
+ ): Promise<{ error?: string, createdRecord?: any }>;
274
+
275
+ updateResourceRecord(
276
+ params: { resource: AdminForthResource, recordId: any, record: any, oldRecord: any, adminUser: AdminUser }
277
+ ): Promise<{ error?: string }>;
278
+
279
+ deleteResourceRecord(
280
+ params: { resource: AdminForthResource, recordId: string, adminUser: AdminUser, record: any }
281
+ ): Promise<{ error?: string }>;
270
282
 
271
283
  auth: IAdminForthAuth;
272
284
 
@@ -701,13 +713,13 @@ export type AfterDataSourceResponseFunction = (params: {resource: AdminForthReso
701
713
  * Modify record to change how data is saved to database.
702
714
  * Return ok: false and error: string to stop execution and show error message to user. Return ok: true to continue execution.
703
715
  */
704
- export type BeforeSaveFunction = (params: {resource: AdminForthResource, recordId: any, adminUser: AdminUser, record: any}) => Promise<{ok: boolean, error?: string}>;
716
+ export type BeforeSaveFunction = (params: {resource: AdminForthResource, recordId: any, adminUser: AdminUser, record: any, oldRecord?: any}) => Promise<{ok: boolean, error?: string}>;
705
717
 
706
718
  /**
707
719
  * Modify record to change how data is saved to database.
708
720
  * Return ok: false and error: string to stop execution and show error message to user. Return ok: true to continue execution.
709
721
  */
710
- export type AfterSaveFunction = (params: {resource: AdminForthResource, recordId: any, adminUser: AdminUser, record: any}) => Promise<{ok: boolean, error?: string}>;
722
+ export type AfterSaveFunction = (params: {resource: AdminForthResource, recordId: any, adminUser: AdminUser, record: any, oldRecord?: any}) => Promise<{ok: boolean, error?: string}>;
711
723
 
712
724
  /**
713
725
  * Allow to get user data before login confirmation, will triger when user try to login.
@@ -1508,6 +1520,21 @@ export type ValidationObject = {
1508
1520
  * Example: "Invalid email format"
1509
1521
  */
1510
1522
  message: string,
1523
+
1524
+ /**
1525
+ * Whether to check case sensitivity (i flag)
1526
+ */
1527
+ caseSensitive: boolean,
1528
+
1529
+ /**
1530
+ * Whether to check Multiline strings (m flag)
1531
+ */
1532
+ multiline: boolean,
1533
+
1534
+ /**
1535
+ * Whether to check global strings (g flag)
1536
+ */
1537
+ global: boolean
1511
1538
  }
1512
1539
 
1513
1540
  export type AdminForthComponentDeclarationFull = {