History

Share cloud drizzle schema across DbService + MCP session

mcp-session.ts was building its own combinedSchema from cloudSchema only, dropping every executor-schema table — every /mcp request failed with [storage-drizzle] unknown model "source". Export combinedSchema from services/db.ts and consume it in both DbService.Live and the MCP Durable Object so drift can't happen. Regression coverage: - db.schema.test.ts asserts combinedSchema (and drizzle's _.fullSchema) contain every cloud + executor export - mcp-session.e2e.node.test.ts drives a real @modelcontextprotocol/sdk Client through listTools / callTool / form elicitation round-trips

Rhys Sullivan committed 7 months ago

Commit 38d662b1 parent 807df1d7 files · +388 −6Browse files

Showing the changes to apps/cloud/src/services (2 of 7 files). Show all

Unified · Split

2 files
  1. apps/cloud/src/services/db.schema.test.ts +61 −0
  2. apps/cloud/src/services/db.ts +6 −2
apps/cloud/src/services/db.schema.test.ts+61−0
@@ -0,0 +1,61 @@
1// Regression: every cloud drizzle instance must be constructed with the
2// merge of cloud + executor schemas. If ANY call site builds its own
3// `{ schema }` without spreading both, `db._.fullSchema` comes back
4// missing tables and the drizzle adapter throws
5// `unknown model "source"` at the first request that touches scoped data.
6//
7// We hit this in prod (MCP endpoint) when `mcp-session.ts` built
8// `combinedSchema = { ...cloudSchema }` and forgot to spread the executor
9// schema — web-app requests worked because they went through
10// `DbService.Live`, but the MCP Durable Object constructed its own.
11//
12// This test asserts the only sanctioned constant (`combinedSchema` from
13// `./db`) actually contains every executor-schema export AND that drizzle
14// surfaces them all under `db._.fullSchema`.
15
16import { describe, expect, it } from "vitest";
17import { drizzle } from "drizzle-orm/postgres-js";
18import postgres from "postgres";
19
20import * as cloudSchema from "./schema";
21import * as executorSchema from "./executor-schema";
22import { combinedSchema } from "./db";
23
24describe("combinedSchema", () => {
25 it("spreads every cloud + executor schema export", () => {
26 const expected = new Set([
27 ...Object.keys(cloudSchema),
28 ...Object.keys(executorSchema),
29 ]);
30 for (const key of expected) {
31 expect(combinedSchema, `combinedSchema missing "${key}"`).toHaveProperty(key);
32 }
33 });
34
35 // Executor-schema drives the scope-sharded tables (source/tool/etc). If
36 // any of these go missing the drizzle adapter's `getTable` lookup throws.
37 it("includes scope-sharded executor tables", () => {
38 for (const key of Object.keys(executorSchema)) {
39 expect(combinedSchema, `combinedSchema missing "${key}"`).toHaveProperty(key);
40 }
41 });
42
43 // The prod bug was actually at the drizzle layer: spread + __exportAll
44 // getters could theoretically drop tables if evaluated before their
45 // declarations. Construct a drizzle instance and walk its fullSchema
46 // to catch that class of bug too.
47 it("drizzle(combinedSchema) exposes every table under _.fullSchema", () => {
48 // postgres() lazily connects — safe to build with a dummy url, we
49 // never .query() so no socket is opened.
50 const sql = postgres("postgres://u:p@127.0.0.1:1/x", { max: 1 });
51 try {
52 const db = drizzle(sql, { schema: combinedSchema });
53 const fullSchema = (db as unknown as { _: { fullSchema: Record<string, unknown> } })._.fullSchema;
54 for (const key of Object.keys(executorSchema)) {
55 expect(fullSchema, `fullSchema missing "${key}"`).toHaveProperty(key);
56 }
57 } finally {
58 sql.end({ timeout: 0 }).catch(() => undefined);
59 }
60 });
61});
apps/cloud/src/services/db.ts+6−2
@@ -20,7 +20,11 @@ import * as cloudSchema from "./schema"; · 19 unmodified lines
20import * as executorSchema from "./executor-schema";20import * as executorSchema from "./executor-schema";
21import { server } from "../env";21import { server } from "../env";
22 22
23const schema = { ...cloudSchema, ...executorSchema };23// Exported so every drizzle() call in the cloud app shares one schema
24// object. Historically `mcp-session.ts` built its own and forgot to spread
25// `executorSchema`, producing runtime "unknown model source" errors that
26// only surfaced in prod. See apps/cloud/src/services/db.schema.test.ts.
27export const combinedSchema = { ...cloudSchema, ...executorSchema };
24 28
25// eslint-disable-next-line @typescript-eslint/no-explicit-any29// eslint-disable-next-line @typescript-eslint/no-explicit-any
26export type DrizzleDb = PgDatabase<any, any, any>;30export type DrizzleDb = PgDatabase<any, any, any>;
@@ -64,7 +68,7 @@ export class DbService extends Context.Tag("@executor/cloud/DbService")< · 37 unmodified lines
64 Effect.acquireRelease(68 Effect.acquireRelease(
65 Effect.sync((): DbServiceShape => {69 Effect.sync((): DbServiceShape => {
66 const sql = makeSql();70 const sql = makeSql();
67 return { sql, db: drizzle(sql, { schema }) as DrizzleDb };71 return { sql, db: drizzle(sql, { schema: combinedSchema }) as DrizzleDb };
68 }),72 }),
69 ({ sql }) =>73 ({ sql }) =>
70 // Fire-and-forget: the Terminate round-trip sometimes hangs, and74 // Fire-and-forget: the Terminate round-trip sometimes hangs, and

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t