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.
- package/assets/{add-BhTWhSNx.js → add-B_hn__Ba.js} +1 -1
- package/assets/{add-group-BEOEkMGK.js → add-group-0SdYgiXZ.js} +1 -1
- package/assets/{add-user-MW-fSILU.js → add-user-Bh1iqSem.js} +1 -1
- package/assets/ai-assistant/agent.es.js +3948 -3889
- package/assets/app.js +32 -32
- package/assets/{catalog-D-lFVV9U.js → catalog-DjK8r0Mn.js} +1 -1
- package/assets/controls/handsontable.es.js +11637 -11632
- package/assets/controls/jsoneditor.es.js +6308 -6297
- package/assets/controls/toast-ui.es.js +18 -17
- package/assets/{dashboard-Cmj4Df_C.js → dashboard-CbJIK7ka.js} +1 -1
- package/assets/{history-BXfeZHTD.js → history-CZAJgVXP.js} +1 -1
- package/assets/{list-Dp8F_hor.js → list-C6w_xDK2.js} +1 -1
- package/assets/manifest.json +24 -24
- package/assets/{module-Q-1AzZH9.js → module-DvPjKyF_.js} +1 -1
- package/assets/{notification-CSeMEsPn.js → notification-N_b-N0Zu.js} +1 -1
- package/assets/{user-filters-list-Do6Dis5D.js → user-filters-list-xbJoY_5_.js} +1 -1
- package/assets/{welcome-D3dg746Y.js → welcome-BUkdkKa5.js} +1 -1
- package/assets/{with-app-layout-Bjo8B9zO.js → with-app-layout-WhRyO5WK.js} +1 -1
- package/controllers/view.js +1 -1
- package/helpers/configHelper.js +12 -17
- package/helpers/controllerHelper.d.ts +1 -0
- package/helpers/controllerHelper.js +25 -15
- package/helpers/fieldsHelper.js +3 -3
- package/helpers/inertiaAddHelper.js +1 -1
- package/helpers/modelResourceHelper.d.ts +1 -1
- package/helpers/modelResourceHelper.js +8 -4
- package/interfaces/adminpanelConfig.d.ts +6 -1
- package/lib/DataAccessor.d.ts +1 -0
- package/lib/DataAccessor.js +45 -47
- package/lib/filters/FilterService.js +4 -3
- package/lib/history-actions/AbstractHistoryAdapter.js +11 -9
- package/lib/model/AbstractModel.d.ts +2 -0
- package/lib/model/ModelHandler.d.ts +47 -2
- package/lib/model/ModelHandler.js +107 -8
- package/package.json +1 -1
- package/system/bindAccessRights.js +2 -3
- package/system/buildInternalModelAccess.js +5 -2
- 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
|
-
|
|
93
|
-
|
|
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 (
|
|
96
|
-
return
|
|
102
|
+
if (Object.prototype.hasOwnProperty.call(models, requestedName)) {
|
|
103
|
+
return requestedName;
|
|
97
104
|
}
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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.
|
|
210
|
+
modelResource.model = req.adminizer.modelHandler.getResource(modelResourceName);
|
|
201
211
|
}
|
|
202
212
|
// Return the completed ModelResource object
|
|
203
213
|
return modelResource;
|
package/helpers/fieldsHelper.js
CHANGED
|
@@ -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.
|
|
34
|
-
let Model = req.adminizer.modelHandler.
|
|
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 (
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
38
|
-
|
|
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
|
-
*
|
|
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
|
*/
|
package/lib/DataAccessor.d.ts
CHANGED
|
@@ -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.
|
package/lib/DataAccessor.js
CHANGED
|
@@ -46,29 +46,28 @@ export class DataAccessor {
|
|
|
46
46
|
this.action = action;
|
|
47
47
|
this.actionVerb = getTokenAction(this.action);
|
|
48
48
|
}
|
|
49
|
-
getModelConfig(
|
|
50
|
-
const
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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.
|
|
137
|
+
Adminizer.log.silly(`No access rights to model: ${this.modelResource.name}`);
|
|
139
138
|
return undefined;
|
|
140
139
|
}
|
|
141
|
-
const model = this.adminizer.modelHandler.
|
|
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(
|
|
190
|
-
const
|
|
191
|
-
const
|
|
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-${
|
|
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: ${
|
|
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.
|
|
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 || !['
|
|
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 === '
|
|
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 === '
|
|
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 === '
|
|
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 === '
|
|
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.
|
|
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) !== '
|
|
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 && ['
|
|
476
|
-
if (relationModel === '
|
|
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 === '
|
|
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.
|
|
501
|
+
throw new Error(`Invalid intermediate relation configuration for field "${field}" in model ${this.modelResource.name}`);
|
|
504
502
|
}
|
|
505
|
-
const intermediateModel = this.
|
|
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) !== '
|
|
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
|
-
|
|
29
|
-
|
|
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.
|
|
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.
|
|
90
|
-
.filter(model => !this.excludedModels.has(model.modelname))
|
|
91
|
-
.map(
|
|
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.
|
|
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
|
-
|
|
345
|
-
|
|
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:
|
|
355
|
-
model: Model, uri: `${this.adminizer.config.routePrefix}/model/${
|
|
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);
|
|
@@ -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>(
|
|
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
|
-
/**
|
|
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;
|