adminizer 4.6.0-build.203 → 4.6.0-build.205
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/helpers/accessRightsHelper.d.ts +1 -0
- package/helpers/accessRightsHelper.js +7 -0
- package/interfaces/adminpanelConfig.d.ts +0 -2
- package/lib/DataAccessor.js +33 -48
- package/lib/model/ModelHandler.d.ts +9 -0
- package/lib/model/ModelHandler.js +9 -0
- package/lib/model/adapter/sequelize.js +33 -7
- package/package.json +1 -1
- package/system/Router.js +1 -1
- package/system/bindAccessRights.js +1 -22
|
@@ -7,6 +7,7 @@ export declare class AccessRightsHelper {
|
|
|
7
7
|
constructor(adminizer: Adminizer);
|
|
8
8
|
registerToken(accessRightsToken: AccessRightsToken): void;
|
|
9
9
|
registerTokens(accessRightsTokens: AccessRightsToken[]): void;
|
|
10
|
+
registerModelTokens(modelName: string, department?: string): void;
|
|
10
11
|
getTokens(): AccessRightsToken[];
|
|
11
12
|
getTokensByDepartment(department: string): AccessRightsToken[];
|
|
12
13
|
getAllDepartments(): string[];
|
|
@@ -23,6 +23,13 @@ export class AccessRightsHelper {
|
|
|
23
23
|
this.registerToken(token);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
registerModelTokens(modelName, department) {
|
|
27
|
+
department = department ?? `Model ${modelName}`;
|
|
28
|
+
this.registerToken({ id: `create-${modelName}-model`, name: "Create", description: "Access to creating record in database", department });
|
|
29
|
+
this.registerToken({ id: `read-${modelName}-model`, name: "Read", description: "Access to reading records in database", department });
|
|
30
|
+
this.registerToken({ id: `update-${modelName}-model`, name: "Update", description: "Access to updating records in database", department });
|
|
31
|
+
this.registerToken({ id: `delete-${modelName}-model`, name: "Delete", description: "Access to deleting records in database", department });
|
|
32
|
+
}
|
|
26
33
|
getTokens() {
|
|
27
34
|
return this._tokens;
|
|
28
35
|
}
|
package/lib/DataAccessor.js
CHANGED
|
@@ -351,20 +351,11 @@ export class DataAccessor {
|
|
|
351
351
|
throw new Error(`Unsupported or invalid via field "${via}" in intermediate model "${intermediateRelation.model}". ` +
|
|
352
352
|
`Currently, only relations to "userap" are supported`);
|
|
353
353
|
}
|
|
354
|
-
// Fetch
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
// Ensure there is only one associated intermediate record
|
|
361
|
-
const intermediateRecordCount = await intermediateModel.count({ [via]: this.user.id }, this);
|
|
362
|
-
if (intermediateRecordCount > 1) {
|
|
363
|
-
throw new Error(`Multiple intermediate records found in model "${intermediateRelation.model}" associated with user ID "${this.user.id}". ` +
|
|
364
|
-
`Expected only one`);
|
|
365
|
-
}
|
|
366
|
-
// Add the intermediate record ID to the criteria
|
|
367
|
-
sanitizedCriteria = { ...sanitizedCriteria, [field]: intermediateRecord.id };
|
|
354
|
+
// Fetch all intermediate records associated with the user
|
|
355
|
+
const intermediateRecords = await intermediateModel["_find"]({ [via]: this.user.id });
|
|
356
|
+
const intermediateIds = (intermediateRecords || []).map((r) => r.id);
|
|
357
|
+
// Filter main model by all matching intermediate record IDs
|
|
358
|
+
sanitizedCriteria = { ...sanitizedCriteria, [field]: { in: intermediateIds } };
|
|
368
359
|
}
|
|
369
360
|
}
|
|
370
361
|
// TODO fix types when deleting waterline (temporary decision here)
|
|
@@ -412,42 +403,36 @@ export class DataAccessor {
|
|
|
412
403
|
else if (typeof userAccessRelation === 'object' && userAccessRelation !== null) {
|
|
413
404
|
// If userAccessRelation is an object
|
|
414
405
|
const { field, via } = userAccessRelation;
|
|
415
|
-
//
|
|
406
|
+
// For non-admins: validate that the chosen intermediate record belongs to the user
|
|
416
407
|
if (updatedRecord[field] && !this.user.isAdministrator) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const intermediateRecordCount = await intermediateModel.count({ [via]: this.user.id }, this);
|
|
445
|
-
if (intermediateRecordCount > 1) {
|
|
446
|
-
throw new Error(`Multiple intermediate records found in model "${intermediateRelation.model}" associated with user ID "${this.user.id}". ` +
|
|
447
|
-
`Expected only one`);
|
|
408
|
+
if (!via) {
|
|
409
|
+
throw new Error(`Invalid userAccessRelation configuration: "via" is required`);
|
|
410
|
+
}
|
|
411
|
+
const modelAttributes = this.entity.model.attributes;
|
|
412
|
+
const intermediateRelation = modelAttributes[field];
|
|
413
|
+
if (!intermediateRelation || !intermediateRelation.model) {
|
|
414
|
+
throw new Error(`Invalid intermediate relation configuration for field "${field}" in model ${this.entity.model.modelname}`);
|
|
415
|
+
}
|
|
416
|
+
const intermediateModel = this.adminizer.modelHandler.model.get(intermediateRelation.model.toLowerCase());
|
|
417
|
+
if (!intermediateModel) {
|
|
418
|
+
throw new Error(`Intermediate model "${intermediateRelation.model}" not found`);
|
|
419
|
+
}
|
|
420
|
+
// Validate the `via` field in the intermediate model
|
|
421
|
+
const intermediateAttributes = intermediateModel.attributes;
|
|
422
|
+
const viaRelation = intermediateAttributes[via];
|
|
423
|
+
if (!viaRelation || !viaRelation.model || viaRelation.model.toLowerCase() !== 'userap') {
|
|
424
|
+
throw new Error(`Unsupported or invalid via field "${via}" in intermediate model "${intermediateRelation.model}". ` +
|
|
425
|
+
`Currently, only relations to "userap" are supported`);
|
|
426
|
+
}
|
|
427
|
+
const intermediatePk = (intermediateModel.primaryKey ?? 'id');
|
|
428
|
+
const chosenId = typeof updatedRecord[field] === 'object'
|
|
429
|
+
? updatedRecord[field][intermediatePk]
|
|
430
|
+
: updatedRecord[field];
|
|
431
|
+
const record = await intermediateModel["_findOne"]({ [intermediatePk]: chosenId, [via]: this.user.id });
|
|
432
|
+
if (!record) {
|
|
433
|
+
throw new Error(`Access denied: "${field}" does not belong to the current user`);
|
|
434
|
+
}
|
|
448
435
|
}
|
|
449
|
-
// Set the ID of the intermediate record to the main record's field
|
|
450
|
-
updatedRecord[field] = intermediateRecord.id;
|
|
451
436
|
}
|
|
452
437
|
}
|
|
453
438
|
return updatedRecord;
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { AbstractModel } from "./AbstractModel";
|
|
2
|
+
/**
|
|
3
|
+
* Central registry (service locator) for all AbstractModel instances in the application.
|
|
4
|
+
* A single instance is created in Adminizer and shared across the entire app via `req.adminizer.modelHandler`.
|
|
5
|
+
*
|
|
6
|
+
* Models are registered at startup via `bindModels`, then looked up by name from controllers,
|
|
7
|
+
* services, helpers, and system utilities — without importing each model directly.
|
|
8
|
+
*
|
|
9
|
+
* Lookup is case-insensitive: `"UserAP"`, `"userap"`, and `"USERAP"` all resolve to the same model.
|
|
10
|
+
*/
|
|
2
11
|
export declare class ModelHandler {
|
|
3
12
|
private models;
|
|
4
13
|
add<T>(modelName: string, modelInstance: AbstractModel<T>): void;
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { Adminizer } from "../Adminizer.js";
|
|
2
|
+
/**
|
|
3
|
+
* Central registry (service locator) for all AbstractModel instances in the application.
|
|
4
|
+
* A single instance is created in Adminizer and shared across the entire app via `req.adminizer.modelHandler`.
|
|
5
|
+
*
|
|
6
|
+
* Models are registered at startup via `bindModels`, then looked up by name from controllers,
|
|
7
|
+
* services, helpers, and system utilities — without importing each model directly.
|
|
8
|
+
*
|
|
9
|
+
* Lookup is case-insensitive: `"UserAP"`, `"userap"`, and `"USERAP"` all resolve to the same model.
|
|
10
|
+
*/
|
|
2
11
|
export class ModelHandler {
|
|
3
12
|
models = new Map();
|
|
4
13
|
add(modelName, modelInstance) {
|
|
@@ -275,18 +275,26 @@ export class SequelizeModel extends AbstractModel {
|
|
|
275
275
|
const assocNames = Object.keys(this.model.associations);
|
|
276
276
|
const plainData = {};
|
|
277
277
|
const assocData = {};
|
|
278
|
-
// console.debug(">> _create: input data:", data);
|
|
279
|
-
// console.debug(">> Available associations:", assocNames);
|
|
280
278
|
for (const [key, val] of Object.entries(data)) {
|
|
281
279
|
if (assocNames.includes(key)) {
|
|
282
|
-
|
|
280
|
+
const assoc = this.model.associations[key];
|
|
281
|
+
// For BelongsTo, set the FK directly in plainData so NOT NULL constraints are satisfied
|
|
282
|
+
if (assoc && assoc.associationType === 'BelongsTo') {
|
|
283
|
+
const fk = assoc.foreignKey;
|
|
284
|
+
if (val !== null && val !== undefined) {
|
|
285
|
+
plainData[fk] = typeof val === 'object' ? val[assoc.target.primaryKeyAttribute] : val;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
assocData[key] = val;
|
|
290
|
+
}
|
|
283
291
|
}
|
|
284
292
|
else {
|
|
285
293
|
plainData[key] = val;
|
|
286
294
|
}
|
|
287
295
|
}
|
|
288
|
-
|
|
289
|
-
|
|
296
|
+
console.debug(">> Normal fields for create():", JSON.stringify(plainData));
|
|
297
|
+
console.debug(">> Association data:", JSON.stringify(assocData));
|
|
290
298
|
let instance;
|
|
291
299
|
try {
|
|
292
300
|
instance = await this.model.create(plainData);
|
|
@@ -415,7 +423,16 @@ export class SequelizeModel extends AbstractModel {
|
|
|
415
423
|
const assocData = {};
|
|
416
424
|
for (const [key, val] of Object.entries(data)) {
|
|
417
425
|
if (assocNames.includes(key)) {
|
|
418
|
-
|
|
426
|
+
const assoc = this.model.associations[key];
|
|
427
|
+
if (assoc && assoc.associationType === 'BelongsTo') {
|
|
428
|
+
const fk = assoc.foreignKey;
|
|
429
|
+
if (val !== null && val !== undefined) {
|
|
430
|
+
plainData[fk] = typeof val === 'object' ? val[assoc.target.primaryKeyAttribute] : val;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
assocData[key] = val;
|
|
435
|
+
}
|
|
419
436
|
}
|
|
420
437
|
else {
|
|
421
438
|
plainData[key] = val;
|
|
@@ -434,7 +451,16 @@ export class SequelizeModel extends AbstractModel {
|
|
|
434
451
|
const assocData = {};
|
|
435
452
|
for (const [key, val] of Object.entries(data)) {
|
|
436
453
|
if (assocNames.includes(key)) {
|
|
437
|
-
|
|
454
|
+
const assoc = this.model.associations[key];
|
|
455
|
+
if (assoc && assoc.associationType === 'BelongsTo') {
|
|
456
|
+
const fk = assoc.foreignKey;
|
|
457
|
+
if (val !== null && val !== undefined) {
|
|
458
|
+
plainData[fk] = typeof val === 'object' ? val[assoc.target.primaryKeyAttribute] : val;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
assocData[key] = val;
|
|
463
|
+
}
|
|
438
464
|
}
|
|
439
465
|
else {
|
|
440
466
|
plainData[key] = val;
|
package/package.json
CHANGED
package/system/Router.js
CHANGED
|
@@ -194,7 +194,7 @@ export default class Router {
|
|
|
194
194
|
register(`${prefix}/edit/:id`, _edit);
|
|
195
195
|
register(`${prefix}/remove/:id`, _remove);
|
|
196
196
|
}
|
|
197
|
-
else if (typeof modelConfig !== "boolean") {
|
|
197
|
+
else if (modelConfig !== undefined && typeof modelConfig !== "boolean") {
|
|
198
198
|
Adminizer.log.debug(`Adminpanel create CRUD routes for \`${model}\` by ModelConfig`);
|
|
199
199
|
/**
|
|
200
200
|
* Create new record
|
|
@@ -4,28 +4,7 @@ export default async function bindAccessRights(adminizer) {
|
|
|
4
4
|
for (let key of Object.keys(models)) {
|
|
5
5
|
const model = models[key];
|
|
6
6
|
if (typeof model !== "boolean") {
|
|
7
|
-
|
|
8
|
-
let department = `Model ${key}`;
|
|
9
|
-
// create
|
|
10
|
-
adminizer.accessRightsHelper.registerToken({
|
|
11
|
-
id: `create-${modelName}-model`, name: "Create",
|
|
12
|
-
description: "Access to creating record in database", department: department
|
|
13
|
-
});
|
|
14
|
-
// read
|
|
15
|
-
adminizer.accessRightsHelper.registerToken({
|
|
16
|
-
id: `read-${modelName}-model`, name: "Read",
|
|
17
|
-
description: "Access to reading records in database", department: department
|
|
18
|
-
});
|
|
19
|
-
// update
|
|
20
|
-
adminizer.accessRightsHelper.registerToken({
|
|
21
|
-
id: `update-${modelName}-model`, name: "Update",
|
|
22
|
-
description: "Access to updating records in database", department: department
|
|
23
|
-
});
|
|
24
|
-
// delete
|
|
25
|
-
adminizer.accessRightsHelper.registerToken({
|
|
26
|
-
id: `delete-${modelName}-model`, name: "Delete",
|
|
27
|
-
description: "Access to deleting records in database", department: department
|
|
28
|
-
});
|
|
7
|
+
adminizer.accessRightsHelper.registerModelTokens(model.model);
|
|
29
8
|
}
|
|
30
9
|
}
|
|
31
10
|
}
|