adminizer 5.0.0-build.8 → 5.1.0-build.24
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/abnfDiagram-VCTEODGH-DFY3Pztx.js +1 -0
- package/assets/add-31tS-g4V.js +1 -0
- package/assets/add-group-Cf3zT7WE.js +1 -0
- package/assets/add-user-C62wOna-.js +1 -0
- package/assets/ai-assistant/agent.es.js +28464 -0
- package/assets/app-DPmI-MvN.css +1 -0
- package/assets/app-vdHbjOQX.js +138 -0
- package/assets/arc-DP4o1zz3.js +1 -0
- package/assets/architectureDiagram-5GKGNRK7-BjXW5Zu0.js +36 -0
- package/assets/asmcrypto-DZeQH0LC.js +1 -0
- package/assets/blockDiagram-I7D4REHJ-BUPnDwN0.js +129 -0
- package/assets/c4Diagram-7LVT6UL2-Cmb_dAhd.js +38 -0
- package/assets/{catalog-BEvJZYJP.js → catalog-C-y5rMaG.js} +3 -3
- package/assets/channel-CkJRUsq_.js +1 -0
- package/assets/chunk-2Q5K7J3B-CmS-dWwQ.js +1 -0
- package/assets/chunk-5VM5RSS4-Cqs2Ow64.js +15 -0
- package/assets/chunk-F27PBJKO-BoMCpJA1.js +1 -0
- package/assets/chunk-IMKFNOWR-DlDcPk1x.js +231 -0
- package/assets/chunk-JWPE2WC7-DzL_SU8s.js +1 -0
- package/assets/chunk-POPQ4Y6H-Cwq2-Vb8.js +1 -0
- package/assets/chunk-SVP7TREG-BfRHY5FU.js +88 -0
- package/assets/chunk-TICWLB2K-CRP7rHkW.js +206 -0
- package/assets/chunk-XXDRQBXY-Bg2jg-98.js +1 -0
- package/assets/classDiagram-ZZMXUADV-D4EF3HGj.js +1 -0
- package/assets/classDiagram-v2-VYDZK3BY-D4EF3HGj.js +1 -0
- package/assets/controls/ckeditor.es.js +9382 -9320
- package/assets/controls/handsontable.es.js +11971 -11938
- package/assets/controls/jsoneditor.es.js +10442 -10233
- package/assets/controls/toast-ui.es.js +1308 -1279
- package/assets/cose-bilkent-JH36ORCC-CCJ0SGyO.js +1 -0
- package/assets/cynefin-OW5HDTMX-B2Jx5fw3.js +166 -0
- package/assets/cynefinDiagram-5FMLGOSQ-DuK0Lb62.js +62 -0
- package/assets/cytoscape.esm-DzSz-X2X.js +321 -0
- package/assets/dagre-GXQ25YYZ-DnOYexET.js +4 -0
- package/assets/dashboard-X8k4X6Sw.js +3 -0
- package/assets/debounce-B3mXI_sx.js +1 -0
- package/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/assets/diagram-S7CK7UJ4-DDgPBHsK.js +30 -0
- package/assets/diagram-UQ7AKVKN-qlL0GZs8.js +41 -0
- package/assets/diagram-VSXAHHWV-CQbiU1_c.js +3 -0
- package/assets/diagram-VX7I27RA-DFDI9_au.js +24 -0
- package/assets/diagram-Z3DM3KII-B5C9dtmJ.js +24 -0
- package/assets/docs-BiZOD7PP.js +1 -0
- package/assets/ebnfDiagram-PWID7BFC-CLMUsXEI.js +1 -0
- package/assets/erDiagram-RLTQ6QDP-ax9Xol5-.js +99 -0
- package/assets/flowDiagram-HODETNUW-CvaH7l3S.js +168 -0
- package/assets/ganttDiagram-EL5Y4UJY-Cga5U19j.js +292 -0
- package/assets/gitGraphDiagram-WWUBYQGX-B6o1T3-d.js +106 -0
- package/assets/history-CVBJUNSQ.js +1 -0
- package/assets/infoDiagram-27XIBGKW-BpWxgBCE.js +2 -0
- package/assets/init-Gi6I4Gst.js +1 -0
- package/assets/init-user-xSvnFTF0.js +1 -0
- package/assets/isObject-DL-BuaBl.js +1 -0
- package/assets/ishikawaDiagram-5VMMS53U-zE83db4H.js +70 -0
- package/assets/journeyDiagram-3NMN7TZE-DZNVFqrX.js +139 -0
- package/assets/kanban-definition-UXKFOSKX-CqxNgk27.js +89 -0
- package/assets/katex-HP8lGamR.js +257 -0
- package/assets/layout-CN3kt_jt.js +1 -0
- package/assets/linear-nvkaQ6BA.js +1 -0
- package/assets/{list-DhK0McVn.js → list-CrIU2eZz.js} +2 -2
- package/assets/login-CX9xVjCn.js +1 -0
- package/assets/manifest.json +834 -58
- package/assets/media-manager-BfYftLIH.js +6 -0
- package/assets/mermaid.core-C0V1NaVJ.js +310 -0
- package/assets/mindmap-definition-YA3MSWOX-Bh7_d9f1.js +96 -0
- package/assets/module-DPEvnad3.js +1 -0
- package/assets/notification-Dqpt5AIZ.js +20 -0
- package/assets/ordinal-Cboi1Yqb.js +1 -0
- package/assets/pegDiagram-XKGWAZYB-DMnjm6Z1.js +1 -0
- package/assets/pieDiagram-E7YTZNPT-D-RWCgYb.js +39 -0
- package/assets/quadrantDiagram-AXDQQJYC-BNBGP7RM.js +7 -0
- package/assets/railroadDiagram-O6MQD6OU-BIjVPT01.js +1 -0
- package/assets/register-CF4KQ97e.js +1 -0
- package/assets/requirementDiagram-BXWQKSXE-VPrtGdjM.js +84 -0
- package/assets/ru-CFJ4fysR.js +1 -0
- package/assets/sankeyDiagram-P5KCCOFB-ct9mLF3U.js +40 -0
- package/assets/sequenceDiagram-WJ2MYXX4-TC06Mbmw.js +162 -0
- package/assets/sizeCapture-INFHLROL-BMMwriCf.js +1 -0
- package/assets/stateDiagram-D77RDMKH-DtwYRPCX.js +1 -0
- package/assets/stateDiagram-v2-MP3YSRHH-C1yB0O5U.js +1 -0
- package/assets/swimlanes-42K2YHIH-DE3hRbbr.js +1 -0
- package/assets/swimlanesDiagram-VR7AAH4N-Ce4l1sQD.js +8 -0
- package/assets/timeline-definition-24CTP7MA-N7s_hfjj.js +120 -0
- package/assets/toNumber-DZb-r9sy.js +1 -0
- package/assets/use-appearance-DYTIKpT2.js +1 -0
- package/assets/user-filters-list-fR0YNJ3l.js +1 -0
- package/assets/vennDiagram-4TSXK5OY-Dj5bFQkR.js +34 -0
- package/assets/wardleyDiagram-VM6X3IG4-83ax25Z6.js +78 -0
- package/assets/welcome-v11MTsRK.js +1 -0
- package/assets/with-app-layout-CH6RIib1.js +31 -0
- package/assets/xychartDiagram-S5SC5T6Z-CW9Br5Xp.js +7 -0
- package/controllers/access-rights/permissionOptions.d.ts +2 -0
- package/controllers/access-rights/permissionOptions.js +23 -0
- package/controllers/add.js +2 -2
- package/controllers/addGroup.js +15 -6
- package/controllers/ai/AiAgentController.d.ts +66 -0
- package/controllers/ai/AiAgentController.js +435 -0
- package/controllers/ai/AiAssistantController.d.ts +0 -4
- package/controllers/ai/AiAssistantController.js +4 -78
- package/controllers/catalog/FrontendCatalogAdapter.js +7 -8
- package/controllers/docs/index.d.ts +22 -0
- package/controllers/docs/index.js +126 -0
- package/controllers/edit.js +2 -2
- package/controllers/editGroup.js +15 -6
- package/controllers/filter-fields/savedFilters.js +1 -1
- package/controllers/history-actions/HistoryController.js +7 -7
- package/controllers/inlineUpdate.js +10 -3
- package/controllers/list.js +1 -1
- package/controllers/login.js +1 -1
- package/controllers/notifications/NotificationController.js +14 -8
- package/controllers/view.js +3 -3
- package/controllers/widgets/filterInfo.js +1 -1
- package/helpers/accessRightsHelper.d.ts +30 -3
- package/helpers/accessRightsHelper.js +140 -12
- package/helpers/assetVersionHelper.d.ts +14 -0
- package/helpers/assetVersionHelper.js +42 -0
- package/helpers/configHelper.js +19 -18
- package/helpers/controllerHelper.d.ts +1 -0
- package/helpers/controllerHelper.js +25 -15
- package/helpers/fieldsHelper.d.ts +2 -1
- package/helpers/fieldsHelper.js +11 -5
- package/helpers/inertiaActionsHelper.d.ts +1 -1
- package/helpers/inertiaActionsHelper.js +4 -5
- package/helpers/inertiaAddHelper.d.ts +1 -1
- package/helpers/inertiaAddHelper.js +7 -6
- package/helpers/inertiaGroupHelper.js +10 -2
- package/helpers/inertiaListHelper.d.ts +1 -1
- package/helpers/inertiaListHelper.js +7 -8
- package/helpers/inertiaMenuHelper.d.ts +7 -2
- package/helpers/inertiaMenuHelper.js +12 -27
- package/helpers/menuHelper.d.ts +11 -1
- package/helpers/menuHelper.js +12 -0
- package/helpers/modelResourceHelper.d.ts +9 -0
- package/helpers/modelResourceHelper.js +43 -0
- package/helpers/navigationAccessHelper.d.ts +13 -0
- package/helpers/navigationAccessHelper.js +45 -0
- package/index.d.ts +11 -0
- package/index.js +11 -0
- package/interfaces/adminpanelConfig.d.ts +160 -2
- package/interfaces/queryCriteria.d.ts +13 -0
- package/interfaces/types.d.ts +169 -0
- package/lib/Adminizer.d.ts +8 -0
- package/lib/Adminizer.js +29 -2
- package/lib/DataAccessor.d.ts +100 -3
- package/lib/DataAccessor.js +454 -245
- package/lib/access-graph/AccessGraphResolver.d.ts +136 -0
- package/lib/access-graph/AccessGraphResolver.js +605 -0
- package/lib/access-graph/RecordAccessCache.d.ts +23 -0
- package/lib/access-graph/RecordAccessCache.js +30 -0
- package/lib/access-graph/RecordAccessResolver.d.ts +83 -0
- package/lib/access-graph/RecordAccessResolver.js +295 -0
- package/lib/access-graph/shared.d.ts +60 -0
- package/lib/access-graph/shared.js +121 -0
- package/lib/admin-links/AdminLinkHandler.d.ts +72 -0
- package/lib/admin-links/AdminLinkHandler.js +183 -0
- package/lib/ai-assistant/AbstractAiConversationHistoryService.d.ts +39 -0
- package/lib/ai-assistant/AbstractAiConversationHistoryService.js +98 -0
- package/lib/ai-assistant/AbstractAiModelService.d.ts +139 -2
- package/lib/ai-assistant/AbstractAiModelService.js +154 -1
- package/lib/ai-assistant/AiAgentRunManager.d.ts +35 -0
- package/lib/ai-assistant/AiAgentRunManager.js +109 -0
- package/lib/ai-assistant/AiAssistantAgentSkillHandler.d.ts +61 -0
- package/lib/ai-assistant/AiAssistantAgentSkillHandler.js +90 -0
- package/lib/ai-assistant/AiAssistantHandler.d.ts +1 -0
- package/lib/ai-assistant/AiAssistantHandler.js +4 -0
- package/lib/ai-assistant/AiAssistantUiMethodHandler.d.ts +60 -0
- package/lib/ai-assistant/AiAssistantUiMethodHandler.js +144 -0
- package/lib/ai-assistant/builtinAgentSkills.d.ts +9 -0
- package/lib/ai-assistant/builtinAgentSkills.js +259 -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 +31 -2
- package/lib/app-manager/AppManager.js +43 -42
- package/lib/app-manager/ConfigLayerHandler.js +4 -0
- package/lib/docs/AbstractDocumentation.d.ts +100 -0
- package/lib/docs/AbstractDocumentation.js +108 -0
- package/lib/docs/DocumentationHandler.d.ts +100 -0
- package/lib/docs/DocumentationHandler.js +198 -0
- package/lib/docs/FileDocumentation.d.ts +47 -0
- package/lib/docs/FileDocumentation.js +189 -0
- package/lib/docs/docsAgentSkills.d.ts +10 -0
- package/lib/docs/docsAgentSkills.js +93 -0
- package/lib/filters/FilterService.js +4 -3
- package/lib/history-actions/AbstractHistoryAdapter.d.ts +26 -1
- package/lib/history-actions/AbstractHistoryAdapter.js +155 -25
- package/lib/media-manager/helpers/MediaManagerHelper.js +9 -7
- package/lib/model/AbstractModel.d.ts +48 -0
- package/lib/model/AbstractModel.js +14 -4
- package/lib/model/ModelHandler.d.ts +50 -2
- package/lib/model/ModelHandler.js +117 -8
- package/lib/model/adapter/sequelize.d.ts +43 -3
- package/lib/model/adapter/sequelize.js +317 -138
- package/lib/model/adapter/typeorm.d.ts +16 -0
- package/lib/model/adapter/typeorm.js +136 -9
- package/lib/notifications/GeneralNotificationService.js +6 -5
- package/lib/notifications/SystemNotificationService.js +1 -1
- package/lib/ui-i18n/uiTranslationKeys.d.ts +2 -1
- package/lib/ui-i18n/uiTranslationKeys.js +42 -1
- package/lib/widgets/widgetHandler.js +10 -6
- package/models/Group.d.ts +2 -1
- package/package.json +35 -23
- package/policies/authPolicies.js +6 -6
- package/policies/permissionResolvers.d.ts +1 -0
- package/policies/permissionResolvers.js +3 -0
- package/system/Router.js +21 -3
- package/system/bindAccessRights.js +13 -3
- package/system/bindDocs.d.ts +13 -0
- package/system/bindDocs.js +77 -0
- package/system/bindInertia.js +30 -7
- package/system/buildInternalModelAccess.d.ts +8 -0
- package/system/buildInternalModelAccess.js +36 -4
- package/system/defaults.js +7 -1
- package/system/validateSystemModels.js +29 -9
- package/assets/add-DED_wpga.js +0 -1
- package/assets/add-group-CL80RdYb.js +0 -1
- package/assets/add-user-BRmwvQB8.js +0 -1
- package/assets/app-DNXiApCk.css +0 -1
- package/assets/app-layout-86yxiZOy.js +0 -1
- package/assets/app.js +0 -138
- package/assets/dashboard-ZP6K2s_8.js +0 -3
- package/assets/debounce-BMmXVQ06.js +0 -1
- package/assets/history-M61Bcv02.js +0 -1
- package/assets/init-user-8aUohVDq.js +0 -1
- package/assets/login-CO4laf6m.js +0 -1
- package/assets/media-manager-BDbpSSjH.js +0 -6
- package/assets/module-DfBneaUl.js +0 -1
- package/assets/notification-CVnQf3ef.js +0 -20
- package/assets/register-DBYIMXGQ.js +0 -1
- package/assets/ru-rYKdQbWF.js +0 -1
- package/assets/use-appearance-_EbbjHTh.js +0 -1
- package/assets/user-filters-list-DXhGLFTn.js +0 -1
- package/assets/welcome-CLxvXkcg.js +0 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { listModelResources, resolveModelResource } from '../../helpers/modelResourceHelper.js';
|
|
2
|
+
import { listAccessibleMenuItems } from '../../helpers/navigationAccessHelper.js';
|
|
3
|
+
import { DataAccessor } from '../DataAccessor.js';
|
|
4
|
+
import { toJsonSafe } from './jsonSafe.js';
|
|
5
|
+
const MAX_RECORDS = 50;
|
|
6
|
+
const DEFAULT_RECORDS = 10;
|
|
7
|
+
/** The model resources the current user may read or edit, with the CRUD flags of each. */
|
|
8
|
+
async function listPermittedModels(adminizer, user) {
|
|
9
|
+
const has = (token) => adminizer.accessRightsHelper.hasPermission(token, user);
|
|
10
|
+
const permitted = [];
|
|
11
|
+
for (const resource of listModelResources(adminizer)) {
|
|
12
|
+
const entry = {
|
|
13
|
+
resource,
|
|
14
|
+
canRead: await has(`read-${resource.name}-model`),
|
|
15
|
+
canUpdate: await has(`update-${resource.name}-model`),
|
|
16
|
+
canCreate: await has(`create-${resource.name}-model`),
|
|
17
|
+
};
|
|
18
|
+
if (entry.canRead || entry.canUpdate)
|
|
19
|
+
permitted.push(entry);
|
|
20
|
+
}
|
|
21
|
+
return permitted;
|
|
22
|
+
}
|
|
23
|
+
/** Resolves a model this user may use for `action`, throwing when they may not. */
|
|
24
|
+
async function requireModel(adminizer, user, name, action) {
|
|
25
|
+
const resource = resolveModelResource(adminizer, name);
|
|
26
|
+
if (!resource?.model)
|
|
27
|
+
throw new Error(`Model "${name}" is not available.`);
|
|
28
|
+
const token = action === 'read' ? `read-${resource.name}-model` : `update-${resource.name}-model`;
|
|
29
|
+
if (!await adminizer.accessRightsHelper.hasPermission(token, user)) {
|
|
30
|
+
throw new Error(`You are not allowed to ${action} the "${resource.name}" model.`);
|
|
31
|
+
}
|
|
32
|
+
return resource;
|
|
33
|
+
}
|
|
34
|
+
async function modelNames(adminizer, user, action) {
|
|
35
|
+
return (await listPermittedModels(adminizer, user))
|
|
36
|
+
.filter((entry) => (action === 'read' ? entry.canRead : entry.canUpdate))
|
|
37
|
+
.map((entry) => entry.resource.name);
|
|
38
|
+
}
|
|
39
|
+
/** Adds an `enum` of permitted models, so the agent cannot invent a model name. */
|
|
40
|
+
function withModelEnum(schema, models) {
|
|
41
|
+
const properties = { ...schema.properties };
|
|
42
|
+
properties.model = { ...properties.model, enum: models };
|
|
43
|
+
return { ...schema, properties };
|
|
44
|
+
}
|
|
45
|
+
function identifierField(adminizer, resource) {
|
|
46
|
+
return resource.config?.identifierField
|
|
47
|
+
?? adminizer.config.identifierField
|
|
48
|
+
?? resource.model?.primaryKey
|
|
49
|
+
?? 'id';
|
|
50
|
+
}
|
|
51
|
+
function parseCriteria(filter) {
|
|
52
|
+
if (filter === undefined || filter === null || filter === '')
|
|
53
|
+
return {};
|
|
54
|
+
if (typeof filter === 'object')
|
|
55
|
+
return filter;
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(String(filter));
|
|
58
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
59
|
+
throw new Error('not an object');
|
|
60
|
+
return parsed;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new Error('filter must be a JSON object, e.g. {"title": "example"}');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function pickFields(record, fields) {
|
|
67
|
+
return Object.fromEntries(fields.filter((field) => field in record).map((field) => [field, record[field]]));
|
|
68
|
+
}
|
|
69
|
+
function slugify(value) {
|
|
70
|
+
return String(value ?? '').trim().toLowerCase().replace(/[\s_-]+/g, '-');
|
|
71
|
+
}
|
|
72
|
+
/** The sub-actions of a menu item, recursively, as the agent sees them. */
|
|
73
|
+
function describeNavigationActions(actions) {
|
|
74
|
+
return (actions ?? []).map((action) => ({
|
|
75
|
+
id: action.id || action.title,
|
|
76
|
+
title: action.title,
|
|
77
|
+
link: action.link,
|
|
78
|
+
icon: action.icon || undefined,
|
|
79
|
+
target: action.type === 'blank' ? 'blank' : undefined,
|
|
80
|
+
actions: action.subItems?.length ? describeNavigationActions(action.subItems) : undefined,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
function describeNavigationItem(item) {
|
|
84
|
+
return {
|
|
85
|
+
id: item.id,
|
|
86
|
+
title: item.title,
|
|
87
|
+
link: item.link,
|
|
88
|
+
icon: item.icon || undefined,
|
|
89
|
+
target: item.type === 'blank' ? 'blank' : undefined,
|
|
90
|
+
model: item.modelResourceName || undefined,
|
|
91
|
+
actions: item.actions?.length ? describeNavigationActions(item.actions) : undefined,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Field names of a model resource that this user may see or write. */
|
|
95
|
+
function fieldNames(adminizer, user, resource, action) {
|
|
96
|
+
// undefined = no rights on the model at all: report no fields instead of throwing.
|
|
97
|
+
return Object.keys(new DataAccessor(adminizer, user, resource, action).getFieldsConfig() ?? {});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Skills every agent gets for free: who it is talking to, and read/edit access
|
|
101
|
+
* scoped to exactly the data that user may reach in the admin panel. Because
|
|
102
|
+
* every call re-checks the user's permissions, an agent built on them can be
|
|
103
|
+
* offered to non-administrator accounts as well.
|
|
104
|
+
*/
|
|
105
|
+
export function buildBuiltinAgentSkills(adminizer) {
|
|
106
|
+
const currentUser = {
|
|
107
|
+
id: 'current_user',
|
|
108
|
+
description: 'Get the admin user this conversation belongs to: login, name, email, locale, groups and administrator flag.',
|
|
109
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
110
|
+
execute: (_input, { userIdentity }) => userIdentity,
|
|
111
|
+
};
|
|
112
|
+
const listModels = {
|
|
113
|
+
id: 'list_data_models',
|
|
114
|
+
description: 'List the data models the current user may read or edit, with their fields, allowed operations and admin URLs.',
|
|
115
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
116
|
+
execute: async (_input, { user }) => ({
|
|
117
|
+
models: (await listPermittedModels(adminizer, user)).map(({ resource, canRead, canUpdate, canCreate }) => ({
|
|
118
|
+
name: resource.name,
|
|
119
|
+
title: resource.config?.title ?? resource.name,
|
|
120
|
+
canRead,
|
|
121
|
+
canUpdate,
|
|
122
|
+
canCreate,
|
|
123
|
+
identifierField: identifierField(adminizer, resource),
|
|
124
|
+
listUrl: resource.uri,
|
|
125
|
+
recordUrlTemplate: canUpdate ? `${resource.uri}/edit/:id` : undefined,
|
|
126
|
+
fields: fieldNames(adminizer, user, resource, canUpdate ? 'edit' : 'list'),
|
|
127
|
+
})),
|
|
128
|
+
}),
|
|
129
|
+
};
|
|
130
|
+
const listNavigation = {
|
|
131
|
+
id: 'list_admin_navigation',
|
|
132
|
+
description: 'Show the admin panel navigation menu of the current user: sections, their links with sub-actions,'
|
|
133
|
+
+ ' and the templates of parametrized pages such as a single record. Only entries this user may open are returned.',
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
query: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
description: 'Optional words to filter menu items by title, section or path. Omit to get the whole menu.',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
additionalProperties: false,
|
|
143
|
+
},
|
|
144
|
+
execute: async (input, { user }) => {
|
|
145
|
+
const needle = slugify(input.query);
|
|
146
|
+
const matches = (...values) => !needle || slugify(values.filter(Boolean).join(' ')).includes(needle);
|
|
147
|
+
const sections = new Map();
|
|
148
|
+
for (const item of await listAccessibleMenuItems(adminizer, user)) {
|
|
149
|
+
if (!matches(item.title, item.id, item.section, item.link, item.modelResourceName))
|
|
150
|
+
continue;
|
|
151
|
+
const section = item.section || 'Platform';
|
|
152
|
+
if (!sections.has(section))
|
|
153
|
+
sections.set(section, []);
|
|
154
|
+
sections.get(section).push(describeNavigationItem(item));
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
sections: [...sections].map(([section, items]) => ({ section, items })),
|
|
158
|
+
// Record and other parametrized pages have no fixed URL, so the
|
|
159
|
+
// agent gets them as templates to fill in.
|
|
160
|
+
templates: (await adminizer.adminLinkHandler.listTemplates(user))
|
|
161
|
+
.filter((template) => matches(template.title, template.id, template.section, template.description, template.template))
|
|
162
|
+
.map((template) => ({
|
|
163
|
+
id: template.id,
|
|
164
|
+
title: template.title,
|
|
165
|
+
template: template.template,
|
|
166
|
+
description: template.description,
|
|
167
|
+
section: template.section,
|
|
168
|
+
params: template.params,
|
|
169
|
+
})),
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
const readRecords = {
|
|
174
|
+
id: 'read_model_records',
|
|
175
|
+
description: 'Read records of a data model the current user may read.',
|
|
176
|
+
inputSchema: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
properties: {
|
|
179
|
+
model: { type: 'string', description: 'Model name as returned by list_data_models.' },
|
|
180
|
+
filter: { type: 'string', description: 'Optional JSON criteria object, e.g. {"title": "example"}.' },
|
|
181
|
+
fields: { type: 'array', items: { type: 'string' }, description: 'Optional subset of fields to return.' },
|
|
182
|
+
limit: {
|
|
183
|
+
type: 'integer', minimum: 1, maximum: MAX_RECORDS,
|
|
184
|
+
description: `Maximum number of records to return (default ${DEFAULT_RECORDS}).`,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
required: ['model'],
|
|
188
|
+
additionalProperties: false,
|
|
189
|
+
},
|
|
190
|
+
requiresUser: true,
|
|
191
|
+
describe: async (user) => {
|
|
192
|
+
const models = await modelNames(adminizer, user, 'read');
|
|
193
|
+
return {
|
|
194
|
+
description: `Read records of a data model the current user may read. Readable models: ${models.join(', ') || 'none'}.`,
|
|
195
|
+
inputSchema: withModelEnum(readRecords.inputSchema, models),
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
execute: async (input, { user }) => {
|
|
199
|
+
const resource = await requireModel(adminizer, user, String(input.model ?? ''), 'read');
|
|
200
|
+
const criteria = parseCriteria(input.filter);
|
|
201
|
+
const limit = Math.min(Math.max(Math.trunc(Number(input.limit) || DEFAULT_RECORDS), 1), MAX_RECORDS);
|
|
202
|
+
const fields = Array.isArray(input.fields) ? input.fields.map(String) : [];
|
|
203
|
+
const accessor = new DataAccessor(adminizer, user, resource, 'list');
|
|
204
|
+
// `limit` belongs in the query: slicing afterwards would read the whole table.
|
|
205
|
+
// `total` is then a separate COUNT over the same access-scoped criteria.
|
|
206
|
+
const records = await resource.model.find({ ...criteria, limit }, accessor);
|
|
207
|
+
const total = await resource.model.count(criteria, accessor);
|
|
208
|
+
return toJsonSafe({
|
|
209
|
+
model: resource.name,
|
|
210
|
+
total,
|
|
211
|
+
count: records.length,
|
|
212
|
+
records: fields.length ? records.map((record) => pickFields(record, fields)) : records,
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
const updateRecord = {
|
|
217
|
+
id: 'update_model_record',
|
|
218
|
+
description: 'Update one record of a data model the current user may edit.',
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: 'object',
|
|
221
|
+
properties: {
|
|
222
|
+
model: { type: 'string', description: 'Model name as returned by list_data_models.' },
|
|
223
|
+
id: { type: 'string', description: 'Identifier of the record to update.' },
|
|
224
|
+
values: {
|
|
225
|
+
type: 'object',
|
|
226
|
+
description: 'Field values to write, e.g. {"title": "new title"}. Fields this user may not edit are dropped by the panel.',
|
|
227
|
+
additionalProperties: true,
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
required: ['model', 'id', 'values'],
|
|
231
|
+
additionalProperties: false,
|
|
232
|
+
},
|
|
233
|
+
requiresUser: true,
|
|
234
|
+
describe: async (user) => {
|
|
235
|
+
const models = await modelNames(adminizer, user, 'update');
|
|
236
|
+
return {
|
|
237
|
+
description: `Update one record of a data model the current user may edit. Editable models: ${models.join(', ') || 'none'}.`,
|
|
238
|
+
inputSchema: withModelEnum(updateRecord.inputSchema, models),
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
execute: async (input, { user }) => {
|
|
242
|
+
const resource = await requireModel(adminizer, user, String(input.model ?? ''), 'update');
|
|
243
|
+
const id = String(input.id ?? '').trim();
|
|
244
|
+
if (!id)
|
|
245
|
+
throw new Error('id of the record to update is required');
|
|
246
|
+
const values = input.values && typeof input.values === 'object' && !Array.isArray(input.values)
|
|
247
|
+
? input.values
|
|
248
|
+
: null;
|
|
249
|
+
if (!values || !Object.keys(values).length)
|
|
250
|
+
throw new Error('values must be a non-empty object of field values');
|
|
251
|
+
const accessor = new DataAccessor(adminizer, user, resource, 'edit');
|
|
252
|
+
const record = await resource.model.updateOne({ [identifierField(adminizer, resource)]: id }, values, accessor);
|
|
253
|
+
if (!record)
|
|
254
|
+
throw new Error(`Record "${id}" was not found in "${resource.name}".`);
|
|
255
|
+
return toJsonSafe({ model: resource.name, id, record });
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
return [currentUser, listNavigation, listModels, readRecords, updateRecord];
|
|
259
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts arbitrary values (ORM records, Dates, Buffers, cyclic graphs) into
|
|
3
|
+
* plain JSON data.
|
|
4
|
+
*
|
|
5
|
+
* Tool results are stored verbatim in the agent's message history and are
|
|
6
|
+
* re-validated by the AI SDK against its `ModelMessage[]` schema on every
|
|
7
|
+
* subsequent turn. A single non-JSON value — a `Date` from a Sequelize record,
|
|
8
|
+
* for example — therefore does not fail the call that produced it: it poisons
|
|
9
|
+
* the session and every later turn dies with `Invalid prompt: The messages do
|
|
10
|
+
* not match the ModelMessage[] schema`. Passing tool output through this
|
|
11
|
+
* helper keeps that class of failure out of the history.
|
|
12
|
+
*/
|
|
13
|
+
export declare function toJsonSafe<T>(value: T): unknown;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts arbitrary values (ORM records, Dates, Buffers, cyclic graphs) into
|
|
3
|
+
* plain JSON data.
|
|
4
|
+
*
|
|
5
|
+
* Tool results are stored verbatim in the agent's message history and are
|
|
6
|
+
* re-validated by the AI SDK against its `ModelMessage[]` schema on every
|
|
7
|
+
* subsequent turn. A single non-JSON value — a `Date` from a Sequelize record,
|
|
8
|
+
* for example — therefore does not fail the call that produced it: it poisons
|
|
9
|
+
* the session and every later turn dies with `Invalid prompt: The messages do
|
|
10
|
+
* not match the ModelMessage[] schema`. Passing tool output through this
|
|
11
|
+
* helper keeps that class of failure out of the history.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_DEPTH = 12;
|
|
14
|
+
export function toJsonSafe(value) {
|
|
15
|
+
return convert(value, new WeakSet(), 0);
|
|
16
|
+
}
|
|
17
|
+
function convert(value, seen, depth) {
|
|
18
|
+
if (value === null)
|
|
19
|
+
return null;
|
|
20
|
+
switch (typeof value) {
|
|
21
|
+
case 'string':
|
|
22
|
+
case 'boolean':
|
|
23
|
+
return value;
|
|
24
|
+
case 'number':
|
|
25
|
+
return Number.isFinite(value) ? value : null;
|
|
26
|
+
case 'bigint':
|
|
27
|
+
return value.toString();
|
|
28
|
+
case 'undefined':
|
|
29
|
+
case 'function':
|
|
30
|
+
case 'symbol':
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const object = value;
|
|
34
|
+
if (value instanceof Date)
|
|
35
|
+
return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
36
|
+
if (value instanceof Error)
|
|
37
|
+
return value.message;
|
|
38
|
+
if (value instanceof RegExp)
|
|
39
|
+
return value.toString();
|
|
40
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value))
|
|
41
|
+
return `<binary ${value.length} bytes>`;
|
|
42
|
+
if (ArrayBuffer.isView(value))
|
|
43
|
+
return `<binary ${value.byteLength} bytes>`;
|
|
44
|
+
if (value instanceof ArrayBuffer)
|
|
45
|
+
return `<binary ${value.byteLength} bytes>`;
|
|
46
|
+
if (seen.has(object))
|
|
47
|
+
return '[Circular]';
|
|
48
|
+
if (depth >= MAX_DEPTH)
|
|
49
|
+
return '[MaxDepth]';
|
|
50
|
+
seen.add(object);
|
|
51
|
+
try {
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
// JSON.stringify turns array holes and `undefined` items into null.
|
|
54
|
+
return value.map((item) => {
|
|
55
|
+
const converted = convert(item, seen, depth + 1);
|
|
56
|
+
return converted === undefined ? null : converted;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (value instanceof Set)
|
|
60
|
+
return convert([...value], seen, depth);
|
|
61
|
+
if (value instanceof Map) {
|
|
62
|
+
return convert(Object.fromEntries([...value].map(([key, item]) => [String(key), item])), seen, depth);
|
|
63
|
+
}
|
|
64
|
+
// Sequelize instances, Decimal, Luxon and friends: let them describe
|
|
65
|
+
// themselves first, then normalize whatever they produced.
|
|
66
|
+
const toJSON = value.toJSON;
|
|
67
|
+
if (typeof toJSON === 'function') {
|
|
68
|
+
return convert(toJSON.call(value), seen, depth + 1);
|
|
69
|
+
}
|
|
70
|
+
const result = {};
|
|
71
|
+
for (const [key, item] of Object.entries(value)) {
|
|
72
|
+
const converted = convert(item, seen, depth + 1);
|
|
73
|
+
if (converted !== undefined)
|
|
74
|
+
result[key] = converted;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
seen.delete(object);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ActionType, AdminpanelConfig, AdminpanelIcon, ModelConfig } from "../../interfaces/adminpanelConfig.js";
|
|
2
|
-
import type { AccessRightsToken, INotification, ModelResource } from "../../interfaces/types.js";
|
|
2
|
+
import type { AccessRightsToken, INotification, ModelResource, PermissionContext } from "../../interfaces/types.js";
|
|
3
3
|
import type { AbstractCatalog } from "../catalog/AbstractCatalog.js";
|
|
4
4
|
import type { CatalogTemplateComponentResource } from "../catalog/CatalogTemplateComponentHandler.js";
|
|
5
5
|
import type { AppModelAccess } from "../model/ModelHandler.js";
|
|
@@ -8,6 +8,9 @@ import type { Config, ControlType } from "../controls/Control.js";
|
|
|
8
8
|
import type { User } from "../../models/User.js";
|
|
9
9
|
import type { DataAccessor } from "../DataAccessor.js";
|
|
10
10
|
import type { AbstractAiModelService } from "../ai-assistant/AbstractAiModelService.js";
|
|
11
|
+
import type { AiAssistantUiMethod } from '../ai-assistant/AiAssistantUiMethodHandler.js';
|
|
12
|
+
import type { AdminLink, AdminLinkTemplate } from '../admin-links/AdminLinkHandler.js';
|
|
13
|
+
import type { AiAssistantAgentSkill } from '../ai-assistant/AiAssistantAgentSkillHandler.js';
|
|
11
14
|
export type AppDisposer = () => void | Promise<void>;
|
|
12
15
|
export type AppEventName = string | symbol;
|
|
13
16
|
export type AppEventHandler<TPayload = any> = (payload: TPayload, runtime: AppRuntime) => void | Promise<void>;
|
|
@@ -127,9 +130,14 @@ export type AppWidgetResource = AppInfoWidgetResource | AppActionWidgetResource
|
|
|
127
130
|
export interface AppRuntime {
|
|
128
131
|
models: AppModelAccess;
|
|
129
132
|
config: AppRuntimeConfig;
|
|
133
|
+
accessRights: AppRuntimeAccessRights;
|
|
130
134
|
notifications: AppRuntimeNotifications;
|
|
131
135
|
apps: AppRuntimeApps;
|
|
132
136
|
}
|
|
137
|
+
export interface AppRuntimeAccessRights {
|
|
138
|
+
hasPermission(token: string, user: User, context?: PermissionContext): Promise<boolean>;
|
|
139
|
+
getPermissionRights(token: string, user: User): string[] | null;
|
|
140
|
+
}
|
|
133
141
|
export interface AppRuntimeConfig {
|
|
134
142
|
readonly routePrefix: string;
|
|
135
143
|
getModelConfig(modelName: string): ModelConfig | undefined;
|
|
@@ -171,12 +179,28 @@ export interface AppAiAssistantContext {
|
|
|
171
179
|
routePrefix: string;
|
|
172
180
|
getModelResources(): ModelResource[];
|
|
173
181
|
resolveModelResource(modelName: string): ModelResource | undefined;
|
|
174
|
-
hasPermission(token: string, user: User): boolean
|
|
182
|
+
hasPermission(token: string, user: User): Promise<boolean>;
|
|
175
183
|
createDataAccessor(modelResource: ModelResource, user: User, action: ActionType): DataAccessor;
|
|
184
|
+
/** UI tools available to this user, including methods registered by apps. */
|
|
185
|
+
getUiMethods(user: User): Promise<AiAssistantUiMethod[]>;
|
|
176
186
|
}
|
|
177
187
|
export interface AppAiAssistantResource {
|
|
178
188
|
models: Array<(context: AppAiAssistantContext) => AbstractAiModelService | Promise<AbstractAiModelService>>;
|
|
179
189
|
}
|
|
190
|
+
export interface AppAiAssistantUiMethodResource extends AiAssistantUiMethod {
|
|
191
|
+
id: string;
|
|
192
|
+
}
|
|
193
|
+
export interface AppAiAssistantAgentSkillResource extends AiAssistantAgentSkill {
|
|
194
|
+
}
|
|
195
|
+
/** Skills contributed by an app. UI methods are browser skills; agent skills run on the server. */
|
|
196
|
+
export interface AppSkills {
|
|
197
|
+
uiMethod(method: AppAiAssistantUiMethodResource): void;
|
|
198
|
+
agent(skill: AppAiAssistantAgentSkillResource): void;
|
|
199
|
+
}
|
|
200
|
+
export interface AppAdminLinkResource extends AdminLink {
|
|
201
|
+
}
|
|
202
|
+
export interface AppAdminLinkTemplateResource extends AdminLinkTemplate {
|
|
203
|
+
}
|
|
180
204
|
/**
|
|
181
205
|
* Resource registration API provided while an app is being enabled.
|
|
182
206
|
*
|
|
@@ -197,6 +221,11 @@ export interface AppSetupContext {
|
|
|
197
221
|
model(model: AppModelResource): void;
|
|
198
222
|
modelAccess(access: AppModelAccessResource): void;
|
|
199
223
|
aiAssistant(resource: AppAiAssistantResource): void;
|
|
224
|
+
skills: AppSkills;
|
|
225
|
+
/** Register a standalone server page in the admin navigation and agent search. */
|
|
226
|
+
adminLink(link: AppAdminLinkResource): void;
|
|
227
|
+
/** Register a parametrized page (e.g. `/admin/orders/:id/invoice`) the assistant may open. */
|
|
228
|
+
adminLinkTemplate(template: AppAdminLinkTemplateResource): void;
|
|
200
229
|
listener(event: AppEventName, handler: AppEventHandler): void;
|
|
201
230
|
}
|
|
202
231
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Adminizer } from "../Adminizer.js";
|
|
2
2
|
import { DataAccessor } from "../DataAccessor.js";
|
|
3
|
+
import { listModelResources, resolveModelResource } from "../../helpers/modelResourceHelper.js";
|
|
3
4
|
class RuntimeAppSetupContext {
|
|
4
5
|
adminizer;
|
|
5
6
|
appName;
|
|
@@ -7,6 +8,8 @@ class RuntimeAppSetupContext {
|
|
|
7
8
|
pendingModelRegistrations = [];
|
|
8
9
|
pendingModelAccessRegistrations = [];
|
|
9
10
|
pendingAiAssistantRegistrations = [];
|
|
11
|
+
pendingAiAssistantUiMethodRegistrations = [];
|
|
12
|
+
pendingAiAssistantAgentSkillRegistrations = [];
|
|
10
13
|
pendingMediaManagerRegistrations = [];
|
|
11
14
|
pendingCatalogRegistrations = [];
|
|
12
15
|
configLayerIndex = 0;
|
|
@@ -14,7 +17,12 @@ class RuntimeAppSetupContext {
|
|
|
14
17
|
constructor(adminizer, appName) {
|
|
15
18
|
this.adminizer = adminizer;
|
|
16
19
|
this.appName = appName;
|
|
20
|
+
this.skills = {
|
|
21
|
+
uiMethod: (method) => this.registerAiAssistantUiMethod(method),
|
|
22
|
+
agent: (skill) => this.registerAiAssistantAgentSkill(skill),
|
|
23
|
+
};
|
|
17
24
|
}
|
|
25
|
+
skills;
|
|
18
26
|
asset(asset) {
|
|
19
27
|
const resourceId = `${this.appName}:${asset.id}`;
|
|
20
28
|
const url = this.adminizer.assetHandler.register(this.appName, asset);
|
|
@@ -114,6 +122,32 @@ class RuntimeAppSetupContext {
|
|
|
114
122
|
aiAssistant(resource) {
|
|
115
123
|
this.pendingAiAssistantRegistrations.push(() => this.registerAiAssistant(resource));
|
|
116
124
|
}
|
|
125
|
+
registerAiAssistantUiMethod(method) {
|
|
126
|
+
this.pendingAiAssistantUiMethodRegistrations.push(async () => {
|
|
127
|
+
this.adminizer.aiAssistantUiMethodHandler.register(method, this.appName);
|
|
128
|
+
this.disposers.push(() => this.adminizer.aiAssistantUiMethodHandler.unregister(method.id, this.appName));
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
registerAiAssistantAgentSkill(skill) {
|
|
132
|
+
this.pendingAiAssistantAgentSkillRegistrations.push(async () => {
|
|
133
|
+
this.adminizer.aiAssistantAgentSkillHandler.add(skill, this.appName);
|
|
134
|
+
this.disposers.push(() => {
|
|
135
|
+
this.adminizer.aiAssistantAgentSkillHandler.remove(skill.id, this.appName);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
adminLink(link) {
|
|
140
|
+
const resourceId = this.adminizer.adminLinkHandler.add(link, this.appName);
|
|
141
|
+
this.disposers.push(() => {
|
|
142
|
+
this.adminizer.adminLinkHandler.remove(resourceId, this.appName);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
adminLinkTemplate(template) {
|
|
146
|
+
const resourceId = this.adminizer.adminLinkHandler.addTemplate(template, this.appName);
|
|
147
|
+
this.disposers.push(() => {
|
|
148
|
+
this.adminizer.adminLinkHandler.removeTemplate(resourceId, this.appName);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
117
151
|
listener(event, handler) {
|
|
118
152
|
const resourceId = `${this.appName}:${event.toString()}:${this.disposers.length + 1}`;
|
|
119
153
|
const listener = (payload) => handler(payload, this.adminizer.appManager.createRuntime(this.appName));
|
|
@@ -136,6 +170,8 @@ class RuntimeAppSetupContext {
|
|
|
136
170
|
await this.runRegistrationPhase(this.pendingModelRegistrations);
|
|
137
171
|
await this.runRegistrationPhase(this.pendingModelAccessRegistrations);
|
|
138
172
|
await this.runRegistrationPhase(this.pendingAiAssistantRegistrations);
|
|
173
|
+
await this.runRegistrationPhase(this.pendingAiAssistantUiMethodRegistrations);
|
|
174
|
+
await this.runRegistrationPhase(this.pendingAiAssistantAgentSkillRegistrations);
|
|
139
175
|
await this.runRegistrationPhase(this.pendingMediaManagerRegistrations);
|
|
140
176
|
await this.runRegistrationPhase(this.pendingCatalogRegistrations);
|
|
141
177
|
}
|
|
@@ -350,52 +386,13 @@ class RuntimeAppSetupContext {
|
|
|
350
386
|
return {
|
|
351
387
|
runtime: this.adminizer.appManager.createRuntime(this.appName),
|
|
352
388
|
routePrefix: this.adminizer.config.routePrefix,
|
|
353
|
-
getModelResources: () => this.
|
|
354
|
-
resolveModelResource: (modelName) => this.
|
|
389
|
+
getModelResources: () => listModelResources(this.adminizer),
|
|
390
|
+
resolveModelResource: (modelName) => resolveModelResource(this.adminizer, modelName),
|
|
355
391
|
hasPermission: (token, user) => this.adminizer.accessRightsHelper.hasPermission(token, user),
|
|
356
392
|
createDataAccessor: (modelResource, user, action) => new DataAccessor(this.adminizer, user, modelResource, action),
|
|
393
|
+
getUiMethods: (user) => this.adminizer.aiAssistantUiMethodHandler.getAvailable(user),
|
|
357
394
|
};
|
|
358
395
|
}
|
|
359
|
-
getModelResources() {
|
|
360
|
-
const resources = [];
|
|
361
|
-
for (const [configName, configValue] of Object.entries(this.adminizer.config.models ?? {})) {
|
|
362
|
-
const normalizedConfig = this.normalizeModelConfig(configName, configValue);
|
|
363
|
-
const model = this.adminizer.modelHandler.model.get(normalizedConfig.model.toLowerCase());
|
|
364
|
-
if (!model) {
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
resources.push({
|
|
368
|
-
name: configName,
|
|
369
|
-
uri: `${this.adminizer.config.routePrefix}/model/${configName}`,
|
|
370
|
-
config: normalizedConfig,
|
|
371
|
-
model,
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
return resources;
|
|
375
|
-
}
|
|
376
|
-
resolveModelResource(modelName) {
|
|
377
|
-
const loweredName = modelName.toLowerCase();
|
|
378
|
-
return this.getModelResources().find((resource) => {
|
|
379
|
-
const modelId = resource.config.model?.toLowerCase();
|
|
380
|
-
return resource.name.toLowerCase() === loweredName || modelId === loweredName;
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
normalizeModelConfig(name, config) {
|
|
384
|
-
const baseConfig = {
|
|
385
|
-
model: name,
|
|
386
|
-
icon: "description",
|
|
387
|
-
title: name,
|
|
388
|
-
list: true,
|
|
389
|
-
add: true,
|
|
390
|
-
edit: true,
|
|
391
|
-
remove: true,
|
|
392
|
-
view: true,
|
|
393
|
-
};
|
|
394
|
-
if (typeof config === "boolean") {
|
|
395
|
-
return config ? baseConfig : { ...baseConfig, list: false, add: false, edit: false, remove: false, view: false };
|
|
396
|
-
}
|
|
397
|
-
return { ...baseConfig, ...config };
|
|
398
|
-
}
|
|
399
396
|
}
|
|
400
397
|
export class AppManager {
|
|
401
398
|
adminizer;
|
|
@@ -508,6 +505,10 @@ export class AppManager {
|
|
|
508
505
|
return modelKey ? models[modelKey] : undefined;
|
|
509
506
|
},
|
|
510
507
|
},
|
|
508
|
+
accessRights: {
|
|
509
|
+
hasPermission: async (token, user, context) => await this.adminizer.accessRightsHelper.hasPermission(token, user, context),
|
|
510
|
+
getPermissionRights: (token, user) => this.adminizer.accessRightsHelper.getPermissionRights(token, user),
|
|
511
|
+
},
|
|
511
512
|
notifications: {
|
|
512
513
|
send: (notification) => this.adminizer.sendNotification(notification),
|
|
513
514
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { refreshInternalModelAccess } from "../../system/buildInternalModelAccess.js";
|
|
1
2
|
function cloneConfigValue(value) {
|
|
2
3
|
if (Array.isArray(value)) {
|
|
3
4
|
return value.map((item) => cloneConfigValue(item));
|
|
@@ -112,6 +113,9 @@ export class ConfigLayerHandler {
|
|
|
112
113
|
delete targetConfig[key];
|
|
113
114
|
}
|
|
114
115
|
Object.assign(this.adminizer.config, rebuilt);
|
|
116
|
+
// The rebuilt config may change what the record-access resolvers query (e.g. an
|
|
117
|
+
// app-patched accessGraph), so the internal allowlists must follow synchronously.
|
|
118
|
+
refreshInternalModelAccess(this.adminizer);
|
|
115
119
|
}
|
|
116
120
|
getLayerId(appName, layerId) {
|
|
117
121
|
return `${appName}:${layerId}`;
|