adminizer 5.0.0-build.12 → 5.0.0-build.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/assets/{add-BhTWhSNx.js → add-B_hn__Ba.js} +1 -1
  2. package/assets/{add-group-BEOEkMGK.js → add-group-0SdYgiXZ.js} +1 -1
  3. package/assets/{add-user-MW-fSILU.js → add-user-Bh1iqSem.js} +1 -1
  4. package/assets/ai-assistant/agent.es.js +3948 -3889
  5. package/assets/app.js +32 -32
  6. package/assets/{catalog-D-lFVV9U.js → catalog-DjK8r0Mn.js} +1 -1
  7. package/assets/controls/handsontable.es.js +11637 -11632
  8. package/assets/controls/jsoneditor.es.js +6308 -6297
  9. package/assets/controls/toast-ui.es.js +18 -17
  10. package/assets/{dashboard-Cmj4Df_C.js → dashboard-CbJIK7ka.js} +1 -1
  11. package/assets/{history-BXfeZHTD.js → history-CZAJgVXP.js} +1 -1
  12. package/assets/{list-Dp8F_hor.js → list-C6w_xDK2.js} +1 -1
  13. package/assets/manifest.json +24 -24
  14. package/assets/{module-Q-1AzZH9.js → module-DvPjKyF_.js} +1 -1
  15. package/assets/{notification-CSeMEsPn.js → notification-N_b-N0Zu.js} +1 -1
  16. package/assets/{user-filters-list-Do6Dis5D.js → user-filters-list-xbJoY_5_.js} +1 -1
  17. package/assets/{welcome-D3dg746Y.js → welcome-BUkdkKa5.js} +1 -1
  18. package/assets/{with-app-layout-Bjo8B9zO.js → with-app-layout-WhRyO5WK.js} +1 -1
  19. package/controllers/view.js +1 -1
  20. package/helpers/configHelper.js +12 -17
  21. package/helpers/controllerHelper.d.ts +1 -0
  22. package/helpers/controllerHelper.js +25 -15
  23. package/helpers/fieldsHelper.js +3 -3
  24. package/helpers/inertiaAddHelper.js +1 -1
  25. package/helpers/modelResourceHelper.d.ts +1 -1
  26. package/helpers/modelResourceHelper.js +8 -4
  27. package/interfaces/adminpanelConfig.d.ts +6 -1
  28. package/lib/DataAccessor.d.ts +1 -0
  29. package/lib/DataAccessor.js +45 -47
  30. package/lib/filters/FilterService.js +4 -3
  31. package/lib/history-actions/AbstractHistoryAdapter.js +11 -9
  32. package/lib/model/AbstractModel.d.ts +2 -0
  33. package/lib/model/ModelHandler.d.ts +47 -2
  34. package/lib/model/ModelHandler.js +107 -8
  35. package/package.json +1 -1
  36. package/system/bindAccessRights.js +2 -3
  37. package/system/buildInternalModelAccess.js +5 -2
  38. package/system/validateSystemModels.js +29 -9
