adminizer 5.0.0-build.2 → 5.0.0-build.21
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-CkmsvdDi.js +1 -0
- package/assets/add-CESPH_aI.js +1 -0
- package/assets/add-group-CQ4Xm8Ru.js +1 -0
- package/assets/add-user-B-QNjR4B.js +1 -0
- package/assets/ai-assistant/agent.es.js +28362 -0
- package/assets/app-C5Ju5Zra.css +1 -0
- package/assets/app-Ca5yUSBv.js +138 -0
- package/assets/arc-B-Dt8YNl.js +1 -0
- package/assets/architectureDiagram-5GKGNRK7-Ca58kq-l.js +36 -0
- package/assets/asmcrypto-DZeQH0LC.js +1 -0
- package/assets/blockDiagram-NRAW4CY4-CuqE52IG.js +129 -0
- package/assets/c4Diagram-UCG6FXSJ-Cx8z-0Bw.js +38 -0
- package/assets/catalog-CkSbxvx0.js +6 -0
- package/assets/channel-C9z6Pi_j.js +1 -0
- package/assets/chunk-2Q5K7J3B-CXISVKTC.js +1 -0
- package/assets/chunk-5VM5RSS4-DYGeeVYe.js +15 -0
- package/assets/chunk-F27PBJKO-BeY-dbKj.js +1 -0
- package/assets/chunk-G27WJ6UU-DeX_eH7C.js +231 -0
- package/assets/chunk-JWPE2WC7-DaxI9q0u.js +1 -0
- package/assets/chunk-LCL6LL3I-B7PkaEz_.js +206 -0
- package/assets/chunk-POPQ4Y6H-lwu6ZpsN.js +1 -0
- package/assets/chunk-SVP7TREG-CVFllgl1.js +88 -0
- package/assets/chunk-XXDRQBXY-DDLN0rcg.js +1 -0
- package/assets/classDiagram-DTDB5LWJ-BKtCIC3B.js +1 -0
- package/assets/classDiagram-v2-JRS7N3AN-BKtCIC3B.js +1 -0
- package/assets/controls/ckeditor.es.js +9370 -9306
- package/assets/controls/handsontable.es.js +11971 -11938
- package/assets/controls/jsoneditor.es.js +13104 -13008
- package/assets/controls/leaflet.es.js +1023 -1004
- package/assets/controls/toast-ui.es.js +1843 -1834
- package/assets/cose-bilkent-JH36ORCC-CWRyUkhQ.js +1 -0
- package/assets/cynefin-OW5HDTMX-B1iLDXCC.js +166 -0
- package/assets/cynefinDiagram-5FMLGOSQ-DaXQHr3l.js +62 -0
- package/assets/cytoscape.esm-yzknjiTM.js +321 -0
- package/assets/dagre-3AP2YEHR-DFORa4Hz.js +4 -0
- package/assets/dashboard-Ssa1_Of3.js +3 -0
- package/assets/debounce-B3mXI_sx.js +1 -0
- package/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/assets/diagram-S7CK7UJ4-CgP6KBo0.js +30 -0
- package/assets/diagram-UQ7AKVKN-oYfQe6w0.js +41 -0
- package/assets/diagram-VSXAHHWV-DfCU5wgt.js +3 -0
- package/assets/diagram-VX7I27RA-BITJVrNY.js +24 -0
- package/assets/diagram-Z3DM3KII-D8J9Ssjy.js +24 -0
- package/assets/docs-DHMt_Rq1.js +1 -0
- package/assets/ebnfDiagram-PWID7BFC-qpNmoYgx.js +1 -0
- package/assets/erDiagram-SSCWMZ5O-DDF-Y0zs.js +99 -0
- package/assets/flowDiagram-A5DVABFB-CjKVkR9T.js +168 -0
- package/assets/ganttDiagram-EL5Y4UJY-BXGQ53w6.js +292 -0
- package/assets/gitGraphDiagram-WWUBYQGX-BLMevdLh.js +106 -0
- package/assets/history-tBOo2gd5.js +1 -0
- package/assets/infoDiagram-RXCK75RN-BAnPdnyw.js +2 -0
- package/assets/init-Gi6I4Gst.js +1 -0
- package/assets/init-user-DrNUHa6r.js +1 -0
- package/assets/isObject-DL-BuaBl.js +1 -0
- package/assets/ishikawaDiagram-5VMMS53U-PnL0Z4Li.js +70 -0
- package/assets/journeyDiagram-EYS64GPL-CLcsCSwF.js +139 -0
- package/assets/kanban-definition-3QL26DDD-C2eU3uBE.js +89 -0
- package/assets/katex-HP8lGamR.js +257 -0
- package/assets/layout-BXoYdHdx.js +1 -0
- package/assets/linear-BSzrP0oP.js +1 -0
- package/assets/list-CLg6xeIK.js +4 -0
- package/assets/login-Ce7o3-o_.js +1 -0
- package/assets/manifest.json +834 -58
- package/assets/{media-manager-BDbpSSjH.js → media-manager-D7G1P3fz.js} +3 -3
- package/assets/mermaid.core-aKMJHeEk.js +310 -0
- package/assets/mindmap-definition-FBJOCRG2-Dzbz5ghA.js +96 -0
- package/assets/module-BsXph_pA.js +1 -0
- package/assets/notification-D-Zz1gF_.js +20 -0
- package/assets/ordinal-Cboi1Yqb.js +1 -0
- package/assets/pegDiagram-XKGWAZYB-BTdWkU0Y.js +1 -0
- package/assets/pieDiagram-E7YTZNPT-Dnw8-TB-.js +39 -0
- package/assets/quadrantDiagram-AXDQQJYC-CALzLlg9.js +7 -0
- package/assets/railroadDiagram-O6MQD6OU-CTA1vzcv.js +1 -0
- package/assets/register-CNnBkzej.js +1 -0
- package/assets/requirementDiagram-EFPCY7ZU-UX2CF5HA.js +84 -0
- package/assets/ru-CoNgAwFK.js +1 -0
- package/assets/sankeyDiagram-P5KCCOFB-4aPp3ATV.js +40 -0
- package/assets/sequenceDiagram-WJ2MYXX4--kltkwJw.js +162 -0
- package/assets/sizeCapture-X5ZJPWSS-Ca9-g5QY.js +1 -0
- package/assets/stateDiagram-HBIQ2CUA-Diaf5Q7I.js +1 -0
- package/assets/stateDiagram-v2-4QOOHH4V-C6FlRp7u.js +1 -0
- package/assets/swimlanes-XN3QIQJK-DhqX_myE.js +1 -0
- package/assets/swimlanesDiagram-VK2B7HYN-i_3LTEBk.js +8 -0
- package/assets/timeline-definition-24CTP7MA-CwkJQbhq.js +120 -0
- package/assets/toNumber-DZb-r9sy.js +1 -0
- package/assets/use-appearance-DC-DESKB.js +1 -0
- package/assets/user-filters-list-W47DTBp2.js +1 -0
- package/assets/vennDiagram-4TSXK5OY-BNM3_cVS.js +34 -0
- package/assets/wardleyDiagram-VM6X3IG4-BODb1NP8.js +78 -0
- package/assets/welcome-BeBm6NIi.js +1 -0
- package/assets/with-app-layout-B90DtzYC.js +31 -0
- package/assets/xychartDiagram-S5SC5T6Z-DthqEh3g.js +7 -0
- package/controllers/access-rights/permissionOptions.d.ts +2 -0
- package/controllers/access-rights/permissionOptions.js +23 -0
- package/controllers/add.js +1 -1
- 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.d.ts +1 -1
- package/controllers/catalog/FrontentCatalogAdapter.d.ts +1 -1
- package/controllers/docs/index.d.ts +22 -0
- package/controllers/docs/index.js +126 -0
- package/controllers/edit.js +1 -1
- package/controllers/editGroup.js +15 -6
- package/controllers/feedback/feedbackController.d.ts +1 -1
- package/controllers/filter-fields/savedFilters.js +1 -1
- package/controllers/history-actions/HistoryController.d.ts +1 -1
- package/controllers/history-actions/HistoryController.js +6 -6
- package/controllers/list.js +1 -1
- package/controllers/login.js +1 -1
- package/controllers/media-manager/mediaManagerAdapter.d.ts +1 -1
- package/controllers/notifications/NotificationController.js +14 -8
- package/controllers/view.js +2 -2
- package/controllers/widgets/filterInfo.js +1 -1
- package/helpers/accessRightsHelper.d.ts +11 -4
- package/helpers/accessRightsHelper.js +82 -12
- package/helpers/assetVersionHelper.d.ts +14 -0
- package/helpers/assetVersionHelper.js +42 -0
- package/helpers/configHelper.d.ts +3 -3
- package/helpers/configHelper.js +20 -18
- package/helpers/controllerHelper.d.ts +3 -2
- package/helpers/controllerHelper.js +25 -15
- package/helpers/fieldsHelper.d.ts +2 -2
- package/helpers/fieldsHelper.js +3 -3
- package/helpers/filterDatetimeHelper.d.ts +1 -1
- package/helpers/inertiaActionsHelper.d.ts +3 -3
- package/helpers/inertiaActionsHelper.js +4 -5
- package/helpers/inertiaAddHelper.d.ts +5 -5
- package/helpers/inertiaAddHelper.js +7 -6
- package/helpers/inertiaGroupHelper.d.ts +3 -3
- package/helpers/inertiaGroupHelper.js +10 -2
- package/helpers/inertiaListHelper.d.ts +4 -4
- package/helpers/inertiaListHelper.js +7 -8
- package/helpers/inertiaMenuHelper.d.ts +2 -3
- package/helpers/inertiaMenuHelper.js +3 -26
- package/helpers/inertiaUserHelper.d.ts +3 -3
- package/helpers/menuHelper.d.ts +2 -2
- 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/helpers/translationHelper.d.ts +2 -2
- package/index.d.ts +69 -58
- package/index.js +11 -0
- package/interfaces/adminpanelConfig.d.ts +18 -5
- package/interfaces/global.d.ts +6 -6
- package/interfaces/internalModelAccess.d.ts +1 -1
- package/interfaces/listQueryBuilder.d.ts +1 -1
- package/interfaces/types.d.ts +171 -2
- package/lib/Adminizer.d.ts +46 -26
- package/lib/Adminizer.js +37 -1
- package/lib/DataAccessor.d.ts +7 -6
- package/lib/DataAccessor.js +50 -48
- package/lib/MiddlewareManager.d.ts +1 -1
- package/lib/access-rights/AccessRightsHandler.d.ts +2 -2
- package/lib/admin-links/AdminLinkHandler.d.ts +72 -0
- package/lib/admin-links/AdminLinkHandler.js +175 -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 +140 -3
- 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 +58 -0
- package/lib/ai-assistant/AiAssistantAgentSkillHandler.js +87 -0
- package/lib/ai-assistant/AiAssistantHandler.d.ts +16 -5
- package/lib/ai-assistant/AiAssistantHandler.js +35 -1
- 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 +40 -11
- package/lib/app-manager/AppManager.d.ts +2 -2
- package/lib/app-manager/AppManager.js +54 -50
- package/lib/app-manager/AssetHandler.d.ts +2 -2
- package/lib/app-manager/ConfigLayerHandler.d.ts +2 -2
- package/lib/app-manager/ControllerHandler.d.ts +2 -2
- package/lib/catalog/AbstractCatalog.d.ts +1 -1
- package/lib/catalog/CatalogHandler.d.ts +1 -1
- package/lib/controls/ControlsHandler.d.ts +1 -1
- package/lib/controls/codeEditor/MonacoEditor.d.ts +1 -1
- package/lib/controls/geojsoneditor/GeoEditor.d.ts +1 -1
- package/lib/controls/jsoneditor/JsonEditor.d.ts +1 -1
- package/lib/controls/markdown/ToastUiEditor.d.ts +1 -1
- package/lib/controls/table/Handsontable.d.ts +1 -1
- package/lib/controls/wysiwyg/CKeditor.d.ts +1 -1
- 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/feedback/FeedbackHandler.d.ts +2 -2
- package/lib/filters/ConditionValidator.d.ts +1 -1
- package/lib/filters/FilterCustomFieldHandler.d.ts +2 -2
- package/lib/filters/FilterService.d.ts +4 -4
- package/lib/filters/FilterService.js +4 -3
- package/lib/filters/index.d.ts +6 -6
- package/lib/history-actions/AbstractHistoryAdapter.d.ts +6 -6
- package/lib/history-actions/AbstractHistoryAdapter.js +11 -9
- package/lib/history-actions/DefaultHistoryAdapter.d.ts +4 -4
- package/lib/history-actions/HistoryHandler.d.ts +1 -1
- package/lib/list-query-builder/ListQueryBuilder.d.ts +7 -7
- package/lib/media-manager/AbstractMediaManager.d.ts +2 -2
- package/lib/media-manager/MediaManagerHandler.d.ts +1 -1
- package/lib/media-manager/Thumb.d.ts +1 -1
- package/lib/media-manager/helpers/MediaManagerHelper.d.ts +5 -5
- package/lib/model/AbstractModel.d.ts +11 -4
- package/lib/model/InternalModelAccessFactory.d.ts +2 -2
- package/lib/model/ModelHandler.d.ts +50 -5
- package/lib/model/ModelHandler.js +107 -8
- package/lib/model/adapter/sequelize.d.ts +7 -5
- package/lib/model/adapter/sequelize.js +79 -46
- package/lib/model/adapter/typeorm.d.ts +2 -2
- package/lib/model/adapter/typeorm.js +1 -1
- package/lib/model/internalModelRepository.d.ts +2 -2
- package/lib/notifications/AbstractNotificationService.d.ts +4 -4
- package/lib/notifications/GeneralNotificationService.d.ts +2 -2
- package/lib/notifications/GeneralNotificationService.js +6 -5
- package/lib/notifications/NotificationHandler.d.ts +2 -2
- package/lib/notifications/SystemNotificationService.d.ts +2 -2
- package/lib/notifications/SystemNotificationService.js +1 -1
- package/lib/requestProcessor.d.ts +1 -1
- package/lib/ui-i18n/uiTranslationKeys.d.ts +2 -1
- package/lib/ui-i18n/uiTranslationKeys.js +42 -1
- package/lib/widgets/abstractAction.d.ts +1 -1
- package/lib/widgets/abstractCustom.d.ts +2 -2
- package/lib/widgets/abstractInfo.d.ts +1 -1
- package/lib/widgets/abstractLink.d.ts +2 -2
- package/lib/widgets/abstractSwitch.d.ts +1 -1
- package/lib/widgets/abstractWidgetBase.d.ts +1 -1
- package/lib/widgets/widgetHandler.d.ts +9 -9
- package/lib/widgets/widgetHandler.js +5 -1
- package/models/Filter.d.ts +1 -1
- package/models/FilterColumn.d.ts +1 -1
- package/models/Group.d.ts +3 -2
- package/models/HistoryActions.d.ts +1 -1
- package/models/MediaManagerAP.d.ts +2 -2
- package/models/MediaManagerAssociationsAP.d.ts +1 -1
- package/models/MediaManagerMetaAP.d.ts +1 -1
- package/models/User.d.ts +2 -2
- package/models/UserNotification.d.ts +1 -1
- package/package.json +29 -20
- package/policies/authPolicies.js +6 -6
- package/policies/permissionResolvers.d.ts +1 -0
- package/policies/permissionResolvers.js +3 -0
- package/services/FeedService.d.ts +3 -3
- package/system/Router.d.ts +1 -1
- package/system/Router.js +27 -1
- package/system/bindAccessRights.d.ts +1 -1
- package/system/bindAccessRights.js +2 -3
- package/system/bindAuthorization.d.ts +1 -1
- package/system/bindControls.d.ts +1 -1
- package/system/bindCors.d.ts +1 -1
- package/system/bindCustomFilterHandlers.d.ts +1 -1
- package/system/bindDashboardWidgets.d.ts +1 -1
- package/system/bindDev.d.ts +1 -1
- package/system/bindDocs.d.ts +13 -0
- package/system/bindDocs.js +77 -0
- package/system/bindHistory.d.ts +1 -1
- package/system/bindInertia.d.ts +1 -1
- package/system/bindInertia.js +23 -1
- package/system/bindMediaManager.d.ts +1 -1
- package/system/bindNotifications.d.ts +1 -1
- package/system/bindReqFunctions.d.ts +1 -1
- package/system/bindTranslations.d.ts +1 -1
- package/system/buildInternalModelAccess.d.ts +3 -3
- package/system/buildInternalModelAccess.js +5 -2
- package/system/defaults.d.ts +1 -1
- package/system/routePolicies.d.ts +1 -1
- package/system/systemModelContracts.d.ts +1 -1
- package/system/systemModelContracts.js +11 -7
- package/system/validateSystemModels.d.ts +1 -1
- package/system/validateSystemModels.js +29 -9
- package/assets/add-group-BdHFzKng.js +0 -1
- package/assets/add-p6yGCoZ7.js +0 -1
- package/assets/add-user-CDspCLu4.js +0 -1
- package/assets/app-DNXiApCk.css +0 -1
- package/assets/app-layout-DCr_-JY3.js +0 -1
- package/assets/app.js +0 -138
- package/assets/catalog-XN-_p9TB.js +0 -6
- package/assets/dashboard-CHZO2QFY.js +0 -3
- package/assets/debounce-BMmXVQ06.js +0 -1
- package/assets/history-CQd0FojQ.js +0 -1
- package/assets/init-user-8aUohVDq.js +0 -1
- package/assets/list-PHWgTHsg.js +0 -4
- package/assets/login-CO4laf6m.js +0 -1
- package/assets/module-DVyQtIxm.js +0 -1
- package/assets/notification-B87XG8nQ.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-BRzDbPkx.js +0 -1
- package/assets/welcome-DiXmrTgc.js +0 -1
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);
|
|
@@ -157,7 +156,11 @@ export class DataAccessor {
|
|
|
157
156
|
// Set required and type attributes
|
|
158
157
|
fldConfig.required = Boolean(fldConfig.required ?? modelField.required);
|
|
159
158
|
// Default type for field. Could be fetched from config file or model if not defined in config file.
|
|
160
|
-
|
|
159
|
+
// The model layer has a single temporal type (`date`), the UI distinguishes
|
|
160
|
+
// date/datetime/time. Default to `datetime` so the time part survives editing;
|
|
161
|
+
// a date-only column can be narrowed with `type: "date"` in the model config.
|
|
162
|
+
const modelFieldType = modelField.type === "date" ? "datetime" : modelField.type;
|
|
163
|
+
fldConfig.type = (fldConfig.type || modelFieldType).toLowerCase();
|
|
161
164
|
// Normalize final configuration (fldConfig is always an object here, normalize never returns undefined)
|
|
162
165
|
fldConfig = this.adminizer.configHelper.normalizeFieldConfig(this.adminizer, fldConfig, key, modelField);
|
|
163
166
|
// Add new field to result set
|
|
@@ -182,17 +185,16 @@ export class DataAccessor {
|
|
|
182
185
|
Object.keys(fields).forEach(appendField);
|
|
183
186
|
return orderedFields;
|
|
184
187
|
}
|
|
185
|
-
getAssociatedFieldsConfig(
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const modelConfig = this.getModelConfig(resolvedModelName);
|
|
188
|
+
getAssociatedFieldsConfig(resourceName) {
|
|
189
|
+
const model = this.adminizer.modelHandler.getResource(resourceName);
|
|
190
|
+
const modelConfig = this.getModelConfig(resourceName);
|
|
189
191
|
if (!model || !modelConfig) {
|
|
190
192
|
return undefined;
|
|
191
193
|
}
|
|
192
194
|
// Check if user has access to the associated model
|
|
193
|
-
const tokenId = `read-${
|
|
195
|
+
const tokenId = `read-${resourceName}-${MODEL_TOKEN_SUFFIX}`;
|
|
194
196
|
if (!this.adminizer.accessRightsHelper.hasPermission(tokenId, this.user)) {
|
|
195
|
-
Adminizer.log.debug(`getAssociatedFieldsConfig > No access rights to ${this.actionVerb} model: ${
|
|
197
|
+
Adminizer.log.debug(`getAssociatedFieldsConfig > No access rights to ${this.actionVerb} model: ${resourceName}`);
|
|
196
198
|
return undefined;
|
|
197
199
|
}
|
|
198
200
|
const associatedFields = {};
|
|
@@ -375,7 +377,7 @@ export class DataAccessor {
|
|
|
375
377
|
async sanitizeUserRelationAccess(criteria = {}) {
|
|
376
378
|
let sanitizedCriteria = {};
|
|
377
379
|
// Retrieve model configuration from adminpanel config
|
|
378
|
-
const modelName = this.modelResource.
|
|
380
|
+
const modelName = this.modelResource.name;
|
|
379
381
|
const modelConfig = this.modelResource.config;
|
|
380
382
|
// Check if the model has `userAccessRelation` configured
|
|
381
383
|
if (!this.user.isAdministrator && modelConfig && modelConfig.userAccessRelation) {
|
|
@@ -388,18 +390,18 @@ export class DataAccessor {
|
|
|
388
390
|
// Check if the relation points to `User` or `Group` in the model's attributes
|
|
389
391
|
const modelAttributes = this.modelResource.model.attributes;
|
|
390
392
|
const relation = modelAttributes[accessField];
|
|
391
|
-
const relationModel = this.resolveRelationTarget(relation?.model);
|
|
392
|
-
const relationCollection = this.resolveRelationTarget(relation?.collection);
|
|
393
|
-
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 ?? "")) {
|
|
394
396
|
throw new Error(`Invalid userAccessRelation configuration for model ${modelName}`);
|
|
395
397
|
}
|
|
396
398
|
// Determine if the current user matches the access criteria
|
|
397
399
|
if (relationModel) {
|
|
398
|
-
if (relationModel === '
|
|
400
|
+
if (relationModel === 'User') {
|
|
399
401
|
// Filter by the user's ID if related to User as a model
|
|
400
402
|
sanitizedCriteria = { ...sanitizedCriteria, [accessField]: this.user.id };
|
|
401
403
|
}
|
|
402
|
-
else if (relationModel === '
|
|
404
|
+
else if (relationModel === 'Group') {
|
|
403
405
|
// Filter by user's group membership if related to Group as a model
|
|
404
406
|
const userGroups = this.user.groups?.map((group) => group.id);
|
|
405
407
|
sanitizedCriteria = { ...sanitizedCriteria, [accessField]: { in: userGroups } };
|
|
@@ -408,11 +410,11 @@ export class DataAccessor {
|
|
|
408
410
|
/** Warning: collection relation access is not supported and needs adapter-level processing */
|
|
409
411
|
if (relationCollection) {
|
|
410
412
|
Adminizer.log.warn(`Collection relation is not supported and was not tested. You may have an error here: ${JSON.stringify(relation, null, 2)}`);
|
|
411
|
-
if (relationCollection === '
|
|
413
|
+
if (relationCollection === 'User') {
|
|
412
414
|
// Ensure user's ID is part of the associated collection to User
|
|
413
415
|
sanitizedCriteria = { ...sanitizedCriteria, [accessField]: { contains: this.user.id } };
|
|
414
416
|
}
|
|
415
|
-
else if (relationCollection === '
|
|
417
|
+
else if (relationCollection === 'Group') {
|
|
416
418
|
// Ensure user's groups intersect with the collection to Group
|
|
417
419
|
const userGroups = this.user.groups?.map((group) => group.id);
|
|
418
420
|
sanitizedCriteria = { ...sanitizedCriteria, [accessField]: { intersects: userGroups } };
|
|
@@ -429,19 +431,19 @@ export class DataAccessor {
|
|
|
429
431
|
throw new Error(`Invalid intermediate relation configuration for field "${field}" in model ${modelName}`);
|
|
430
432
|
}
|
|
431
433
|
// Retrieve the intermediate model
|
|
432
|
-
const intermediateModel = this.
|
|
434
|
+
const intermediateModel = this.getRelationModel(intermediateRelation.model, intermediateRelation.resourceName);
|
|
433
435
|
if (!intermediateModel) {
|
|
434
436
|
throw new Error(`Intermediate model "${intermediateRelation.model}" not found`);
|
|
435
437
|
}
|
|
436
438
|
// Validate the `via` field in the intermediate model
|
|
437
439
|
const intermediateAttributes = intermediateModel.attributes;
|
|
438
440
|
const viaRelation = intermediateAttributes[via];
|
|
439
|
-
if (!viaRelation || this.resolveRelationTarget(viaRelation.model) !== '
|
|
441
|
+
if (!viaRelation || this.resolveRelationTarget(viaRelation.model, viaRelation.resourceName) !== 'User') {
|
|
440
442
|
throw new Error(`Unsupported or invalid via field "${via}" in intermediate model "${intermediateRelation.model}". ` +
|
|
441
443
|
`Currently, only relations to "User" are supported`);
|
|
442
444
|
}
|
|
443
445
|
// Fetch all intermediate records associated with the user
|
|
444
|
-
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 } });
|
|
445
447
|
const intermediateIds = (intermediateRecords || []).map((r) => r.id);
|
|
446
448
|
// Filter main model by all matching intermediate record IDs
|
|
447
449
|
sanitizedCriteria = { ...sanitizedCriteria, [field]: { in: intermediateIds } };
|
|
@@ -467,14 +469,14 @@ export class DataAccessor {
|
|
|
467
469
|
// Check if the relation points to `User` or `Group` in the model's attributes
|
|
468
470
|
const modelAttributes = this.modelResource.model.attributes;
|
|
469
471
|
const relation = modelAttributes[accessField];
|
|
470
|
-
const relationModel = this.resolveRelationTarget(relation?.model);
|
|
471
|
-
if (relation && ['
|
|
472
|
-
if (relationModel === '
|
|
472
|
+
const relationModel = this.resolveRelationTarget(relation?.model, relation?.resourceName);
|
|
473
|
+
if (relation && ['User', 'Group'].includes(relationModel ?? "")) {
|
|
474
|
+
if (relationModel === 'User') {
|
|
473
475
|
if (!this.user.isAdministrator) {
|
|
474
476
|
updatedRecord[accessField] = this.user.id;
|
|
475
477
|
}
|
|
476
478
|
}
|
|
477
|
-
else if (relationModel === '
|
|
479
|
+
else if (relationModel === 'Group') {
|
|
478
480
|
const userGroups = this.user.groups || [];
|
|
479
481
|
if (userGroups.length === 1) {
|
|
480
482
|
updatedRecord[accessField] = userGroups[0].id;
|
|
@@ -496,16 +498,16 @@ export class DataAccessor {
|
|
|
496
498
|
const modelAttributes = this.modelResource.model.attributes;
|
|
497
499
|
const intermediateRelation = modelAttributes[field];
|
|
498
500
|
if (!intermediateRelation || !intermediateRelation.model) {
|
|
499
|
-
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}`);
|
|
500
502
|
}
|
|
501
|
-
const intermediateModel = this.
|
|
503
|
+
const intermediateModel = this.getRelationModel(intermediateRelation.model, intermediateRelation.resourceName);
|
|
502
504
|
if (!intermediateModel) {
|
|
503
505
|
throw new Error(`Intermediate model "${intermediateRelation.model}" not found`);
|
|
504
506
|
}
|
|
505
507
|
// Validate the `via` field in the intermediate model
|
|
506
508
|
const intermediateAttributes = intermediateModel.attributes;
|
|
507
509
|
const viaRelation = intermediateAttributes[via];
|
|
508
|
-
if (!viaRelation || this.resolveRelationTarget(viaRelation.model) !== '
|
|
510
|
+
if (!viaRelation || this.resolveRelationTarget(viaRelation.model, viaRelation.resourceName) !== 'User') {
|
|
509
511
|
throw new Error(`Unsupported or invalid via field "${via}" in intermediate model "${intermediateRelation.model}". ` +
|
|
510
512
|
`Currently, only relations to "User" are supported`);
|
|
511
513
|
}
|
|
@@ -513,7 +515,7 @@ export class DataAccessor {
|
|
|
513
515
|
const chosenId = typeof updatedRecord[field] === 'object'
|
|
514
516
|
? updatedRecord[field][intermediatePk]
|
|
515
517
|
: updatedRecord[field];
|
|
516
|
-
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 } });
|
|
517
519
|
if (!record) {
|
|
518
520
|
throw new Error(`Access denied: "${field}" does not belong to the current user`);
|
|
519
521
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { AccessRightsToken } from "../../interfaces/types";
|
|
2
|
-
import type { Adminizer } from "../Adminizer";
|
|
1
|
+
import type { AccessRightsToken } from "../../interfaces/types.js";
|
|
2
|
+
import type { Adminizer } from "../Adminizer.js";
|
|
3
3
|
export interface AccessRightsRecord {
|
|
4
4
|
id: string;
|
|
5
5
|
appName: string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { User } from '../../models/User.js';
|
|
2
|
+
import type { Adminizer } from '../Adminizer.js';
|
|
3
|
+
export interface AdminLink {
|
|
4
|
+
id: string;
|
|
5
|
+
type: string;
|
|
6
|
+
name: string;
|
|
7
|
+
link: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
section?: string;
|
|
10
|
+
accessRightsToken?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ResolvedAdminLink extends AdminLink {
|
|
13
|
+
owner: string;
|
|
14
|
+
title: string;
|
|
15
|
+
}
|
|
16
|
+
/** A placeholder of an admin link template, e.g. `id` in `/model/Test/edit/:id`. */
|
|
17
|
+
export interface AdminLinkTemplateParam {
|
|
18
|
+
name: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A parametrized admin page, e.g. `/admin/model/Test/edit/:id`. Templates are
|
|
23
|
+
* what makes a record page reachable: the concrete URL only exists once the
|
|
24
|
+
* caller knows the record id.
|
|
25
|
+
*/
|
|
26
|
+
export interface AdminLinkTemplate {
|
|
27
|
+
id: string;
|
|
28
|
+
title: string;
|
|
29
|
+
/** Absolute admin path with `:param` placeholders. */
|
|
30
|
+
template: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
section?: string;
|
|
33
|
+
accessRightsToken?: string;
|
|
34
|
+
/** Placeholder descriptions; placeholders absent here are still required. */
|
|
35
|
+
params?: AdminLinkTemplateParam[];
|
|
36
|
+
}
|
|
37
|
+
export interface ResolvedAdminLinkTemplate extends AdminLinkTemplate {
|
|
38
|
+
owner: string;
|
|
39
|
+
params: AdminLinkTemplateParam[];
|
|
40
|
+
}
|
|
41
|
+
/** Server-side registry of standalone admin pages contributed by applications. */
|
|
42
|
+
export declare class AdminLinkHandler {
|
|
43
|
+
private readonly adminizer;
|
|
44
|
+
private readonly links;
|
|
45
|
+
private readonly templates;
|
|
46
|
+
constructor(adminizer: Adminizer);
|
|
47
|
+
add(link: AdminLink, owner?: string): string;
|
|
48
|
+
remove(id: string, owner?: string): boolean;
|
|
49
|
+
list(user: User, type?: string): ResolvedAdminLink[];
|
|
50
|
+
resolve(user: User, type: string, name: string): ResolvedAdminLink | undefined;
|
|
51
|
+
addTemplate(template: AdminLinkTemplate, owner?: string): string;
|
|
52
|
+
removeTemplate(id: string, owner?: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Every parametrized page this user may open: model record pages, catalog
|
|
55
|
+
* items, templates registered by apps and any navigation link that carries
|
|
56
|
+
* placeholders of its own.
|
|
57
|
+
*/
|
|
58
|
+
listTemplates(user: User): ResolvedAdminLinkTemplate[];
|
|
59
|
+
/**
|
|
60
|
+
* Turns a template id (or its title) plus placeholder values into a real
|
|
61
|
+
* admin URL, rejecting templates this user may not open.
|
|
62
|
+
*/
|
|
63
|
+
resolveTemplate(user: User, id: string, params?: Record<string, unknown>): string;
|
|
64
|
+
/** Fills the placeholders of a template with url-encoded values. */
|
|
65
|
+
buildHref(template: ResolvedAdminLinkTemplate, params?: Record<string, unknown>): string;
|
|
66
|
+
/** True for paths that change data and therefore must never be navigated to. */
|
|
67
|
+
isDestructivePath(path: string): boolean;
|
|
68
|
+
/** Templates derived from the current configuration, catalogs and navigation. */
|
|
69
|
+
private builtinTemplates;
|
|
70
|
+
private describeParams;
|
|
71
|
+
private slug;
|
|
72
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/** Segments no template may point at: opening one mutates data. */
|
|
2
|
+
const DESTRUCTIVE_PATH = /\/model\/[^/]+\/remove(\/|$)/i;
|
|
3
|
+
/** Server-side registry of standalone admin pages contributed by applications. */
|
|
4
|
+
export class AdminLinkHandler {
|
|
5
|
+
adminizer;
|
|
6
|
+
links = new Map();
|
|
7
|
+
templates = new Map();
|
|
8
|
+
constructor(adminizer) {
|
|
9
|
+
this.adminizer = adminizer;
|
|
10
|
+
}
|
|
11
|
+
add(link, owner = 'host') {
|
|
12
|
+
const type = link.type.trim();
|
|
13
|
+
const name = link.name.trim();
|
|
14
|
+
const href = link.link.trim();
|
|
15
|
+
if (!type || !name || !href)
|
|
16
|
+
throw new Error('Admin link requires type, name and link');
|
|
17
|
+
if (!href.startsWith('/'))
|
|
18
|
+
throw new Error(`Admin link "${name}" must use an absolute admin path`);
|
|
19
|
+
const id = `${this.slug(type)}:${this.slug(name)}`;
|
|
20
|
+
if (this.links.has(id))
|
|
21
|
+
throw new Error(`Admin link "${id}" is already registered`);
|
|
22
|
+
this.links.set(id, {
|
|
23
|
+
...link, id, type, name, link: href, owner,
|
|
24
|
+
title: link.title?.trim() || name,
|
|
25
|
+
section: link.section?.trim() || undefined,
|
|
26
|
+
});
|
|
27
|
+
return id;
|
|
28
|
+
}
|
|
29
|
+
remove(id, owner) {
|
|
30
|
+
const link = this.links.get(id);
|
|
31
|
+
if (!link || (owner && link.owner !== owner))
|
|
32
|
+
return false;
|
|
33
|
+
return this.links.delete(id);
|
|
34
|
+
}
|
|
35
|
+
list(user, type) {
|
|
36
|
+
const wanted = type ? this.slug(type) : null;
|
|
37
|
+
return [...this.links.values()].filter((link) => (!wanted || this.slug(link.type) === wanted)
|
|
38
|
+
&& (!link.accessRightsToken || this.adminizer.accessRightsHelper.hasPermission(link.accessRightsToken, user)));
|
|
39
|
+
}
|
|
40
|
+
resolve(user, type, name) {
|
|
41
|
+
const wanted = this.slug(name);
|
|
42
|
+
return this.list(user, type).find((link) => this.slug(link.name) === wanted || this.slug(link.title) === wanted);
|
|
43
|
+
}
|
|
44
|
+
addTemplate(template, owner = 'host') {
|
|
45
|
+
const id = template.id.trim();
|
|
46
|
+
const title = template.title.trim();
|
|
47
|
+
const path = template.template.trim();
|
|
48
|
+
if (!id || !title || !path)
|
|
49
|
+
throw new Error('Admin link template requires id, title and template');
|
|
50
|
+
if (!path.startsWith('/'))
|
|
51
|
+
throw new Error(`Admin link template "${id}" must use an absolute admin path`);
|
|
52
|
+
if (this.templates.has(id))
|
|
53
|
+
throw new Error(`Admin link template "${id}" is already registered`);
|
|
54
|
+
this.templates.set(id, {
|
|
55
|
+
...template, id, title, template: path, owner,
|
|
56
|
+
description: template.description?.trim() || undefined,
|
|
57
|
+
section: template.section?.trim() || undefined,
|
|
58
|
+
params: this.describeParams(path, template.params),
|
|
59
|
+
});
|
|
60
|
+
return id;
|
|
61
|
+
}
|
|
62
|
+
removeTemplate(id, owner) {
|
|
63
|
+
const template = this.templates.get(id);
|
|
64
|
+
if (!template || (owner && template.owner !== owner))
|
|
65
|
+
return false;
|
|
66
|
+
return this.templates.delete(id);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Every parametrized page this user may open: model record pages, catalog
|
|
70
|
+
* items, templates registered by apps and any navigation link that carries
|
|
71
|
+
* placeholders of its own.
|
|
72
|
+
*/
|
|
73
|
+
listTemplates(user) {
|
|
74
|
+
const templates = [];
|
|
75
|
+
const push = (template) => {
|
|
76
|
+
if (DESTRUCTIVE_PATH.test(template.template))
|
|
77
|
+
return;
|
|
78
|
+
if (template.accessRightsToken && !this.adminizer.accessRightsHelper.hasPermission(template.accessRightsToken, user))
|
|
79
|
+
return;
|
|
80
|
+
if (templates.some((existing) => existing.template === template.template || existing.id === template.id))
|
|
81
|
+
return;
|
|
82
|
+
templates.push(template);
|
|
83
|
+
};
|
|
84
|
+
for (const template of this.builtinTemplates(user))
|
|
85
|
+
push(template);
|
|
86
|
+
for (const template of this.templates.values())
|
|
87
|
+
push(template);
|
|
88
|
+
return templates;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Turns a template id (or its title) plus placeholder values into a real
|
|
92
|
+
* admin URL, rejecting templates this user may not open.
|
|
93
|
+
*/
|
|
94
|
+
resolveTemplate(user, id, params = {}) {
|
|
95
|
+
const wanted = this.slug(id);
|
|
96
|
+
const available = this.listTemplates(user);
|
|
97
|
+
const template = available.find((candidate) => this.slug(candidate.id) === wanted)
|
|
98
|
+
?? available.find((candidate) => this.slug(candidate.title) === wanted || this.slug(candidate.template) === wanted);
|
|
99
|
+
if (!template)
|
|
100
|
+
throw new Error(`Admin link template "${id}" is not available`);
|
|
101
|
+
return this.buildHref(template, params);
|
|
102
|
+
}
|
|
103
|
+
/** Fills the placeholders of a template with url-encoded values. */
|
|
104
|
+
buildHref(template, params = {}) {
|
|
105
|
+
return template.template.replace(/:([A-Za-z0-9_]+)/g, (_match, name) => {
|
|
106
|
+
const value = params[name];
|
|
107
|
+
if (value === undefined || value === null || value === '') {
|
|
108
|
+
throw new Error(`Admin link template "${template.id}" requires the "${name}" parameter`);
|
|
109
|
+
}
|
|
110
|
+
return encodeURIComponent(String(value));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** True for paths that change data and therefore must never be navigated to. */
|
|
114
|
+
isDestructivePath(path) {
|
|
115
|
+
return DESTRUCTIVE_PATH.test(path);
|
|
116
|
+
}
|
|
117
|
+
/** Templates derived from the current configuration, catalogs and navigation. */
|
|
118
|
+
builtinTemplates(user) {
|
|
119
|
+
const prefix = (this.adminizer.config.routePrefix || '').replace(/\/+$/, '');
|
|
120
|
+
const templates = [];
|
|
121
|
+
const builtin = (id, title, path, accessRightsToken, description, section, params = []) => {
|
|
122
|
+
templates.push({
|
|
123
|
+
id, title, template: path, accessRightsToken, description, section, owner: 'host',
|
|
124
|
+
params: this.describeParams(path, params),
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
const recordId = [
|
|
128
|
+
{ name: 'id', description: 'Identifier of the record, as returned when reading its data.' },
|
|
129
|
+
];
|
|
130
|
+
for (const [model, config] of Object.entries(this.adminizer.config.models ?? {})) {
|
|
131
|
+
const title = typeof config === 'object' && config?.title ? config.title : model;
|
|
132
|
+
const section = typeof config === 'object' ? config?.navbar?.section : undefined;
|
|
133
|
+
builtin(`model-${model}-edit`, `${title}: open record`, `${prefix}/model/${model}/edit/:id`, `update-${model}-model`, `Open the edit page of a single ${title} record by its id.`, section, recordId);
|
|
134
|
+
builtin(`model-${model}-add`, `${title}: create record`, `${prefix}/model/${model}/add`, `create-${model}-model`, `Open the create form of ${title}.`, section);
|
|
135
|
+
}
|
|
136
|
+
for (const catalog of this.adminizer.catalogHandler.getAll()) {
|
|
137
|
+
if (!catalog?.slug)
|
|
138
|
+
continue;
|
|
139
|
+
builtin(`catalog-${catalog.slug}-item`, `${catalog.name || catalog.slug}: open item`, `${prefix}/catalog/${catalog.slug}/:id`, `catalog-${catalog.slug}`, `Open a single item of the ${catalog.name || catalog.slug} catalog by its id.`, undefined, recordId);
|
|
140
|
+
}
|
|
141
|
+
// Navigation links may carry placeholders themselves (app pages, custom
|
|
142
|
+
// tools). Such a link cannot be opened as-is, so it is a template.
|
|
143
|
+
const fromNavigation = (item, section) => {
|
|
144
|
+
const path = typeof item?.link === 'string' ? item.link : '';
|
|
145
|
+
if (path.startsWith('/') && /:[A-Za-z0-9_]+/.test(path) && item.title) {
|
|
146
|
+
templates.push({
|
|
147
|
+
id: `link-${this.slug(String(item.id || item.title))}`,
|
|
148
|
+
title: String(item.title),
|
|
149
|
+
template: path,
|
|
150
|
+
section: item.section || section,
|
|
151
|
+
accessRightsToken: item.accessRightsToken || undefined,
|
|
152
|
+
owner: 'host',
|
|
153
|
+
params: this.describeParams(path),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
for (const child of item?.actions ?? item?.subItems ?? [])
|
|
157
|
+
fromNavigation(child, item?.section || section);
|
|
158
|
+
};
|
|
159
|
+
for (const item of this.adminizer.menuHelper.getMenuItems(user))
|
|
160
|
+
fromNavigation(item, item.section);
|
|
161
|
+
for (const link of this.links.values())
|
|
162
|
+
fromNavigation(link, link.section);
|
|
163
|
+
return templates;
|
|
164
|
+
}
|
|
165
|
+
describeParams(path, described = []) {
|
|
166
|
+
const names = [...path.matchAll(/:([A-Za-z0-9_]+)/g)].map((match) => match[1]);
|
|
167
|
+
return [...new Set(names)].map((name) => ({
|
|
168
|
+
name,
|
|
169
|
+
description: described.find((param) => param.name === name)?.description,
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
slug(value) {
|
|
173
|
+
return String(value).trim().toLowerCase().replace(/[\s_-]+/g, '-');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Adminizer } from '../Adminizer.js';
|
|
2
|
+
import type { AbstractAiModelService } from './AbstractAiModelService.js';
|
|
3
|
+
import type { User } from '../../models/User.js';
|
|
4
|
+
export interface AiConversation {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
updatedAt: string;
|
|
9
|
+
messages: Array<Record<string, unknown>>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Storage contract for an agent's dialogs. Applications can extend this class
|
|
13
|
+
* to persist conversations in their own database instead of process memory.
|
|
14
|
+
*/
|
|
15
|
+
export declare abstract class AbstractAiConversationHistoryService {
|
|
16
|
+
protected agent: AbstractAiModelService;
|
|
17
|
+
protected adminizer: Adminizer;
|
|
18
|
+
initialize(agent: AbstractAiModelService, adminizer: Adminizer): void;
|
|
19
|
+
abstract list(user: User): AiConversation[];
|
|
20
|
+
abstract getActive(user: User): AiConversation;
|
|
21
|
+
abstract create(user: User, title?: string): AiConversation;
|
|
22
|
+
abstract select(user: User, conversationId: string): AiConversation | undefined;
|
|
23
|
+
abstract remove(user: User, conversationId: string): boolean;
|
|
24
|
+
abstract saveActive(user: User, messages: Array<Record<string, unknown>>, title?: string): AiConversation;
|
|
25
|
+
}
|
|
26
|
+
/** Default storage used when an agent does not provide a database-backed one. */
|
|
27
|
+
export declare class InMemoryAiConversationHistoryService extends AbstractAiConversationHistoryService {
|
|
28
|
+
private readonly conversations;
|
|
29
|
+
private readonly activeConversationIds;
|
|
30
|
+
list(user: User): AiConversation[];
|
|
31
|
+
getActive(user: User): AiConversation;
|
|
32
|
+
create(user: User, title?: string): AiConversation;
|
|
33
|
+
select(user: User, conversationId: string): AiConversation | undefined;
|
|
34
|
+
remove(user: User, conversationId: string): boolean;
|
|
35
|
+
saveActive(user: User, messages: Array<Record<string, unknown>>, title?: string): AiConversation;
|
|
36
|
+
private getConversations;
|
|
37
|
+
private getKey;
|
|
38
|
+
private findFirstUserText;
|
|
39
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage contract for an agent's dialogs. Applications can extend this class
|
|
3
|
+
* to persist conversations in their own database instead of process memory.
|
|
4
|
+
*/
|
|
5
|
+
export class AbstractAiConversationHistoryService {
|
|
6
|
+
agent;
|
|
7
|
+
adminizer;
|
|
8
|
+
initialize(agent, adminizer) {
|
|
9
|
+
this.agent = agent;
|
|
10
|
+
this.adminizer = adminizer;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Default storage used when an agent does not provide a database-backed one. */
|
|
14
|
+
export class InMemoryAiConversationHistoryService extends AbstractAiConversationHistoryService {
|
|
15
|
+
conversations = new Map();
|
|
16
|
+
activeConversationIds = new Map();
|
|
17
|
+
list(user) {
|
|
18
|
+
return [...this.getConversations(user)]
|
|
19
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
20
|
+
}
|
|
21
|
+
getActive(user) {
|
|
22
|
+
const key = this.getKey(user);
|
|
23
|
+
const conversations = this.getConversations(user);
|
|
24
|
+
const activeId = this.activeConversationIds.get(key);
|
|
25
|
+
const active = conversations.find((conversation) => conversation.id === activeId);
|
|
26
|
+
if (active)
|
|
27
|
+
return active;
|
|
28
|
+
const conversation = this.create(user);
|
|
29
|
+
this.activeConversationIds.set(key, conversation.id);
|
|
30
|
+
return conversation;
|
|
31
|
+
}
|
|
32
|
+
create(user, title = 'New conversation') {
|
|
33
|
+
const now = new Date().toISOString();
|
|
34
|
+
const conversation = {
|
|
35
|
+
id: `conversation-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
|
|
36
|
+
title,
|
|
37
|
+
createdAt: now,
|
|
38
|
+
updatedAt: now,
|
|
39
|
+
messages: [],
|
|
40
|
+
};
|
|
41
|
+
this.getConversations(user).push(conversation);
|
|
42
|
+
this.activeConversationIds.set(this.getKey(user), conversation.id);
|
|
43
|
+
return conversation;
|
|
44
|
+
}
|
|
45
|
+
select(user, conversationId) {
|
|
46
|
+
const conversation = this.getConversations(user).find((item) => item.id === conversationId);
|
|
47
|
+
if (conversation)
|
|
48
|
+
this.activeConversationIds.set(this.getKey(user), conversation.id);
|
|
49
|
+
return conversation;
|
|
50
|
+
}
|
|
51
|
+
remove(user, conversationId) {
|
|
52
|
+
const conversations = this.getConversations(user);
|
|
53
|
+
const index = conversations.findIndex((conversation) => conversation.id === conversationId);
|
|
54
|
+
if (index < 0)
|
|
55
|
+
return false;
|
|
56
|
+
conversations.splice(index, 1);
|
|
57
|
+
if (this.activeConversationIds.get(this.getKey(user)) === conversationId) {
|
|
58
|
+
this.activeConversationIds.delete(this.getKey(user));
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
saveActive(user, messages, title) {
|
|
63
|
+
const conversation = this.getActive(user);
|
|
64
|
+
conversation.messages = messages;
|
|
65
|
+
conversation.updatedAt = new Date().toISOString();
|
|
66
|
+
if (title)
|
|
67
|
+
conversation.title = title;
|
|
68
|
+
else if (conversation.title === 'New conversation') {
|
|
69
|
+
const firstText = this.findFirstUserText(messages);
|
|
70
|
+
if (firstText)
|
|
71
|
+
conversation.title = firstText.slice(0, 80);
|
|
72
|
+
}
|
|
73
|
+
return conversation;
|
|
74
|
+
}
|
|
75
|
+
getConversations(user) {
|
|
76
|
+
const key = this.getKey(user);
|
|
77
|
+
let conversations = this.conversations.get(key);
|
|
78
|
+
if (!conversations) {
|
|
79
|
+
conversations = [];
|
|
80
|
+
this.conversations.set(key, conversations);
|
|
81
|
+
}
|
|
82
|
+
return conversations;
|
|
83
|
+
}
|
|
84
|
+
getKey(user) {
|
|
85
|
+
return `${user.id}:${this.agent.id}`;
|
|
86
|
+
}
|
|
87
|
+
findFirstUserText(messages) {
|
|
88
|
+
const message = messages.find((item) => item.role === 'user');
|
|
89
|
+
const content = message?.content;
|
|
90
|
+
if (typeof content === 'string')
|
|
91
|
+
return content.trim();
|
|
92
|
+
if (Array.isArray(content)) {
|
|
93
|
+
const text = content.find((part) => part && typeof part === 'object' && part.type === 'text');
|
|
94
|
+
return typeof text?.text === 'string' ? text.text.trim() : undefined;
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|