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.
Files changed (232) hide show
  1. package/assets/abnfDiagram-VCTEODGH-DFY3Pztx.js +1 -0
  2. package/assets/add-31tS-g4V.js +1 -0
  3. package/assets/add-group-Cf3zT7WE.js +1 -0
  4. package/assets/add-user-C62wOna-.js +1 -0
  5. package/assets/ai-assistant/agent.es.js +28464 -0
  6. package/assets/app-DPmI-MvN.css +1 -0
  7. package/assets/app-vdHbjOQX.js +138 -0
  8. package/assets/arc-DP4o1zz3.js +1 -0
  9. package/assets/architectureDiagram-5GKGNRK7-BjXW5Zu0.js +36 -0
  10. package/assets/asmcrypto-DZeQH0LC.js +1 -0
  11. package/assets/blockDiagram-I7D4REHJ-BUPnDwN0.js +129 -0
  12. package/assets/c4Diagram-7LVT6UL2-Cmb_dAhd.js +38 -0
  13. package/assets/{catalog-BEvJZYJP.js → catalog-C-y5rMaG.js} +3 -3
  14. package/assets/channel-CkJRUsq_.js +1 -0
  15. package/assets/chunk-2Q5K7J3B-CmS-dWwQ.js +1 -0
  16. package/assets/chunk-5VM5RSS4-Cqs2Ow64.js +15 -0
  17. package/assets/chunk-F27PBJKO-BoMCpJA1.js +1 -0
  18. package/assets/chunk-IMKFNOWR-DlDcPk1x.js +231 -0
  19. package/assets/chunk-JWPE2WC7-DzL_SU8s.js +1 -0
  20. package/assets/chunk-POPQ4Y6H-Cwq2-Vb8.js +1 -0
  21. package/assets/chunk-SVP7TREG-BfRHY5FU.js +88 -0
  22. package/assets/chunk-TICWLB2K-CRP7rHkW.js +206 -0
  23. package/assets/chunk-XXDRQBXY-Bg2jg-98.js +1 -0
  24. package/assets/classDiagram-ZZMXUADV-D4EF3HGj.js +1 -0
  25. package/assets/classDiagram-v2-VYDZK3BY-D4EF3HGj.js +1 -0
  26. package/assets/controls/ckeditor.es.js +9382 -9320
  27. package/assets/controls/handsontable.es.js +11971 -11938
  28. package/assets/controls/jsoneditor.es.js +10442 -10233
  29. package/assets/controls/toast-ui.es.js +1308 -1279
  30. package/assets/cose-bilkent-JH36ORCC-CCJ0SGyO.js +1 -0
  31. package/assets/cynefin-OW5HDTMX-B2Jx5fw3.js +166 -0
  32. package/assets/cynefinDiagram-5FMLGOSQ-DuK0Lb62.js +62 -0
  33. package/assets/cytoscape.esm-DzSz-X2X.js +321 -0
  34. package/assets/dagre-GXQ25YYZ-DnOYexET.js +4 -0
  35. package/assets/dashboard-X8k4X6Sw.js +3 -0
  36. package/assets/debounce-B3mXI_sx.js +1 -0
  37. package/assets/defaultLocale-DX6XiGOO.js +1 -0
  38. package/assets/diagram-S7CK7UJ4-DDgPBHsK.js +30 -0
  39. package/assets/diagram-UQ7AKVKN-qlL0GZs8.js +41 -0
  40. package/assets/diagram-VSXAHHWV-CQbiU1_c.js +3 -0
  41. package/assets/diagram-VX7I27RA-DFDI9_au.js +24 -0
  42. package/assets/diagram-Z3DM3KII-B5C9dtmJ.js +24 -0
  43. package/assets/docs-BiZOD7PP.js +1 -0
  44. package/assets/ebnfDiagram-PWID7BFC-CLMUsXEI.js +1 -0
  45. package/assets/erDiagram-RLTQ6QDP-ax9Xol5-.js +99 -0
  46. package/assets/flowDiagram-HODETNUW-CvaH7l3S.js +168 -0
  47. package/assets/ganttDiagram-EL5Y4UJY-Cga5U19j.js +292 -0
  48. package/assets/gitGraphDiagram-WWUBYQGX-B6o1T3-d.js +106 -0
  49. package/assets/history-CVBJUNSQ.js +1 -0
  50. package/assets/infoDiagram-27XIBGKW-BpWxgBCE.js +2 -0
  51. package/assets/init-Gi6I4Gst.js +1 -0
  52. package/assets/init-user-xSvnFTF0.js +1 -0
  53. package/assets/isObject-DL-BuaBl.js +1 -0
  54. package/assets/ishikawaDiagram-5VMMS53U-zE83db4H.js +70 -0
  55. package/assets/journeyDiagram-3NMN7TZE-DZNVFqrX.js +139 -0
  56. package/assets/kanban-definition-UXKFOSKX-CqxNgk27.js +89 -0
  57. package/assets/katex-HP8lGamR.js +257 -0
  58. package/assets/layout-CN3kt_jt.js +1 -0
  59. package/assets/linear-nvkaQ6BA.js +1 -0
  60. package/assets/{list-DhK0McVn.js → list-CrIU2eZz.js} +2 -2
  61. package/assets/login-CX9xVjCn.js +1 -0
  62. package/assets/manifest.json +834 -58
  63. package/assets/media-manager-BfYftLIH.js +6 -0
  64. package/assets/mermaid.core-C0V1NaVJ.js +310 -0
  65. package/assets/mindmap-definition-YA3MSWOX-Bh7_d9f1.js +96 -0
  66. package/assets/module-DPEvnad3.js +1 -0
  67. package/assets/notification-Dqpt5AIZ.js +20 -0
  68. package/assets/ordinal-Cboi1Yqb.js +1 -0
  69. package/assets/pegDiagram-XKGWAZYB-DMnjm6Z1.js +1 -0
  70. package/assets/pieDiagram-E7YTZNPT-D-RWCgYb.js +39 -0
  71. package/assets/quadrantDiagram-AXDQQJYC-BNBGP7RM.js +7 -0
  72. package/assets/railroadDiagram-O6MQD6OU-BIjVPT01.js +1 -0
  73. package/assets/register-CF4KQ97e.js +1 -0
  74. package/assets/requirementDiagram-BXWQKSXE-VPrtGdjM.js +84 -0
  75. package/assets/ru-CFJ4fysR.js +1 -0
  76. package/assets/sankeyDiagram-P5KCCOFB-ct9mLF3U.js +40 -0
  77. package/assets/sequenceDiagram-WJ2MYXX4-TC06Mbmw.js +162 -0
  78. package/assets/sizeCapture-INFHLROL-BMMwriCf.js +1 -0
  79. package/assets/stateDiagram-D77RDMKH-DtwYRPCX.js +1 -0
  80. package/assets/stateDiagram-v2-MP3YSRHH-C1yB0O5U.js +1 -0
  81. package/assets/swimlanes-42K2YHIH-DE3hRbbr.js +1 -0
  82. package/assets/swimlanesDiagram-VR7AAH4N-Ce4l1sQD.js +8 -0
  83. package/assets/timeline-definition-24CTP7MA-N7s_hfjj.js +120 -0
  84. package/assets/toNumber-DZb-r9sy.js +1 -0
  85. package/assets/use-appearance-DYTIKpT2.js +1 -0
  86. package/assets/user-filters-list-fR0YNJ3l.js +1 -0
  87. package/assets/vennDiagram-4TSXK5OY-Dj5bFQkR.js +34 -0
  88. package/assets/wardleyDiagram-VM6X3IG4-83ax25Z6.js +78 -0
  89. package/assets/welcome-v11MTsRK.js +1 -0
  90. package/assets/with-app-layout-CH6RIib1.js +31 -0
  91. package/assets/xychartDiagram-S5SC5T6Z-CW9Br5Xp.js +7 -0
  92. package/controllers/access-rights/permissionOptions.d.ts +2 -0
  93. package/controllers/access-rights/permissionOptions.js +23 -0
  94. package/controllers/add.js +2 -2
  95. package/controllers/addGroup.js +15 -6
  96. package/controllers/ai/AiAgentController.d.ts +66 -0
  97. package/controllers/ai/AiAgentController.js +435 -0
  98. package/controllers/ai/AiAssistantController.d.ts +0 -4
  99. package/controllers/ai/AiAssistantController.js +4 -78
  100. package/controllers/catalog/FrontendCatalogAdapter.js +7 -8
  101. package/controllers/docs/index.d.ts +22 -0
  102. package/controllers/docs/index.js +126 -0
  103. package/controllers/edit.js +2 -2
  104. package/controllers/editGroup.js +15 -6
  105. package/controllers/filter-fields/savedFilters.js +1 -1
  106. package/controllers/history-actions/HistoryController.js +7 -7
  107. package/controllers/inlineUpdate.js +10 -3
  108. package/controllers/list.js +1 -1
  109. package/controllers/login.js +1 -1
  110. package/controllers/notifications/NotificationController.js +14 -8
  111. package/controllers/view.js +3 -3
  112. package/controllers/widgets/filterInfo.js +1 -1
  113. package/helpers/accessRightsHelper.d.ts +30 -3
  114. package/helpers/accessRightsHelper.js +140 -12
  115. package/helpers/assetVersionHelper.d.ts +14 -0
  116. package/helpers/assetVersionHelper.js +42 -0
  117. package/helpers/configHelper.js +19 -18
  118. package/helpers/controllerHelper.d.ts +1 -0
  119. package/helpers/controllerHelper.js +25 -15
  120. package/helpers/fieldsHelper.d.ts +2 -1
  121. package/helpers/fieldsHelper.js +11 -5
  122. package/helpers/inertiaActionsHelper.d.ts +1 -1
  123. package/helpers/inertiaActionsHelper.js +4 -5
  124. package/helpers/inertiaAddHelper.d.ts +1 -1
  125. package/helpers/inertiaAddHelper.js +7 -6
  126. package/helpers/inertiaGroupHelper.js +10 -2
  127. package/helpers/inertiaListHelper.d.ts +1 -1
  128. package/helpers/inertiaListHelper.js +7 -8
  129. package/helpers/inertiaMenuHelper.d.ts +7 -2
  130. package/helpers/inertiaMenuHelper.js +12 -27
  131. package/helpers/menuHelper.d.ts +11 -1
  132. package/helpers/menuHelper.js +12 -0
  133. package/helpers/modelResourceHelper.d.ts +9 -0
  134. package/helpers/modelResourceHelper.js +43 -0
  135. package/helpers/navigationAccessHelper.d.ts +13 -0
  136. package/helpers/navigationAccessHelper.js +45 -0
  137. package/index.d.ts +11 -0
  138. package/index.js +11 -0
  139. package/interfaces/adminpanelConfig.d.ts +160 -2
  140. package/interfaces/queryCriteria.d.ts +13 -0
  141. package/interfaces/types.d.ts +169 -0
  142. package/lib/Adminizer.d.ts +8 -0
  143. package/lib/Adminizer.js +29 -2
  144. package/lib/DataAccessor.d.ts +100 -3
  145. package/lib/DataAccessor.js +454 -245
  146. package/lib/access-graph/AccessGraphResolver.d.ts +136 -0
  147. package/lib/access-graph/AccessGraphResolver.js +605 -0
  148. package/lib/access-graph/RecordAccessCache.d.ts +23 -0
  149. package/lib/access-graph/RecordAccessCache.js +30 -0
  150. package/lib/access-graph/RecordAccessResolver.d.ts +83 -0
  151. package/lib/access-graph/RecordAccessResolver.js +295 -0
  152. package/lib/access-graph/shared.d.ts +60 -0
  153. package/lib/access-graph/shared.js +121 -0
  154. package/lib/admin-links/AdminLinkHandler.d.ts +72 -0
  155. package/lib/admin-links/AdminLinkHandler.js +183 -0
  156. package/lib/ai-assistant/AbstractAiConversationHistoryService.d.ts +39 -0
  157. package/lib/ai-assistant/AbstractAiConversationHistoryService.js +98 -0
  158. package/lib/ai-assistant/AbstractAiModelService.d.ts +139 -2
  159. package/lib/ai-assistant/AbstractAiModelService.js +154 -1
  160. package/lib/ai-assistant/AiAgentRunManager.d.ts +35 -0
  161. package/lib/ai-assistant/AiAgentRunManager.js +109 -0
  162. package/lib/ai-assistant/AiAssistantAgentSkillHandler.d.ts +61 -0
  163. package/lib/ai-assistant/AiAssistantAgentSkillHandler.js +90 -0
  164. package/lib/ai-assistant/AiAssistantHandler.d.ts +1 -0
  165. package/lib/ai-assistant/AiAssistantHandler.js +4 -0
  166. package/lib/ai-assistant/AiAssistantUiMethodHandler.d.ts +60 -0
  167. package/lib/ai-assistant/AiAssistantUiMethodHandler.js +144 -0
  168. package/lib/ai-assistant/builtinAgentSkills.d.ts +9 -0
  169. package/lib/ai-assistant/builtinAgentSkills.js +259 -0
  170. package/lib/ai-assistant/jsonSafe.d.ts +13 -0
  171. package/lib/ai-assistant/jsonSafe.js +81 -0
  172. package/lib/app-manager/AdminizerApp.d.ts +31 -2
  173. package/lib/app-manager/AppManager.js +43 -42
  174. package/lib/app-manager/ConfigLayerHandler.js +4 -0
  175. package/lib/docs/AbstractDocumentation.d.ts +100 -0
  176. package/lib/docs/AbstractDocumentation.js +108 -0
  177. package/lib/docs/DocumentationHandler.d.ts +100 -0
  178. package/lib/docs/DocumentationHandler.js +198 -0
  179. package/lib/docs/FileDocumentation.d.ts +47 -0
  180. package/lib/docs/FileDocumentation.js +189 -0
  181. package/lib/docs/docsAgentSkills.d.ts +10 -0
  182. package/lib/docs/docsAgentSkills.js +93 -0
  183. package/lib/filters/FilterService.js +4 -3
  184. package/lib/history-actions/AbstractHistoryAdapter.d.ts +26 -1
  185. package/lib/history-actions/AbstractHistoryAdapter.js +155 -25
  186. package/lib/media-manager/helpers/MediaManagerHelper.js +9 -7
  187. package/lib/model/AbstractModel.d.ts +48 -0
  188. package/lib/model/AbstractModel.js +14 -4
  189. package/lib/model/ModelHandler.d.ts +50 -2
  190. package/lib/model/ModelHandler.js +117 -8
  191. package/lib/model/adapter/sequelize.d.ts +43 -3
  192. package/lib/model/adapter/sequelize.js +317 -138
  193. package/lib/model/adapter/typeorm.d.ts +16 -0
  194. package/lib/model/adapter/typeorm.js +136 -9
  195. package/lib/notifications/GeneralNotificationService.js +6 -5
  196. package/lib/notifications/SystemNotificationService.js +1 -1
  197. package/lib/ui-i18n/uiTranslationKeys.d.ts +2 -1
  198. package/lib/ui-i18n/uiTranslationKeys.js +42 -1
  199. package/lib/widgets/widgetHandler.js +10 -6
  200. package/models/Group.d.ts +2 -1
  201. package/package.json +35 -23
  202. package/policies/authPolicies.js +6 -6
  203. package/policies/permissionResolvers.d.ts +1 -0
  204. package/policies/permissionResolvers.js +3 -0
  205. package/system/Router.js +21 -3
  206. package/system/bindAccessRights.js +13 -3
  207. package/system/bindDocs.d.ts +13 -0
  208. package/system/bindDocs.js +77 -0
  209. package/system/bindInertia.js +30 -7
  210. package/system/buildInternalModelAccess.d.ts +8 -0
  211. package/system/buildInternalModelAccess.js +36 -4
  212. package/system/defaults.js +7 -1
  213. package/system/validateSystemModels.js +29 -9
  214. package/assets/add-DED_wpga.js +0 -1
  215. package/assets/add-group-CL80RdYb.js +0 -1
  216. package/assets/add-user-BRmwvQB8.js +0 -1
  217. package/assets/app-DNXiApCk.css +0 -1
  218. package/assets/app-layout-86yxiZOy.js +0 -1
  219. package/assets/app.js +0 -138
  220. package/assets/dashboard-ZP6K2s_8.js +0 -3
  221. package/assets/debounce-BMmXVQ06.js +0 -1
  222. package/assets/history-M61Bcv02.js +0 -1
  223. package/assets/init-user-8aUohVDq.js +0 -1
  224. package/assets/login-CO4laf6m.js +0 -1
  225. package/assets/media-manager-BDbpSSjH.js +0 -6
  226. package/assets/module-DfBneaUl.js +0 -1
  227. package/assets/notification-CVnQf3ef.js +0 -20
  228. package/assets/register-DBYIMXGQ.js +0 -1
  229. package/assets/ru-rYKdQbWF.js +0 -1
  230. package/assets/use-appearance-_EbbjHTh.js +0 -1
  231. package/assets/user-filters-list-DXhGLFTn.js +0 -1
  232. 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.getModelResources(),
354
- resolveModelResource: (modelName) => this.resolveModelResource(modelName),
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}`;