diff --git a/packages/core/schema.json b/packages/core/schema.json index 283a3bb44..954b3b94f 100644 --- a/packages/core/schema.json +++ b/packages/core/schema.json @@ -382,7 +382,7 @@ }, { "type": "text", - "notNull": true, + "notNull": false, "autoincrement": false, "default": null, "generated": null, @@ -410,6 +410,36 @@ "entityType": "columns", "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", "notNull": true, diff --git a/packages/core/src/credential.ts b/packages/core/src/credential.ts index e7cb2370e..77bece277 100644 --- a/packages/core/src/credential.ts +++ b/packages/core/src/credential.ts @@ -65,13 +65,15 @@ export const layer = Layer.effect( Effect.gen(function* () { const { db } = yield* Database.Service 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, integrationID: row.integration_id, label: row.label, value: decode(row.value), }) + } return Service.of({ all: Effect.fn("Credential.all")(function* () { @@ -80,7 +82,10 @@ export const layer = Layer.effect( .from(CredentialTable) .orderBy(asc(CredentialTable.time_created)) .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) { return (yield* db @@ -89,7 +94,10 @@ export const layer = Layer.effect( .where(eq(CredentialTable.integration_id, integrationID)) .orderBy(asc(CredentialTable.time_created)) .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) { const credential = new Stored({ diff --git a/packages/core/src/credential/sql.ts b/packages/core/src/credential/sql.ts index a1039eb1f..a849092ea 100644 --- a/packages/core/src/credential/sql.ts +++ b/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 type { IntegrationSchema } from "../integration/schema" import type { Credential } from "../credential" export const CredentialTable = sqliteTable("credential", { id: text().$type().primaryKey(), - integration_id: text().$type().notNull(), + integration_id: text().$type(), label: text().notNull(), value: text({ mode: "json" }).$type().notNull(), + connector_id: text(), + method_id: text(), + active: integer({ mode: "boolean" }), ...Timestamps, }) diff --git a/packages/core/src/database/migration/20260611192811_lush_chimera.ts b/packages/core/src/database/migration/20260611192811_lush_chimera.ts index e794db293..306b10d53 100644 --- a/packages/core/src/database/migration/20260611192811_lush_chimera.ts +++ b/packages/core/src/database/migration/20260611192811_lush_chimera.ts @@ -5,11 +5,21 @@ export default { id: "20260611192811_lush_chimera", up(tx) { 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(`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 diff --git a/packages/core/src/database/schema.gen.ts b/packages/core/src/database/schema.gen.ts index 5d115e717..e19ac99c8 100644 --- a/packages/core/src/database/schema.gen.ts +++ b/packages/core/src/database/schema.gen.ts @@ -59,9 +59,12 @@ export default { yield* tx.run(` CREATE TABLE \`credential\` ( \`id\` text PRIMARY KEY, - \`integration_id\` text NOT NULL, + \`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 ); @@ -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 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_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_project_idx\` ON \`session\` (\`project_id\`);`) yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`) diff --git a/packages/core/test/database-migration.test.ts b/packages/core/test/database-migration.test.ts index 5f224676e..d93ccee82 100644 --- a/packages/core/test/database-migration.test.ts +++ b/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 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 simplifyIntegrationCredentialsMigration from "@opencode-ai/core/database/migration/20260611192811_lush_chimera" import { ProjectV2 } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" 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 () => { await run( Effect.gen(function* () {