@teamscala/service-boot 0.23.433 → 0.23.434
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/fn-routers/index.ts
CHANGED
|
@@ -13,10 +13,24 @@
|
|
|
13
13
|
* RegionPageSwap can fetch a region's Puck page on demand and render it into a
|
|
14
14
|
* slot — the no-reload half of `region-views-are-pages` (every region is a Puck
|
|
15
15
|
* page; switching a view loads a different page's content).
|
|
16
|
+
*
|
|
17
|
+
* `resolvePanelData` WAS RESTORED 2026-08-31. `72ac1a7` ("Retire unoffered page
|
|
18
|
+
* ORPC procedures") deleted it three weeks earlier as unoffered, but its
|
|
19
|
+
* consumer is live and in ANOTHER REPO: `use-region-page.ts` in
|
|
20
|
+
* @teamscala/ui-foundation calls `regions.resolvePanelData` on every panel
|
|
21
|
+
* open. Only the comment describing it was left behind — the shape
|
|
22
|
+
* `a-deleted-producer-leaves-its-consumers-executing` names, where deleting a
|
|
23
|
+
* producer leaves its consumers demanding something that can no longer exist.
|
|
24
|
+
*
|
|
25
|
+
* It failed SILENTLY for three weeks because the client treats a failure as
|
|
26
|
+
* best-effort and maps it to null, so the 404 rendered an empty panel with no
|
|
27
|
+
* error anywhere. Observed live as `POST /api/rpc/regions/resolvePanelData 404`
|
|
28
|
+
* on every panel open on projects-dev.
|
|
16
29
|
*/
|
|
17
30
|
|
|
18
31
|
import { registerCustomRouter } from "@teamscala/orpc/app-router";
|
|
19
32
|
import { puckPagesRouter } from "../page-server-orpc/puck-pages-router";
|
|
33
|
+
import { resolvePanelDataHandler } from "./panel-data-router.ts";
|
|
20
34
|
import { watchDataChange } from "@teamscala/realtime-utils/data-change-procedure";
|
|
21
35
|
import { watchPresence } from "@teamscala/realtime-utils/presence-procedure";
|
|
22
36
|
import { agreementSendRouter } from "./agreement-send-router.ts";
|
|
@@ -51,6 +65,8 @@ export function registerFnRouters(): void {
|
|
|
51
65
|
loadPage: puckPagesRouter.loadFromWebsite,
|
|
52
66
|
// Resolve a page-kind panel's OWN loaderData (panel_view.loader_slug) so a
|
|
53
67
|
// panel region renders against the panel page's data, not the host's.
|
|
68
|
+
//
|
|
69
|
+
resolvePanelData: resolvePanelDataHandler,
|
|
54
70
|
});
|
|
55
71
|
registerCustomRouter("realtime", {
|
|
56
72
|
watchDataChange,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* `regions.resolvePanelData` — resolves a page-kind panel's OWN loaderData so a
|
|
7
|
+
* panel region renders blocks against the panel page's data, not the host
|
|
8
|
+
* page's. RegionPageSwap calls this on panel-open + injects the result via
|
|
9
|
+
* LiveDataProvider("page-data"), making panel-embedded data blocks (e.g. the
|
|
10
|
+
* workload Chart) self-sufficient: each panel carries its own data, no host
|
|
11
|
+
* coupling + no preloading the host route.
|
|
12
|
+
*
|
|
13
|
+
* Flow (registry reads via the loadRegistryConfig helper — registry-via-helper):
|
|
14
|
+
* panel_view.content.loader_slug → resource_loader config → orgId derived from
|
|
15
|
+
* the serving website (builda_websites.organisation_id) → resolveFromLoaderConfig
|
|
16
|
+
* → the loader's resolved sources. Panels without a loader_slug (e.g. the AI
|
|
17
|
+
* panel, whose block self-loads) return null — no extra round-trip, no behavior
|
|
18
|
+
* change.
|
|
19
|
+
*/
|
|
20
|
+
import { os } from "@orpc/server";
|
|
21
|
+
import * as v from "valibot";
|
|
22
|
+
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
23
|
+
import type { PanelViewKey } from "@teamscala/db/registry/generated/panel-view";
|
|
24
|
+
import type { ResourceLoaderKey } from "@teamscala/db/registry/generated/resource-loader";
|
|
25
|
+
import { resolveFromLoaderConfig } from "../resolve-from-loader-config.ts";
|
|
26
|
+
|
|
27
|
+
interface Context {
|
|
28
|
+
prisma: {
|
|
29
|
+
builda_websites: {
|
|
30
|
+
findFirst: (args: unknown) => Promise<{
|
|
31
|
+
organisation_id?: string | null;
|
|
32
|
+
} | null>;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const panelViewSchema = v.looseObject({
|
|
38
|
+
content: v.looseObject({ loader_slug: v.optional(v.string()) }),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const resolvePanelDataHandler = os
|
|
42
|
+
.$context<Context>()
|
|
43
|
+
.input(
|
|
44
|
+
v.object({
|
|
45
|
+
panelSlug: v.string(),
|
|
46
|
+
websiteSlug: v.string(),
|
|
47
|
+
/** Tenant orgId (preferred over the website's organisation_id, which is
|
|
48
|
+
* the platform org on a multi-tenant portal — not the tenant). The
|
|
49
|
+
* caller (RegionPageSwap) threads this from __PAGE_DATA__.orgId. */
|
|
50
|
+
orgId: v.optional(v.string()),
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
.handler(async ({ input, context }) => {
|
|
54
|
+
// panel_view → loader_slug (null when missing or the panel has no loader).
|
|
55
|
+
let loaderSlug: string | undefined;
|
|
56
|
+
try {
|
|
57
|
+
// THE SLUG IS RUNTIME-SUPPLIED, so it cannot be statically a
|
|
58
|
+
// PanelViewKey: it arrives from the browser as the opened panel's
|
|
59
|
+
// name. The lookup itself is the validation — loadRegistryConfig
|
|
60
|
+
// throws when no such row exists, and the catch below turns that into
|
|
61
|
+
// the documented null. Narrowing here states that contract rather
|
|
62
|
+
// than asserting the value is known good.
|
|
63
|
+
const pv = await loadRegistryConfig(
|
|
64
|
+
"panel_view",
|
|
65
|
+
input.panelSlug as PanelViewKey,
|
|
66
|
+
panelViewSchema,
|
|
67
|
+
);
|
|
68
|
+
loaderSlug = pv.content?.loader_slug;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (!loaderSlug) return null;
|
|
73
|
+
// resource_loader → its config (loose schema; resolveFromLoaderConfig owns
|
|
74
|
+
// the typed LoaderConfig contract).
|
|
75
|
+
let loader: unknown;
|
|
76
|
+
try {
|
|
77
|
+
loader = await loadRegistryConfig(
|
|
78
|
+
"resource_loader",
|
|
79
|
+
// Same contract: loader_slug is read from the panel row's loose
|
|
80
|
+
// content schema as a plain string, and a missing row throws into
|
|
81
|
+
// the catch below.
|
|
82
|
+
loaderSlug as ResourceLoaderKey,
|
|
83
|
+
v.unknown(),
|
|
84
|
+
);
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
// Tenant orgId: prefer the caller-provided value (the route's tenant org,
|
|
89
|
+
// threaded from __PAGE_DATA__.orgId); fall back to the website's
|
|
90
|
+
// organisation_id (the platform org — wrong for multi-tenant portals, but
|
|
91
|
+
// the only derivable value when the caller omits it).
|
|
92
|
+
let orgId = input.orgId;
|
|
93
|
+
if (!orgId) {
|
|
94
|
+
const site = await context.prisma.builda_websites.findFirst({
|
|
95
|
+
where: { OR: [{ slug: input.websiteSlug }, { id: input.websiteSlug }] },
|
|
96
|
+
select: { organisation_id: true },
|
|
97
|
+
});
|
|
98
|
+
orgId = site?.organisation_id ?? undefined;
|
|
99
|
+
}
|
|
100
|
+
return await resolveFromLoaderConfig(
|
|
101
|
+
loader as Parameters<typeof resolveFromLoaderConfig>[0],
|
|
102
|
+
{ orgId },
|
|
103
|
+
);
|
|
104
|
+
});
|