@teamscala/service-boot 0.23.457 → 0.23.458
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
|
@@ -104,6 +104,116 @@ describe("resource-loader model capability", () => {
|
|
|
104
104
|
});
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
describe("resource_loader refs read both the snake and the camelCase spelling", () => {
|
|
108
|
+
// Loader rows author snake_case column names (page_id, organisation_id,
|
|
109
|
+
// prepared_for) while the v8 ORM hands back camelCase keys (pageId,
|
|
110
|
+
// organisationId, preparedFor). readField reads the spelled key first
|
|
111
|
+
// then its camelCase equivalent so neither side has to know the other.
|
|
112
|
+
// Measured through the ORPC branch: what the dependent source's findFirst
|
|
113
|
+
// RECEIVES as its where IS the resolution of `:proposal.page_id`.
|
|
114
|
+
const original = isServerClientRegistered() ? getServerClient() : null;
|
|
115
|
+
|
|
116
|
+
test("a snake_case ref resolves against a camelCase parent record", async () => {
|
|
117
|
+
const captured: { where: unknown } = { where: null };
|
|
118
|
+
const calls: string[] = [];
|
|
119
|
+
setServerClient({
|
|
120
|
+
builda_pages: {
|
|
121
|
+
findFirst: async (args: { where: Record<string, unknown> }) => {
|
|
122
|
+
calls.push(JSON.stringify(args.where));
|
|
123
|
+
// First call: the proposal source (id lookup) returns a
|
|
124
|
+
// camelCase row, exactly what the v8 chain hands back.
|
|
125
|
+
if (calls.length === 1) {
|
|
126
|
+
return {
|
|
127
|
+
id: "p1",
|
|
128
|
+
pageId: "page-uuid",
|
|
129
|
+
organisationId: "org-uuid",
|
|
130
|
+
preparedFor: "Klavdia Vainshtein",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Second call: the dependent page source.
|
|
134
|
+
captured.where = args.where;
|
|
135
|
+
return { id: "page-uuid" };
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
} as never);
|
|
139
|
+
try {
|
|
140
|
+
await resolveFromLoaderConfig(
|
|
141
|
+
{
|
|
142
|
+
sources: [
|
|
143
|
+
{
|
|
144
|
+
key: "proposal",
|
|
145
|
+
source: "orpc",
|
|
146
|
+
model: "builda_pages",
|
|
147
|
+
where: { id: ":id" },
|
|
148
|
+
optional: false,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
key: "page",
|
|
152
|
+
source: "orpc",
|
|
153
|
+
model: "builda_pages",
|
|
154
|
+
where: { id: ":proposal.page_id" },
|
|
155
|
+
optional: false,
|
|
156
|
+
depends_on: ["proposal"],
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
{ id: "p1" },
|
|
161
|
+
);
|
|
162
|
+
} finally {
|
|
163
|
+
setServerClient(original as never);
|
|
164
|
+
}
|
|
165
|
+
// The dependent source queried WITH the resolved page id, not an
|
|
166
|
+
// emptied filter matching an arbitrary row.
|
|
167
|
+
expect(captured.where).toEqual({ id: "page-uuid" });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("identity-name fields resolve unchanged (id, path, slug)", async () => {
|
|
171
|
+
const captured: { where: unknown } = { where: null };
|
|
172
|
+
const calls: string[] = [];
|
|
173
|
+
setServerClient({
|
|
174
|
+
builda_pages: {
|
|
175
|
+
findFirst: async (args: { where: Record<string, unknown> }) => {
|
|
176
|
+
calls.push(JSON.stringify(args.where));
|
|
177
|
+
if (calls.length === 1) {
|
|
178
|
+
return { id: "p1", path: "/header", websiteId: "w-1" };
|
|
179
|
+
}
|
|
180
|
+
captured.where = args.where;
|
|
181
|
+
return { id: "p1" };
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
} as never);
|
|
185
|
+
try {
|
|
186
|
+
await resolveFromLoaderConfig(
|
|
187
|
+
{
|
|
188
|
+
sources: [
|
|
189
|
+
{
|
|
190
|
+
key: "page",
|
|
191
|
+
source: "orpc",
|
|
192
|
+
model: "builda_pages",
|
|
193
|
+
where: { id: ":id" },
|
|
194
|
+
optional: false,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
key: "footer",
|
|
198
|
+
source: "orpc",
|
|
199
|
+
model: "builda_pages",
|
|
200
|
+
where: { path: ":page.path", website_id: ":page.website_id" },
|
|
201
|
+
optional: false,
|
|
202
|
+
depends_on: ["page"],
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
{ id: "p1" },
|
|
207
|
+
);
|
|
208
|
+
} finally {
|
|
209
|
+
setServerClient(original as never);
|
|
210
|
+
}
|
|
211
|
+
// path (identity) and website_id (snake → camel websiteId) both
|
|
212
|
+
// resolve through the same readField walk.
|
|
213
|
+
expect(captured.where).toEqual({ path: "/header", website_id: "w-1" });
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
107
217
|
describe("required sources refuse unresolvable where refs", () => {
|
|
108
218
|
// The 2026-09-03 proposal outage class: interpolateTemplate DROPS a key
|
|
109
219
|
// whose ref resolves undefined, so a required source queries with a
|
|
@@ -27,13 +27,34 @@ export async function resolveServiceContext(
|
|
|
27
27
|
serviceSlug: string,
|
|
28
28
|
databaseUrl: string,
|
|
29
29
|
): Promise<ServiceContext> {
|
|
30
|
+
// THE V8 CLIENT REGISTERS FIRST — before any registry read. getPrisma()
|
|
31
|
+
// serves the v8 model-indexed adapter (a81b9f0a), so an early registry
|
|
32
|
+
// read (getCookieDomain in buildMinimalApp, loadRegistryEntries) that ran
|
|
33
|
+
// against a v7-only registration found no `.orm` surface: 43695 threw
|
|
34
|
+
// "model not in the v8 client" (fleet-wide boot crash) and 43696+ degraded
|
|
35
|
+
// to EMPTY rows ("Config entry cookie-domain not found" on a row that has
|
|
36
|
+
// existed since July). Registering the real v8 client HERE — the earliest
|
|
37
|
+
// boot step — makes every later read resolve against the client the
|
|
38
|
+
// adapter expects. Measured 2026-09-03: this ordering gap took down every
|
|
39
|
+
// UI service for the second half of the Prisma 8 migration night.
|
|
40
|
+
const { createDbClientV8 } = await import("@teamscala/db/create-db-client-v8");
|
|
41
|
+
const db8 = await createDbClientV8({
|
|
42
|
+
connectionString: databaseUrl,
|
|
43
|
+
serviceName: serviceSlug,
|
|
44
|
+
});
|
|
45
|
+
const { registerDb8 } = await import("@teamscala/db/db8-registry");
|
|
46
|
+
registerDb8(db8 as never);
|
|
47
|
+
registerPrismaClient(db8 as never);
|
|
48
|
+
|
|
30
49
|
const PrismaClientConstructor = await loadServicePrismaClient(serviceSlug);
|
|
31
50
|
const dbClient = await createDbClient({
|
|
32
51
|
connectionString: databaseUrl,
|
|
33
52
|
serviceName: serviceSlug,
|
|
34
53
|
PrismaClientConstructor: PrismaClientConstructor as never,
|
|
35
54
|
});
|
|
36
|
-
|
|
55
|
+
// The v7 client stays ONLY as the explicit per-call client (loadRegistry-
|
|
56
|
+
// Entries below, callers' disconnect) — it must NOT re-register, or the
|
|
57
|
+
// v7 registration overwrites the v8 one and the reads break again.
|
|
37
58
|
await loadRegistryEntries(dbClient.prisma as never);
|
|
38
59
|
|
|
39
60
|
const rowResult = await loadServiceInstallRow(serviceSlug);
|