mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-24 06:45:22 +00:00
core: simplify account repo/service after review
- persistToken: positional params → named object to prevent swaps - persistAccount: wrap in Database.transaction for atomicity - Internal response schemas: Schema.Class → Schema.Struct (lighter) - fromRow: pass row directly to decoder (strips unknown keys) - Test: add afterAll runtime disposal, use branded ID in assertion Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,13 +17,7 @@ const db = <A>(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<void, AccountServiceError>
|
||||
readonly use: (accountID: AccountID, orgID: Option.Option<OrgID>) => Effect.Effect<void, AccountServiceError>
|
||||
readonly getRow: (accountID: AccountID) => Effect.Effect<Option.Option<AccountRow>, AccountServiceError>
|
||||
readonly persistToken: (
|
||||
accountID: AccountID,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
expiry: Option.Option<number>,
|
||||
) => Effect.Effect<void, AccountServiceError>
|
||||
readonly persistToken: (input: {
|
||||
accountID: AccountID
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
expiry: Option.Option<number>
|
||||
}) => Effect.Effect<void, AccountServiceError>
|
||||
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<number>) =>
|
||||
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)
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -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>("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>("RemoteConfig")({
|
||||
const RemoteConfig = Schema.Struct({
|
||||
config: Schema.Record(Schema.String, Schema.Json),
|
||||
}) {}
|
||||
})
|
||||
|
||||
class TokenRefresh extends Schema.Class<TokenRefresh>("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>("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>("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>("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))
|
||||
})
|
||||
|
||||
@@ -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 = <A>(effect: Effect.Effect<A, unknown, AccountRepo>) => 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()
|
||||
|
||||
Reference in New Issue
Block a user