@teamscala/service-boot 0.23.366 → 0.23.367

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
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "name": "@teamscala/service-boot",
8
8
  "type": "module",
9
- "version": "0.23.366",
9
+ "version": "0.23.367",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -51,7 +51,6 @@ import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
51
51
  import { recordEvent } from "@teamscala/event-log/record-event";
52
52
  import { getAppLogger } from "@teamscala/logger/app-loggers";
53
53
  import { pgBossHandlerRegistry } from "../pg-boss-handler-registry.ts";
54
- import { getServerClient } from "../page-server-orpc/server-client.ts";
55
54
  import {
56
55
  composeEscalationDescription,
57
56
  composeEscalationMessage,
@@ -118,17 +117,55 @@ async function getSystemIdentity(): Promise<{ userId: string | undefined; organi
118
117
  }
119
118
 
120
119
  /**
121
- * The system-authed in-process ORPC client (set at boot after the router is
122
- * built — no-arg form carries the system super-admin context, correct for a
123
- * platform system action). Per `no-direct-prisma-outside-orpc`, every WRITE in
124
- * this handler routes through it; the single raw read (the event_log
125
- * recurrence aggregate, a GROUP BY no generated CRUD expresses) uses the
126
- * assigned-client form with $queryRawUnsafe — the lane-attention-sweep
120
+ * The system-authed in-process ORPC client, built LAZILY on first use from the
121
+ * SAME composed appRouter the /api/rpc mount uses (getAppRouter — never a raw
122
+ * re-import of the generated routers, per register-page-server-client's
123
+ * documented module-identity trap). Built here rather than read from
124
+ * page-server-orpc/server-client because a pg-boss handler can fire on a
125
+ * BACKEND service, where the page-server client is never registered — the
126
+ * empty-router fallback returns a client with no model namespaces (the
127
+ * TypeError this replaced). Per `no-direct-prisma-outside-orpc`, every WRITE
128
+ * in this handler routes through this client; the single raw read (the
129
+ * event_log recurrence aggregate, a GROUP BY no generated CRUD expresses) uses
130
+ * the assigned-client form with $queryRawUnsafe — the lane-attention-sweep
127
131
  * precedent in this same directory. Timestamps are ISO strings, never Date
128
132
  * instances (the generated valibot schemas require it).
129
133
  */
130
- function orpc(): OrpcSurface {
131
- return getServerClient() as unknown as OrpcSurface;
134
+ let cachedOrpc: OrpcSurface | null = null;
135
+ async function orpc(): Promise<OrpcSurface> {
136
+ if (cachedOrpc) return cachedOrpc;
137
+ const { createRouterClient } = await import("@orpc/server");
138
+ const { getAppRouter } = await import("@teamscala/orpc/app-router");
139
+ const appRouter = (await getAppRouter()) as Record<string, unknown> | null;
140
+ if (!appRouter) {
141
+ throw new Error(
142
+ "platform-fault-escalation: no ORPC appRouter — the service has no generated routers to write through",
143
+ );
144
+ }
145
+ const { getPrisma } = await import("@teamscala/db/prisma-registry");
146
+ const { loadRegistryConfig } = await import("@teamscala/db/registry/load-config");
147
+ const { PlatformIdentitySchema } = await import(
148
+ "@teamscala/db-validation/registry-schemas/platform-identity"
149
+ );
150
+ const platformIdentity = await loadRegistryConfig(
151
+ "config",
152
+ "platform-identity",
153
+ PlatformIdentitySchema,
154
+ );
155
+ cachedOrpc = createRouterClient(
156
+ { ...appRouter } as never,
157
+ {
158
+ context: {
159
+ prisma: getPrisma(),
160
+ user: {
161
+ id: "system",
162
+ isSuperAdmin: true,
163
+ organisationId: platformIdentity.platformOrganisationId,
164
+ },
165
+ },
166
+ } as never,
167
+ ) as unknown as OrpcSurface;
168
+ return cachedOrpc;
132
169
  }
133
170
 
134
171
  /** The router subset this handler uses — mirrors the generated procedure args. */
@@ -187,7 +224,7 @@ async function fetchAggregates(patterns: string[], windowMinutes: number): Promi
187
224
  }
188
225
 
189
226
  async function resolveOperatorRelay(): Promise<OperatorRelayBot> {
190
- const bot = (await orpc().messaging_bots.findFirst({
227
+ const bot = (await (await orpc()).messaging_bots.findFirst({
191
228
  where: {
192
229
  is_active: true,
193
230
  platform: "whatsapp",
@@ -225,7 +262,7 @@ async function deliverEscalation(
225
262
 
226
263
  // 1. Claim the episode BEFORE sending, so a send failure leaves a durable
227
264
  // claim the next sweep retries instead of a lost escalation.
228
- await orpc().platform_fault_escalations.upsert({
265
+ await (await orpc()).platform_fault_escalations.upsert({
229
266
  where: { kind },
230
267
  create: {
231
268
  kind,
@@ -280,7 +317,7 @@ async function deliverEscalation(
280
317
  }
281
318
 
282
319
  // 4. Durable record: the work_items task the PA reads back / the operator answers on.
283
- const workItem = await orpc().work_items.create({
320
+ const workItem = await (await orpc()).work_items.create({
284
321
  data: {
285
322
  kind: "task",
286
323
  parent_id: config.parent_project_id,
@@ -302,7 +339,7 @@ async function deliverEscalation(
302
339
  });
303
340
 
304
341
  // 5. Readback-correct the episode: delivered + linked in one update.
305
- await orpc().platform_fault_escalations.update({
342
+ await (await orpc()).platform_fault_escalations.update({
306
343
  where: { kind },
307
344
  data: {
308
345
  escalated_at: now.toISOString(),
@@ -327,7 +364,7 @@ async function deliverEscalation(
327
364
 
328
365
  async function updateEpisode(aggregate: FaultAggregate, episode: EscalationEpisode): Promise<void> {
329
366
  const now = new Date();
330
- await orpc().platform_fault_escalations.update({
367
+ await (await orpc()).platform_fault_escalations.update({
331
368
  where: { kind: episode.kind },
332
369
  data: {
333
370
  last_seen_at: aggregate.lastSeen.toISOString(),
@@ -337,7 +374,7 @@ async function updateEpisode(aggregate: FaultAggregate, episode: EscalationEpiso
337
374
  },
338
375
  });
339
376
  if (episode.workItemId) {
340
- await orpc().comments.create({
377
+ await (await orpc()).comments.create({
341
378
  data: {
342
379
  work_item_id: episode.workItemId,
343
380
  content: `Still recurring: ${aggregate.count} events, last at ${aggregate.lastSeen.toISOString()}. No new escalation (already escalated and unresolved).`,
@@ -354,12 +391,12 @@ async function updateEpisode(aggregate: FaultAggregate, episode: EscalationEpiso
354
391
 
355
392
  async function resolveEpisode(episode: EscalationEpisode, quietMinutes: number): Promise<void> {
356
393
  const now = new Date();
357
- await orpc().platform_fault_escalations.update({
394
+ await (await orpc()).platform_fault_escalations.update({
358
395
  where: { kind: episode.kind },
359
396
  data: { resolved_at: now.toISOString(), updated_at: now.toISOString() },
360
397
  });
361
398
  if (episode.workItemId) {
362
- await orpc().comments.create({
399
+ await (await orpc()).comments.create({
363
400
  data: {
364
401
  work_item_id: episode.workItemId,
365
402
  content: `Fault stopped — no '${episode.kind}' events for ${Math.round(quietMinutes)} min. Episode closed; the fault will re-escalate if it recurs.`,
@@ -383,7 +420,7 @@ async function platformFaultEscalationSweep(ctx: { config: ScheduledJobConfig })
383
420
  // Only OPEN episodes matter: a resolved episode and no episode at all have
384
421
  // the same meaning to decideSweep (a recurring sustained fault opens a NEW
385
422
  // episode and escalates again).
386
- const openEpisodes = (await orpc().platform_fault_escalations.findMany({
423
+ const openEpisodes = (await (await orpc()).platform_fault_escalations.findMany({
387
424
  where: { resolved_at: null },
388
425
  })) as Array<{
389
426
  kind: string;