@betterportal/config-manager 10.1.61 → 10.1.63

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 (164) hide show
  1. package/lib/.bsb/clients/service-betterportal-config-manager.d.ts +1 -1
  2. package/lib/.bsb/clients/service-betterportal-config-manager.js +1 -1
  3. package/lib/bp-contracts/org.betterportal.config-manager.json +4137 -2714
  4. package/lib/plugins/service-betterportal-config-manager/.bp-generated/registry.d.ts.map +1 -1
  5. package/lib/plugins/service-betterportal-config-manager/.bp-generated/registry.js +32 -89
  6. package/lib/plugins/service-betterportal-config-manager/.bp-generated/registry.js.map +1 -1
  7. package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts +100 -14
  8. package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts.map +1 -1
  9. package/lib/plugins/service-betterportal-config-manager/adminApi.js +111 -86
  10. package/lib/plugins/service-betterportal-config-manager/adminApi.js.map +1 -1
  11. package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.js +12 -12
  12. package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.js.map +1 -1
  13. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/GET.d.ts +3 -1
  14. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/GET.d.ts.map +1 -1
  15. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/GET.js +3 -1
  16. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/GET.js.map +1 -1
  17. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_renderer.bootstrap5/GET.d.ts.map +1 -1
  18. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_renderer.bootstrap5/GET.js +1 -1
  19. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_renderer.bootstrap5/GET.js.map +1 -1
  20. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/index.d.ts +1 -1
  21. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/index.d.ts.map +1 -1
  22. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/index.js +1 -1
  23. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/index.js.map +1 -1
  24. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/route.impl.d.ts +18 -12
  25. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/route.impl.d.ts.map +1 -1
  26. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/route.impl.js +21 -18
  27. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/route.impl.js.map +1 -1
  28. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/GET.d.ts +3 -1
  29. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/GET.d.ts.map +1 -1
  30. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/GET.js +3 -1
  31. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/GET.js.map +1 -1
  32. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/index.d.ts +1 -1
  33. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/index.d.ts.map +1 -1
  34. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/index.js +1 -1
  35. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/index.js.map +1 -1
  36. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/route.impl.d.ts.map +1 -1
  37. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/route.impl.js +5 -5
  38. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/route.impl.js.map +1 -1
  39. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/GET.d.ts +2 -0
  40. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/GET.d.ts.map +1 -1
  41. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/GET.js +2 -0
  42. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/GET.js.map +1 -1
  43. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/index.d.ts +1 -1
  44. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/index.d.ts.map +1 -1
  45. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/index.js +1 -1
  46. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/index.js.map +1 -1
  47. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/route.impl.d.ts.map +1 -1
  48. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/route.impl.js +2 -2
  49. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/route.impl.js.map +1 -1
  50. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/GET.d.ts +2 -0
  51. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/GET.d.ts.map +1 -1
  52. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/GET.js +2 -0
  53. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/GET.js.map +1 -1
  54. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/index.d.ts +1 -1
  55. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/index.d.ts.map +1 -1
  56. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/index.js +1 -1
  57. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/index.js.map +1 -1
  58. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/route.impl.d.ts.map +1 -1
  59. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/route.impl.js +4 -4
  60. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/route.impl.js.map +1 -1
  61. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/GET.d.ts +2 -0
  62. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/GET.d.ts.map +1 -1
  63. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/GET.js +2 -0
  64. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/GET.js.map +1 -1
  65. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/index.d.ts +1 -1
  66. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/index.d.ts.map +1 -1
  67. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/index.js +1 -1
  68. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/index.js.map +1 -1
  69. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/route.impl.d.ts.map +1 -1
  70. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/route.impl.js +4 -4
  71. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/route.impl.js.map +1 -1
  72. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/GET.d.ts +2 -0
  73. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/GET.d.ts.map +1 -1
  74. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/GET.js +2 -0
  75. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/GET.js.map +1 -1
  76. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_renderer.bootstrap5/GET.d.ts.map +1 -1
  77. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_renderer.bootstrap5/GET.js +45 -33
  78. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_renderer.bootstrap5/GET.js.map +1 -1
  79. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/index.d.ts +1 -1
  80. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/index.d.ts.map +1 -1
  81. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/index.js +1 -1
  82. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/index.js.map +1 -1
  83. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/route.impl.d.ts +10 -4
  84. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/route.impl.d.ts.map +1 -1
  85. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/route.impl.js +11 -8
  86. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/route.impl.js.map +1 -1
  87. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/GET.d.ts +2 -0
  88. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/GET.d.ts.map +1 -1
  89. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/GET.js +2 -0
  90. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/GET.js.map +1 -1
  91. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/index.d.ts +1 -1
  92. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/index.d.ts.map +1 -1
  93. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/index.js +1 -1
  94. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/index.js.map +1 -1
  95. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/route.impl.d.ts.map +1 -1
  96. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/route.impl.js +7 -7
  97. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/route.impl.js.map +1 -1
  98. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/GET.d.ts +2 -0
  99. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/GET.d.ts.map +1 -1
  100. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/GET.js +2 -0
  101. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/GET.js.map +1 -1
  102. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/index.d.ts +1 -1
  103. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/index.d.ts.map +1 -1
  104. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/index.js +1 -1
  105. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/index.js.map +1 -1
  106. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/route.impl.d.ts.map +1 -1
  107. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/route.impl.js +9 -9
  108. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/route.impl.js.map +1 -1
  109. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/DELETE.d.ts +6 -0
  110. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/DELETE.d.ts.map +1 -1
  111. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/DELETE.js +8 -0
  112. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/DELETE.js.map +1 -1
  113. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/GET.d.ts +6 -0
  114. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/GET.d.ts.map +1 -1
  115. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/GET.js +8 -0
  116. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/GET.js.map +1 -1
  117. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/POST.d.ts +6 -0
  118. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/POST.d.ts.map +1 -1
  119. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/POST.js +8 -0
  120. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/POST.js.map +1 -1
  121. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/PUT.d.ts +6 -0
  122. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/PUT.d.ts.map +1 -1
  123. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/PUT.js +8 -0
  124. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/PUT.js.map +1 -1
  125. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/index.d.ts +1 -1
  126. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/index.d.ts.map +1 -1
  127. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/index.js +1 -1
  128. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/index.js.map +1 -1
  129. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/route.impl.d.ts +5 -6
  130. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/route.impl.d.ts.map +1 -1
  131. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/route.impl.js +29 -38
  132. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/route.impl.js.map +1 -1
  133. package/lib/plugins/service-betterportal-config-manager/cpBootstrap.d.ts +2 -1
  134. package/lib/plugins/service-betterportal-config-manager/cpBootstrap.d.ts.map +1 -1
  135. package/lib/plugins/service-betterportal-config-manager/cpBootstrap.js.map +1 -1
  136. package/lib/plugins/service-betterportal-config-manager/index.d.ts.map +1 -1
  137. package/lib/plugins/service-betterportal-config-manager/index.js +30 -22
  138. package/lib/plugins/service-betterportal-config-manager/index.js.map +1 -1
  139. package/lib/plugins/service-betterportal-config-manager/menuEditor.d.ts.map +1 -1
  140. package/lib/plugins/service-betterportal-config-manager/menuEditor.js +3 -2
  141. package/lib/plugins/service-betterportal-config-manager/menuEditor.js.map +1 -1
  142. package/lib/plugins/service-betterportal-config-manager/setupTokens.d.ts.map +1 -1
  143. package/lib/plugins/service-betterportal-config-manager/setupTokens.js +9 -2
  144. package/lib/plugins/service-betterportal-config-manager/setupTokens.js.map +1 -1
  145. package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts +17 -0
  146. package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts.map +1 -1
  147. package/lib/plugins/service-betterportal-config-manager/storage/core.js +96 -0
  148. package/lib/plugins/service-betterportal-config-manager/storage/core.js.map +1 -1
  149. package/lib/plugins/service-betterportal-config-manager/storage/file.d.ts.map +1 -1
  150. package/lib/plugins/service-betterportal-config-manager/storage/file.js +2 -2
  151. package/lib/plugins/service-betterportal-config-manager/storage/file.js.map +1 -1
  152. package/lib/plugins/service-betterportal-config-manager/storage/index.d.ts.map +1 -1
  153. package/lib/plugins/service-betterportal-config-manager/storage/index.js +2 -2
  154. package/lib/plugins/service-betterportal-config-manager/storage/index.js.map +1 -1
  155. package/lib/plugins/service-betterportal-config-manager/storage/postgres.d.ts.map +1 -1
  156. package/lib/plugins/service-betterportal-config-manager/storage/postgres.js +2 -2
  157. package/lib/plugins/service-betterportal-config-manager/storage/postgres.js.map +1 -1
  158. package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts +22 -14
  159. package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts.map +1 -1
  160. package/lib/plugins/service-betterportal-config-manager/syncApi.js +167 -90
  161. package/lib/plugins/service-betterportal-config-manager/syncApi.js.map +1 -1
  162. package/lib/schemas/service-betterportal-config-manager.json +1 -1
  163. package/lib/schemas/service-betterportal-config-manager.plugin.json +1 -1
  164. package/package.json +3 -3
