Skip to content

Commit 7793db3

Browse files
committed
fix(core): preserve credential schema compatibility
1 parent 5f77482 commit 7793db3

6 files changed

Lines changed: 100 additions & 36 deletions

File tree

packages/core/schema.json

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@
382382
},
383383
{
384384
"type": "text",
385-
"notNull": true,
385+
"notNull": false,
386386
"autoincrement": false,
387387
"default": null,
388388
"generated": null,
@@ -410,6 +410,36 @@
410410
"entityType": "columns",
411411
"table": "credential"
412412
},
413+
{
414+
"type": "text",
415+
"notNull": false,
416+
"autoincrement": false,
417+
"default": null,
418+
"generated": null,
419+
"name": "connector_id",
420+
"entityType": "columns",
421+
"table": "credential"
422+
},
423+
{
424+
"type": "text",
425+
"notNull": false,
426+
"autoincrement": false,
427+
"default": null,
428+
"generated": null,
429+
"name": "method_id",
430+
"entityType": "columns",
431+
"table": "credential"
432+
},
433+
{
434+
"type": "integer",
435+
"notNull": false,
436+
"autoincrement": false,
437+
"default": null,
438+
"generated": null,
439+
"name": "active",
440+
"entityType": "columns",
441+
"table": "credential"
442+
},
413443
{
414444
"type": "integer",
415445
"notNull": true,

packages/core/src/credential.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,15 @@ export const layer = Layer.effect(
6565
Effect.gen(function* () {
6666
const { db } = yield* Database.Service
6767
const decode = Schema.decodeUnknownSync(Info)
68-
const stored = (row: typeof CredentialTable.$inferSelect) =>
69-
new Stored({
68+
const stored = (row: typeof CredentialTable.$inferSelect) => {
69+
if (!row.integration_id) return
70+
return new Stored({
7071
id: row.id,
7172
integrationID: row.integration_id,
7273
label: row.label,
7374
value: decode(row.value),
7475
})
76+
}
7577

7678
return Service.of({
7779
all: Effect.fn("Credential.all")(function* () {
@@ -80,7 +82,10 @@ export const layer = Layer.effect(
8082
.from(CredentialTable)
8183
.orderBy(asc(CredentialTable.time_created))
8284
.all()
83-
.pipe(Effect.orDie)).map(stored)
85+
.pipe(Effect.orDie)).flatMap((row) => {
86+
const credential = stored(row)
87+
return credential ? [credential] : []
88+
})
8489
}),
8590
list: Effect.fn("Credential.list")(function* (integrationID) {
8691
return (yield* db
@@ -89,7 +94,10 @@ export const layer = Layer.effect(
8994
.where(eq(CredentialTable.integration_id, integrationID))
9095
.orderBy(asc(CredentialTable.time_created))
9196
.all()
92-
.pipe(Effect.orDie)).map(stored)
97+
.pipe(Effect.orDie)).flatMap((row) => {
98+
const credential = stored(row)
99+
return credential ? [credential] : []
100+
})
93101
}),
94102
create: Effect.fn("Credential.create")(function* (input) {
95103
const credential = new Stored({
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
1+
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
22
import { Timestamps } from "../database/schema.sql"
33
import type { IntegrationSchema } from "../integration/schema"
44
import type { Credential } from "../credential"
55

66
export const CredentialTable = sqliteTable("credential", {
77
id: text().$type<Credential.ID>().primaryKey(),
8-
integration_id: text().$type<IntegrationSchema.ID>().notNull(),
8+
integration_id: text().$type<IntegrationSchema.ID>(),
99
label: text().notNull(),
1010
value: text({ mode: "json" }).$type<Credential.Info>().notNull(),
11+
connector_id: text(),
12+
method_id: text(),
13+
active: integer({ mode: "boolean" }),
1114
...Timestamps,
1215
})

packages/core/src/database/migration/20260611192811_lush_chimera.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,21 @@ export default {
55
id: "20260611192811_lush_chimera",
66
up(tx) {
77
return Effect.gen(function* () {
8-
yield* tx.run(`ALTER TABLE \`credential\` ADD \`integration_id\` text NOT NULL;`)
98
yield* tx.run(`DROP INDEX IF EXISTS \`credential_connector_active_idx\`;`)
10-
yield* tx.run(`ALTER TABLE \`credential\` DROP COLUMN \`connector_id\`;`)
11-
yield* tx.run(`ALTER TABLE \`credential\` DROP COLUMN \`method_id\`;`)
12-
yield* tx.run(`ALTER TABLE \`credential\` DROP COLUMN \`active\`;`)
9+
yield* tx.run(`DROP TABLE \`credential\`;`)
10+
yield* tx.run(`
11+
CREATE TABLE \`credential\` (
12+
\`id\` text PRIMARY KEY,
13+
\`integration_id\` text,
14+
\`label\` text NOT NULL,
15+
\`value\` text NOT NULL,
16+
\`connector_id\` text,
17+
\`method_id\` text,
18+
\`active\` integer,
19+
\`time_created\` integer NOT NULL,
20+
\`time_updated\` integer NOT NULL
21+
);
22+
`)
1323
})
1424
},
1525
} satisfies DatabaseMigration.Migration

packages/core/src/database/schema.gen.ts

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,12 @@ export default {
5959
yield* tx.run(`
6060
CREATE TABLE \`credential\` (
6161
\`id\` text PRIMARY KEY,
62-
\`integration_id\` text NOT NULL,
62+
\`integration_id\` text,
6363
\`label\` text NOT NULL,
6464
\`value\` text NOT NULL,
65+
\`connector_id\` text,
66+
\`method_id\` text,
67+
\`active\` integer,
6568
\`time_created\` integer NOT NULL,
6669
\`time_updated\` integer NOT NULL
6770
);
@@ -237,32 +240,16 @@ export default {
237240
`)
238241
yield* tx.run(`CREATE UNIQUE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`)
239242
yield* tx.run(`CREATE INDEX \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`)
240-
yield* tx.run(
241-
`CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`,
242-
)
243-
yield* tx.run(
244-
`CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`,
245-
)
243+
yield* tx.run(`CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`)
244+
yield* tx.run(`CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`)
246245
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
247246
yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`)
248-
yield* tx.run(
249-
`CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`,
250-
)
251-
yield* tx.run(
252-
`CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`,
253-
)
254-
yield* tx.run(
255-
`CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`,
256-
)
257-
yield* tx.run(
258-
`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`,
259-
)
260-
yield* tx.run(
261-
`CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`,
262-
)
263-
yield* tx.run(
264-
`CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`,
265-
)
247+
yield* tx.run(`CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`)
248+
yield* tx.run(`CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`)
249+
yield* tx.run(`CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`)
250+
yield* tx.run(`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`)
251+
yield* tx.run(`CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`)
252+
yield* tx.run(`CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`)
266253
yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`)
267254
yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`)
268255
yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`)

packages/core/test/database-migration.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import normalizeStoragePathsMigration from "@opencode-ai/core/database/migration
1313
import sessionMessageProjectionOrderMigration from "@opencode-ai/core/database/migration/20260603040000_session_message_projection_order"
1414
import eventSourcedSessionInputMigration from "@opencode-ai/core/database/migration/20260604172448_event_sourced_session_input"
1515
import contextEpochAgentMigration from "@opencode-ai/core/database/migration/20260605042240_add_context_epoch_agent"
16+
import simplifyIntegrationCredentialsMigration from "@opencode-ai/core/database/migration/20260611192811_lush_chimera"
1617
import { ProjectV2 } from "@opencode-ai/core/project"
1718
import { ProjectTable } from "@opencode-ai/core/project/sql"
1819
import { AbsolutePath } from "@opencode-ai/core/schema"
@@ -124,6 +125,31 @@ describe("DatabaseMigration", () => {
124125
)
125126
})
126127

128+
test("keeps legacy credential fields nullable", async () => {
129+
await run(
130+
Effect.gen(function* () {
131+
const db = yield* makeDb
132+
yield* db.run(
133+
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)`,
134+
)
135+
yield* db.run(
136+
sql`CREATE UNIQUE INDEX credential_connector_active_idx ON credential (connector_id) WHERE active = 1`,
137+
)
138+
yield* DatabaseMigration.applyOnly(db, [simplifyIntegrationCredentialsMigration])
139+
140+
yield* db.run(
141+
sql`INSERT INTO credential (id, connector_id, method_id, label, value, active, time_created, time_updated) VALUES ('legacy', 'openai', 'oauth', 'Legacy', '{}', 1, 1, 1)`,
142+
)
143+
yield* db.run(
144+
sql`INSERT INTO credential (id, integration_id, label, value, time_created, time_updated) VALUES ('current', 'anthropic', 'Current', '{}', 2, 2)`,
145+
)
146+
expect(
147+
yield* db.get(sql`SELECT connector_id, method_id, active FROM credential WHERE id = 'current'`),
148+
).toEqual({ connector_id: null, method_id: null, active: null })
149+
}),
150+
)
151+
})
152+
127153
test("resets beta history and rebuilds event-sourced Session input storage", async () => {
128154
await run(
129155
Effect.gen(function* () {

0 commit comments

Comments
 (0)