@teamscala/service-boot 0.23.516 → 0.23.518
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 +232 -1127
- package/src/agreement-gate/settings.ts +8 -8
- package/src/apply-logger-config.test.ts +46 -0
- package/src/apply-logger-config.ts +31 -0
- package/src/asset-proxy.test.ts +9 -5
- package/src/auth/sign-out.ts +122 -0
- package/src/build-client-assets/hydrate-bundle.ts +14 -4
- package/src/build-client-assets/manifest.ts +5 -18
- package/src/build-client-assets/styles-build.test.ts +11 -8
- package/src/build-client-assets/styles-build.ts +14 -63
- package/src/build-client-assets.test.ts +0 -161
- package/src/build-client-assets.ts +20 -67
- package/src/content-gate/token.ts +9 -2
- package/src/install-logging.ts +10 -26
- package/src/mount-orpc-router.ts +8 -2
- package/src/mutable-app-ref.ts +0 -7
- package/src/page-server-loader-graph.test.ts +31 -0
- package/src/page-server-loader-graph.ts +21 -0
- package/src/pg-boss-mount.ts +3 -16
- package/src/readiness-gate.ts +0 -4
- package/src/register-api-routes.ts +8 -17
- package/src/register-page-server-client.ts +6 -0
- package/src/register-registry-page-route/meta-title.test.ts +50 -0
- package/src/register-registry-page-route/meta-title.ts +1 -1
- package/src/register-registry-page-route/page-data-shared.ts +7 -11
- package/src/register-registry-page-route/resolve-request-graph.test.ts +1 -1
- package/src/register-registry-page-route/resolve-request-graph.ts +1 -2
- package/src/register-registry-page-route.ts +14 -16
- package/src/resolve-brand-favicon.test.ts +63 -0
- package/src/resolve-theme-mode.test.ts +51 -0
- package/src/resolvers/fetch-org-brand.test.ts +48 -0
- package/src/resolvers/website-by-host-map.test.ts +63 -0
- package/src/resolvers/website-by-host-map.ts +15 -3
- package/src/run-boot-hook-list.test.ts +47 -0
- package/src/run-boot-hooks.test.ts +56 -0
- package/src/scala-tools-currency-watchdog.test.ts +43 -0
- package/src/scala-tools-currency-watchdog.ts +0 -5
- package/src/transfers/presigned-upload.ts +165 -0
- package/src/two-phase-minimal-app.test.ts +24 -0
- package/src/two-phase-readiness.test.ts +102 -0
- package/src/types.test.ts +20 -0
- package/src/ui-service.test.ts +24 -0
- package/src/ws-boot-self-test.test.ts +43 -0
- package/src/build-client-assets/input-fingerprint.ts +0 -92
- package/src/build-client-assets/input-hashing.ts +0 -206
- package/src/handlers/bun-stable-watch.ts +0 -86
- package/src/handlers/messaging-send-reconcile-logic.test.ts +0 -79
- package/src/handlers/messaging-send-reconcile-logic.ts +0 -83
- package/src/handlers/messaging-send-recording-logic.test.ts +0 -86
- package/src/handlers/messaging-send-recording-logic.ts +0 -68
- package/src/handlers/messaging-send-recording-watch.test.ts +0 -91
- package/src/handlers/messaging-send-recording-watch.ts +0 -244
- package/src/handlers/messaging-send-sweep-selection.test.ts +0 -47
- package/src/handlers/messaging-send-sweep-selection.ts +0 -35
- package/src/handlers/platform-fault-escalation/config.ts +0 -95
- package/src/handlers/platform-fault-escalation/delivery.ts +0 -327
- package/src/handlers/platform-fault-escalation/event-log-queries.ts +0 -182
- package/src/handlers/platform-fault-escalation/orpc-client.ts +0 -114
- package/src/handlers/platform-fault-escalation/probe.test.ts +0 -63
- package/src/handlers/platform-fault-escalation/probe.ts +0 -19
- package/src/handlers/platform-fault-escalation-logic.ts +0 -382
- package/src/handlers/platform-fault-escalation-refresh.test.ts +0 -72
- package/src/handlers/platform-fault-escalation.test.ts +0 -497
- package/src/handlers/platform-fault-escalation.ts +0 -246
- package/src/handlers/projects-digest.ts +0 -198
- package/src/handlers/quiz-research.ts +0 -178
- package/src/handlers/registry-stale-inactive-sweep.ts +0 -82
- package/src/handlers/scala-tools-currency-watch.test.ts +0 -65
- package/src/handlers/scala-tools-currency-watch.ts +0 -162
- package/src/handlers/singleton-tools-currency-watch.test.ts +0 -51
- package/src/handlers/singleton-tools-currency-watch.ts +0 -380
- package/src/handlers/sweep-event-log.ts +0 -98
- package/src/handlers/upstream-issue-sync.ts +0 -220
- package/src/handlers/whatsapp-liveness-handler.test.ts +0 -56
- package/src/handlers/whatsapp-liveness-logic.ts +0 -53
- package/src/handlers/whatsapp-liveness.test.ts +0 -78
- package/src/handlers/whatsapp-liveness.ts +0 -138
- package/src/interpolate-meta.ts +0 -123
- package/src/load-service-prisma-client.ts +0 -53
- package/src/loader-errors.ts +0 -41
- package/src/read-viewport-from-request.ts +0 -36
- package/src/register-page-server-client.test.ts +0 -185
- package/src/resolve-app-base-url.ts +0 -35
- package/src/resolve-from-loader-config.test.ts +0 -429
- package/src/resolve-from-loader-config.ts +0 -316
- package/src/resolve-service-context.ts +0 -88
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
// @system ui-service-boot
|
|
2
|
-
// @status handwritten
|
|
3
|
-
// @edit edit directly
|
|
4
|
-
//
|
|
5
|
-
// register-page-server-client.test.ts — the declaredModels lookup must never
|
|
6
|
-
// quietly report "declares nothing".
|
|
7
|
-
//
|
|
8
|
-
// The regression this exists for (measured scala-sign 2026-09-24/25): the boot
|
|
9
|
-
// looked up service_schema with `undefined as never`, so loadRegistryConfig's
|
|
10
|
-
// `v.parse(undefined, raw)` threw a TypeError on EVERY service, the bare
|
|
11
|
-
// `catch {}` swallowed it into `declaredModels = []`, and the
|
|
12
|
-
// "declares > 0 models + no router → refuse the boot" check below never fired.
|
|
13
|
-
// A service declaring 18 models logged "0 declared model(s)", registered a
|
|
14
|
-
// router-less server client as healthy, and served 500 on every page while
|
|
15
|
-
// /health answered 200.
|
|
16
|
-
//
|
|
17
|
-
// Three assertions, each the operator's demand ("the catch must tell 'no
|
|
18
|
-
// service_schema row' apart from 'the read failed'. A read error must never
|
|
19
|
-
// quietly become 'no models'. Test both"):
|
|
20
|
-
// 1. a service that DECLARES models with no generated router → REFUSES the
|
|
21
|
-
// boot (proves the refusal actually fires now — it did not before);
|
|
22
|
-
// 2. a service with NO service_schema row → passes through clean (legitimate
|
|
23
|
-
// daemon/portal shell owns no DB models);
|
|
24
|
-
// 3. a service_schema READ that FAILS → REFUSES the boot naming the error
|
|
25
|
-
// (a read error is never treated as "declares nothing").
|
|
26
|
-
|
|
27
|
-
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
28
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
29
|
-
import { tmpdir } from "node:os";
|
|
30
|
-
import { join } from "node:path";
|
|
31
|
-
|
|
32
|
-
const originalCwd = process.cwd();
|
|
33
|
-
const tempDirs: string[] = [];
|
|
34
|
-
|
|
35
|
-
afterEach(async () => {
|
|
36
|
-
process.chdir(originalCwd);
|
|
37
|
-
for (const directory of tempDirs.splice(0)) {
|
|
38
|
-
await rm(directory, { recursive: true, force: true });
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// The registry read is scenario-driven: ONE load-config mock serves all three
|
|
43
|
-
// tests, and each test flips the mode before calling the function. The mock
|
|
44
|
-
// function is re-invoked on every call (the target imports it dynamically
|
|
45
|
-
// inside registerPageServerClient), so switching the mode takes effect.
|
|
46
|
-
let registryMode:
|
|
47
|
-
| { kind: "row"; models: string[] }
|
|
48
|
-
| { kind: "missing" }
|
|
49
|
-
| { kind: "missing-thrown" }
|
|
50
|
-
| { kind: "read-failed"; message: string } = { kind: "missing" };
|
|
51
|
-
|
|
52
|
-
// The class identity the catch's `instanceof` compares against. The target
|
|
53
|
-
// imports RegistryConfigMissingError from @teamscala/db/registry/errors, so the
|
|
54
|
-
// SAME mocked class must back both the throw path and the instanceof check.
|
|
55
|
-
class MockRegistryConfigMissingError extends Error {
|
|
56
|
-
readonly type: string;
|
|
57
|
-
readonly slug: string;
|
|
58
|
-
constructor(type: string, slug: string) {
|
|
59
|
-
super(`[registry] Config entry "${slug}" of type "${type}" not found`);
|
|
60
|
-
this.name = "RegistryConfigMissingError";
|
|
61
|
-
this.type = type;
|
|
62
|
-
this.slug = slug;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
mock.module("@teamscala/db/registry/load-config", () => ({
|
|
67
|
-
loadOptionalRegistryConfig: async () => {
|
|
68
|
-
if (registryMode.kind === "missing") return null;
|
|
69
|
-
if (registryMode.kind === "missing-thrown") {
|
|
70
|
-
throw new MockRegistryConfigMissingError("service_schema", "scala-sign");
|
|
71
|
-
}
|
|
72
|
-
if (registryMode.kind === "row") return { models: registryMode.models };
|
|
73
|
-
throw new Error(registryMode.message);
|
|
74
|
-
},
|
|
75
|
-
loadRegistryConfig: async () => ({ platformOrganisationId: "org-platform" }),
|
|
76
|
-
}));
|
|
77
|
-
|
|
78
|
-
mock.module("@teamscala/db/registry/errors", () => ({
|
|
79
|
-
RegistryConfigMissingError: MockRegistryConfigMissingError,
|
|
80
|
-
}));
|
|
81
|
-
|
|
82
|
-
mock.module("@teamscala/db-validation/registry-schemas/service-schema", () => ({
|
|
83
|
-
ServiceSchemaConfigSchema: {},
|
|
84
|
-
}));
|
|
85
|
-
|
|
86
|
-
mock.module("@teamscala/db-validation/registry-schemas/platform-identity", () => ({
|
|
87
|
-
PlatformIdentitySchema: {},
|
|
88
|
-
}));
|
|
89
|
-
|
|
90
|
-
mock.module("@teamscala/logger/app-loggers", () => ({
|
|
91
|
-
getAppLogger: () => ({
|
|
92
|
-
info: (..._args: unknown[]) => {},
|
|
93
|
-
warn: (..._args: unknown[]) => {},
|
|
94
|
-
error: (..._args: unknown[]) => {},
|
|
95
|
-
}),
|
|
96
|
-
}));
|
|
97
|
-
|
|
98
|
-
mock.module("@orpc/server", () => ({
|
|
99
|
-
createRouterClient: () => ({}),
|
|
100
|
-
}));
|
|
101
|
-
|
|
102
|
-
mock.module("@teamscala/db/prisma-registry", () => ({
|
|
103
|
-
getPrisma: () => ({}),
|
|
104
|
-
}));
|
|
105
|
-
|
|
106
|
-
// The page-server server-client is imported dynamically via a string-cast
|
|
107
|
-
// specifier inside the function; mock the exact specifier so setServerClient
|
|
108
|
-
// is a recording no-op and the real runtime never loads.
|
|
109
|
-
const setServerClientCalls: number[] = [];
|
|
110
|
-
mock.module("@teamscala/service-boot/page-server-orpc/server-client", () => ({
|
|
111
|
-
setServerClient: () => {
|
|
112
|
-
setServerClientCalls.push(1);
|
|
113
|
-
},
|
|
114
|
-
}));
|
|
115
|
-
|
|
116
|
-
const { registerPageServerClient } = await import(
|
|
117
|
-
"./register-page-server-client.ts"
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
/** Make an empty service dir with NO generated router anywhere, and reset the
|
|
121
|
-
* setServerClient recorder so each test's count is its own. */
|
|
122
|
-
async function cwdWithoutRouter(): Promise<string> {
|
|
123
|
-
setServerClientCalls.length = 0;
|
|
124
|
-
const dir = await mkdtemp(join(tmpdir(), "register-psc-test-"));
|
|
125
|
-
tempDirs.push(dir);
|
|
126
|
-
process.chdir(dir);
|
|
127
|
-
return dir;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
describe("registerPageServerClient — declaredModels lookup never lies", () => {
|
|
131
|
-
test("refuses the boot when the service DECLARES models but no router exists", async () => {
|
|
132
|
-
await cwdWithoutRouter();
|
|
133
|
-
registryMode = {
|
|
134
|
-
kind: "row",
|
|
135
|
-
// 18 models — the exact scala-sign row size whose absence from the
|
|
136
|
-
// boot log was the outage.
|
|
137
|
-
models: Array.from({ length: 18 }, (_unused, index) => `model_${index}`),
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
await expect(registerPageServerClient("scala-sign")).rejects.toThrow(
|
|
141
|
-
/service_schema declares 18 model\(s\).*no generated ORPC router is present/s,
|
|
142
|
-
);
|
|
143
|
-
expect(setServerClientCalls.length).toBe(0);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("passes through clean when there is NO service_schema row", async () => {
|
|
147
|
-
await cwdWithoutRouter();
|
|
148
|
-
// loadOptionalRegistryConfig returns null — the legitimately-declares-
|
|
149
|
-
// nothing case (daemon/portal shell). With no models declared there is no
|
|
150
|
-
// router required, so the boot must NOT refuse.
|
|
151
|
-
registryMode = { kind: "missing" };
|
|
152
|
-
|
|
153
|
-
await expect(registerPageServerClient("some-daemon-shell")).resolves.toBeUndefined();
|
|
154
|
-
expect(setServerClientCalls.length).toBeGreaterThan(0);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("refuses the boot when the service_schema READ fails, naming the error", async () => {
|
|
158
|
-
await cwdWithoutRouter();
|
|
159
|
-
// A read error (NOT a missing row) must fail the boot, never degrade to
|
|
160
|
-
// "declares nothing" — otherwise a data-backed service boots "0 models"
|
|
161
|
-
// and 500s on every page.
|
|
162
|
-
registryMode = { kind: "read-failed", message: "connection refused: db down" };
|
|
163
|
-
|
|
164
|
-
await expect(registerPageServerClient("scala-sign")).rejects.toThrow(
|
|
165
|
-
/service_schema read FAILED for "scala-sign"/,
|
|
166
|
-
);
|
|
167
|
-
await expect(registerPageServerClient("scala-sign")).rejects.toThrow(
|
|
168
|
-
/connection refused: db down/,
|
|
169
|
-
);
|
|
170
|
-
expect(setServerClientCalls.length).toBe(0);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("treats a thrown RegistryConfigMissingError as declares-nothing, not failure", async () => {
|
|
174
|
-
await cwdWithoutRouter();
|
|
175
|
-
// The read path throws RegistryConfigMissingError (a genuinely-absent row,
|
|
176
|
-
// the same signal as a null return, just a different code path): the catch
|
|
177
|
-
// must classify it as "declares nothing" via the instanceof check and boot
|
|
178
|
-
// CLEAN — this is exactly the branch that must NOT fail the boot. Contrast
|
|
179
|
-
// the read-failed test above, where an unrelated error DOES fail the boot.
|
|
180
|
-
registryMode = { kind: "missing-thrown" };
|
|
181
|
-
|
|
182
|
-
await expect(registerPageServerClient("scala-sign")).resolves.toBeUndefined();
|
|
183
|
-
expect(setServerClientCalls.length).toBeGreaterThan(0);
|
|
184
|
-
});
|
|
185
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The scala-projects app's own public hostname, read from its `service_install`
|
|
7
|
-
* row rather than hardcoded — a service knows its own deploy config only
|
|
8
|
-
* through the registry (registries-are-source-of-truth), never a literal in
|
|
9
|
-
* source. Cached for the process lifetime: this changes only on a redeploy,
|
|
10
|
-
* not per-request. Shared by every server-side path that builds a link back
|
|
11
|
-
* into the app (approval emails, mention emails, digest emails).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { ServiceInstallConfigSchema } from "@teamscala/db-validation/registry-schemas/service-install";
|
|
15
|
-
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
16
|
-
import { createCache } from "@teamscala/cache/create-cache";
|
|
17
|
-
|
|
18
|
-
const appUrlCache = createCache<string>("service-boot:app-base-url", {
|
|
19
|
-
ttlMs: Number.POSITIVE_INFINITY,
|
|
20
|
-
maxSize: 1,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
export async function resolveAppBaseUrl(): Promise<string> {
|
|
24
|
-
const hit = appUrlCache.get("default");
|
|
25
|
-
if (hit) return hit;
|
|
26
|
-
try {
|
|
27
|
-
const config = await loadRegistryConfig("service_install", "scala-projects", ServiceInstallConfigSchema);
|
|
28
|
-
const url = `https://${config.public_hostname}`;
|
|
29
|
-
appUrlCache.set("default", url);
|
|
30
|
-
return url;
|
|
31
|
-
} catch {
|
|
32
|
-
appUrlCache.set("default", "https://projects.scala.business");
|
|
33
|
-
return "https://projects.scala.business";
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
// @system codegen
|
|
2
|
-
// @status generated
|
|
3
|
-
// @edit change the suite in the owned-suites band, then re-run codegen. Hand-edits are overwritten.
|
|
4
|
-
//
|
|
5
|
-
// This suite's assertions are OWNED by the codegen band: the band module
|
|
6
|
-
// carries them verbatim, this file is the emission, and hand edits here are
|
|
7
|
-
// overwritten on the next run. The rationale each assertion carries moved
|
|
8
|
-
// with it into the band.
|
|
9
|
-
|
|
10
|
-
import { describe, expect, test } from "bun:test";
|
|
11
|
-
import { resolveFromLoaderConfig } from "./resolve-from-loader-config";
|
|
12
|
-
import { interpolateMeta } from "./interpolate-meta.ts";
|
|
13
|
-
import {
|
|
14
|
-
getServerClient,
|
|
15
|
-
isServerClientRegistered,
|
|
16
|
-
setServerClient,
|
|
17
|
-
} from "./page-server-orpc/server-client";
|
|
18
|
-
|
|
19
|
-
describe("interpolateMeta (resolver_input ref contract)", () => {
|
|
20
|
-
test("a bare :sourceKey ref passes the whole resolved source object", () => {
|
|
21
|
-
const out = interpolateMeta(
|
|
22
|
-
{ response: ":response", assessment: ":assessment" },
|
|
23
|
-
{
|
|
24
|
-
response: { total_score: 48, category_scores: { "Be Seen": 46 } },
|
|
25
|
-
assessment: { title: "Launcha" },
|
|
26
|
-
},
|
|
27
|
-
);
|
|
28
|
-
// Whole objects flow through — the bug that blanked every report was these
|
|
29
|
-
// resolving to undefined (bare refs read from URL params, not resolved).
|
|
30
|
-
expect(out.response).toEqual({ total_score: 48, category_scores: { "Be Seen": 46 } });
|
|
31
|
-
expect(out.assessment).toEqual({ title: "Launcha" });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("a bare ref with no resolved source key is left as the literal (interpolateMeta fallback)", () => {
|
|
35
|
-
// `:id` has no resolved source named "id" and params is empty here, so it
|
|
36
|
-
// does not resolve — interpolateMeta leaves the literal in place.
|
|
37
|
-
const out = interpolateMeta({ id: ":id" }, {});
|
|
38
|
-
expect(out.id).toBe(":id");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("a dotted ref still reads a field off a resolved source", () => {
|
|
42
|
-
const out = interpolateMeta(
|
|
43
|
-
{ score: ":response.total_score" },
|
|
44
|
-
{ response: { total_score: 48 } },
|
|
45
|
-
);
|
|
46
|
-
expect(out.score).toBe(48);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("resource-loader model capability", () => {
|
|
51
|
-
test("an optional source still refuses a model absent from the generated client", async () => { const original = isServerClientRegistered() ? getServerClient() : null;
|
|
52
|
-
setServerClient({} as never);
|
|
53
|
-
try {
|
|
54
|
-
await expect(
|
|
55
|
-
resolveFromLoaderConfig(
|
|
56
|
-
{
|
|
57
|
-
sources: [
|
|
58
|
-
{
|
|
59
|
-
key: "body",
|
|
60
|
-
source: "orpc",
|
|
61
|
-
model: "builda_pages",
|
|
62
|
-
optional: true,
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
},
|
|
66
|
-
{},
|
|
67
|
-
),
|
|
68
|
-
).rejects.toThrow(
|
|
69
|
-
'requires model "builda_pages", but the generated server client does not expose findFirst',
|
|
70
|
-
);
|
|
71
|
-
} finally {
|
|
72
|
-
setServerClient(original as never);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("an optional source may resolve an absent row when the model capability exists", async () => {
|
|
77
|
-
const original = isServerClientRegistered() ? getServerClient() : null;
|
|
78
|
-
setServerClient({
|
|
79
|
-
builda_pages: { findFirst: async () => null },
|
|
80
|
-
} as never);
|
|
81
|
-
try {
|
|
82
|
-
await expect(
|
|
83
|
-
resolveFromLoaderConfig(
|
|
84
|
-
{
|
|
85
|
-
sources: [
|
|
86
|
-
{
|
|
87
|
-
key: "body",
|
|
88
|
-
source: "orpc",
|
|
89
|
-
model: "builda_pages",
|
|
90
|
-
optional: true,
|
|
91
|
-
},
|
|
92
|
-
],
|
|
93
|
-
},
|
|
94
|
-
{},
|
|
95
|
-
),
|
|
96
|
-
).resolves.toEqual({ body: null });
|
|
97
|
-
} finally {
|
|
98
|
-
setServerClient(original as never);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe("resource_loader refs read both the snake and the camelCase spelling", () => {
|
|
104
|
-
// Loader rows author snake_case column names (page_id, organisation_id,
|
|
105
|
-
// prepared_for) while the v8 ORM hands back camelCase keys (pageId,
|
|
106
|
-
// organisationId, preparedFor). readField reads the spelled key first
|
|
107
|
-
// then its camelCase equivalent so neither side has to know the other.
|
|
108
|
-
// Measured through the ORPC branch: what the dependent source's findFirst
|
|
109
|
-
// RECEIVES as its where IS the resolution of `:proposal.page_id`.
|
|
110
|
-
const original = isServerClientRegistered() ? getServerClient() : null;
|
|
111
|
-
|
|
112
|
-
test("a snake_case ref resolves against a camelCase parent record", async () => {
|
|
113
|
-
const captured: { where: unknown } = { where: null };
|
|
114
|
-
const calls: string[] = [];
|
|
115
|
-
setServerClient({
|
|
116
|
-
builda_pages: {
|
|
117
|
-
findFirst: async (args: { where: Record<string, unknown> }) => {
|
|
118
|
-
calls.push(JSON.stringify(args.where));
|
|
119
|
-
// First call: the proposal source (id lookup) returns a
|
|
120
|
-
// camelCase row, exactly what the v8 chain hands back.
|
|
121
|
-
if (calls.length === 1) {
|
|
122
|
-
return {
|
|
123
|
-
id: "p1",
|
|
124
|
-
pageId: "page-uuid",
|
|
125
|
-
organisationId: "org-uuid",
|
|
126
|
-
preparedFor: "Klavdia Vainshtein",
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
// Second call: the dependent page source.
|
|
130
|
-
captured.where = args.where;
|
|
131
|
-
return { id: "page-uuid" };
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
} as never);
|
|
135
|
-
try {
|
|
136
|
-
await resolveFromLoaderConfig(
|
|
137
|
-
{
|
|
138
|
-
sources: [
|
|
139
|
-
{
|
|
140
|
-
key: "proposal",
|
|
141
|
-
source: "orpc",
|
|
142
|
-
model: "builda_pages",
|
|
143
|
-
where: { id: ":id" },
|
|
144
|
-
optional: false,
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
key: "page",
|
|
148
|
-
source: "orpc",
|
|
149
|
-
model: "builda_pages",
|
|
150
|
-
where: { id: ":proposal.page_id" },
|
|
151
|
-
optional: false,
|
|
152
|
-
depends_on: ["proposal"],
|
|
153
|
-
},
|
|
154
|
-
],
|
|
155
|
-
},
|
|
156
|
-
{ id: "p1" },
|
|
157
|
-
);
|
|
158
|
-
} finally {
|
|
159
|
-
setServerClient(original as never);
|
|
160
|
-
}
|
|
161
|
-
// The dependent source queried WITH the resolved page id, not an
|
|
162
|
-
// emptied filter matching an arbitrary row.
|
|
163
|
-
expect(captured.where).toEqual({ id: "page-uuid" });
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test("identity-name fields resolve unchanged (id, path, slug)", async () => {
|
|
167
|
-
const captured: { where: unknown } = { where: null };
|
|
168
|
-
const calls: string[] = [];
|
|
169
|
-
setServerClient({
|
|
170
|
-
builda_pages: {
|
|
171
|
-
findFirst: async (args: { where: Record<string, unknown> }) => {
|
|
172
|
-
calls.push(JSON.stringify(args.where));
|
|
173
|
-
if (calls.length === 1) {
|
|
174
|
-
return { id: "p1", path: "/header", websiteId: "w-1" };
|
|
175
|
-
}
|
|
176
|
-
captured.where = args.where;
|
|
177
|
-
return { id: "p1" };
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
} as never);
|
|
181
|
-
try {
|
|
182
|
-
await resolveFromLoaderConfig(
|
|
183
|
-
{
|
|
184
|
-
sources: [
|
|
185
|
-
{
|
|
186
|
-
key: "page",
|
|
187
|
-
source: "orpc",
|
|
188
|
-
model: "builda_pages",
|
|
189
|
-
where: { id: ":id" },
|
|
190
|
-
optional: false,
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
key: "footer",
|
|
194
|
-
source: "orpc",
|
|
195
|
-
model: "builda_pages",
|
|
196
|
-
where: { path: ":page.path", website_id: ":page.website_id" },
|
|
197
|
-
optional: false,
|
|
198
|
-
depends_on: ["page"],
|
|
199
|
-
},
|
|
200
|
-
],
|
|
201
|
-
},
|
|
202
|
-
{ id: "p1" },
|
|
203
|
-
);
|
|
204
|
-
} finally {
|
|
205
|
-
setServerClient(original as never);
|
|
206
|
-
}
|
|
207
|
-
// path (identity) and website_id (snake → camel websiteId) both
|
|
208
|
-
// resolve through the same readField walk.
|
|
209
|
-
expect(captured.where).toEqual({ path: "/header", website_id: "w-1" });
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
describe("required sources refuse unresolvable where refs", () => {
|
|
214
|
-
// The 2026-09-03 proposal outage class: interpolateTemplate DROPS a key
|
|
215
|
-
// whose ref resolves undefined, so a required source queries with a
|
|
216
|
-
// partial (or empty) filter — which matches an ARBITRARY row or none,
|
|
217
|
-
// serving a silent empty page instead of failing. Null is a real value
|
|
218
|
-
// (an unset column) and must keep flowing through.
|
|
219
|
-
const original = isServerClientRegistered() ? getServerClient() : null;
|
|
220
|
-
const capturingClient = (captured: { where: unknown }) => ({
|
|
221
|
-
builda_pages: {
|
|
222
|
-
findFirst: async (args: { where: unknown }) => {
|
|
223
|
-
captured.where = args.where;
|
|
224
|
-
return { id: "row" };
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
} as never);
|
|
228
|
-
|
|
229
|
-
test("a required source with an undefined ref throws naming it", async () => {
|
|
230
|
-
setServerClient(capturingClient({ where: null }));
|
|
231
|
-
try {
|
|
232
|
-
await expect(
|
|
233
|
-
resolveFromLoaderConfig(
|
|
234
|
-
{
|
|
235
|
-
sources: [
|
|
236
|
-
{
|
|
237
|
-
key: "page",
|
|
238
|
-
source: "orpc",
|
|
239
|
-
model: "builda_pages",
|
|
240
|
-
where: { id: ":proposal.page_id" },
|
|
241
|
-
optional: false,
|
|
242
|
-
},
|
|
243
|
-
],
|
|
244
|
-
},
|
|
245
|
-
{ id: "p1" },
|
|
246
|
-
),
|
|
247
|
-
).rejects.toThrow(/required source "page".*:proposal\.page_id.*undefined/);
|
|
248
|
-
} finally {
|
|
249
|
-
setServerClient(original as never);
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
test("an optional source with the same undefined ref still resolves null", async () => {
|
|
254
|
-
setServerClient({
|
|
255
|
-
builda_pages: { findFirst: async () => null },
|
|
256
|
-
} as never);
|
|
257
|
-
try {
|
|
258
|
-
await expect(
|
|
259
|
-
resolveFromLoaderConfig(
|
|
260
|
-
{
|
|
261
|
-
sources: [
|
|
262
|
-
{
|
|
263
|
-
key: "page",
|
|
264
|
-
source: "orpc",
|
|
265
|
-
model: "builda_pages",
|
|
266
|
-
where: { id: ":proposal.page_id" },
|
|
267
|
-
optional: true,
|
|
268
|
-
},
|
|
269
|
-
],
|
|
270
|
-
},
|
|
271
|
-
{ id: "p1" },
|
|
272
|
-
),
|
|
273
|
-
).resolves.toEqual({ page: null });
|
|
274
|
-
} finally {
|
|
275
|
-
setServerClient(original as never);
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test("a resolved null flows through as a null filter on a required source", async () => {
|
|
280
|
-
const captured = { where: null as unknown };
|
|
281
|
-
// The first source resolves a row whose partner_id IS null — null is a
|
|
282
|
-
// real value flowing through interpolation, not an unresolved ref.
|
|
283
|
-
setServerClient({
|
|
284
|
-
builda_pages: {
|
|
285
|
-
findFirst: async (args: { where: unknown }) => {
|
|
286
|
-
captured.where = args.where;
|
|
287
|
-
return (args.where as { id: string }).id === "p1"
|
|
288
|
-
? { id: "p1", partner_id: null }
|
|
289
|
-
: null;
|
|
290
|
-
},
|
|
291
|
-
},
|
|
292
|
-
} as never);
|
|
293
|
-
try {
|
|
294
|
-
await resolveFromLoaderConfig(
|
|
295
|
-
{
|
|
296
|
-
sources: [
|
|
297
|
-
{
|
|
298
|
-
key: "proposal",
|
|
299
|
-
source: "orpc",
|
|
300
|
-
model: "builda_pages",
|
|
301
|
-
where: { id: ":id" },
|
|
302
|
-
optional: false,
|
|
303
|
-
},
|
|
304
|
-
{
|
|
305
|
-
key: "partnerOrg",
|
|
306
|
-
source: "orpc",
|
|
307
|
-
model: "builda_pages",
|
|
308
|
-
where: { partner_id: ":proposal.partner_id" },
|
|
309
|
-
optional: false,
|
|
310
|
-
depends_on: ["proposal"],
|
|
311
|
-
},
|
|
312
|
-
],
|
|
313
|
-
},
|
|
314
|
-
{ id: "p1" },
|
|
315
|
-
);
|
|
316
|
-
} finally {
|
|
317
|
-
setServerClient(original as never);
|
|
318
|
-
}
|
|
319
|
-
// The partnerOrg source queried WITH { partner_id: null }, not refused.
|
|
320
|
-
expect(captured.where).toEqual({ partner_id: null });
|
|
321
|
-
});
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
describe("non-UUID URL refs resolve absent, never throw into the breaker", () => {
|
|
325
|
-
// A URL param that is not a uuid (a slug link, a scanner path) can never
|
|
326
|
-
// match a uuid column. The query failure IS the resource being missing
|
|
327
|
-
// for this URL — the same state as a deleted row — so it resolves null
|
|
328
|
-
// and the upstream primary-missing check renders the branded 404.
|
|
329
|
-
// Throwing here instead counted every dead shape as a CONSECUTIVE
|
|
330
|
-
// render-budget failure: 53 scanner hits in a row on scala-quiz prod
|
|
331
|
-
// (2026-09-11) sat the layer DEGRADED fail-closed and every real page
|
|
332
|
-
// 503'd for hours.
|
|
333
|
-
const original = isServerClientRegistered() ? getServerClient() : null;
|
|
334
|
-
const sqlError = (msg: string) => {
|
|
335
|
-
const e = new Error(msg);
|
|
336
|
-
e.name = "SqlQueryError";
|
|
337
|
-
return e;
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
test("a required source rejecting a non-uuid URL ref resolves null", async () => {
|
|
341
|
-
setServerClient({
|
|
342
|
-
builda_pages: {
|
|
343
|
-
findFirst: async () => {
|
|
344
|
-
throw sqlError('invalid input syntax for type uuid: "launcha-authority-index"');
|
|
345
|
-
},
|
|
346
|
-
},
|
|
347
|
-
} as never);
|
|
348
|
-
try {
|
|
349
|
-
await expect(
|
|
350
|
-
resolveFromLoaderConfig(
|
|
351
|
-
{
|
|
352
|
-
sources: [
|
|
353
|
-
{
|
|
354
|
-
key: "assessment",
|
|
355
|
-
source: "orpc",
|
|
356
|
-
model: "builda_pages",
|
|
357
|
-
where: { id: ":id" },
|
|
358
|
-
optional: false,
|
|
359
|
-
},
|
|
360
|
-
],
|
|
361
|
-
},
|
|
362
|
-
{ id: "launcha-authority-index" },
|
|
363
|
-
),
|
|
364
|
-
).resolves.toEqual({ assessment: null });
|
|
365
|
-
} finally {
|
|
366
|
-
setServerClient(original as never);
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
test("a SqlQueryError that is NOT a non-uuid ref still throws", async () => {
|
|
371
|
-
setServerClient({
|
|
372
|
-
builda_pages: {
|
|
373
|
-
findFirst: async () => {
|
|
374
|
-
throw sqlError('column "page_id" does not exist');
|
|
375
|
-
},
|
|
376
|
-
},
|
|
377
|
-
} as never);
|
|
378
|
-
try {
|
|
379
|
-
await expect(
|
|
380
|
-
resolveFromLoaderConfig(
|
|
381
|
-
{
|
|
382
|
-
sources: [
|
|
383
|
-
{
|
|
384
|
-
key: "page",
|
|
385
|
-
source: "orpc",
|
|
386
|
-
model: "builda_pages",
|
|
387
|
-
where: { id: ":id" },
|
|
388
|
-
optional: false,
|
|
389
|
-
},
|
|
390
|
-
],
|
|
391
|
-
},
|
|
392
|
-
{ id: "p1" },
|
|
393
|
-
),
|
|
394
|
-
).rejects.toThrow('column "page_id" does not exist');
|
|
395
|
-
} finally {
|
|
396
|
-
setServerClient(original as never);
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
test("a non-SqlQueryError rejection on a required source still throws", async () => {
|
|
401
|
-
setServerClient({
|
|
402
|
-
builda_pages: {
|
|
403
|
-
findFirst: async () => {
|
|
404
|
-
throw new Error("connection refused");
|
|
405
|
-
},
|
|
406
|
-
},
|
|
407
|
-
} as never);
|
|
408
|
-
try {
|
|
409
|
-
await expect(
|
|
410
|
-
resolveFromLoaderConfig(
|
|
411
|
-
{
|
|
412
|
-
sources: [
|
|
413
|
-
{
|
|
414
|
-
key: "page",
|
|
415
|
-
source: "orpc",
|
|
416
|
-
model: "builda_pages",
|
|
417
|
-
where: { id: ":id" },
|
|
418
|
-
optional: false,
|
|
419
|
-
},
|
|
420
|
-
],
|
|
421
|
-
},
|
|
422
|
-
{ id: "p1" },
|
|
423
|
-
),
|
|
424
|
-
).rejects.toThrow("connection refused");
|
|
425
|
-
} finally {
|
|
426
|
-
setServerClient(original as never);
|
|
427
|
-
}
|
|
428
|
-
});
|
|
429
|
-
});
|