@@ -1,5 +1,5 @@
1
1
  import * as av from "anyvali";
2
- import { AppAuthRoleSchema, eventHeaders, htmlResponse, jsonResponse, resolveEmbeddedRequestContext, signServiceConfigTicket, uuidv7, BetterPortalThemeConfigSchema } from "@betterportal/framework";
2
+ import { AppAuthRoleSchema, eventHeaders, htmlResponse, jsonResponse, resolveEmbeddedRequestContext, signServiceConfigTicket, uuidv7, BetterPortalMenuItemSchema, BetterPortalThemeConfigSchema } from "@betterportal/framework";
3
3
  import { generateApiKey, hashApiKey } from "./storage/index.js";
4
4
  import { getManifestCache } from "./syncApi.js";
5
5
  import { apiRoutePath, appRoutePatternKey, isApiRoute, pageRoutePath } from "./routeMounts.js";
@@ -90,6 +90,19 @@ function requiredRouteString(body, key, label) {
90
90
  return { error: `${label} is required.` };
91
91
  return { value };
92
92
  }
93
+ function normalizeMenuItem(raw) {
94
+ const item = raw && typeof raw === "object" ? raw : {};
95
+ return BetterPortalMenuItemSchema.parse({
96
+ id: typeof item.id === "string" ? item.id : uuidv7(),
97
+ type: item.type === "group" ? "group" : "link",
98
+ title: typeof item.title === "string" ? item.title : undefined,
99
+ icon: typeof item.icon === "string" ? item.icon : undefined,
100
+ routeId: typeof item.routeId === "string" ? item.routeId : undefined,
101
+ href: typeof item.href === "string" ? item.href : undefined,
102
+ enabled: item.enabled !== false,
103
+ children: Array.isArray(item.children) ? item.children.map(normalizeMenuItem) : []
104
+ });
105
+ }
93
106
  function routesReloadPath(appId, apiServiceId) {
94
107
  return `/routes?appId=${encodeURIComponent(appId)}${apiServiceId ? `&apiServiceId=${encodeURIComponent(apiServiceId)}` : ""}`;
95
108
  }
