fix(core): preserve credential schema compatibility
This commit is contained in:
parent
5f77482a29
commit
7793db3ac8
@ -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,
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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<Credential.ID>().primaryKey(),
|
||||
integration_id: text().$type<IntegrationSchema.ID>().notNull(),
|
||||
integration_id: text().$type<IntegrationSchema.ID>(),
|
||||
label: text().notNull(),
|
||||
value: text({ mode: "json" }).$type<Credential.Info>().notNull(),
|
||||
connector_id: text(),
|
||||
method_id: text(),
|
||||
active: integer({ mode: "boolean" }),
|
||||
...Timestamps,
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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\`);`)
|
||||
|
||||
@ -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* () {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user