@betterportal/config-manager 11.0.2 → 11.0.4

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 (37) 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 +1 -1
  4. package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts.map +1 -1
  5. package/lib/plugins/service-betterportal-config-manager/adminApi.js +49 -27
  6. package/lib/plugins/service-betterportal-config-manager/adminApi.js.map +1 -1
  7. package/lib/plugins/service-betterportal-config-manager/appQueries.d.ts +202 -4
  8. package/lib/plugins/service-betterportal-config-manager/appQueries.d.ts.map +1 -1
  9. package/lib/plugins/service-betterportal-config-manager/appQueries.js +22 -6
  10. package/lib/plugins/service-betterportal-config-manager/appQueries.js.map +1 -1
  11. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_renderer.bootstrap5/GET.d.ts.map +1 -1
  12. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_renderer.bootstrap5/GET.js +8 -5
  13. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_renderer.bootstrap5/GET.js.map +1 -1
  14. package/lib/plugins/service-betterportal-config-manager/index.d.ts +1 -0
  15. package/lib/plugins/service-betterportal-config-manager/index.d.ts.map +1 -1
  16. package/lib/plugins/service-betterportal-config-manager/index.js +15 -12
  17. package/lib/plugins/service-betterportal-config-manager/index.js.map +1 -1
  18. package/lib/plugins/service-betterportal-config-manager/menuEditor.d.ts.map +1 -1
  19. package/lib/plugins/service-betterportal-config-manager/menuEditor.js +3 -2
  20. package/lib/plugins/service-betterportal-config-manager/menuEditor.js.map +1 -1
  21. package/lib/plugins/service-betterportal-config-manager/previewEnvironments.d.ts.map +1 -1
  22. package/lib/plugins/service-betterportal-config-manager/previewEnvironments.js +2 -1
  23. package/lib/plugins/service-betterportal-config-manager/previewEnvironments.js.map +1 -1
  24. package/lib/plugins/service-betterportal-config-manager/routeMounts.d.ts +5 -0
  25. package/lib/plugins/service-betterportal-config-manager/routeMounts.d.ts.map +1 -1
  26. package/lib/plugins/service-betterportal-config-manager/routeMounts.js +3 -0
  27. package/lib/plugins/service-betterportal-config-manager/routeMounts.js.map +1 -1
  28. package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts +2 -1
  29. package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts.map +1 -1
  30. package/lib/plugins/service-betterportal-config-manager/syncApi.js +180 -141
  31. package/lib/plugins/service-betterportal-config-manager/syncApi.js.map +1 -1
  32. package/lib/plugins/service-betterportal-config-manager/tenantManagement.d.ts.map +1 -1
  33. package/lib/plugins/service-betterportal-config-manager/tenantManagement.js +3 -1
  34. package/lib/plugins/service-betterportal-config-manager/tenantManagement.js.map +1 -1
  35. package/lib/schemas/service-betterportal-config-manager.json +1 -1
  36. package/lib/schemas/service-betterportal-config-manager.plugin.json +1 -1
  37. package/package.json +3 -3
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Auto-generated BSB virtual client for service-betterportal-config-manager
3
3
  * DO NOT EDIT - Regenerated on every build
4
- * @version 11.0.2
4
+ * @version 11.0.4
5
5
  */
6
6
  import { ServiceClient, BSBService } from "@bsb/base";
7
7
  import type { Observable, BSBServiceClientDefinition, EventInputType } from "@bsb/base";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Auto-generated BSB virtual client for service-betterportal-config-manager
3
3
  * DO NOT EDIT - Regenerated on every build
4
- * @version 11.0.2
4
+ * @version 11.0.4
5
5
  */
6
6
  import { ServiceClient, importPortableSchema, createFireAndForgetEvent, createBroadcastEvent, createEventSchemas } from "@bsb/base";
7
7
  const _webhookDeliveryAvailableSchema = importPortableSchema(JSON.parse("{\"anyvaliVersion\":\"1.0\",\"schemaVersion\":\"1.1\",\"root\":{\"kind\":\"object\",\"properties\":{},\"required\":[],\"unknownKeys\":\"strip\"},\"definitions\":{},\"extensions\":{}}"));