@@ -163,15 +176,13 @@ function validateRouteParamMapping(appPath, servicePath, fixedParams, paramsSche
163
176
  }
164
177
  return undefined;
165
178
  }
166
- function routeMethodsFromManifest(methods) {
167
- const normalized = (methods ?? []).filter((method) => method === "GET" || method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE" || method === "OPTIONS");
168
- return normalized.length ? normalized : ["GET"];
169
- }
170
179
  function hasPageRenderer(view) {
171
- const renderers = view.html?.renderers;
172
- if (!renderers)
173
- return view.renderable !== false;
174
- return Object.values(renderers).some((renderer) => Array.isArray(renderer.renderModes) && renderer.renderModes.includes("page"));
180
+ return (view.operations ?? []).some((operation) => {
181
+ if (operation.method !== "GET" || operation.renderable === false)
182
+ return false;
183
+ const renderers = operation.html?.renderers;
184
+ return renderers && Object.values(renderers).some((renderer) => Array.isArray(renderer.renderModes) && renderer.renderModes.includes("page"));
185
+ });
175
186
  }
176
187
  function appMatchesTenantUrl(app, tenantUrl) {
177
188
  let host = "";
@@ -279,21 +290,22 @@ function automationServiceCatalog(config, appDef) {
279
290
  ...resource,
280
291
  url: `${service.url.replace(/\/+$/, "")}/.well-known/bp/resources/${encodeURIComponent(resource.id)}`
281
292
  })),
