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.
@@ -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
  }
@@ -54,9 +54,7 @@ export interface AdminpanelConfig {
54
54
  };
55
55
  };
56
56
  /**
57
- * @alpha
58
57
  * Models configuration
59
- * reference upload contoroller ~50 line
60
58
  * */
61
59
  models: {
62
60
  [key: string]: ModelConfig;
@@ -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 the intermediate record associated with the user
355
- // TODO refactor CRUD functions for DataAccessor usage
356
- const intermediateRecord = await intermediateModel["_findOne"]({ [via]: this.user.id });
357
- if (!intermediateRecord) {
358
- throw new Error(`No intermediate record found in model "${intermediateRelation.model}" associated with user ID "${this.user.id}"`);
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
- // only admin can set user access relation manually
406
+ // For non-admins: validate that the chosen intermediate record belongs to the user
416
407
  if (updatedRecord[field] && !this.user.isAdministrator) {
417
- delete updatedRecord[field];
418
- }
419
- // Get attributes of the current model and validate the intermediate relation
420
- const modelAttributes = this.entity.model.attributes;
421
- const intermediateRelation = modelAttributes[field];
422
- if (!intermediateRelation || !intermediateRelation.model) {
423
- throw new Error(`Invalid intermediate relation configuration for field "${field}" in model ${this.entity.model.modelname}`);
424
- }
425
- // Retrieve the intermediate model
426
- const intermediateModel = this.adminizer.modelHandler.model.get(intermediateRelation.model.toLowerCase());
427
- if (!intermediateModel) {
428
- throw new Error(`Intermediate model "${intermediateRelation.model}" not found`);
429
- }
430
- // Validate the `via` field in the intermediate model
431
- const intermediateAttributes = intermediateModel.attributes;
432
- const viaRelation = intermediateAttributes[via];
433
- if (!viaRelation || viaRelation.model.toLowerCase() !== 'userap') {
434
- throw new Error(`Unsupported or invalid via field "${via}" in intermediate model "${intermediateRelation.model}". ` +
435
- `Currently, only relations to "userap" are supported`);
436
- }
437
- // Find an existing intermediate record linking the user to the main record
438
- // TODO refactor CRUD functions for DataAccessor usage
439
- const intermediateRecord = await intermediateModel["_findOne"]({ [via]: this.user.id });
440
- if (!intermediateRecord) {
441
- throw new Error(`No intermediate record found in model "${intermediateRelation.model}" linking user "${this.user.id}" to the main record`);
442
- }
443
- // Ensure there is only one associated intermediate record
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
- assocData[key] = val;
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
- // console.debug(">> Normal fields for create():", plainData);
289
- // console.debug(">> Association data:", assocData);
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
- assocData[key] = val;
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
- assocData[key] = val;
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "adminizer",
3
3
  "type": "module",
4
- "version": "4.6.0-build.203",
4
+ "version": "4.6.0-build.205",
5
5
  "main": "index.js",
6
6
  "exports": {
7
7
  ".": "./index.js",
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
- let modelName = model.model;
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
  }