@@ -89,19 +89,27 @@ export class ControllerHelper {
89
89
  * @returns {?string}
90
90
  */
91
91
  static findModelResourceName(req) {
92
- if (req.params.modelResourceName) {
93
- return req.params.modelResourceName;
92
+ const models = req.adminizer.config.models;
93
+ const requestedName = req.params.modelResourceName
94
+ ?? req.params.model
95
+ ?? req.originalUrl.split('/')[3];
96
+ return this.resolveConfiguredResourceName(models, requestedName);
97
+ }
98
+ static resolveConfiguredResourceName(models, requestedName) {
99
+ if (!models || !requestedName) {
100
+ throw new Error(`Model "${requestedName}" not found`);
94
101
  }
95
- if (req.params.model) {
96
- return req.params.model;
102
+ if (Object.prototype.hasOwnProperty.call(models, requestedName)) {
103
+ return requestedName;
97
104
  }
98
- const urlParts = req.originalUrl.split('/');
99
- const modelResourceName = urlParts[3];
100
- const models = req.adminizer.config.models;
101
- if (!models || !Object.keys(models).some(key => key.toLowerCase() === modelResourceName.toLowerCase())) {
102
- throw new Error(`Model "${modelResourceName}" not found`);
105
+ const candidates = Object.keys(models).filter((key) => key.toLowerCase() === requestedName.toLowerCase());
106
+ if (candidates.length === 1) {
107
+ return candidates[0];
108
+ }
109
+ if (candidates.length > 1) {
110
+ throw new Error(`Model resource "${requestedName}" is ambiguous: ${candidates.join(", ")}`);
103
111
  }
104
- return Object.keys(models).find(key => key.toLowerCase() === modelResourceName.toLowerCase());
112
+ throw new Error(`Model "${requestedName}" not found`);
105
113
  }
106
114
  /**
107
115
  * Searches for config from admin panel
@@ -116,12 +124,14 @@ export class ControllerHelper {
116
124
  Adminizer.log.error('No models configuration found');
117
125
  return null;
118
126
  }
119
- const foundKey = Object.keys(models).find(key => key.toLowerCase() === modelResourceName.toLowerCase());
120
- if (!foundKey) {
121
- Adminizer.log.error(`No such route exists: ${modelResourceName}`);
127
+ try {
128
+ const foundKey = this.resolveConfiguredResourceName(models, modelResourceName);
129
+ return this._normalizeModelConfig(foundKey, models[foundKey]);
130
+ }
131
+ catch (error) {
132
+ Adminizer.log.error(error);
122
133
  return null;
123
134
  }
124
- return this._normalizeModelConfig(foundKey, models[foundKey]);
125
135
  }
126
136
  /**
127
137
  * Will get action config from configuration file depending to given action
@@ -197,7 +207,7 @@ export class ControllerHelper {
197
207
  modelResource.config = this.findModelConfig(req, modelResourceName);
198
208
  // Find and add the model itself to the ModelResource
199
209
  if (this._isValidModelConfig(modelResource.config)) {
200
- modelResource.model = req.adminizer.modelHandler.model.get(modelResource.config.model);
210
+ modelResource.model = req.adminizer.modelHandler.getResource(modelResourceName);
201
211
  }
202
212
  // Return the completed ModelResource object
203
213
  return modelResource;
@@ -30,9 +30,9 @@ export class FieldsHelper {
30
30
  Adminizer.log.error('No model found for field: ', fields[key]);
31
31
  return;
32
32
  }
33
- const resolvedModelName = req.adminizer.modelHandler.resolveModelName(modelName);
34
- let Model = req.adminizer.modelHandler.model.get(resolvedModelName);
35
- if (!Model) {
33
+ const resolvedModelName = req.adminizer.modelHandler.resolveAssociationResource(modelName, fields[key].model.resourceName);
34
+ let Model = resolvedModelName ? req.adminizer.modelHandler.getResource(resolvedModelName) : undefined;
35
+ if (!Model || !resolvedModelName) {
36
36
  return;
37
37
  }
38
38
  let list;
@@ -62,7 +62,7 @@ export default function inertiaAddHelper(req, modelResource, fields, record, vie
62
62
  let canCreateRelated = false;
63
63
  const controlContext = `${modelResource.name}.${key}`;
64
64
  //@ts-ignore TODO: fix field type
65
- if (modelResource.config.model && req.adminizer.configHelper.isId(field, modelResource.config.model)) {
65
+ if (req.adminizer.configHelper.isId(field, modelResource.name)) {
66
66
  disabled = true;
67
67
  }
68
68
  if (['string', 'password', 'date', 'datetime', 'time', 'integer', 'number', 'float', 'email', 'month', 'week', 'range'].includes(type)) {
@@ -5,5 +5,5 @@ import type { Adminizer } from '../lib/Adminizer.js';
5
5
  export declare function normalizeModelConfig(name: string, config: ModelConfig | boolean): ModelConfig;
6
6
  /** Every configured model resource whose model is actually registered. */
7
7
  export declare function listModelResources(adminizer: Adminizer): ModelResource[];
8
- /** Resolves a resource by its config key or by the underlying model name. */
8
+ /** Resolves a resource by its Adminizer name, then by a uniquely registered host model name. */
9
9
  export declare function resolveModelResource(adminizer: Adminizer, modelName: string): ModelResource | undefined;
@@ -20,7 +20,7 @@ export function listModelResources(adminizer) {
20
20
  const resources = [];
21
21
  for (const [configName, configValue] of Object.entries(adminizer.config.models ?? {})) {
22
22
  const config = normalizeModelConfig(configName, configValue);
23
- const model = adminizer.modelHandler.model.get(config.model.toLowerCase());
23
+ const model = adminizer.modelHandler.getResource(configName);
24
24
  if (!model)
25
25
  continue;
26
26
  resources.push({
@@ -32,8 +32,12 @@ export function listModelResources(adminizer) {
32
32
  }
33
33
  return resources;
34
34
  }
35
- /** Resolves a resource by its config key or by the underlying model name. */
35
+ /** Resolves a resource by its Adminizer name, then by a uniquely registered host model name. */
36
36
  export function resolveModelResource(adminizer, modelName) {
37
- const loweredName = modelName.trim().toLowerCase();
38
- return listModelResources(adminizer).find((resource) => resource.name.toLowerCase() === loweredName || resource.config?.model?.toLowerCase() === loweredName);
37
+ const requestedName = modelName.trim();
38
+ const resourceName = adminizer.modelHandler.getResourceRecord(requestedName)?.name
39
+ ?? adminizer.modelHandler.resolveResourceByHostModel(requestedName);
40
+ return resourceName
41
+ ? listModelResources(adminizer).find((resource) => resource.name === resourceName)
42
+ : undefined;
39
43
  }
@@ -299,9 +299,14 @@ export interface ModelConfig {
299
299
  adapter?: string;
300
300
  title: string;
301
301
  /**
302
- * Model name
302
+ * Physical host ORM model name.
303
303
  * */
304
304
  model: string;
305
+ /**
306
+ * Required only when two or more Adminizer resources use the same host model.
307
+ * The primary resource is selected for associations that reference the host model.
308
+ */
309
+ primary?: boolean;
305
310
  /**
306
311
  * Optional display name for history. Should be a attribute of the model, or a function.
307
312
  */
@@ -18,6 +18,7 @@ export declare class DataAccessor {
18
18
  private getModelConfig;
19
19
  private accessRelationModel;
20
20
  private resolveRelationTarget;
21
+ private getRelationModel;
21
22
  /**
22
23
  * Retrieves the fields for the given ModelResource based on action type,
23
24
  * taking into account access rights and configuration settings.
@@ -46,29 +46,28 @@ export class DataAccessor {
46
46
  this.action = action;
47
47
  this.actionVerb = getTokenAction(this.action);
48
48
  }
49
- getModelConfig(modelName) {
50
- const resolvedModelName = this.adminizer.modelHandler.resolveModelName(modelName);
51
- const entry = Object.entries(this.adminizer.config.models)
52
- .find(([key, config]) => {
53
- if (key.toLowerCase() === resolvedModelName.toLowerCase()) {
54
- return true;
55
- }
56
- if (!isObject(config)) {
57
- return false;
58
- }
59
- return config.model?.toLowerCase() === resolvedModelName.toLowerCase();
60
- });
61
- const config = entry ? entry[1] : undefined;
49
+ getModelConfig(resourceName) {
50
+ const config = this.adminizer.config.models?.[resourceName];
62
51
  return isObject(config) ? config : undefined;
63
52
  }
64
- accessRelationModel(modelName) {
65
- return this.adminizer.modelHandler.internal("data-accessor").get(modelName);
53
+ accessRelationModel(modelName, resourceName) {
54
+ const resolvedResourceName = this.resolveRelationTarget(modelName, resourceName);
55
+ if (!resolvedResourceName) {
56
+ throw new Error(`Relation model "${modelName}" was not found`);
57
+ }
58
+ return this.adminizer.modelHandler.internal("data-accessor").get(resolvedResourceName);
66
59
  }
67
- resolveRelationTarget(modelName) {
60
+ resolveRelationTarget(modelName, resourceName) {
68
61
  if (!modelName) {
69
62
  return undefined;
70
63
  }
71
- return this.adminizer.modelHandler.resolveModelName(modelName).toLowerCase();
64
+ return this.adminizer.modelHandler.resolveAssociationResource(modelName, resourceName);
65
+ }
66
+ getRelationModel(modelName, resourceName) {
67
+ const resolvedResourceName = this.resolveRelationTarget(modelName, resourceName);
68
+ return resolvedResourceName
69
+ ? this.adminizer.modelHandler.getResource(resolvedResourceName)
70
+ : undefined;
72
71
  }
73
72
  /**
74
73
  * Retrieves the fields for the given ModelResource based on action type,
@@ -86,9 +85,9 @@ export class DataAccessor {
86
85
  const actionConfig = ControllerHelper.findActionConfig(this.modelResource, this.action);
87
86
  const fieldsConfig = this.modelResource.config?.fields || {};
88
87
  const modelAttributes = this.modelResource.model.attributes;
89
- const tokenId = `${this.actionVerb}-${this.modelResource.model.modelname}-${MODEL_TOKEN_SUFFIX}`;
88
+ const tokenId = `${this.actionVerb}-${this.modelResource.name}-${MODEL_TOKEN_SUFFIX}`;
90
89
  if (!this.adminizer.accessRightsHelper.hasPermission(tokenId, this.user)) {
91
- Adminizer.log.debug(`getFieldsConfig > No access rights to ${this.actionVerb} model: ${this.modelResource.model.modelname}`);
90
+ Adminizer.log.debug(`getFieldsConfig > No access rights to ${this.actionVerb} model: ${this.modelResource.name}`);
92
91
  return undefined;
93
92
  }
94
93
  const result = {};
@@ -131,14 +130,14 @@ export class DataAccessor {
131
130
  let populatedModelFieldsConfig = {};
132
131
  if (modelField.type === "association" || modelField.type === "association-many") {
133
132
  const modelName = modelField.model || modelField.collection;
134
- const resolvedModelName = modelName ? this.adminizer.modelHandler.resolveModelName(modelName) : undefined;
133
+ const resolvedModelName = this.resolveRelationTarget(modelName, modelField.resourceName);
135
134
  if (modelName && resolvedModelName) {
136
135
  const tokenId = `read-${resolvedModelName}-${MODEL_TOKEN_SUFFIX}`;
137
136
  if (!this.adminizer.accessRightsHelper.hasPermission(tokenId, this.user)) {
138
- Adminizer.log.silly(`No access rights to model: ${this.modelResource.model.modelname}`);
137
+ Adminizer.log.silly(`No access rights to model: ${this.modelResource.name}`);
139
138
  return undefined;
140
139
  }
141
- const model = this.adminizer.modelHandler.model.get(modelName);
140
+ const model = this.adminizer.modelHandler.getResource(resolvedModelName);
142
141
  if (model) {
143
142
  populatedModelFieldsConfig = this.getAssociatedFieldsConfig(resolvedModelName);
144
143
  const modelCfg = this.getModelConfig(resolvedModelName);
@@ -186,17 +185,16 @@ export class DataAccessor {
186
185
  Object.keys(fields).forEach(appendField);
187
186
  return orderedFields;
188
187
  }
189
- getAssociatedFieldsConfig(modelName) {
190
- const resolvedModelName = this.adminizer.modelHandler.resolveModelName(modelName);
191
- const model = this.adminizer.modelHandler.model.get(resolvedModelName);
192
- const modelConfig = this.getModelConfig(resolvedModelName);
188
+ getAssociatedFieldsConfig(resourceName) {
189
+ const model = this.adminizer.modelHandler.getResource(resourceName);
190
+ const modelConfig = this.getModelConfig(resourceName);
193
191
  if (!model || !modelConfig) {
194
192
  return undefined;
195
193
  }
196
194
  // Check if user has access to the associated model
197
- const tokenId = `read-${resolvedModelName}-${MODEL_TOKEN_SUFFIX}`;
195
+ const tokenId = `read-${resourceName}-${MODEL_TOKEN_SUFFIX}`;
198
196
  if (!this.adminizer.accessRightsHelper.hasPermission(tokenId, this.user)) {
199
- Adminizer.log.debug(`getAssociatedFieldsConfig > No access rights to ${this.actionVerb} model: ${resolvedModelName}`);
197
+ Adminizer.log.debug(`getAssociatedFieldsConfig > No access rights to ${this.actionVerb} model: ${resourceName}`);
200
198
  return undefined;
201
199
  }
202
200
  const associatedFields = {};
@@ -379,7 +377,7 @@ export class DataAccessor {
379
377
  async sanitizeUserRelationAccess(criteria = {}) {
380
378
  let sanitizedCriteria = {};
381
379
  // Retrieve model configuration from adminpanel config
382
- const modelName = this.modelResource.model.modelname;
380
+ const modelName = this.modelResource.name;
383
381
  const modelConfig = this.modelResource.config;
384
382
  // Check if the model has `userAccessRelation` configured
385
383
  if (!this.user.isAdministrator && modelConfig && modelConfig.userAccessRelation) {
@@ -392,18 +390,18 @@ export class DataAccessor {
392
390
  // Check if the relation points to `User` or `Group` in the model's attributes
393
391
  const modelAttributes = this.modelResource.model.attributes;
394
392
  const relation = modelAttributes[accessField];
395
- const relationModel = this.resolveRelationTarget(relation?.model);
396
- const relationCollection = this.resolveRelationTarget(relation?.collection);
397
- if (!relation || !['user', 'group'].includes(relationModel ?? relationCollection ?? "")) {
393
+ const relationModel = this.resolveRelationTarget(relation?.model, relation?.resourceName);
394
+ const relationCollection = this.resolveRelationTarget(relation?.collection, relation?.resourceName);
395
+ if (!relation || !['User', 'Group'].includes(relationModel ?? relationCollection ?? "")) {
398
396
  throw new Error(`Invalid userAccessRelation configuration for model ${modelName}`);
399
397
  }
400
398
  // Determine if the current user matches the access criteria
401
399
  if (relationModel) {
402
- if (relationModel === 'user') {
400
+ if (relationModel === 'User') {
403
401
  // Filter by the user's ID if related to User as a model
404
402
  sanitizedCriteria = { ...sanitizedCriteria, [accessField]: this.user.id };
405
403
  }
406
- else if (relationModel === 'group') {
404
+ else if (relationModel === 'Group') {
407
405
  // Filter by user's group membership if related to Group as a model
408
406
  const userGroups = this.user.groups?.map((group) => group.id);
409
407
  sanitizedCriteria = { ...sanitizedCriteria, [accessField]: { in: userGroups } };
@@ -412,11 +410,11 @@ export class DataAccessor {
412
410
  /** Warning: collection relation access is not supported and needs adapter-level processing */
413
411
  if (relationCollection) {
414
412
  Adminizer.log.warn(`Collection relation is not supported and was not tested. You may have an error here: ${JSON.stringify(relation, null, 2)}`);
415
- if (relationCollection === 'user') {
413
+ if (relationCollection === 'User') {
416
414
  // Ensure user's ID is part of the associated collection to User
417
415
  sanitizedCriteria = { ...sanitizedCriteria, [accessField]: { contains: this.user.id } };
418
416
  }
419
- else if (relationCollection === 'group') {
417
+ else if (relationCollection === 'Group') {
420
418
  // Ensure user's groups intersect with the collection to Group
421
419
  const userGroups = this.user.groups?.map((group) => group.id);
422
420
  sanitizedCriteria = { ...sanitizedCriteria, [accessField]: { intersects: userGroups } };
@@ -433,19 +431,19 @@ export class DataAccessor {
433
431
  throw new Error(`Invalid intermediate relation configuration for field "${field}" in model ${modelName}`);
434
432
  }
435
433
  // Retrieve the intermediate model
436
- const intermediateModel = this.adminizer.modelHandler.model.get(intermediateRelation.model.toLowerCase());
434
+ const intermediateModel = this.getRelationModel(intermediateRelation.model, intermediateRelation.resourceName);
437
435
  if (!intermediateModel) {
438
436
  throw new Error(`Intermediate model "${intermediateRelation.model}" not found`);
439
437
  }
440
438
  // Validate the `via` field in the intermediate model
441
439
  const intermediateAttributes = intermediateModel.attributes;
442
440
  const viaRelation = intermediateAttributes[via];
443
- if (!viaRelation || this.resolveRelationTarget(viaRelation.model) !== 'user') {
441
+ if (!viaRelation || this.resolveRelationTarget(viaRelation.model, viaRelation.resourceName) !== 'User') {
444
442
  throw new Error(`Unsupported or invalid via field "${via}" in intermediate model "${intermediateRelation.model}". ` +
445
443
  `Currently, only relations to "User" are supported`);
446
444
  }
447
445
  // Fetch all intermediate records associated with the user
448
- const intermediateRecords = await this.accessRelationModel(intermediateRelation.model).find({ where: { [via]: this.user.id } });
446
+ const intermediateRecords = await this.accessRelationModel(intermediateRelation.model, intermediateRelation.resourceName).find({ where: { [via]: this.user.id } });
449
447
  const intermediateIds = (intermediateRecords || []).map((r) => r.id);
450
448
  // Filter main model by all matching intermediate record IDs
451
449
  sanitizedCriteria = { ...sanitizedCriteria, [field]: { in: intermediateIds } };
@@ -471,14 +469,14 @@ export class DataAccessor {
471
469
  // Check if the relation points to `User` or `Group` in the model's attributes
472
470
  const modelAttributes = this.modelResource.model.attributes;
473
471
  const relation = modelAttributes[accessField];
474
- const relationModel = this.resolveRelationTarget(relation?.model);
475
- if (relation && ['user', 'group'].includes(relationModel ?? "")) {
476
- if (relationModel === 'user') {
472
+ const relationModel = this.resolveRelationTarget(relation?.model, relation?.resourceName);
473
+ if (relation && ['User', 'Group'].includes(relationModel ?? "")) {
474
+ if (relationModel === 'User') {
477
475
  if (!this.user.isAdministrator) {
478
476
  updatedRecord[accessField] = this.user.id;
479
477
  }
480
478
  }
481
- else if (relationModel === 'group') {
479
+ else if (relationModel === 'Group') {
482
480
  const userGroups = this.user.groups || [];
483
481
  if (userGroups.length === 1) {
484
482
  updatedRecord[accessField] = userGroups[0].id;
@@ -500,16 +498,16 @@ export class DataAccessor {
500
498
  const modelAttributes = this.modelResource.model.attributes;
501
499
  const intermediateRelation = modelAttributes[field];
502
500
  if (!intermediateRelation || !intermediateRelation.model) {
503
- throw new Error(`Invalid intermediate relation configuration for field "${field}" in model ${this.modelResource.model.modelname}`);
501
+ throw new Error(`Invalid intermediate relation configuration for field "${field}" in model ${this.modelResource.name}`);
504
502
  }
505
- const intermediateModel = this.adminizer.modelHandler.model.get(intermediateRelation.model.toLowerCase());
503
+ const intermediateModel = this.getRelationModel(intermediateRelation.model, intermediateRelation.resourceName);
506
504
  if (!intermediateModel) {
507
505
  throw new Error(`Intermediate model "${intermediateRelation.model}" not found`);
508
506
  }
509
507
  // Validate the `via` field in the intermediate model
510
508
  const intermediateAttributes = intermediateModel.attributes;
511
509
  const viaRelation = intermediateAttributes[via];
512
- if (!viaRelation || this.resolveRelationTarget(viaRelation.model) !== 'user') {
510
+ if (!viaRelation || this.resolveRelationTarget(viaRelation.model, viaRelation.resourceName) !== 'User') {
513
511
  throw new Error(`Unsupported or invalid via field "${via}" in intermediate model "${intermediateRelation.model}". ` +
514
512
  `Currently, only relations to "User" are supported`);
515
513
  }
@@ -517,7 +515,7 @@ export class DataAccessor {
517
515
  const chosenId = typeof updatedRecord[field] === 'object'
518
516
  ? updatedRecord[field][intermediatePk]
519
517
  : updatedRecord[field];
520
- const record = await this.accessRelationModel(intermediateRelation.model).findOne({ where: { [intermediatePk]: chosenId, [via]: this.user.id } });
518
+ const record = await this.accessRelationModel(intermediateRelation.model, intermediateRelation.resourceName).findOne({ where: { [intermediatePk]: chosenId, [via]: this.user.id } });
521
519
  if (!record) {
522
520
  throw new Error(`Access denied: "${field}" does not belong to the current user`);
523
521
  }
@@ -25,8 +25,9 @@ export class FilterService {
25
25
  if (!models) {
26
26
  return null;
27
27
  }
28
- // Find config (case-insensitive)
29
- const foundKey = Object.keys(models).find(key => key.toLowerCase() === modelName.toLowerCase());
28
+ const foundKey = Object.prototype.hasOwnProperty.call(models, modelName)
29
+ ? modelName
30
+ : Object.keys(models).find(key => key.toLowerCase() === modelName.toLowerCase());
30
31
  if (!foundKey) {
31
32
  return null;
32
33
  }
@@ -35,7 +36,7 @@ export class FilterService {
35
36
  return null;
36
37
  }
37
38
  // Get AbstractModel
38
- const model = this.adminizer.modelHandler.model.get(config.model);
39
+ const model = this.adminizer.modelHandler.getResource(foundKey);
39
40
  if (!model) {
40
41
  return null;
41
42
  }
@@ -86,9 +86,9 @@ export class AbstractHistoryAdapter {
86
86
  * @returns Array of model names (in lowercase) the user can access.
87
87
  */
88
88
  getModels(user) {
89
- const models = this.adminizer.modelHandler.all
90
- .filter(model => !this.excludedModels.has(model.modelname))
91
- .map(model => model.modelname.toLowerCase());
89
+ const models = Array.from(this.adminizer.modelHandler.model.entries())
90
+ .filter(([, model]) => !this.excludedModels.has(model.modelname))
91
+ .map(([resourceName]) => resourceName);
92
92
  const accessModels = [];
93
93
  for (const model of models) {
94
94
  const access = this.adminizer.accessRightsHelper.enoughPermissions([
@@ -304,14 +304,15 @@ export class AbstractHistoryAdapter {
304
304
  * @protected
305
305
  */
306
306
  findModelResource(history) {
307
- const modelResourceName = history.modelName;
307
+ const modelResourceName = this.adminizer.modelHandler.getResourceRecord(history.modelName)?.name
308
+ ?? history.modelName;
308
309
  const modelResourceUri = `${this.adminizer.config.routePrefix}/model/${modelResourceName}`;
309
310
  const models = this.adminizer.config.models;
310
311
  const foundKey = Object.keys(models).find(key => key.toLowerCase() === modelResourceName.toLowerCase());
311
312
  const modelResource = {
312
313
  name: modelResourceName,
313
314
  uri: modelResourceUri,
314
- model: this.adminizer.modelHandler.model.get(history.modelName),
315
+ model: this.adminizer.modelHandler.getResource(modelResourceName),
315
316
  config: models[foundKey]
316
317
  };
317
318
  return modelResource;
@@ -341,8 +342,9 @@ export class AbstractHistoryAdapter {
341
342
  Adminizer.log.error('No model found for field: ', fields[key]);
342
343
  return;
343
344
  }
344
- let Model = this.adminizer.modelHandler.model.get(modelName);
345
- if (!Model) {
345
+ const resourceName = this.adminizer.modelHandler.resolveAssociationResource(modelName, fields[key].model.resourceName);
346
+ let Model = resourceName ? this.adminizer.modelHandler.getResource(resourceName) : undefined;
347
+ if (!Model || !resourceName) {
346
348
  return;
347
349
  }
348
350
  let list;
@@ -351,8 +353,8 @@ export class AbstractHistoryAdapter {
351
353
  Adminizer.log.warn("Warning: executing malicious job trying to add a huge amount of records in field config," +
352
354
  " please rewrite this part of code in the nearest future");
353
355
  let modelResource = {
354
- name: modelName, config: this.adminizer.config.models[modelName],
355
- model: Model, uri: `${this.adminizer.config.routePrefix}/model/${modelName}`
356
+ name: resourceName, config: this.adminizer.config.models[resourceName],
357
+ model: Model, uri: `${this.adminizer.config.routePrefix}/model/${resourceName}`
356
358
  };
357
359
  let dataAccessor = new DataAccessor(this.adminizer, user, modelResource, "view");
358
360
  list = await Model.find({}, dataAccessor);
@@ -14,6 +14,8 @@ export interface Attribute {
14
14
  columnName?: string;
15
15
  model?: string;
16
16
  collection?: string;
17
+ /** Canonical Adminizer resource target for a system-model association. */
18
+ resourceName?: string;
17
19
  via?: string;
18
20
  allowNull?: boolean;
19
21
  /**
@@ -9,10 +9,26 @@ export interface AppModelAccessRecord {
9
9
  export interface ModelRecord {
10
10
  id: string;
11
11
  name: string;
12
+ hostModelName: string;
13
+ primaryHostModel: boolean;
12
14
  model: AbstractModel<any>;
13
15
  ownerApp?: string;
14
16
  enabled: boolean;
15
17
  }
18
+ export interface ModelRegistrationOptions {
19
+ /** Physical ORM model name. It is intentionally independent from the Adminizer resource name. */
20
+ hostModelName?: string;
21
+ /**
22
+ * Selects this resource as the association target when multiple resources use the same host model.
23
+ * Exactly one resource must be primary for every shared host model.
24
+ */
25
+ primary?: boolean;
26
+ /**
27
+ * @deprecated Register and use the canonical Adminizer resource name instead.
28
+ * Kept only for compatibility with existing integrations. Host model names must not be used as aliases.
29
+ */
30
+ aliases?: string[];
31
+ }
16
32
  export declare class AppModelAccess {
17
33
  private readonly getModel;
18
34
  private readonly appName;
@@ -33,9 +49,12 @@ export declare class AppModelAccess {
33
49
  export declare class ModelHandler {
34
50
  private models;
35
51
  private modelAliases;
52
+ private hostModelResources;
36
53
  private internalAccess?;
37
54
  private appAccessRecords;
38
- add<T>(modelName: string, modelInstance: AbstractModel<T>, aliases?: string[]): void;
55
+ add<T>(resourceName: string, modelInstance: AbstractModel<T>, options?: ModelRegistrationOptions): void;
56
+ /** @deprecated Pass `{ aliases }` in ModelRegistrationOptions, or preferably use the canonical resource name. */
57
+ add<T>(resourceName: string, modelInstance: AbstractModel<T>, aliases?: string[]): void;
39
58
  register<T>(appName: string, modelName: string, modelInstance: AbstractModel<T>): string;
40
59
  unregister(id: string): void;
41
60
  disable(id: string): void;
@@ -45,7 +64,10 @@ export declare class ModelHandler {
45
64
  unregisterAppAccess(id: string): void;
46
65
  getAppAccessRecords(appName: string): AppModelAccessRecord[];
47
66
  createAppAccess(appName: string): AppModelAccess;
48
- /** Improved model getter with case-insensitive lookup and configured alias support. */
67
+ /**
68
+ * Improved model getter with case-insensitive lookup and configured alias support.
69
+ * @deprecated New code should use getResource() and getByHostModel() explicitly.
70
+ */
49
71
  get model(): {
50
72
  get: (modelName: string) => AbstractModel<any>;
51
73
  has: (modelName: string) => boolean;
@@ -53,6 +75,26 @@ export declare class ModelHandler {
53
75
  keys: () => IterableIterator<string>;
54
76
  values: () => IterableIterator<AbstractModel<any>>;
55
77
  };
78
+ /**
79
+ * Returns a model by its Adminizer resource name or an explicit deprecated compatibility alias.
80
+ * New code must use canonical resource names.
81
+ */
82
+ getResource(resourceName: string): AbstractModel<any> | undefined;
83
+ getResourceRecord(resourceName: string): ModelRecord | undefined;
84
+ /**
85
+ * Resolves a physical ORM model name to its Adminizer resource.
86
+ * This lookup is deliberately separate from getResource(): a host model may have the
87
+ * same name as a system resource while being exposed under a project resource alias.
88
+ */
89
+ resolveResourceByHostModel(hostModelName: string): string | undefined;
90
+ getByHostModel(hostModelName: string): AbstractModel<any> | undefined;
91
+ /**
92
+ * Resolves an ORM association target. Adapter-provided host model names take precedence;
93
+ * system-model relations may pass their explicit canonical resource name instead.
94
+ */
95
+ resolveAssociationResource(targetName: string, resourceName?: string): string | undefined;
96
+ /** Validates that every host ORM model used by multiple resources has one primary resource. */
97
+ validateHostModelMappings(): void;
56
98
  resolveModelName(modelName: string): string;
57
99
  configureInternalAccess(accessMap?: InternalModelAccessMap): void;
58
100
  internal(scopeName: string): InternalModelScope;
@@ -63,6 +105,9 @@ export declare class ModelHandler {
63
105
  get all(): AbstractModel<any>[];
64
106
  private getAllowedAppModels;
65
107
  private registerAliases;
108
+ private registerHostModel;
109
+ private unregisterHostModel;
110
+ private getSharedHostModelError;
66
111
  private resolveName;
67
112
  private enabledModelEntries;
68
113
  private enabledModelKeys;