282
- actions: Object.values(manifest?.viewIndex ?? {}).map((view) => ({
293
+ actions: Object.values(manifest?.viewIndex ?? {}).flatMap((view) => view.operations.map((operation) => ({
283
294
  viewId: view.viewId,
295
+ operationId: operation.operationId,
284
296
  path: view.path,
285
- methods: view.methods,
286
- title: view.viewId,
287
- renderable: view.renderable,
288
- permissions: view.permissions,
289
- role: view.role,
290
- chrome: view.chrome,
291
- dependencies: view.dependencies,
292
- schemas: view.schemas,
293
- raw: view.raw === true,
294
- apiContracts: view.apiContracts,
295
- demoScenarios: view.demoScenarios
296
- }))
297
+ method: operation.method,
298
+ title: operation.title,
299
+ renderable: operation.renderable,
300
+ permissions: operation.permissions,
301
+ role: operation.role,
302
+ chrome: operation.chrome,
303
+ dependencies: operation.dependencies,
304
+ schemas: operation.schemas,
305
+ raw: operation.raw === true,
306
+ apiContracts: operation.apiContracts,
307
+ demoScenarios: operation.demoScenarios
308
+ })))
297
309
  };
298
310
  })
299
311
  };
@@ -320,7 +332,7 @@ export function renderAutomationLlmsApi(catalog) {
320
332
  if (service.actions.length === 0)
321
333
  lines.push("No synced actions.", "");
322
334
  for (const action of service.actions) {
323
- lines.push(`#### ${action.title}`, "", `- View ID: \`${action.viewId}\``, `- Methods: ${action.methods.map((method) => `\`${method}\``).join(", ")}`, `- URL: ${base}${action.path}`, `- Renderable: ${action.renderable ? "yes" : "no"}`, `- Required permissions: \`${JSON.stringify(action.permissions)}\``, "");
335
+ lines.push(`#### ${action.title}`, "", `- View ID: \`${action.viewId}\``, `- Operation ID: \`${action.operationId}\``, `- Method: \`${action.method}\``, `- URL: ${base}${action.path}`, `- Renderable: ${action.renderable ? "yes" : "no"}`, `- Required permissions: \`${JSON.stringify(action.permissions)}\``, "");
324
336
  if (action.schemas)
325
337
  lines.push("```json", JSON.stringify(action.schemas, null, 2), "```", "");
326
338
  if (action.demoScenarios.length)
