| export * as Credential from "./credential" |
|
|
| import { asc, eq } from "drizzle-orm" |
| import { Context, Effect, Layer, Schema } from "effect" |
| import { Credential } from "@opencode-ai/schema/credential" |
| import { Integration } from "@opencode-ai/schema/integration" |
| import { Database } from "./database/database" |
| import { makeGlobalNode } from "./effect/app-node" |
| import { CredentialTable } from "./credential/sql" |
|
|
| export const ID = Credential.ID |
| export type ID = Credential.ID |
|
|
| export const OAuth = Credential.OAuth |
| export type OAuth = Credential.OAuth |
|
|
| export const Key = Credential.Key |
| export type Key = Credential.Key |
|
|
| export const Value = Credential.Value |
| export type Value = Credential.Value |
|
|
| export class Info extends Schema.Class<Info>("Credential.Info")({ |
| id: ID, |
| integrationID: Integration.ID, |
| label: Schema.String, |
| value: Value, |
| }) {} |
|
|
| export interface Interface { |
| |
| readonly all: () => Effect.Effect<Info[]> |
| |
| readonly list: (integrationID: Integration.ID) => Effect.Effect<Info[]> |
| |
| readonly get: (id: ID) => Effect.Effect<Info | undefined> |
| |
| readonly create: (input: { |
| readonly integrationID: Integration.ID |
| readonly value: Value |
| readonly label?: string |
| }) => Effect.Effect<Info> |
| |
| readonly update: (id: ID, updates: Partial<Pick<Info, "label" | "value">>) => Effect.Effect<void> |
| |
| readonly remove: (id: ID) => Effect.Effect<void> |
| } |
|
|
| export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Credential") {} |
|
|
| const layer = Layer.effect( |
| Service, |
| Effect.gen(function* () { |
| const { db } = yield* Database.Service |
| const decode = Schema.decodeUnknownSync(Value) |
| const stored = (row: typeof CredentialTable.$inferSelect) => { |
| if (!row.integration_id) return |
| return new Info({ |
| id: row.id, |
| integrationID: row.integration_id, |
| label: row.label, |
| value: decode(row.value), |
| }) |
| } |
|
|
| return Service.of({ |
| all: Effect.fn("Credential.all")(function* () { |
| return (yield* db |
| .select() |
| .from(CredentialTable) |
| .orderBy(asc(CredentialTable.time_created)) |
| .all() |
| .pipe(Effect.orDie)).flatMap((row) => { |
| const credential = stored(row) |
| return credential ? [credential] : [] |
| }) |
| }), |
| list: Effect.fn("Credential.list")(function* (integrationID) { |
| return (yield* db |
| .select() |
| .from(CredentialTable) |
| .where(eq(CredentialTable.integration_id, integrationID)) |
| .orderBy(asc(CredentialTable.time_created)) |
| .all() |
| .pipe(Effect.orDie)).flatMap((row) => { |
| const credential = stored(row) |
| return credential ? [credential] : [] |
| }) |
| }), |
| get: Effect.fn("Credential.get")(function* (id) { |
| const row = yield* db.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get().pipe(Effect.orDie) |
| return row ? stored(row) : undefined |
| }), |
| create: Effect.fn("Credential.create")(function* (input) { |
| const credential = new Info({ |
| id: ID.create(), |
| integrationID: input.integrationID, |
| label: input.label ?? "default", |
| value: input.value, |
| }) |
| yield* db |
| .transaction((tx) => |
| Effect.gen(function* () { |
| yield* tx |
| .delete(CredentialTable) |
| .where(eq(CredentialTable.integration_id, credential.integrationID)) |
| .run() |
| yield* tx |
| .insert(CredentialTable) |
| .values({ |
| id: credential.id, |
| integration_id: credential.integrationID, |
| label: credential.label, |
| value: credential.value, |
| }) |
| .run() |
| }), |
| ) |
| .pipe(Effect.orDie) |
| return credential |
| }), |
| update: Effect.fn("Credential.update")(function* (id, updates) { |
| if (!updates.label && !updates.value) return |
| yield* db |
| .update(CredentialTable) |
| .set({ label: updates.label, value: updates.value }) |
| .where(eq(CredentialTable.id, id)) |
| .run() |
| .pipe(Effect.orDie) |
| }), |
| remove: Effect.fn("Credential.remove")(function* (id) { |
| yield* db.delete(CredentialTable).where(eq(CredentialTable.id, id)).run().pipe(Effect.orDie) |
| }), |
| }) |
| }), |
| ) |
|
|
| export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] }) |
|
|