diff --git a/packages/opencode/src/account/repo.ts b/packages/opencode/src/account/repo.ts index d2cf468f42..c435bc27dd 100644 --- a/packages/opencode/src/account/repo.ts +++ b/packages/opencode/src/account/repo.ts @@ -17,13 +17,7 @@ const db = (run: (db: DbClient) => A) => catch: (cause) => new AccountServiceError({ operation: "db", message: "Database operation failed", cause }), }) -const fromRow = (row: AccountRow) => - decodeAccount({ - id: row.id, - email: row.email, - url: row.url, - org_id: row.org_id, - }) +const fromRow = (row: AccountRow) => decodeAccount(row) export class AccountRepo extends ServiceMap.Service< AccountRepo, @@ -33,12 +27,12 @@ export class AccountRepo extends ServiceMap.Service< readonly remove: (accountID: AccountID) => Effect.Effect readonly use: (accountID: AccountID, orgID: Option.Option) => Effect.Effect readonly getRow: (accountID: AccountID) => Effect.Effect, AccountServiceError> - readonly persistToken: ( - accountID: AccountID, - accessToken: string, - refreshToken: string, - expiry: Option.Option, - ) => Effect.Effect + readonly persistToken: (input: { + accountID: AccountID + accessToken: string + refreshToken: string + expiry: Option.Option + }) => Effect.Effect readonly persistAccount: (input: { id: AccountID email: string @@ -81,45 +75,48 @@ export class AccountRepo extends ServiceMap.Service< ), ), - persistToken: Effect.fn("AccountRepo.persistToken")( - (accountID: AccountID, accessToken: string, refreshToken: string, expiry: Option.Option) => - db((db) => - db - .update(AccountTable) - .set({ - access_token: accessToken, - refresh_token: refreshToken, - token_expiry: Option.getOrNull(expiry), - }) - .where(eq(AccountTable.id, accountID)) - .run(), - ).pipe(Effect.asVoid), + persistToken: Effect.fn("AccountRepo.persistToken")((input) => + db((db) => + db + .update(AccountTable) + .set({ + access_token: input.accessToken, + refresh_token: input.refreshToken, + token_expiry: Option.getOrNull(input.expiry), + }) + .where(eq(AccountTable.id, input.accountID)) + .run(), + ).pipe(Effect.asVoid), ), persistAccount: Effect.fn("AccountRepo.persistAccount")((input) => { const orgID = Option.getOrNull(input.orgID) - return db((db) => { - db.update(AccountTable).set({ org_id: null }).where(isNotNull(AccountTable.org_id)).run() - db.insert(AccountTable) - .values({ - id: input.id, - email: input.email, - url: input.url, - access_token: input.accessToken, - refresh_token: input.refreshToken, - token_expiry: input.expiry, - org_id: orgID, - }) - .onConflictDoUpdate({ - target: AccountTable.id, - set: { - access_token: input.accessToken, - refresh_token: input.refreshToken, - token_expiry: input.expiry, - org_id: orgID, - }, - }) - .run() + return Effect.try({ + try: () => + Database.transaction((tx) => { + tx.update(AccountTable).set({ org_id: null }).where(isNotNull(AccountTable.org_id)).run() + tx.insert(AccountTable) + .values({ + id: input.id, + email: input.email, + url: input.url, + access_token: input.accessToken, + refresh_token: input.refreshToken, + token_expiry: input.expiry, + org_id: orgID, + }) + .onConflictDoUpdate({ + target: AccountTable.id, + set: { + access_token: input.accessToken, + refresh_token: input.refreshToken, + token_expiry: input.expiry, + org_id: orgID, + }, + }) + .run() + }), + catch: (cause) => new AccountServiceError({ operation: "db", message: "Database operation failed", cause }), }).pipe(Effect.asVoid) }), }), diff --git a/packages/opencode/src/account/service.ts b/packages/opencode/src/account/service.ts index c17045f4ee..2055bf7902 100644 --- a/packages/opencode/src/account/service.ts +++ b/packages/opencode/src/account/service.ts @@ -13,43 +13,43 @@ import { AccessToken, Account, AccountID, AccountServiceError, Login, OrgID, typ export { AccessToken, Account, AccountID, AccountServiceError, Login, OrgID, type PollResult } from "./schema" -class RemoteOrg extends Schema.Class("RemoteOrg")({ +const RemoteOrg = Schema.Struct({ id: Schema.optional(OrgID), name: Schema.optional(Schema.String), -}) {} +}) const RemoteOrgs = Schema.Array(RemoteOrg) -class RemoteConfig extends Schema.Class("RemoteConfig")({ +const RemoteConfig = Schema.Struct({ config: Schema.Record(Schema.String, Schema.Json), -}) {} +}) -class TokenRefresh extends Schema.Class("TokenRefresh")({ +const TokenRefresh = Schema.Struct({ access_token: Schema.String, refresh_token: Schema.optional(Schema.String), expires_in: Schema.optional(Schema.Number), -}) {} +}) -class DeviceCode extends Schema.Class("DeviceCode")({ +const DeviceCode = Schema.Struct({ device_code: Schema.String, user_code: Schema.String, verification_uri_complete: Schema.String, expires_in: Schema.Number, interval: Schema.Number, -}) {} +}) -class DeviceToken extends Schema.Class("DeviceToken")({ +const DeviceToken = Schema.Struct({ access_token: Schema.optional(Schema.String), refresh_token: Schema.optional(Schema.String), expires_in: Schema.optional(Schema.Number), error: Schema.optional(Schema.String), error_description: Schema.optional(Schema.String), -}) {} +}) -class User extends Schema.Class("User")({ +const User = Schema.Struct({ id: Schema.optional(AccountID), email: Schema.optional(Schema.String), -}) {} +}) const ClientId = Schema.Struct({ client_id: Schema.String }) @@ -145,12 +145,12 @@ export class AccountService extends ServiceMap.Service< const expiry = Option.fromNullishOr(parsed.expires_in).pipe(Option.map((e) => now + e * 1000)) - yield* repo.persistToken( - AccountID.make(found.id), - parsed.access_token, - parsed.refresh_token ?? found.refresh_token, + yield* repo.persistToken({ + accountID: AccountID.make(found.id), + accessToken: parsed.access_token, + refreshToken: parsed.refresh_token ?? found.refresh_token, expiry, - ) + }) return Option.some(AccessToken.make(parsed.access_token)) }) diff --git a/packages/opencode/test/account/repo.test.ts b/packages/opencode/test/account/repo.test.ts index e4cce9a9d2..2454c0b23a 100644 --- a/packages/opencode/test/account/repo.test.ts +++ b/packages/opencode/test/account/repo.test.ts @@ -1,4 +1,4 @@ -import { test, expect, afterEach } from "bun:test" +import { test, expect, afterEach, afterAll } from "bun:test" import { Effect, ManagedRuntime, Option } from "effect" import { AccountRepo } from "../../src/account/repo" @@ -9,6 +9,10 @@ afterEach(async () => { await resetDatabase() }) +afterAll(async () => { + await runtime.dispose() +}) + const runtime = ManagedRuntime.make(AccountRepo.layer) const run = (effect: Effect.Effect) => runtime.runPromise(effect) const repo = AccountRepo @@ -86,7 +90,7 @@ test("persistAccount sets active account (clears previous org_ids)", async () => // Second account should be active const active = await run(repo.use((r) => r.active())) expect(Option.isSome(active)).toBe(true) - expect(Option.getOrThrow(active).id).toBe("user-2") + expect(Option.getOrThrow(active).id).toBe(AccountID.make("user-2")) }) test("list returns all accounts", async () => { @@ -195,7 +199,7 @@ test("persistToken updates token fields", async () => { ) const newExpiry = Date.now() + 7200_000 - await run(repo.use((r) => r.persistToken(id, "new_token", "new_refresh", Option.some(newExpiry)))) + await run(repo.use((r) => r.persistToken({ accountID: id, accessToken: "new_token", refreshToken: "new_refresh", expiry: Option.some(newExpiry) }))) const row = await run(repo.use((r) => r.getRow(id))) const value = Option.getOrThrow(row) @@ -221,7 +225,7 @@ test("persistToken with no expiry sets token_expiry to null", async () => { ), ) - await run(repo.use((r) => r.persistToken(id, "new_token", "new_refresh", Option.none()))) + await run(repo.use((r) => r.persistToken({ accountID: id, accessToken: "new_token", refreshToken: "new_refresh", expiry: Option.none() }))) const row = await run(repo.use((r) => r.getRow(id))) expect(Option.getOrThrow(row).token_expiry).toBeNull()