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+
16+import { describe, expect, it } from "vitest";
17+import { drizzle } from "drizzle-orm/postgres-js";
18+import postgres from "postgres";
19+
20+import * as cloudSchema from "./schema";
21+import * as executorSchema from "./executor-schema";
22+import { combinedSchema } from "./db";
23+
24+describe("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
2020 import * as executorSchema from "./executor-schema";
2121 import { server } from "../env";
2222
23−const 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.
27+export const combinedSchema = { ...cloudSchema, ...executorSchema };
2428
2529 // eslint-disable-next-line @typescript-eslint/no-explicit-any
2630 export type DrizzleDb = PgDatabase<any, any, any>;
@@ -64,7 +68,7 @@ export class DbService extends Context.Tag("@executor/cloud/DbService")< · 37 unmodified lines
6468 Effect.acquireRelease(
6569 Effect.sync((): DbServiceShape => {
6670 const sql = makeSql();
67− return { sql, db: drizzle(sql, { schema }) as DrizzleDb };
71+ return { sql, db: drizzle(sql, { schema: combinedSchema }) as DrizzleDb };
6872 }),
6973 ({ sql }) =>
7074 // 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