Skip to content

Commit 85d87d7

Browse files
committed
feat: add CodebuffTransactionFn type and improve BillingTransactionFn docs
- Add CodebuffTransaction and CodebuffTransactionFn types to @codebuff/internal/db for fully-typed Drizzle transaction usage in production code - Improve BillingTransactionFn documentation explaining why any is needed (Drizzle PgTransaction signatures incompatible with BillingDbConnection) - Document when to use CodebuffTransactionFn vs BillingTransactionFn
1 parent ea1be56 commit 85d87d7

File tree

4 files changed

+76
-11
lines changed

4 files changed

+76
-11
lines changed

common/src/types/contracts/billing.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -227,22 +227,34 @@ export type BillingDbConnection = {
227227
}
228228

229229
/**
230-
* Transaction callback type.
231-
* This matches the signature of drizzle's db.transaction method.
230+
* Transaction callback type for dependency injection.
232231
*
233-
* Note: The callback parameter uses `any` because the real Drizzle transaction
234-
* type (`PgTransaction`) has many additional properties (schema, rollback, etc.)
235-
* that our minimal `BillingDbConnection` doesn't include. Using `any` allows
236-
* both the real transaction and mock implementations to work.
232+
* This type uses `any` for the callback parameter to allow both:
233+
* - Real Drizzle transactions (which have complex generic types)
234+
* - Mock implementations using `BillingDbConnection`
235+
*
236+
* The `any` is necessary because Drizzle's `PgTransaction` type has method
237+
* signatures incompatible with our simplified `BillingDbConnection` interface.
238+
* Using a union or intersection type creates uncallable method signatures.
239+
*
240+
* For production code that needs full Drizzle type safety (no DI), use
241+
* `CodebuffTransactionFn` from `@codebuff/internal/db` instead.
237242
*
238-
* In tests, you can pass a mock that satisfies `BillingDbConnection`:
239243
* @example
240244
* ```typescript
245+
* // In tests - create mocks that satisfy BillingDbConnection
241246
* const mockTransaction: BillingTransactionFn = async (callback) => {
242247
* const mockDb = createMockDb({ users: [...] })
243248
* return callback(mockDb)
244249
* }
250+
*
251+
* // In production - use with db.transaction.bind(db)
252+
* const transaction = deps.transaction ?? db.transaction.bind(db)
253+
* await transaction(async (tx) => { ... })
245254
* ```
255+
*
256+
* @see CodebuffTransactionFn in `@codebuff/internal/db` for fully-typed production use
257+
* @see BillingDbConnection for the minimal interface that mocks should implement
246258
*/
247259
export type BillingTransactionFn = <T>(
248260
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -300,7 +312,12 @@ export type GetOrganizationUsageResponseFn = (params: {
300312
// ============================================================================
301313

302314
/**
303-
* Dependencies for triggerMonthlyResetAndGrant
315+
* Dependencies for triggerMonthlyResetAndGrant.
316+
*
317+
* Note: The `transaction` field uses `BillingTransactionFn` which accepts
318+
* `BillingDbConnection` in the callback. This works for testing with mocks.
319+
* In production code, the billing package uses `CodebuffTransactionFn` from
320+
* `@codebuff/internal/db` which provides full Drizzle type safety.
304321
*/
305322
export type TriggerMonthlyResetAndGrantDeps = {
306323
db?: BillingDbConnection

packages/billing/src/grant-credits.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ import type {
1818
} from '@codebuff/common/types/contracts/billing'
1919
import type { GrantType } from '@codebuff/internal/db/schema'
2020

21-
type CreditGrantSelect = typeof schema.creditLedger.$inferSelect
21+
// Local type alias for the transaction object - extracts the actual type from db.transaction
2222
type DbTransaction = Parameters<typeof db.transaction>[0] extends (
2323
tx: infer T,
24-
) => any
24+
) => unknown
2525
? T
2626
: never
2727

@@ -328,11 +328,18 @@ export async function processAndGrantCredit(params: {
328328
* @param deps Optional dependencies for testing (transaction function)
329329
* @returns true if the grant was found and revoked, false otherwise
330330
*/
331+
/**
332+
* Dependencies for revokeGrantByOperationId (for testing)
333+
*/
334+
export interface RevokeGrantByOperationIdDeps {
335+
transaction?: BillingTransactionFn
336+
}
337+
331338
export async function revokeGrantByOperationId(params: {
332339
operationId: string
333340
reason: string
334341
logger: Logger
335-
deps?: { transaction?: BillingTransactionFn }
342+
deps?: RevokeGrantByOperationIdDeps
336343
}): Promise<boolean> {
337344
const { operationId, reason, logger, deps = {} } = params
338345
const transaction = deps.transaction ?? db.transaction.bind(db)

packages/internal/src/db/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import * as schema from './schema'
77

88
import type { CodebuffPgDatabase } from './types'
99

10+
export type { CodebuffPgDatabase, CodebuffTransaction, CodebuffTransactionFn } from './types'
11+
1012
const client = postgres(env.DATABASE_URL)
1113

1214
export const db: CodebuffPgDatabase = drizzle(client, { schema })

packages/internal/src/db/types.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,47 @@
11
import type * as schema from './schema'
22
import type { PgDatabase } from 'drizzle-orm/pg-core'
33
import type { PostgresJsQueryResultHKT } from 'drizzle-orm/postgres-js'
4+
import type { ExtractTablesWithRelations } from 'drizzle-orm'
5+
import type { PgTransaction } from 'drizzle-orm/pg-core'
46

57
export type CodebuffPgDatabase = PgDatabase<
68
PostgresJsQueryResultHKT,
79
typeof schema
810
>
11+
12+
/**
13+
* The type of a Drizzle transaction object for Codebuff's database.
14+
* This is the `tx` parameter type in `db.transaction(async (tx) => { ... })`.
15+
*
16+
* Use this type when you need the full Drizzle transaction capabilities.
17+
* For DI/testing scenarios where you only need basic CRUD operations,
18+
* use `BillingDbConnection` from `@codebuff/common/types/contracts/billing`.
19+
*/
20+
export type CodebuffTransaction = PgTransaction<
21+
PostgresJsQueryResultHKT,
22+
typeof schema,
23+
ExtractTablesWithRelations<typeof schema>
24+
>
25+
26+
/**
27+
* Type for the db.transaction function.
28+
* Use this to properly type transaction functions in production code.
29+
*
30+
* @example
31+
* ```typescript
32+
* import type { CodebuffTransactionFn } from '@codebuff/internal/db/types'
33+
*
34+
* async function myFunction(params: {
35+
* deps?: { transaction?: CodebuffTransactionFn }
36+
* }) {
37+
* const transaction = params.deps?.transaction ?? db.transaction.bind(db)
38+
* return transaction(async (tx) => {
39+
* // tx is fully typed as CodebuffTransaction
40+
* await tx.insert(schema.user).values({ ... })
41+
* })
42+
* }
43+
* ```
44+
*/
45+
export type CodebuffTransactionFn = <T>(
46+
callback: (tx: CodebuffTransaction) => Promise<T>,
47+
) => Promise<T>

0 commit comments

Comments
 (0)