@@ -4,7 +4,7 @@
4
4
  "pluginId": "org.betterportal.config-manager",
5
5
  "title": "BetterPortal Config Manager",
6
6
  "description": "Admin-facing BetterPortal service for discovering and managing service config surfaces.",
7
- "version": "11.0.2",
7
+ "version": "11.0.4",
8
8
  "category": "service",
9
9
  "deploymentModes": [
10
10
  "self-hosted"
@@ -1 +1 @@
1
- {"version":3,"file":"adminApi.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/adminApi.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adminApi.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/adminApi.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,11 @@
1
+ import { isPageOperation } from "./routeMounts.js";
1
2
  import { eventObservability } from "@betterportal/framework";
2
3
  import { editAppRows, readApp, readRequestConfig } from "./appQueries.js";
3
4
  import { isMenuRouteExcluded } from "./routeMounts.js";
4
5
  import * as av from "anyvali";
5
6
  import { AppAuthRoleSchema, eventHeaders, htmlResponse, jsonResponse, readBoundedJsonBody, resolveEmbeddedRequestContext, signServiceConfigTicket, uuidv7, BetterPortalMenuItemSchema, BetterPortalThemeConfigSchema } from "@betterportal/framework";
6
7
  import { generateApiKey, hashApiKey } from "./storage/index.js";
7
- import { getManifestCache } from "./syncApi.js";
8
+ import { getCachedManifestForService, getManifestCache, reconcileCachedServiceRoutes } from "./syncApi.js";
8
9
  import { apiRoutePath, appRoutePatternKey, isApiRoute, pageRoutePath } from "./routeMounts.js";
9
10
  import { M2MConnectionError, approveM2MConnections, buildM2MConnectionModel, revokeM2MConnection } from "./m2mConnections.js";
10
11
  import { isPreviewApp } from "./previewEnvironments.js";
@@ -77,7 +78,7 @@ function validationError(event, message) {
77
78
  }
78
79
  // PostgreSQL edits read current rows under an app-scoped transaction.
79
80
  // File stores retain their existing single-process save semantics.
80
- async function editApp(store, appId, action, database, event) {
81
+ async function editApp(store, appId, action, database, event, includeManifests = false) {
81
82
  const apply = async (config) => {
82
83
  const response = await action(config);
83
84
  if (!response.ok)
@@ -86,7 +87,7 @@ async function editApp(store, appId, action, database, event) {
86
87
  };
87
88
  try {
88
89
  if (database)
89
- return await editAppRows(database, appId, event ? eventObservability(event) : undefined, action);
90
+ return await editAppRows(database, appId, event ? eventObservability(event) : undefined, action, includeManifests);
90
91
  if (store.mutateApp)
91
92
  return await store.mutateApp(appId, apply);
92
93
  const config = await store.loadConfig();
@@ -373,7 +374,7 @@ export function renderAutomationLlmsApi(catalog) {
373
374
  }
374
375
  return lines.join("\n");
375
376
  }
376
- function parseRouteCreateBody(body) {
377
+ function parseRouteCreateBody(body, config) {
377
378
  const serviceId = requiredRouteString(body, "serviceId", "Service");
378
379
  if (serviceId.error)
379
380
  return { error: serviceId.error };
@@ -383,12 +384,16 @@ function parseRouteCreateBody(body) {
383
384
  const operationId = requiredRouteString(body, "operationId", "Operation");
384
385
  if (operationId.error)
385
386
  return { error: operationId.error };
386
- const manifestView = getManifestCache().get(serviceId.value)?.viewIndex[viewId.value];
387
+ const manifest = getCachedManifestForService(config, serviceId.value);
388
+ if (!manifest)
389
+ return { error: "Service manifest is not synced yet. Wait for service manifest sync before adding a route." };
390
+ const manifestView = manifest.viewIndex[viewId.value];
391
+ if (!manifestView)
392
+ return { error: "Selected view is not available in the synced service manifest." };
387
393
  const manifestOperation = manifestView?.operations.find((operation) => operation.operationId === operationId.value);
388
394
  if (manifestView && !manifestOperation)
389
395
  return { error: "Selected operation is not available for this view." };
390
- const renderable = manifestOperation?.method === "GET" && manifestOperation.renderModes.includes("page");
391
- const manifest = getManifestCache().get(serviceId.value);
396
+ const renderable = manifestOperation ? isPageOperation(manifestOperation) : false;
392
397
  const servicePathVariant = trimmedString(body, "servicePathVariant") ?? manifestView?.path ?? `/${viewId.value}`;
393
398
  if (manifestView && ![manifestView.path, ...manifestView.pathVariants].includes(servicePathVariant)) {
394
399
  return { error: "Selected service path is not available for this view." };
@@ -486,8 +491,8 @@ function cleanupProvisionalTenantService(config, tenantId, serviceInstanceId) {
486
491
  }
487
492
  return { removed: true };
488
493
  }
489
- function addRouteDependencies(appDef, route) {
490
- const manifest = getManifestCache().get(route.serviceId);
494
+ function addRouteDependencies(config, appDef, route) {
495
+ const manifest = getCachedManifestForService(config, route.serviceId);
491
496
  const view = manifest?.viewIndex[route.viewId];
492
497
  if (!manifest || !view)
493
498
  return;
@@ -503,8 +508,12 @@ function addRouteDependencies(appDef, route) {
503
508
  continue;
504
509
  if (!dependency)
505
510
  continue;
506
- if (appDef.routes.some((candidate) => candidate.serviceId === route.serviceId && candidate.operations.includes(requirement.operationId)))
511
+ const existing = appDef.routes.find((candidate) => candidate.serviceId === route.serviceId && candidate.operations.includes(requirement.operationId));
512
+ if (existing) {
513
+ if (existing.enablement !== "disabled")
514
+ existing.enabled = true;
507
515
  continue;
516
+ }
508
517
  appDef.routes.push({
509
518
  id: uuidv7(),
510
519
  kind: "api",
@@ -1080,6 +1089,7 @@ export function registerAdminApiRoutes(app, store, cpState, database) {
1080
1089
  if (!shared)
1081
1090
  return jsonResponse({ error: "Shared service not found or disabled" }, 404);
1082
1091
  let activation = config.sharedServiceActivations.find((entry) => entry.sharedServiceId === sharedServiceId && entry.tenantId === appDef.tenantId && entry.appId === appDef.id);
1092
+ const wasEnabled = activation?.enabled === true;
1083
1093
  if (!activation) {
1084
1094
  activation = {
1085
1095
  id: uuidv7(),
@@ -1090,8 +1100,14 @@ export function registerAdminApiRoutes(app, store, cpState, database) {
1090
1100
  enabled: true
1091
1101
  };
1092
1102
  config.sharedServiceActivations.push(activation);
1093
- await store.saveConfig(config);
1094
1103
  }
1104
+ else if (!activation.enabled) {
1105
+ activation.enabled = true;
1106
+ activation.activatedAt = new Date().toISOString();
1107
+ }
1108
+ const routesChanged = reconcileCachedServiceRoutes(config, [activation.id]);
1109
+ if (!wasEnabled || routesChanged)
1110
+ await store.saveConfig(config);
1095
1111
  return jsonResponse({ ok: true, activation }, 201);
1096
1112
  });
1097
1113
  app.get("/.well-known/bp/manage/routes", async (event) => {
@@ -1105,13 +1121,13 @@ export function registerAdminApiRoutes(app, store, cpState, database) {
1105
1121
  if (!appDef)
1106
1122
  return jsonResponse({ error: "Unable to resolve current BetterPortal app" }, 404);
1107
1123
  const body = await readFormOrJsonBody(event);
1108
- const parsed = parseRouteCreateBody(body);
1109
- if (parsed.error || !parsed.route)
1110
- return validationError(event, parsed.error ?? "Invalid route");
1111
1124
  return editApp(store, appDef.id, async (data) => {
1112
1125
  const current = data.apps.find(app => app.id === appDef.id);
1113
1126
  if (!current || isPreviewApp(data, current.id))
1114
1127
  return jsonResponse({ error: "App not found" }, 404);
1128
+ const parsed = parseRouteCreateBody(body, data);
1129
+ if (parsed.error || !parsed.route)
1130
+ return validationError(event, parsed.error ?? "Invalid route");
1115
1131
  const serviceError = validateRegisteredRouteService(data, current, parsed.route.serviceId);
1116
1132
  if (serviceError)
1117
1133
  return validationError(event, serviceError);
@@ -1120,9 +1136,9 @@ export function registerAdminApiRoutes(app, store, cpState, database) {
1120
1136
  }
1121
1137
  const route = { id: uuidv7(), ...parsed.route };
1122
1138
  current.routes.push(route);
1123
- addRouteDependencies(current, route);
1139
+ addRouteDependencies(data, current, route);
1124
1140
  return jsonResponse({ ok: true }, 201);
1125
- }, database, event);
1141
+ }, database, event, true);
1126
1142
  });
1127
1143
  app.get("/.well-known/bp/manage/fragments", async (event) => {
1128
1144
  const { appDef } = await readCurrentApp(event);
@@ -1313,13 +1329,14 @@ export function registerAdminApiRoutes(app, store, cpState, database) {
1313
1329
  const existing = config.sharedServiceActivations.find((activation) => activation.sharedServiceId === sharedServiceId
1314
1330
  && activation.tenantId === tenantId
1315
1331
  && activation.appId === appId);
1332
+ const activationId = existing?.id ?? uuidv7();
1316
1333
  if (existing) {
1317
1334
  existing.enabled = true;
1318
1335
  existing.activatedAt = new Date().toISOString();
1319
1336
  }
1320
1337
  else {
1321
1338
  config.sharedServiceActivations.push({
1322
- id: uuidv7(),
1339
+ id: activationId,
1323
1340
  tenantId,
1324
1341
  appId,
1325
1342
  sharedServiceId,
@@ -1327,6 +1344,7 @@ export function registerAdminApiRoutes(app, store, cpState, database) {
1327
1344
  enabled: true
1328
1345
  });
1329
1346
  }
1347
+ reconcileCachedServiceRoutes(config, [activationId]);
1330
1348
  await store.saveConfig(config);
1331
1349
  if (wantsHtmx(event))
1332
1350
  return htmxReload(`/services?tenantId=${encodeURIComponent(tenantId)}`);
@@ -1443,6 +1461,7 @@ export function registerAdminApiRoutes(app, store, cpState, database) {
1443
1461
  enabled: true
1444
1462
  };
1445
1463
  tenant.services.push(service);
1464
+ reconcileCachedServiceRoutes(config, [service.id]);
1446
1465
  await store.saveConfig(config);
1447
1466
  return jsonResponse({ id: service.id, hostname: service.hostname, serviceId: service.serviceId, apiKey }, 201);
1448
1467
  });
@@ -1563,10 +1582,13 @@ export function registerAdminApiRoutes(app, store, cpState, database) {
1563
1582
  const tenant = config.tenants.find((t) => t.id === tenantId);
1564
1583
  if (!tenant)
1565
1584
  return jsonResponse({ error: "Tenant not found" }, 404);
1566
- if (!tenant.activatedPlatformServices.includes(psId)) {
1585
+ const wasActivated = tenant.activatedPlatformServices.includes(psId);
1586
+ if (!wasActivated) {
1567
1587
  tenant.activatedPlatformServices.push(psId);
1568
- await store.saveConfig(config);
1569
1588
  }
1589
+ const routesChanged = reconcileCachedServiceRoutes(config, [psId]);
1590
+ if (!wasActivated || routesChanged)
1591
+ await store.saveConfig(config);
1570
1592
  return jsonResponse({ ok: true });
1571
1593
  });
1572
1594
  app.delete(`${API_BASE}/tenants/:tenantId/activate/:platformServiceId`, async (event) => {
@@ -2040,7 +2062,7 @@ export function registerAdminApiRoutes(app, store, cpState, database) {
2040
2062
  const appDef = config.apps.find((a) => a.id === appId);
2041
2063
  if (!appDef)
2042
2064
  return wantsHtmx(event) ? htmxError("App not found", 404) : jsonResponse({ error: "App not found" }, 404);
2043
- const parsed = parseRouteCreateBody(body);
2065
+ const parsed = parseRouteCreateBody(body, config);
2044
2066
  if (parsed.error || !parsed.route)
2045
2067
  return validationError(event, parsed.error ?? "Invalid route.");
2046
2068
  if (parsed.route.kind === "api")
@@ -2053,11 +2075,11 @@ export function registerAdminApiRoutes(app, store, cpState, database) {
2053
2075
  }
2054
2076
  const route = { ...parsed.route, id: uuidv7() };
2055
2077
  appDef.routes.push(route);
2056
- addRouteDependencies(appDef, route);
2078
+ addRouteDependencies(config, appDef, route);
2057
2079
  if (wantsHtmx(event))
2058
2080
  return htmxReload(routesReloadPath(appId));
2059
2081
  return jsonResponse({ ok: true, id: route.id }, 201);
2060
- }, database, event);
2082
+ }, database, event, true);
2061
2083
  });
2062
2084
  app.put(`${API_BASE}/apps/:appId/routes/:routeId`, async (event) => {
2063
2085
  const appId = getParam(event, "appId");
@@ -2110,7 +2132,7 @@ export function registerAdminApiRoutes(app, store, cpState, database) {
2110
2132
  return validationError(event, "Operation is required.");
2111
2133
  route.operations = [operationId];
2112
2134
  }
2113
- const manifest = getManifestCache().get(route.serviceId);
2135
+ const manifest = getCachedManifestForService(config, route.serviceId);
2114
2136
  const manifestView = manifest?.viewIndex[route.viewId];
2115
2137
  if (manifestView) {
2116
2138
  const manifestOperation = manifestView.operations.find((operation) => route.operations.includes(operation.operationId));
@@ -2127,7 +2149,7 @@ export function registerAdminApiRoutes(app, store, cpState, database) {
2127
2149
  if (servicePathError)
2128
2150
  return validationError(event, servicePathError);
2129
2151
  const submittedFixed = fixedParamsFromBody(body);
2130
- const pageRenderable = manifestOperation.method === "GET" && manifestOperation.renderModes.includes("page");
2152
+ const pageRenderable = isPageOperation(manifestOperation);
2131
2153
  const mappedAppPath = !pageRenderable
2132
2154
  ? apiRoutePath(manifest?.serviceId ?? route.serviceId, requestedServicePath)
2133
2155
  : route.path;
@@ -2164,7 +2186,7 @@ export function registerAdminApiRoutes(app, store, cpState, database) {
2164
2186
  }
2165
2187
  if (body.query !== undefined) {
2166
2188
  const query = trimmedString(body, "query");
2167
- const renderable = manifestView?.operations.some((operation) => route.operations.includes(operation.operationId) && operation.method === "GET" && operation.renderModes.includes("page"));
2189
+ const renderable = manifestView?.operations.some((operation) => route.operations.includes(operation.operationId) && isPageOperation(operation));
2168
2190
  if (isApiRoute(route, renderable))
2169
2191
  delete route.query;
2170
2192
  else if (query)
@@ -2176,7 +2198,7 @@ export function registerAdminApiRoutes(app, store, cpState, database) {
2176
2198
  const title = trimmedString(body, "title");
2177
2199
  if (!title)
2178
2200
  return validationError(event, "Display title is required.");
2179
- const renderable = manifestView?.operations.some((operation) => route.operations.includes(operation.operationId) && operation.method === "GET" && operation.renderModes.includes("page"));
2201
+ const renderable = manifestView?.operations.some((operation) => route.operations.includes(operation.operationId) && isPageOperation(operation));
2180
2202
  if (!isApiRoute(route, renderable))
2181
2203
  route.title = title;
2182
2204
  }
@@ -2190,7 +2212,7 @@ export function registerAdminApiRoutes(app, store, cpState, database) {
2190
2212
  if (wantsHtmx(event))
2191
2213
  return htmxReload(routesReloadPath(appId, route.kind === "api" ? route.serviceId : undefined));
2192
2214
  return jsonResponse({ ok: true });
2193
- }, database, event);
2215
+ }, database, event, true);
2194
2216
  });
2195
2217
  app.delete(`${API_BASE}/apps/:appId/routes/:routeId`, async (event) => {
2196
2218
  const appId = getParam(event, "appId");