Просмотр исходного кода

fix(core): preserve credential schema compatibility

Dax Raad 2 месяцев назад
Родитель
Сommit
7793db3ac8

+ 31 - 1
packages/core/schema.json

@@ -382,7 +382,7 @@
     },
     },
     {
     {
       "type": "text",
       "type": "text",
-      "notNull": true,
+      "notNull": false,
       "autoincrement": false,
       "autoincrement": false,
       "default": null,
       "default": null,
       "generated": null,
       "generated": null,
@@ -410,6 +410,36 @@
       "entityType": "columns",
       "entityType": "columns",
       "table": "credential"
       "table": "credential"
     },
     },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "connector_id",
+      "entityType": "columns",
+      "table": "credential"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "method_id",
+      "entityType": "columns",
+      "table": "credential"
+    },
+    {
+      "type": "integer",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "active",
+      "entityType": "columns",
+      "table": "credential"
+    },
     {
     {
       "type": "integer",
       "type": "integer",
       "notNull": true,
       "notNull": true,

+ 12 - 4
packages/core/src/credential.ts

@@ -65,13 +65,15 @@ export const layer = Layer.effect(
   Effect.gen(function* () {
   Effect.gen(function* () {
     const { db } = yield* Database.Service
     const { db } = yield* Database.Service
     const decode = Schema.decodeUnknownSync(Info)
     const decode = Schema.decodeUnknownSync(Info)
-    const stored = (row: typeof CredentialTable.$inferSelect) =>
-      new Stored({
+    const stored = (row: typeof CredentialTable.$inferSelect) => {
+      if (!row.integration_id) return
+      return new Stored({
         id: row.id,
         id: row.id,
         integrationID: row.integration_id,
         integrationID: row.integration_id,
         label: row.label,
         label: row.label,
         value: decode(row.value),
         value: decode(row.value),
       })
       })
+    }
 
 
     return Service.of({
     return Service.of({
       all: Effect.fn("Credential.all")(function* () {
       all: Effect.fn("Credential.all")(function* () {
@@ -80,7 +82,10 @@ export const layer = Layer.effect(
           .from(CredentialTable)
           .from(CredentialTable)
           .orderBy(asc(CredentialTable.time_created))
           .orderBy(asc(CredentialTable.time_created))
           .all()
           .all()
-          .pipe(Effect.orDie)).map(stored)
+          .pipe(Effect.orDie)).flatMap((row) => {
+          const credential = stored(row)
+          return credential ? [credential] : []
+        })
       }),
       }),
       list: Effect.fn("Credential.list")(function* (integrationID) {
       list: Effect.fn("Credential.list")(function* (integrationID) {
         return (yield* db
         return (yield* db
@@ -89,7 +94,10 @@ export const layer = Layer.effect(
           .where(eq(CredentialTable.integration_id, integrationID))
           .where(eq(CredentialTable.integration_id, integrationID))
           .orderBy(asc(CredentialTable.time_created))
           .orderBy(asc(CredentialTable.time_created))
           .all()
           .all()
-          .pipe(Effect.orDie)).map(stored)
+          .pipe(Effect.orDie)).flatMap((row) => {
+          const credential = stored(row)
+          return credential ? [credential] : []
+        })
       }),
       }),
       create: Effect.fn("Credential.create")(function* (input) {
       create: Effect.fn("Credential.create")(function* (input) {
         const credential = new Stored({
         const credential = new Stored({

+ 5 - 2
packages/core/src/credential/sql.ts

@@ -1,12 +1,15 @@
-import { sqliteTable, text } from "drizzle-orm/sqlite-core"
+import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
 import { Timestamps } from "../database/schema.sql"
 import { Timestamps } from "../database/schema.sql"
 import type { IntegrationSchema } from "../integration/schema"
 import type { IntegrationSchema } from "../integration/schema"
 import type { Credential } from "../credential"
 import type { Credential } from "../credential"
 
 
 export const CredentialTable = sqliteTable("credential", {
 export const CredentialTable = sqliteTable("credential", {
   id: text().$type<Credential.ID>().primaryKey(),
   id: text().$type<Credential.ID>().primaryKey(),
-  integration_id: text().$type<IntegrationSchema.ID>().notNull(),
+  integration_id: text().$type<IntegrationSchema.ID>(),
   label: text().notNull(),
   label: text().notNull(),
   value: text({ mode: "json" }).$type<Credential.Info>().notNull(),
   value: text({ mode: "json" }).$type<Credential.Info>().notNull(),
+  connector_id: text(),
+  method_id: text(),
+  active: integer({ mode: "boolean" }),
   ...Timestamps,
   ...Timestamps,
 })
 })

+ 14 - 4
packages/core/src/database/migration/20260611192811_lush_chimera.ts

@@ -5,11 +5,21 @@ export default {
   id: "20260611192811_lush_chimera",
   id: "20260611192811_lush_chimera",
   up(tx) {
   up(tx) {
     return Effect.gen(function* () {
     return Effect.gen(function* () {
-      yield* tx.run(`ALTER TABLE \`credential\` ADD \`integration_id\` text NOT NULL;`)
       yield* tx.run(`DROP INDEX IF EXISTS \`credential_connector_active_idx\`;`)
       yield* tx.run(`DROP INDEX IF EXISTS \`credential_connector_active_idx\`;`)
-      yield* tx.run(`ALTER TABLE \`credential\` DROP COLUMN \`connector_id\`;`)
-      yield* tx.run(`ALTER TABLE \`credential\` DROP COLUMN \`method_id\`;`)
-      yield* tx.run(`ALTER TABLE \`credential\` DROP COLUMN \`active\`;`)
+      yield* tx.run(`DROP TABLE \`credential\`;`)
+      yield* tx.run(`
+        CREATE TABLE \`credential\` (
+          \`id\` text PRIMARY KEY,
+          \`integration_id\` text,
+          \`label\` text NOT NULL,
+          \`value\` text NOT NULL,
+          \`connector_id\` text,
+          \`method_id\` text,
+          \`active\` integer,
+          \`time_created\` integer NOT NULL,
+          \`time_updated\` integer NOT NULL
+        );
+      `)
     })
     })
   },
   },
 } satisfies DatabaseMigration.Migration
 } satisfies DatabaseMigration.Migration

+ 12 - 25
packages/core/src/database/schema.gen.ts

@@ -59,9 +59,12 @@ export default {
       yield* tx.run(`
       yield* tx.run(`
         CREATE TABLE \`credential\` (
         CREATE TABLE \`credential\` (
           \`id\` text PRIMARY KEY,
           \`id\` text PRIMARY KEY,
-          \`integration_id\` text NOT NULL,
+          \`integration_id\` text,
           \`label\` text NOT NULL,
           \`label\` text NOT NULL,
           \`value\` text NOT NULL,
           \`value\` text NOT NULL,
+          \`connector_id\` text,
+          \`method_id\` text,
+          \`active\` integer,
           \`time_created\` integer NOT NULL,
           \`time_created\` integer NOT NULL,
           \`time_updated\` integer NOT NULL
           \`time_updated\` integer NOT NULL
         );
         );
@@ -237,32 +240,16 @@ export default {
       `)
       `)
       yield* tx.run(`CREATE UNIQUE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`)
       yield* tx.run(`CREATE UNIQUE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`)
       yield* tx.run(`CREATE INDEX \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`)
       yield* tx.run(`CREATE INDEX \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`)
-      yield* tx.run(
-        `CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`,
-      )
-      yield* tx.run(
-        `CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`,
-      )
+      yield* tx.run(`CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`)
+      yield* tx.run(`CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`)
       yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
       yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
       yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`)
       yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`)
-      yield* tx.run(
-        `CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`,
-      )
-      yield* tx.run(
-        `CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`,
-      )
-      yield* tx.run(
-        `CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`,
-      )
-      yield* tx.run(
-        `CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`,
-      )
-      yield* tx.run(
-        `CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`,
-      )
-      yield* tx.run(
-        `CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`,
-      )
+      yield* tx.run(`CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`)
+      yield* tx.run(`CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`)
+      yield* tx.run(`CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`)
+      yield* tx.run(`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`)
+      yield* tx.run(`CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`)
+      yield* tx.run(`CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`)
       yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`)
       yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`)
       yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`)
       yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`)
       yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`)
       yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`)

+ 26 - 0
packages/core/test/database-migration.test.ts

@@ -13,6 +13,7 @@ import normalizeStoragePathsMigration from "@opencode-ai/core/database/migration
 import sessionMessageProjectionOrderMigration from "@opencode-ai/core/database/migration/20260603040000_session_message_projection_order"
 import sessionMessageProjectionOrderMigration from "@opencode-ai/core/database/migration/20260603040000_session_message_projection_order"
 import eventSourcedSessionInputMigration from "@opencode-ai/core/database/migration/20260604172448_event_sourced_session_input"
 import eventSourcedSessionInputMigration from "@opencode-ai/core/database/migration/20260604172448_event_sourced_session_input"
 import contextEpochAgentMigration from "@opencode-ai/core/database/migration/20260605042240_add_context_epoch_agent"
 import contextEpochAgentMigration from "@opencode-ai/core/database/migration/20260605042240_add_context_epoch_agent"
+import simplifyIntegrationCredentialsMigration from "@opencode-ai/core/database/migration/20260611192811_lush_chimera"
 import { ProjectV2 } from "@opencode-ai/core/project"
 import { ProjectV2 } from "@opencode-ai/core/project"
 import { ProjectTable } from "@opencode-ai/core/project/sql"
 import { ProjectTable } from "@opencode-ai/core/project/sql"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { AbsolutePath } from "@opencode-ai/core/schema"
@@ -124,6 +125,31 @@ describe("DatabaseMigration", () => {
     )
     )
   })
   })
 
 
+  test("keeps legacy credential fields nullable", async () => {
+    await run(
+      Effect.gen(function* () {
+        const db = yield* makeDb
+        yield* db.run(
+          sql`CREATE TABLE credential (id text PRIMARY KEY, connector_id text NOT NULL, method_id text NOT NULL, label text NOT NULL, value text NOT NULL, active integer DEFAULT false NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL)`,
+        )
+        yield* db.run(
+          sql`CREATE UNIQUE INDEX credential_connector_active_idx ON credential (connector_id) WHERE active = 1`,
+        )
+        yield* DatabaseMigration.applyOnly(db, [simplifyIntegrationCredentialsMigration])
+
+        yield* db.run(
+          sql`INSERT INTO credential (id, connector_id, method_id, label, value, active, time_created, time_updated) VALUES ('legacy', 'openai', 'oauth', 'Legacy', '{}', 1, 1, 1)`,
+        )
+        yield* db.run(
+          sql`INSERT INTO credential (id, integration_id, label, value, time_created, time_updated) VALUES ('current', 'anthropic', 'Current', '{}', 2, 2)`,
+        )
+        expect(
+          yield* db.get(sql`SELECT connector_id, method_id, active FROM credential WHERE id = 'current'`),
+        ).toEqual({ connector_id: null, method_id: null, active: null })
+      }),
+    )
+  })
+
   test("resets beta history and rebuilds event-sourced Session input storage", async () => {
   test("resets beta history and rebuilds event-sourced Session input storage", async () => {
     await run(
     await run(
       Effect.gen(function* () {
       Effect.gen(function* () {