adminizer 5.0.0-build.11 → 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-DQE53mIb.js → add-B_hn__Ba.js} +1 -1
- package/assets/{add-group-BsizwU4R.js → add-group-0SdYgiXZ.js} +1 -1
- package/assets/{add-user-BkSrCzw4.js → add-user-Bh1iqSem.js} +1 -1
- package/assets/ai-assistant/agent.es.js +5036 -4931
- package/assets/app.js +32 -32
- package/assets/{catalog-DPbsKvEY.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-DDbX6YWf.js → dashboard-CbJIK7ka.js} +1 -1
- package/assets/{history-DzHm8RJs.js → history-CZAJgVXP.js} +1 -1
- package/assets/{list-CMymFRqO.js → list-C6w_xDK2.js} +1 -1
- package/assets/manifest.json +24 -24
- package/assets/{module-DkFrKFpO.js → module-DvPjKyF_.js} +1 -1
- package/assets/{notification-ChERJE5c.js → notification-N_b-N0Zu.js} +1 -1
- package/assets/{user-filters-list-BytQFcmQ.js → user-filters-list-xbJoY_5_.js} +1 -1
- package/assets/{welcome-Daw8Hukl.js → welcome-BUkdkKa5.js} +1 -1
- package/assets/{with-app-layout-0iXcLqi5.js → with-app-layout-WhRyO5WK.js} +1 -1
- package/controllers/ai/AiAgentController.js +4 -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/inertiaMenuHelper.d.ts +0 -1
- package/helpers/inertiaMenuHelper.js +3 -26
- package/helpers/modelResourceHelper.d.ts +9 -0
- package/helpers/modelResourceHelper.js +43 -0
- package/helpers/navigationAccessHelper.d.ts +13 -0
- package/helpers/navigationAccessHelper.js +40 -0
- package/index.d.ts +6 -0
- package/index.js +6 -0
- package/interfaces/adminpanelConfig.d.ts +6 -1
- package/interfaces/types.d.ts +40 -3
- package/lib/Adminizer.d.ts +6 -0
- package/lib/Adminizer.js +9 -0
- package/lib/DataAccessor.d.ts +1 -0
- package/lib/DataAccessor.js +45 -47
- package/lib/admin-links/AdminLinkHandler.d.ts +72 -0
- package/lib/admin-links/AdminLinkHandler.js +175 -0
- package/lib/ai-assistant/AbstractAiModelService.d.ts +44 -3
- package/lib/ai-assistant/AbstractAiModelService.js +61 -2
- package/lib/ai-assistant/AiAssistantAgentSkillHandler.d.ts +58 -0
- package/lib/ai-assistant/AiAssistantAgentSkillHandler.js +87 -0
- package/lib/ai-assistant/AiAssistantUiMethodHandler.d.ts +60 -0
- package/lib/ai-assistant/AiAssistantUiMethodHandler.js +138 -0
- package/lib/ai-assistant/builtinAgentSkills.d.ts +9 -0
- package/lib/ai-assistant/builtinAgentSkills.js +256 -0
- package/lib/ai-assistant/jsonSafe.d.ts +13 -0
- package/lib/ai-assistant/jsonSafe.js +81 -0
- package/lib/app-manager/AdminizerApp.d.ts +24 -0
- package/lib/app-manager/AppManager.js +39 -42
- 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)) {
|
|
@@ -1,35 +1,12 @@
|
|
|
1
|
+
import { listAccessibleMenuItems } from "./navigationAccessHelper.js";
|
|
1
2
|
export class InertiaMenuHelper {
|
|
2
3
|
adminizer;
|
|
3
4
|
constructor(adminizer) {
|
|
4
5
|
this.adminizer = adminizer;
|
|
5
6
|
}
|
|
6
7
|
getMenuItems(req) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const filteredActions = this.filterAccessibleItems(menuItem.actions ?? [], req);
|
|
10
|
-
const menuItemTokens = filteredActions.map(item => {
|
|
11
|
-
return item.accessRightsToken;
|
|
12
|
-
}).filter(item => {
|
|
13
|
-
return item;
|
|
14
|
-
});
|
|
15
|
-
if (menuItem.accessRightsToken)
|
|
16
|
-
menuItemTokens.push(menuItem.accessRightsToken);
|
|
17
|
-
if (this.adminizer.accessRightsHelper.enoughPermissions(menuItemTokens, req.user)) {
|
|
18
|
-
menu.push(this.translateMenuItem(req, {
|
|
19
|
-
...menuItem,
|
|
20
|
-
actions: filteredActions
|
|
21
|
-
}));
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return menu;
|
|
25
|
-
}
|
|
26
|
-
filterAccessibleItems(items, req) {
|
|
27
|
-
return items
|
|
28
|
-
.filter((item) => !item.accessRightsToken || this.adminizer.accessRightsHelper.hasPermission(item.accessRightsToken, req.user))
|
|
29
|
-
.map((item) => ({
|
|
30
|
-
...item,
|
|
31
|
-
subItems: item.subItems ? this.filterAccessibleItems(item.subItems, req) : item.subItems
|
|
32
|
-
}));
|
|
8
|
+
return listAccessibleMenuItems(this.adminizer, req.user)
|
|
9
|
+
.map((menuItem) => this.translateMenuItem(req, menuItem));
|
|
33
10
|
}
|
|
34
11
|
translateMenuItem(req, menuItem) {
|
|
35
12
|
return {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ModelConfig } from '../interfaces/adminpanelConfig.js';
|
|
2
|
+
import type { ModelResource } from '../interfaces/types.js';
|
|
3
|
+
import type { Adminizer } from '../lib/Adminizer.js';
|
|
4
|
+
/** Config defaults of a model resource declared as `true` in the panel config. */
|
|
5
|
+
export declare function normalizeModelConfig(name: string, config: ModelConfig | boolean): ModelConfig;
|
|
6
|
+
/** Every configured model resource whose model is actually registered. */
|
|
7
|
+
export declare function listModelResources(adminizer: Adminizer): ModelResource[];
|
|
8
|
+
/** Resolves a resource by its Adminizer name, then by a uniquely registered host model name. */
|
|
9
|
+
export declare function resolveModelResource(adminizer: Adminizer, modelName: string): ModelResource | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Config defaults of a model resource declared as `true` in the panel config. */
|
|
2
|
+
export function normalizeModelConfig(name, config) {
|
|
3
|
+
const baseConfig = {
|
|
4
|
+
model: name,
|
|
5
|
+
icon: "description",
|
|
6
|
+
title: name,
|
|
7
|
+
list: true,
|
|
8
|
+
add: true,
|
|
9
|
+
edit: true,
|
|
10
|
+
remove: true,
|
|
11
|
+
view: true,
|
|
12
|
+
};
|
|
13
|
+
if (typeof config === "boolean") {
|
|
14
|
+
return config ? baseConfig : { ...baseConfig, list: false, add: false, edit: false, remove: false, view: false };
|
|
15
|
+
}
|
|
16
|
+
return { ...baseConfig, ...config };
|
|
17
|
+
}
|
|
18
|
+
/** Every configured model resource whose model is actually registered. */
|
|
19
|
+
export function listModelResources(adminizer) {
|
|
20
|
+
const resources = [];
|
|
21
|
+
for (const [configName, configValue] of Object.entries(adminizer.config.models ?? {})) {
|
|
22
|
+
const config = normalizeModelConfig(configName, configValue);
|
|
23
|
+
const model = adminizer.modelHandler.getResource(configName);
|
|
24
|
+
if (!model)
|
|
25
|
+
continue;
|
|
26
|
+
resources.push({
|
|
27
|
+
name: configName,
|
|
28
|
+
uri: `${adminizer.config.routePrefix}/model/${configName}`,
|
|
29
|
+
config,
|
|
30
|
+
model,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return resources;
|
|
34
|
+
}
|
|
35
|
+
/** Resolves a resource by its Adminizer name, then by a uniquely registered host model name. */
|
|
36
|
+
export function resolveModelResource(adminizer, modelName) {
|
|
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;
|
|
43
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HrefConfig } from '../interfaces/adminpanelConfig.js';
|
|
2
|
+
import type { Adminizer } from '../lib/Adminizer.js';
|
|
3
|
+
import type { User } from '../models/User.js';
|
|
4
|
+
import type { MenuItem } from './menuHelper.js';
|
|
5
|
+
/** Drops the sub-items this user may not open, recursively. */
|
|
6
|
+
export declare function filterAccessibleHrefItems(adminizer: Adminizer, user: User, items: HrefConfig[]): HrefConfig[];
|
|
7
|
+
/**
|
|
8
|
+
* The navigation menu a user may actually open: configured models, additional
|
|
9
|
+
* links and app-contributed pages, filtered with the very same rules the
|
|
10
|
+
* sidebar uses. Titles are left untranslated, so callers that need i18n (the
|
|
11
|
+
* Inertia page props) translate on top of this.
|
|
12
|
+
*/
|
|
13
|
+
export declare function listAccessibleMenuItems(adminizer: Adminizer, user: User): MenuItem[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Drops the sub-items this user may not open, recursively. */
|
|
2
|
+
export function filterAccessibleHrefItems(adminizer, user, items) {
|
|
3
|
+
return items
|
|
4
|
+
.filter((item) => !item.accessRightsToken || adminizer.accessRightsHelper.hasPermission(item.accessRightsToken, user))
|
|
5
|
+
.map((item) => ({
|
|
6
|
+
...item,
|
|
7
|
+
subItems: item.subItems ? filterAccessibleHrefItems(adminizer, user, item.subItems) : item.subItems,
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The navigation menu a user may actually open: configured models, additional
|
|
12
|
+
* links and app-contributed pages, filtered with the very same rules the
|
|
13
|
+
* sidebar uses. Titles are left untranslated, so callers that need i18n (the
|
|
14
|
+
* Inertia page props) translate on top of this.
|
|
15
|
+
*/
|
|
16
|
+
export function listAccessibleMenuItems(adminizer, user) {
|
|
17
|
+
const menu = [];
|
|
18
|
+
for (const menuItem of adminizer.menuHelper.getMenuItems(user)) {
|
|
19
|
+
const actions = filterAccessibleHrefItems(adminizer, user, menuItem.actions ?? []);
|
|
20
|
+
const tokens = actions.map((item) => item.accessRightsToken).filter(Boolean);
|
|
21
|
+
if (menuItem.accessRightsToken)
|
|
22
|
+
tokens.push(menuItem.accessRightsToken);
|
|
23
|
+
if (adminizer.accessRightsHelper.enoughPermissions(tokens, user)) {
|
|
24
|
+
menu.push({ ...menuItem, actions });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const link of adminizer.adminLinkHandler.list(user)) {
|
|
28
|
+
menu.push({
|
|
29
|
+
id: link.id,
|
|
30
|
+
link: link.link,
|
|
31
|
+
title: link.title,
|
|
32
|
+
type: link.type === 'blank' ? 'blank' : 'self',
|
|
33
|
+
actions: null,
|
|
34
|
+
icon: null,
|
|
35
|
+
accessRightsToken: link.accessRightsToken ?? null,
|
|
36
|
+
section: link.section || 'Platform',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return menu;
|
|
40
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -46,6 +46,12 @@ export * from "./lib/ai-assistant/AbstractAiModelService.js";
|
|
|
46
46
|
export * from "./lib/ai-assistant/AbstractAiConversationHistoryService.js";
|
|
47
47
|
export * from "./lib/ai-assistant/AiAssistantHandler.js";
|
|
48
48
|
export * from "./lib/ai-assistant/AiAgentRunManager.js";
|
|
49
|
+
export * from "./lib/ai-assistant/jsonSafe.js";
|
|
50
|
+
export * from "./lib/ai-assistant/AiAssistantUiMethodHandler.js";
|
|
51
|
+
export * from "./lib/ai-assistant/AiAssistantAgentSkillHandler.js";
|
|
52
|
+
export * from "./lib/admin-links/AdminLinkHandler.js";
|
|
53
|
+
export * from "./helpers/modelResourceHelper.js";
|
|
54
|
+
export * from "./helpers/navigationAccessHelper.js";
|
|
49
55
|
export * from "./lib/DataAccessor.js";
|
|
50
56
|
export * from "./lib/history-actions/AbstractHistoryAdapter.js";
|
|
51
57
|
export * from "./lib/filters/CustomFilterHandler.js";
|
package/index.js
CHANGED
|
@@ -46,6 +46,12 @@ export * from "./lib/ai-assistant/AbstractAiModelService.js";
|
|
|
46
46
|
export * from "./lib/ai-assistant/AbstractAiConversationHistoryService.js";
|
|
47
47
|
export * from "./lib/ai-assistant/AiAssistantHandler.js";
|
|
48
48
|
export * from "./lib/ai-assistant/AiAgentRunManager.js";
|
|
49
|
+
export * from "./lib/ai-assistant/jsonSafe.js";
|
|
50
|
+
export * from "./lib/ai-assistant/AiAssistantUiMethodHandler.js";
|
|
51
|
+
export * from "./lib/ai-assistant/AiAssistantAgentSkillHandler.js";
|
|
52
|
+
export * from "./lib/admin-links/AdminLinkHandler.js";
|
|
53
|
+
export * from "./helpers/modelResourceHelper.js";
|
|
54
|
+
export * from "./helpers/navigationAccessHelper.js";
|
|
49
55
|
export * from "./lib/DataAccessor.js";
|
|
50
56
|
export * from "./lib/history-actions/AbstractHistoryAdapter.js";
|
|
51
57
|
export * from "./lib/filters/CustomFilterHandler.js";
|
|
@@ -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/interfaces/types.d.ts
CHANGED
|
@@ -123,6 +123,29 @@ export interface AiAgentConnectionStatus {
|
|
|
123
123
|
expiresAt?: string | null;
|
|
124
124
|
[key: string]: unknown;
|
|
125
125
|
}
|
|
126
|
+
/** States for which an agent can describe its connection screen. */
|
|
127
|
+
export type AiAgentConnectionState = Exclude<AiAgentConnectionStatus['state'], 'ready'>;
|
|
128
|
+
/**
|
|
129
|
+
* Declarative connection screen supplied by an agent service. The shared panel
|
|
130
|
+
* owns rendering, while the agent owns its copy, CTA and which status details
|
|
131
|
+
* are useful to expose.
|
|
132
|
+
*/
|
|
133
|
+
export interface AiAgentConnectionScreen {
|
|
134
|
+
title?: string;
|
|
135
|
+
description?: string;
|
|
136
|
+
icon?: 'spinner' | 'bot' | 'error';
|
|
137
|
+
action?: {
|
|
138
|
+
label: string;
|
|
139
|
+
href: string;
|
|
140
|
+
};
|
|
141
|
+
/** Status values the agent wants to render on this screen. */
|
|
142
|
+
details?: Array<{
|
|
143
|
+
field: string;
|
|
144
|
+
label?: string;
|
|
145
|
+
format?: 'text' | 'local-time';
|
|
146
|
+
tone?: 'muted' | 'error';
|
|
147
|
+
}>;
|
|
148
|
+
}
|
|
126
149
|
/** Normalized budget / rate limits of the provider key, when it exposes them. */
|
|
127
150
|
export interface AiAgentLimits {
|
|
128
151
|
provider: string;
|
|
@@ -144,9 +167,8 @@ export interface AiAgentLimits {
|
|
|
144
167
|
[key: string]: unknown;
|
|
145
168
|
}
|
|
146
169
|
/**
|
|
147
|
-
* Copy the panel shows instead of its neutral defaults.
|
|
148
|
-
*
|
|
149
|
-
* still translated.
|
|
170
|
+
* Copy the panel shows instead of its neutral defaults. `getUiHints(locale)`
|
|
171
|
+
* receives the admin user's locale, so agents can return localized copy.
|
|
150
172
|
*/
|
|
151
173
|
export interface AiAgentUiHints {
|
|
152
174
|
title?: string;
|
|
@@ -157,14 +179,29 @@ export interface AiAgentUiHints {
|
|
|
157
179
|
setupSetting?: string;
|
|
158
180
|
/** Where to fill it in; rendered as a button on the connection loader. */
|
|
159
181
|
setupUrl?: string;
|
|
182
|
+
/**
|
|
183
|
+
* Per-state pages rendered while this agent is not ready. `default` is
|
|
184
|
+
* used for any state without its own screen.
|
|
185
|
+
*/
|
|
186
|
+
connectionScreens?: Partial<Record<AiAgentConnectionState | 'default', AiAgentConnectionScreen>>;
|
|
160
187
|
[key: string]: unknown;
|
|
161
188
|
}
|
|
189
|
+
/** A browser-side capability which an agent may call through `ui.method`. */
|
|
190
|
+
export interface AiAgentUiMethod {
|
|
191
|
+
id: string;
|
|
192
|
+
title: string;
|
|
193
|
+
description: string;
|
|
194
|
+
inputSchema: Record<string, unknown>;
|
|
195
|
+
action: string;
|
|
196
|
+
accessRightsToken?: string;
|
|
197
|
+
}
|
|
162
198
|
/** Declarative UI contract consumed by the shared assistant panel. */
|
|
163
199
|
export interface AiAgentUiSchema extends AiAgentUiHints {
|
|
164
200
|
commands: Array<{
|
|
165
201
|
id: string;
|
|
166
202
|
description?: string;
|
|
167
203
|
}>;
|
|
204
|
+
uiMethods?: AiAgentUiMethod[];
|
|
168
205
|
panels: {
|
|
169
206
|
history: boolean;
|
|
170
207
|
models: boolean;
|
package/lib/Adminizer.d.ts
CHANGED
|
@@ -27,6 +27,9 @@ import { ControllerHandler } from "./app-manager/ControllerHandler.js";
|
|
|
27
27
|
import { AssetHandler } from "./app-manager/AssetHandler.js";
|
|
28
28
|
import { ConfigLayerHandler } from "./app-manager/ConfigLayerHandler.js";
|
|
29
29
|
import { AccessRightsHandler } from "./access-rights/AccessRightsHandler.js";
|
|
30
|
+
import { AiAssistantUiMethodHandler } from './ai-assistant/AiAssistantUiMethodHandler.js';
|
|
31
|
+
import { AdminLinkHandler } from './admin-links/AdminLinkHandler.js';
|
|
32
|
+
import { AiAssistantAgentSkillHandler } from './ai-assistant/AiAssistantAgentSkillHandler.js';
|
|
30
33
|
export declare function createAdminizerLogger(): winston.Logger;
|
|
31
34
|
export interface EmitAsyncOptions {
|
|
32
35
|
/**
|
|
@@ -54,6 +57,9 @@ export declare class Adminizer {
|
|
|
54
57
|
notificationHandler: NotificationHandler;
|
|
55
58
|
historyHandler: HistoryHandler;
|
|
56
59
|
private _aiAssistantHandler?;
|
|
60
|
+
aiAssistantUiMethodHandler: AiAssistantUiMethodHandler;
|
|
61
|
+
adminLinkHandler: AdminLinkHandler;
|
|
62
|
+
aiAssistantAgentSkillHandler: AiAssistantAgentSkillHandler;
|
|
57
63
|
modelHandler: ModelHandler;
|
|
58
64
|
widgetHandler: WidgetHandler;
|
|
59
65
|
customFilterHandler: CustomFilterHandler;
|
package/lib/Adminizer.js
CHANGED
|
@@ -43,6 +43,9 @@ import { ControllerHandler } from "./app-manager/ControllerHandler.js";
|
|
|
43
43
|
import { AssetHandler } from "./app-manager/AssetHandler.js";
|
|
44
44
|
import { ConfigLayerHandler } from "./app-manager/ConfigLayerHandler.js";
|
|
45
45
|
import { AccessRightsHandler } from "./access-rights/AccessRightsHandler.js";
|
|
46
|
+
import { AiAssistantUiMethodHandler } from './ai-assistant/AiAssistantUiMethodHandler.js';
|
|
47
|
+
import { AdminLinkHandler } from './admin-links/AdminLinkHandler.js';
|
|
48
|
+
import { AiAssistantAgentSkillHandler } from './ai-assistant/AiAssistantAgentSkillHandler.js';
|
|
46
49
|
const logFilePath = "logs/app.log";
|
|
47
50
|
function ensureLogDirectory(filePath) {
|
|
48
51
|
const logDirectory = path.dirname(filePath);
|
|
@@ -109,6 +112,9 @@ export class Adminizer {
|
|
|
109
112
|
notificationHandler;
|
|
110
113
|
historyHandler;
|
|
111
114
|
_aiAssistantHandler;
|
|
115
|
+
aiAssistantUiMethodHandler;
|
|
116
|
+
adminLinkHandler;
|
|
117
|
+
aiAssistantAgentSkillHandler;
|
|
112
118
|
modelHandler;
|
|
113
119
|
widgetHandler;
|
|
114
120
|
customFilterHandler;
|
|
@@ -141,6 +147,9 @@ export class Adminizer {
|
|
|
141
147
|
this.catalogTemplateComponentHandler = new CatalogTemplateComponentHandler();
|
|
142
148
|
this.mediaManagerHandler = new MediaManagerHandler();
|
|
143
149
|
this.feedbackHandler = new FeedbackHandler(this);
|
|
150
|
+
this.aiAssistantUiMethodHandler = new AiAssistantUiMethodHandler(this);
|
|
151
|
+
this.adminLinkHandler = new AdminLinkHandler(this);
|
|
152
|
+
this.aiAssistantAgentSkillHandler = new AiAssistantAgentSkillHandler(this);
|
|
144
153
|
this.controllerHandler = new ControllerHandler(this);
|
|
145
154
|
this.assetHandler = new AssetHandler(this);
|
|
146
155
|
this.configLayerHandler = new ConfigLayerHandler(this);
|
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
|
}
|