History

Merge remote-tracking branch 'origin/codex/batch14-local-db-migrate-connections' into codex/effect-lint-integration

Rhys Sullivan committed 6 months ago

Commit 6a9df082 parents d8abc3d 78035f22 files · +33 −8Browse files

Unified · Split

2 files
  1. apps/local/src/server/db-upgrade.ts +2 −0
  2. apps/local/src/server/migrate-connections.test.ts +31 −8
apps/local/src/server/db-upgrade.ts+2−0
@@ -21,6 +21,7 @@ import * as fs from "node:fs"; · 20 unmodified lines
21export const isPreScopeSchema = (dbPath: string): boolean => {21export const isPreScopeSchema = (dbPath: string): boolean => {
22 if (!fs.existsSync(dbPath)) return false;22 if (!fs.existsSync(dbPath)) return false;
23 const db = new Database(dbPath, { readonly: true });23 const db = new Database(dbPath, { readonly: true });
24 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: local SQLite schema probe must close the DB handle
24 try {25 try {
25 const tableExists = db26 const tableExists = db
26 .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='source'")27 .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='source'")
@@ -73,6 +74,7 @@ export interface LegacySecret { · 46 unmodified lines
73export const readLegacySecrets = (dbPath: string): readonly LegacySecret[] => {74export const readLegacySecrets = (dbPath: string): readonly LegacySecret[] => {
74 if (!fs.existsSync(dbPath)) return [];75 if (!fs.existsSync(dbPath)) return [];
75 const db = new Database(dbPath, { readonly: true });76 const db = new Database(dbPath, { readonly: true });
77 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: local SQLite legacy-row read must close the DB handle
76 try {78 try {
77 const tableExists = db79 const tableExists = db
78 .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='secret'")80 .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='secret'")
apps/local/src/server/migrate-connections.test.ts+31−8
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from "@effect/vitest"; · 1 unmodified line
2import { Database } from "bun:sqlite";2import { Database } from "bun:sqlite";
3import { migrate } from "drizzle-orm/bun-sqlite/migrator";3import { migrate } from "drizzle-orm/bun-sqlite/migrator";
4import { drizzle } from "drizzle-orm/bun-sqlite";4import { drizzle } from "drizzle-orm/bun-sqlite";
5import { Schema } from "effect";
5import { mkdtempSync, rmSync } from "node:fs";6import { mkdtempSync, rmSync } from "node:fs";
6import { tmpdir } from "node:os";7import { tmpdir } from "node:os";
7import { join } from "node:path";8import { join } from "node:path";
@@ -9,15 +10,26 @@ import { join } from "node:path"; · 1 unmodified line
9import { migrateLegacyConnections } from "./migrate-connections";10import { migrateLegacyConnections } from "./migrate-connections";
10 11
11let workDir: string;12let workDir: string;
13let databases: Array<Database>;
12 14
13beforeEach(() => {15beforeEach(() => {
14 workDir = mkdtempSync(join(tmpdir(), "executor-migrate-connections-"));16 workDir = mkdtempSync(join(tmpdir(), "executor-migrate-connections-"));
17 databases = [];
15});18});
16 19
17afterEach(() => {20afterEach(() => {
21 for (const db of databases) {
22 db.close();
23 }
18 rmSync(workDir, { recursive: true, force: true });24 rmSync(workDir, { recursive: true, force: true });
19});25});
20 26
27const openDatabase = (): Database => {
28 const db = new Database(join(workDir, "data.db"));
29 databases.push(db);
30 return db;
31};
32
21const columnNames = (db: Database, table: string): ReadonlyArray<string> =>33const columnNames = (db: Database, table: string): ReadonlyArray<string> =>
22 (34 (
23 db.prepare(`PRAGMA table_info('${table}')`).all() as ReadonlyArray<{35 db.prepare(`PRAGMA table_info('${table}')`).all() as ReadonlyArray<{
@@ -25,9 +37,24 @@ const columnNames = (db: Database, table: string): ReadonlyArray<string> => · 1 unmodified line
25 }>37 }>
26 ).map((column) => column.name);38 ).map((column) => column.name);
27 39
40const MigratedMcpConfig = Schema.Struct({
41 auth: Schema.optional(Schema.Unknown),
42});
43const decodeMigratedMcpConfig = Schema.decodeUnknownSync(
44 Schema.fromJsonString(MigratedMcpConfig),
45);
46
47const MigratedOpenApiOAuth2 = Schema.Struct({
48 kind: Schema.Literal("oauth2"),
49 connectionId: Schema.String,
50});
51const decodeMigratedOpenApiOAuth2 = Schema.decodeUnknownSync(
52 Schema.fromJsonString(MigratedOpenApiOAuth2),
53);
54
28describe("migrateLegacyConnections", () => {55describe("migrateLegacyConnections", () => {
29 it("backfills legacy MCP OAuth rows after connection.kind has been dropped", async () => {56 it("backfills legacy MCP OAuth rows after connection.kind has been dropped", async () => {
30 const db = new Database(join(workDir, "data.db"));57 const db = openDatabase();
31 migrate(drizzle(db), {58 migrate(drizzle(db), {
32 migrationsFolder: join(import.meta.dirname, "../../drizzle"),59 migrationsFolder: join(import.meta.dirname, "../../drizzle"),
33 });60 });
@@ -98,7 +125,7 @@ describe("migrateLegacyConnections", () => { · 64 unmodified lines
98 readonly auth_kind: string;125 readonly auth_kind: string;
99 readonly auth_connection_id: string;126 readonly auth_connection_id: string;
100 };127 };
101 expect(JSON.parse(source.config).auth).toBeUndefined();128 expect(decodeMigratedMcpConfig(source.config).auth).toBeUndefined();
102 expect(source.auth_kind).toBe("oauth2");129 expect(source.auth_kind).toBe("oauth2");
103 expect(source.auth_connection_id).toBe("mcp-oauth2-remote-mcp");130 expect(source.auth_connection_id).toBe("mcp-oauth2-remote-mcp");
104 131
@@ -117,12 +144,10 @@ describe("migrateLegacyConnections", () => { · 12 unmodified lines
117 owned_by_connection_id: "mcp-oauth2-remote-mcp",144 owned_by_connection_id: "mcp-oauth2-remote-mcp",
118 },145 },
119 ]);146 ]);
120
121 db.close();
122 });147 });
123 148
124 it("backfills legacy OpenAPI OAuth from oauth2 column after invocation_config has been dropped", async () => {149 it("backfills legacy OpenAPI OAuth from oauth2 column after invocation_config has been dropped", async () => {
125 const db = new Database(join(workDir, "data.db"));150 const db = openDatabase();
126 migrate(drizzle(db), {151 migrate(drizzle(db), {
127 migrationsFolder: join(import.meta.dirname, "../../drizzle"),152 migrationsFolder: join(import.meta.dirname, "../../drizzle"),
128 });153 });
@@ -196,10 +221,8 @@ describe("migrateLegacyConnections", () => { · 67 unmodified lines
196 const source = db221 const source = db
197 .prepare("SELECT oauth2 FROM openapi_source WHERE scope_id = ? AND id = ?")222 .prepare("SELECT oauth2 FROM openapi_source WHERE scope_id = ? AND id = ?")
198 .get("scope-1", "legacy-openapi") as { readonly oauth2: string };223 .get("scope-1", "legacy-openapi") as { readonly oauth2: string };
199 const oauth2 = JSON.parse(source.oauth2);224 const oauth2 = decodeMigratedOpenApiOAuth2(source.oauth2);
200 expect(oauth2.kind).toBe("oauth2");225 expect(oauth2.kind).toBe("oauth2");
201 expect(oauth2.connectionId).toBe(connection?.id);226 expect(oauth2.connectionId).toBe(connection?.id);
202
203 db.close();
204 });227 });
205});228});