@@ -336,8 +348,14 @@ function parseRouteCreateBody(body) {
336
348
  const viewId = requiredRouteString(body, "viewId", "View");
337
349
  if (viewId.error)
338
350
  return { error: viewId.error };
351
+ const operationId = requiredRouteString(body, "operationId", "Operation");
352
+ if (operationId.error)
353
+ return { error: operationId.error };
339
354
  const manifestView = getManifestCache().get(serviceId.value)?.viewIndex[viewId.value];
340
- const renderable = manifestView?.renderable !== false;
355
+ const manifestOperation = manifestView?.operations.find((operation) => operation.operationId === operationId.value);
356
+ if (manifestView && !manifestOperation)
357
+ return { error: "Selected operation is not available for this view." };
358
+ const renderable = manifestOperation?.method === "GET" && manifestOperation.renderable === true;
341
359
  const manifest = getManifestCache().get(serviceId.value);
342
360
  const servicePathVariant = trimmedString(body, "servicePathVariant") ?? manifestView?.path ?? `/${viewId.value}`;
343
361
  if (manifestView && ![manifestView.path, ...manifestView.pathVariants].includes(servicePathVariant)) {
@@ -357,7 +375,7 @@ function parseRouteCreateBody(body) {
357
375
  const mappingError = validateRouteParamMapping(path.value, serviceRoutePath, fixedParams, manifestView?.paramsSchema);
358
376
  if (mappingError)
359
377
  return { error: mappingError };
360
- const title = renderable ? requiredRouteString(body, "title", "Display title") : { value: manifestView?.viewId ?? viewId.value };
378
+ const title = renderable ? requiredRouteString(body, "title", "Display title") : { value: manifestOperation?.title ?? operationId.value };
361
379
  if (title.error)
362
380
  return { error: title.error };
363
381
  const route = {
@@ -369,7 +387,7 @@ function parseRouteCreateBody(body) {
369
387
  ...(Object.keys(fixedParams).length > 0 ? { fixedParams } : {}),
370
388
  title: title.value,
371
389
  enabled: true,
372
- methods: routeMethodsFromManifest(manifestView?.methods)
390
+ operations: [operationId.value]
373
391
  };
374
392
  if (manifestView?.path)
375
393
  route.targetPath = serviceRoutePath;
@@ -383,7 +401,7 @@ function serviceRouteTargetKey(route) {
383
401
  ?? route.targetPath
384
402
  ?? getManifestCache().get(route.serviceId)?.viewIndex[route.viewId]?.path;
385
403
  return servicePath
386
- ? `${route.serviceId}\u0000${route.viewId}\u0000${appRoutePatternKey(servicePath)}`
404
+ ? `${route.serviceId}\u0000${route.viewId}\u0000${[...route.operations].sort().join(",")}\u0000${appRoutePatternKey(servicePath)}`
387
405
  : undefined;
388
406
  }
389
407
  function hasDuplicateServiceRouteTarget(appDef, route, excludeRouteId) {
@@ -453,22 +471,26 @@ function addRouteDependencies(appDef, route) {
453
471
  const view = manifest?.viewIndex[route.viewId];
454
472
  if (!manifest || !view)
455
473
  return;
456
- for (const dependencyViewId of view.dependencies) {
457
- const dependency = manifest.viewIndex[dependencyViewId];
474
+ const operations = new Map(Object.values(manifest.viewIndex).flatMap((candidateView) => candidateView.operations.map((operation) => [operation.operationId, { view: candidateView, operation }])));
475
+ const dependencies = view.operations
476
+ .filter((operation) => route.operations.includes(operation.operationId))
477
+ .flatMap((operation) => operation.dependencies);
478
+ for (const dependencyOperationId of dependencies) {
479
+ const dependency = operations.get(dependencyOperationId);
458
480
  if (!dependency)
459
481
  continue;
460
- if (appDef.routes.some((candidate) => candidate.serviceId === route.serviceId && candidate.viewId === dependencyViewId))
482
+ if (appDef.routes.some((candidate) => candidate.serviceId === route.serviceId && candidate.operations.includes(dependencyOperationId)))
461
483
  continue;
462
484
  appDef.routes.push({
463
485
  id: uuidv7(),
464
486
  kind: "api",
465
- path: apiRoutePath(manifest.serviceId, dependency.path),
487
+ path: apiRoutePath(manifest.serviceId, dependency.view.path),
466
488
  serviceId: route.serviceId,
467
- viewId: dependencyViewId,
468
- targetPath: dependency.path,
469
- title: dependency.viewId,
489
+ viewId: dependency.view.viewId,
490
+ targetPath: dependency.view.path,
491
+ title: dependency.operation.title,
470
492
  enabled: true,
471
- methods: routeMethodsFromManifest(dependency.methods)
493
+ operations: [dependencyOperationId]
472
494
  });
473
495
  }
474
496
  }
@@ -1527,16 +1549,16 @@ export function registerAdminApiRoutes(app, store, cpState) {
1527
1549
  type: av.literal("auth"),
1528
1550
  mutation: av.object({
1529
1551
  roles: av.array(AppAuthRoleSchema)
1530
- }, { unknownKeys: "strip" })
1531
- }, { unknownKeys: "strip" }),
1552
+ })
1553
+ }),
1532
1554
  av.object({
1533
1555
  tenantId: av.string().minLength(1),
1534
1556
  appId: av.string().minLength(1),
1535
1557
  type: av.literal("theme"),
1536
1558
  mutation: av.object({
1537
1559
  themeConfig: BetterPortalThemeConfigSchema
1538
- }, { unknownKeys: "strip" })
1539
- }, { unknownKeys: "strip" })
1560
+ })
1561
+ })
1540
1562
  ]);
1541
1563
  const getAppOr404 = async (appId) => {
1542
1564
  const config = await store.loadConfig();
@@ -1979,10 +2001,19 @@ export function registerAdminApiRoutes(app, store, cpState) {
1979
2001
  return validationError(event, "View is required.");
1980
2002
  route.viewId = viewId;
1981
2003
  }
2004
+ if (body.operationId !== undefined) {
2005
+ const operationId = trimmedString(body, "operationId");
2006
+ if (!operationId)
2007
+ return validationError(event, "Operation is required.");
2008
+ route.operations = [operationId];
2009
+ }
1982
2010
  const manifest = getManifestCache().get(route.serviceId);
1983
2011
  const manifestView = manifest?.viewIndex[route.viewId];
1984
2012
  if (manifestView) {
1985
- const routeIdentityChanged = body.serviceId !== undefined || body.viewId !== undefined;
2013
+ const manifestOperation = manifestView.operations.find((operation) => route.operations.includes(operation.operationId));
2014
+ if (!manifestOperation)
2015
+ return validationError(event, "Selected operation is not available for this view.");
2016
+ const routeIdentityChanged = body.serviceId !== undefined || body.viewId !== undefined || body.operationId !== undefined;
1986
2017
  const requestedServicePath = trimmedString(body, "servicePathVariant")
1987
2018
  ?? (routeIdentityChanged ? undefined : route.servicePathVariant)
1988
2019
  ?? manifestView.path;
@@ -1993,7 +2024,8 @@ export function registerAdminApiRoutes(app, store, cpState) {
1993
2024
  if (servicePathError)
1994
2025
  return validationError(event, servicePathError);
1995
2026
  const submittedFixed = fixedParamsFromBody(body);
1996
- const mappedAppPath = manifestView.renderable === false
2027
+ const pageRenderable = manifestOperation.method === "GET" && manifestOperation.renderable;
2028
+ const mappedAppPath = !pageRenderable
1997
2029
  ? apiRoutePath(manifest?.serviceId ?? route.serviceId, requestedServicePath)
1998
2030
  : route.path;
1999
2031
  const appParams = new Set(routeParamNames(mappedAppPath));
@@ -2009,12 +2041,11 @@ export function registerAdminApiRoutes(app, store, cpState) {
2009
2041
  route.fixedParams = fixedParams;
2010
2042
  else
2011
2043
  delete route.fixedParams;
2012
- route.methods = routeMethodsFromManifest(manifestView.methods);
2013
2044
  route.targetPath = requestedServicePath;
2014
- if (manifestView.renderable === false) {
2045
+ if (!pageRenderable) {
2015
2046
  route.kind = "api";
2016
2047
  route.path = apiRoutePath(manifest?.serviceId ?? route.serviceId, requestedServicePath);
2017
- route.title = manifestView.viewId;
2048
+ route.title = manifestOperation.title;
2018
2049
  delete route.query;
2019
2050
  }
2020
2051
  else {
@@ -2030,7 +2061,8 @@ export function registerAdminApiRoutes(app, store, cpState) {
2030
2061
  }
2031
2062
  if (body.query !== undefined) {
2032
2063
  const query = trimmedString(body, "query");
2033
- if (isApiRoute(route, manifestView?.renderable))
2064
+ const renderable = manifestView?.operations.some((operation) => route.operations.includes(operation.operationId) && operation.method === "GET" && operation.renderable);
2065
+ if (isApiRoute(route, renderable))
2034
2066
  delete route.query;
2035
2067
  else if (query)
2036
2068
  route.query = query.replace(/^\?+/, "");
@@ -2041,7 +2073,8 @@ export function registerAdminApiRoutes(app, store, cpState) {
2041
2073
  const title = trimmedString(body, "title");
2042
2074
  if (!title)
2043
2075
  return validationError(event, "Display title is required.");
2044
- if (!isApiRoute(route, manifestView?.renderable))
2076
+ const renderable = manifestView?.operations.some((operation) => route.operations.includes(operation.operationId) && operation.method === "GET" && operation.renderable);
2077
+ if (!isApiRoute(route, renderable))
2045
2078
  route.title = title;
2046
2079
  }
2047
2080
  if (body.enabled !== undefined)
@@ -2111,19 +2144,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
2111
2144
  const appDef = config.apps.find((a) => a.id === appId);
2112
2145
  if (!appDef)
2113
2146
  return jsonResponse({ error: "App not found" }, 404);
2114
- appDef.menu = items.map((raw) => {
2115
- const item = raw && typeof raw === "object" ? raw : {};
2116
- return {
2117
- id: typeof item.id === "string" ? item.id : uuidv7(),
2118
- type: item.type === "group" ? "group" : "link",
2119
- title: typeof item.title === "string" ? item.title : undefined,
2120
- icon: typeof item.icon === "string" ? item.icon : undefined,
2121
- routeId: typeof item.routeId === "string" ? item.routeId : undefined,
2122
- href: typeof item.href === "string" ? item.href : undefined,
2123
- enabled: item.enabled !== false,
2124
- children: Array.isArray(item.children) ? item.children : []
2125
- };
2126
- });
2147
+ appDef.menu = items.map(normalizeMenuItem);
2127
2148
  await store.saveConfig(config);
2128
2149
  return jsonResponse({ ok: true });
2129
2150
  });
@@ -2196,27 +2217,27 @@ export function registerAdminApiRoutes(app, store, cpState) {
2196
2217
  let serviceId;
2197
2218
  let capabilities = [];
2198
2219
  let serviceRoutes = [];
2199
- let viewMeta = new Map();
2200
- let dependencyViewIds = new Set();
2220
+ let dependencyOperationIds = new Set();
2201
2221
  const schemaText = typeof body.schema === "string" ? body.schema : "";
2202
2222
  if (schemaText) {
2203
2223
  try {
2204
2224
  const schema = JSON.parse(schemaText);
2205
2225
  serviceId = schema.manifest?.pluginId;
2206
2226
  capabilities = Array.isArray(schema.manifest?.capabilities) ? schema.manifest.capabilities.filter((value) => typeof value === "string") : [];
2207
- serviceRoutes = schema.routes ?? [];
2208
2227
  const manifestViews = schema.manifest?.views ?? [];
2209
- dependencyViewIds = new Set(manifestViews.flatMap((view) => Array.isArray(view.dependencies)
2210
- ? view.dependencies.filter((value) => typeof value === "string")
2228
+ dependencyOperationIds = new Set(manifestViews.flatMap((view) => (view.operations ?? []).flatMap((operation) => Array.isArray(operation.dependencies)
2229
+ ? operation.dependencies.filter((value) => typeof value === "string")
2230
+ : [])));
2231
+ serviceRoutes = manifestViews.flatMap((view) => (view.operations ?? []).flatMap((operation) => typeof operation.operationId === "string"
2232
+ ? [{
2233
+ viewId: view.viewId,
2234
+ path: view.path ?? `/${view.viewId}`,
2235
+ operationId: operation.operationId,
2236
+ title: typeof operation.title === "string" ? operation.title : view.title,
2237
+ renderable: operation.renderable === true,
2238
+ pageRenderable: operation.method === "GET" && hasPageRenderer({ ...view, operations: [operation] })
2239
+ }]
2211
2240
  : []));
2212
- viewMeta = new Map(manifestViews.map((v) => [v.viewId, {
2213
- title: v.title,
2214
- renderable: v.renderable !== false,
2215
- pageRenderable: hasPageRenderer(v),
2216
- methods: Array.isArray(v.methods)
2217
- ? v.methods.filter((value) => typeof value === "string")
2218
- : []
2219
- }]));
2220
2241
  }
2221
2242
  catch { /* malformed schema - register without routes */ }
2222
2243
  }
@@ -2246,47 +2267,51 @@ export function registerAdminApiRoutes(app, store, cpState) {
2246
2267
  const existingPaths = new Set(appDef.routes.map((r) => r.path));
2247
2268
  for (const r of serviceRoutes) {
2248
2269
  const routeServiceId = serviceId ?? newServiceId;
2249
- const meta = viewMeta.get(r.viewId);
2250
- const renderable = meta?.renderable ?? r.renderable !== false;
2251
- const mountPath = renderable
2270
+ const mountPath = r.pageRenderable
2252
2271
  ? pageRoutePath(routeServiceId, r.path)
2253
2272
  : apiRoutePath(routeServiceId, r.path);
2254
- if (existingPaths.has(mountPath))
2273
+ if (r.pageRenderable && existingPaths.has(mountPath))
2274
+ continue;
2275
+ if (appDef.routes.some((route) => route.serviceId === newServiceId && route.operations.includes(r.operationId)))
2255
2276
  continue;
2256
2277
  const routeId = uuidv7();
2257
- const routeTitle = meta?.title ?? r.viewId;
2278
+ const routeTitle = r.title;
2258
2279
  appDef.routes.push({
2259
2280
  id: routeId,
2260
- kind: renderable ? "page" : "api",
2281
+ kind: r.pageRenderable ? "page" : "api",
2261
2282
  path: mountPath,
2262
2283
  serviceId: newServiceId,
2263
2284
  viewId: r.viewId,
2264
2285
  targetPath: r.path,
2265
2286
  title: routeTitle,
2266
- enabled: renderable || dependencyViewIds.has(r.viewId),
2267
- methods: routeMethodsFromManifest(r.methods)
2287
+ enabled: r.pageRenderable || dependencyOperationIds.has(r.operationId),
2288
+ operations: [r.operationId]
2268
2289
  });
2269
- existingPaths.add(mountPath);
2270
- if (meta?.pageRenderable === true && !isApiRoute({ kind: renderable ? "page" : "api", path: mountPath }, renderable)) {
2290
+ if (r.pageRenderable)
2291
+ existingPaths.add(mountPath);
2292
+ if (r.pageRenderable && !isApiRoute({ kind: "page", path: mountPath }, true)) {
2271
2293
  groupChildren.push({
2272
2294
  id: uuidv7(),
2273
2295
  type: "link",
2274
2296
  title: routeTitle,
2275
2297
  routeId,
2276
- enabled: true
2298
+ enabled: true,
2299
+ serviceStatus: "show",
2300
+ authStatus: "show",
2301
+ children: []
2277
2302
  });
2278
2303
  }
2279
2304
  }
2280
2305
  if (groupChildren.length > 0) {
2281
- const menu = (appDef.menu ?? []);
2282
- menu.push({
2306
+ appDef.menu.push({
2283
2307
  id: groupId,
2284
2308
  type: "group",
2285
2309
  title,
2286
2310
  enabled: true,
2311
+ serviceStatus: "show",
2312
+ authStatus: "show",
2287
2313
  children: groupChildren
2288
2314
  });
2289
- appDef.menu = menu;
2290
2315
  }
2291
2316
  }
2292
2317
  }