ReviewOS

also looking at this

stacks/bun-query-builder

refactor(types): stop publishing `any` across the query builder's surface

#1117
Merged chris wants to merge types/remove-any-from-public-surface into main
13 files +359 -110

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/bun-query-builder/src/browser.tsmodified+17-6
Changes to packages/bun-query-builder/src/browser.ts
@@ -372,7 +372,9 @@ export class BrowserQueryBuilder<T = any> {
372372 /**
373373 * Add a where clause
374374 */
375 where(column: string, operatorOrValue: WhereOperator | any, value?: any): this {
375 where(column: string, value: unknown): this
376 where(column: string, operator: WhereOperator, value: unknown): this
377 where(column: string, operatorOrValue: unknown, value?: unknown): this {
376378 if (value === undefined) {
377379 // where('column', value) shorthand for equality
378380 this.state.wheres.push({
@@ -397,7 +399,9 @@ export class BrowserQueryBuilder<T = any> {
397399 /**
398400 * Add an OR where clause
399401 */
400 orWhere(column: string, operatorOrValue: WhereOperator | any, value?: any): this {
402 orWhere(column: string, value: unknown): this
403 orWhere(column: string, operator: WhereOperator, value: unknown): this
404 orWhere(column: string, operatorOrValue: unknown, value?: unknown): this {
401405 if (value === undefined) {
402406 this.state.wheres.push({
403407 column,
@@ -420,8 +424,15 @@ export class BrowserQueryBuilder<T = any> {
420424 /**
421425 * Add an AND where clause (alias for where)
422426 */
423 andWhere(column: string, operatorOrValue: WhereOperator | any, value?: any): this {
424 return this.where(column, operatorOrValue, value)
427 andWhere(column: string, value: unknown): this
428 andWhere(column: string, operator: WhereOperator, value: unknown): this
429 andWhere(column: string, operatorOrValue: unknown, value?: unknown): this {
430 // Two args means the second is a value; three means it is an operator -
431 // which is exactly what the overloads above declare, so the dispatch is
432 // split here rather than passing an unknown into the operator position.
433 return value === undefined
434 ? this.where(column, operatorOrValue)
435 : this.where(column, operatorOrValue as WhereOperator, value)
425436 }
426437
427438 /**
@@ -441,14 +452,14 @@ export class BrowserQueryBuilder<T = any> {
441452 /**
442453 * Where column is in array
443454 */
444 whereIn(column: string, values: any[]): this {
455 whereIn(column: string, values: readonly unknown[]): this {
445456 return this.where(column, 'in', values)
446457 }
447458
448459 /**
449460 * Where column is not in array
450461 */
451 whereNotIn(column: string, values: any[]): this {
462 whereNotIn(column: string, values: readonly unknown[]): this {
452463 return this.where(column, 'not in', values)
453464 }
454465
packages/bun-query-builder/src/client.tsmodified+31-30
Changes to packages/bun-query-builder/src/client.ts
@@ -3,7 +3,7 @@
33/* eslint-disable no-useless-catch */
44import type { SchemaMeta } from './meta'
55import type { ResolvedPivot } from './pivot'
6import type { DatabaseSchema } from './schema'
6import type { DatabaseSchema, AnyDatabaseSchema } from './schema'
77import type { QueryBuilderOptions, QueryHooks, SupportedDialect} from './types'
88import { config, getPlaceholder, getPlaceholders, isMysqlLike, setConfig } from './config'
99import type { DriverConnection } from './db'
@@ -400,7 +400,7 @@ class QueryCache {
400400 private cache = new Map<string, CacheEntry>()
401401 private maxSize = 100
402402
403 get(key: string): any | null {
403 get<T = unknown>(key: string): T | null {
404404 const entry = this.cache.get(key)
405405 if (!entry)
406406 return null
@@ -420,7 +420,7 @@ class QueryCache {
420420 return entry.data
421421 }
422422
423 set(key: string, data: any, ttlMs: number): void {
423 set(key: string, data: unknown, ttlMs: number): void {
424424 // Re-inserted keys must move to the back or they'd be evicted as if old.
425425 if (this.cache.has(key))
426426 this.cache.delete(key)
@@ -526,15 +526,15 @@ export type SortDirection = 'asc' | 'desc'
526526 *
527527 * Helper type extracting a string union of column names for a given table.
528528 */
529export type ColumnName<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> = keyof DB[TTable]['columns'] & string
529export type ColumnName<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> = keyof DB[TTable]['columns'] & string
530530// Named row alias to improve IDE hover readability
531531export type SelectedRow<
532 DB extends DatabaseSchema<any>,
532 DB extends AnyDatabaseSchema,
533533 _TTable extends keyof DB & string,
534534 TSelected,
535535> = Readonly<TSelected>
536536
537type PrimaryKeyValue<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> =
537type PrimaryKeyValue<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> =
538538 DB[TTable]['primaryKey'] extends keyof DB[TTable]['columns']
539539 ? DB[TTable]['columns'][DB[TTable]['primaryKey']]
540540 : unknown
@@ -545,7 +545,7 @@ type NumericColumnName<Columns> = {
545545 : Exclude<Columns[K], null | undefined> extends number ? K : never
546546}[keyof Columns & string]
547547
548type JoinColumn<DB extends DatabaseSchema<any>, TTables extends string> = TTables extends any
548type JoinColumn<DB extends AnyDatabaseSchema, TTables extends string> = TTables extends any
549549 ? `${TTables}.${keyof DB[TTables]['columns'] & string}`
550550 : never
551551
@@ -559,13 +559,13 @@ type JoinColumn<DB extends DatabaseSchema<any>, TTables extends string> = TTable
559559 * untyped schemas keep compiling. A table that declares ZERO relations
560560 * yields `never` — every relation name is rejected.
561561 */
562export type TableRelationName<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> =
562export type TableRelationName<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> =
563563 DB[TTable] extends { relations?: infer R }
564564 ? unknown extends R ? string : keyof NonNullable<R> & string
565565 : string
566566
567567type RelatedTableName<
568 DB extends DatabaseSchema<any>,
568 DB extends AnyDatabaseSchema,
569569 TTable extends keyof DB & string,
570570 TRelation extends TableRelationName<DB, TTable>,
571571> = DB[TTable] extends { relations?: infer Relations }
@@ -575,7 +575,7 @@ type RelatedTableName<
575575 : keyof DB & string
576576
577577type RelationQueryBuilder<
578 DB extends DatabaseSchema<any>,
578 DB extends AnyDatabaseSchema,
579579 TTable extends keyof DB & string,
580580 TRelation extends TableRelationName<DB, TTable>,
581581> = SelectQueryBuilder<
@@ -584,7 +584,7 @@ type RelationQueryBuilder<
584584 DB[RelatedTableName<DB, TTable, TRelation>]['columns']
585585>
586586
587type RelationConstraintRecord<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> = {
587type RelationConstraintRecord<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> = {
588588 [R in TableRelationName<DB, TTable>]: Partial<Record<R, (qb: RelationQueryBuilder<DB, TTable, R>) => unknown>>
589589}[TableRelationName<DB, TTable>]
590590
@@ -597,7 +597,7 @@ type RelationConstraintRecord<DB extends DatabaseSchema<any>, TTable extends key
597597 * relations accepts nothing (the bare `Partial<Record<never, ...>>` would be
598598 * `{}`, which strings are assignable to — hence the explicit never guard).
599599 */
600export type WithRelationArg<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> =
600export type WithRelationArg<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> =
601601 [TableRelationName<DB, TTable>] extends [never]
602602 ? never
603603 :
@@ -614,7 +614,7 @@ type SnakeToPascal<S extends string> = S extends `${infer H}_${infer T}`
614614// thread a phantom TSql string through method signatures so hovers can show the
615615// composed SQL at compile-time for common operations.
616616type _TypedDynamicWhereMethods<
617 DB extends DatabaseSchema<any>,
617 DB extends AnyDatabaseSchema,
618618 TTable extends keyof DB & string,
619619 TSelected,
620620 TJoined extends string,
@@ -639,7 +639,7 @@ type _TypedDynamicWhereMethods<
639639// resolution, silently downgrading `toSQL()` from the composed literal SQL
640640// type back to `string` after any dynamic-where call.
641641export type TypedSelectQueryBuilder<
642 DB extends DatabaseSchema<any>,
642 DB extends AnyDatabaseSchema,
643643 TTable extends keyof DB & string,
644644 TSelected,
645645 TJoined extends string = TTable,
@@ -685,7 +685,7 @@ export type TypedSelectQueryBuilder<
685685}
686686
687687type DynamicWhereMethods<
688 DB extends DatabaseSchema<any>,
688 DB extends AnyDatabaseSchema,
689689 TTable extends keyof DB & string,
690690 TSelected,
691691 TJoined extends string = TTable,
@@ -698,7 +698,7 @@ type DynamicWhereMethods<
698698}
699699
700700export interface BaseSelectQueryBuilder<
701 DB extends DatabaseSchema<any>,
701 DB extends AnyDatabaseSchema,
702702 TTable extends keyof DB & string,
703703 TSelected,
704704 TJoined extends string = TTable,
@@ -926,7 +926,7 @@ export interface BaseSelectQueryBuilder<
926926 /** OR-connected counterpart of `whereNotNull`. */
927927 orWhereNotNull: (column: keyof DB[TTable]['columns'] & string) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
928928 /** OR-connected counterpart of `whereBetween`. */
929 orWhereBetween: (column: keyof DB[TTable]['columns'] & string, start: any, end: any) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
929 orWhereBetween: <K extends keyof DB[TTable]['columns'] & string>(column: K, start: DB[TTable]['columns'][K], end: DB[TTable]['columns'][K]) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
930930 /** OR-connected counterpart of `whereExists`. */
931931 orWhereExists: (subquery: { toSQL: () => any }) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
932932 /**
@@ -1906,13 +1906,13 @@ export interface BaseSelectQueryBuilder<
19061906}
19071907
19081908export type SelectQueryBuilder<
1909 DB extends DatabaseSchema<any>,
1909 DB extends AnyDatabaseSchema,
19101910 TTable extends keyof DB & string,
19111911 TSelected,
19121912 TJoined extends string = TTable,
19131913> = BaseSelectQueryBuilder<DB, TTable, TSelected, TJoined> & DynamicWhereMethods<DB, TTable, TSelected, TJoined>
19141914
1915export interface InsertQueryBuilder<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> {
1915export interface InsertQueryBuilder<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> {
19161916 /**
19171917 * # `values`
19181918 *
@@ -1965,7 +1965,7 @@ export interface InsertQueryBuilder<DB extends DatabaseSchema<any>, TTable exten
19651965 executeTakeFirstOrThrow: () => Promise<DB[TTable]['columns']>
19661966}
19671967
1968export interface UpdateQueryBuilder<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> {
1968export interface UpdateQueryBuilder<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> {
19691969 /**
19701970 * # `set`
19711971 *
@@ -2064,7 +2064,7 @@ export interface UpdateQueryBuilder<DB extends DatabaseSchema<any>, TTable exten
20642064 executeTakeFirstOrThrow: () => Promise<{ numUpdatedRows: number }>
20652065}
20662066
2067export interface DeleteQueryBuilder<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> {
2067export interface DeleteQueryBuilder<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> {
20682068 /**
20692069 * # `where`
20702070 *
@@ -2144,7 +2144,7 @@ export interface DeleteQueryBuilder<DB extends DatabaseSchema<any>, TTable exten
21442144 executeTakeFirstOrThrow: () => Promise<{ numDeletedRows: number }>
21452145}
21462146
2147export interface TableQueryBuilder<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> {
2147export interface TableQueryBuilder<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> {
21482148 /**
21492149 * # `insert`
21502150 *
@@ -2192,7 +2192,7 @@ export interface TableQueryBuilder<DB extends DatabaseSchema<any>, TTable extend
21922192 select: <K extends keyof DB[TTable]['columns'] & string>(...columns: K[]) => SelectQueryBuilder<DB, TTable, Pick<DB[TTable]['columns'], K>>
21932193}
21942194
2195export interface QueryBuilder<DB extends DatabaseSchema<any>> {
2195export interface QueryBuilder<DB extends AnyDatabaseSchema> {
21962196 // typed select list (column names or raw aliases)
21972197 /**
21982198 * # `select`
@@ -2660,7 +2660,7 @@ finally { reserved.release() }
26602660
26612661// Typed INSERT builder to expose a structured SQL literal in hovers
26622662export type TypedInsertQueryBuilder<
2663 DB extends DatabaseSchema<any>,
2663 DB extends AnyDatabaseSchema,
26642664 TTable extends keyof DB & string,
26652665 TSql extends string = `INSERT INTO ${TTable}`,
26662666> = Omit<InsertQueryBuilder<DB, TTable>, 'toSQL' | 'values' | 'returning'> & {
@@ -2680,9 +2680,10 @@ export type TypedInsertQueryBuilder<
26802680}
26812681
26822682interface InternalState {
2683 sql: any
2683 /** The driver handle - Bun's `SQL` or our SQLite wrapper, both `DriverConnection`. */
2684 sql: DriverConnection
26842685 meta?: SchemaMeta
2685 schema?: any
2686 schema?: AnyDatabaseSchema
26862687 txDefaults?: TransactionOptions
26872688 /**
26882689 * Lifecycle hooks for this builder specifically.
@@ -2725,11 +2726,11 @@ function isRetriableTxError(err: any): boolean { renamed throughout
27252726
27262727type TransactionIsolation = 'read committed' | 'repeatable read' | 'serializable'
27272728interface TxBackoff { baseMs?: number, maxMs?: number, factor?: number, jitter?: boolean }
2728interface TxLoggerEvent { type: 'start' | 'retry' | 'commit' | 'rollback' | 'error', attempt: number, error?: any, durationMs?: number }
2729interface TxLoggerEvent { type: 'start' | 'retry' | 'commit' | 'rollback' | 'error', attempt: number, error?: unknown, durationMs?: number }
27292730export interface TransactionOptions {
27302731 retries?: number
27312732 isolation?: TransactionIsolation
2732 onRetry?: (attempt: number, error: any) => void
2733 onRetry?: (attempt: number, error: unknown) => void
27332734 afterCommit?: () => void
27342735 sqlStates?: string[]
27352736 backoff?: TxBackoff
@@ -2737,7 +2738,7 @@ export interface TransactionOptions {
27372738 /** When true, executes transaction in read-only mode (where supported). */
27382739 readOnly?: boolean
27392740 /** Called when a transaction is rolled back. */
2740 onRollback?: (error: any) => void
2741 onRollback?: (error: unknown) => void
27412742 /** Called after rollback completes. */
27422743 afterRollback?: () => void
27432744}
@@ -2903,7 +2904,7 @@ function computeBackoffMs(attempt: number, cfg?: TxBackoff): number {
29032904}
29042905
29052906// eslint-disable-next-line pickier/no-unused-vars
2906export function createQueryBuilder<DB extends DatabaseSchema<any>>(state?: Partial<InternalState>): QueryBuilder<DB> {
2907export function createQueryBuilder<DB extends AnyDatabaseSchema>(state?: Partial<InternalState>): QueryBuilder<DB> {
29072908 // Single boundary cast: `state.sql` is `any` (allows mock/tx injection) and
29082909 // getOrCreateBunSql() returns Bun's `SQL`; both satisfy DriverConnection. With
29092910 // `_sql` typed, the downstream `.unsafe(...)` calls no longer need casts (#1044).
packages/bun-query-builder/src/db.tsmodified+63-23
Changes to packages/bun-query-builder/src/db.ts
@@ -1,4 +1,5 @@
11import type { DatabaseConfig, PoolConfig, SupportedDialect } from './types'
2import type { SQLQueryBindings } from 'bun:sqlite'
23import { SQL } from 'bun'
34
45/**
@@ -6,6 +7,42 @@ import { SQL } from 'bun'
67 * Both Bun's native `SQL` query and our `createSQLiteSQL` wrapper satisfy this.
78 * See stacksjs/bun-query-builder#1044.
89 */
10/**
11 * What a value interpolated into a tagged template can turn out to be.
12 *
13 * Three things arrive here: a plain bind param, a nested query (carries `sql`
14 * plus a `values` ARRAY), and a raw-SQL marker (carries `__raw`/`raw` and no
15 * `values`). Naming the shape is what lets the branches below read properties
16 * without an `any` - and the order of those branches is load-bearing, see
17 * stacksjs/bun-query-builder#1084.
18 */
19export interface EmbeddedValue {
20 sql?: unknown
21 values?: unknown
22 __raw?: unknown
23 raw?: unknown
24 value?: unknown
25}
26
27/**
28 * The one place user values become driver bindings.
29 *
30 * Everything a caller interpolates arrives as `unknown` - that is honest, we
31 * do not know what it is. bun:sqlite accepts a narrower set. The conversion is
32 * unchecked because the driver itself is the validator: it throws a clear
33 * "can't bind" for anything it cannot take, and a check here would only
34 * duplicate that rule and drift from it. Kept as one named function so the
35 * boundary is visible, rather than an `any` at each of the six call sites.
36 */
37export function toBindings(params: readonly unknown[]): SQLQueryBindings[] {
38 return params as SQLQueryBindings[]
39}
40
41/** A template value seen as a possible fragment; null when it is a plain bind param. */
42export function asEmbeddedValue(value: unknown): EmbeddedValue | null {
43 return value !== null && typeof value === 'object' ? value as EmbeddedValue : null
44}
45
946export interface DriverQuery {
1047 execute: () => Promise<any>
1148 values?: () => any
@@ -30,11 +67,11 @@ export type AwaitableDriverQuery = DriverQuery & PromiseLike<any> renamed throughout
3067 */
3168export interface DriverConnection {
3269 /** Tagged-template form: `` sql`SELECT 1` ``. */
33 (strings: TemplateStringsArray, ...values: any[]): DriverQuery
70 (strings: TemplateStringsArray, ...values: unknown[]): DriverQuery
3471 /** Function form for raw identifiers / value helpers: `sql('col')`. */
3572 (value: any): any
36 unsafe: (sql: string, values?: any[]) => AwaitableDriverQuery
37 query?: (sql: string, params?: any[]) => any
73 unsafe: (sql: string, values?: unknown[]) => AwaitableDriverQuery
74 query?: (sql: string, params?: unknown[]) => any
3875 close?: () => Promise<void> | void
3976 _prepareStatement?: (sql: string) => any
4077 [key: string]: any
@@ -74,11 +111,11 @@ import { applySqliteBootstrapPragmas } from './sqlite-pragmas'
74111 * numbered placeholders is returned untouched, so `?` queries and Bun's own
75112 * tagged-template output are unaffected.
76113 */
77export function bindNumberedPlaceholders(sql: string, params: any[]): { sql: string, params: any[] } {
114export function bindNumberedPlaceholders<T>(sql: string, params: T[]): { sql: string, params: T[] } {
78115 if (params.length === 0 || !/\$\d/.test(sql))
79116 return { sql, params }
80117
81 const out: any[] = []
118 const out: T[] = []
82119 let rewritten = ''
83120 let highest = 0
84121
@@ -185,7 +222,7 @@ class SQLiteWrapper {
185222 * Execute a query with parameters and return results.
186223 * This mimics the SQL tagged template literal behavior.
187224 */
188 query(sql: string, params: any[] = []): any[] {
225 query(sql: string, params: SQLQueryBindings[] = []): any[] {
189226 const bound = bindNumberedPlaceholders(sql, params)
190227 const stmt = this.db.prepare(bound.sql)
191228 return stmt.all(...bound.params)
@@ -194,7 +231,7 @@ class SQLiteWrapper {
194231 /**
195232 * Execute a query that doesn't return results (INSERT, UPDATE, DELETE).
196233 */
197 run(sql: string, params: any[] = []): any {
234 run(sql: string, params: SQLQueryBindings[] = []): any {
198235 const bound = bindNumberedPlaceholders(sql, params)
199236 const stmt = this.db.prepare(bound.sql)
200237 return stmt.run(...bound.params)
@@ -364,10 +401,10 @@ function createSQLiteSQL(filename: string): SQL { renamed throughout
364401 /**
365402 * Process a tagged template literal and return a query object
366403 */
367 function processTaggedTemplate(strings: TemplateStringsArray, ...values: any[]): any {
404 function processTaggedTemplate(strings: TemplateStringsArray, ...values: unknown[]): any {
368405 // Build the SQL string with placeholders
369406 let sql = strings[0]
370 const params: any[] = []
407 const params: unknown[] = []
371408
372409 for (let i = 0; i < values.length; i++) {
373410 const value = values[i]
@@ -396,13 +433,16 @@ function createSQLiteSQL(filename: string): SQL {
396433 // The discriminator is `values` being an ARRAY: raw markers carry
397434 // `__raw` and no `values`, and Bun's native query objects expose `values`
398435 // as a method, so neither is captured here.
399 if (value && typeof value === 'object' && typeof value.sql === 'string' && Array.isArray(value.values)) {
400 sql += value.sql + (strings[i + 1] || '')
401 params.push(...value.values)
436 const embedded = asEmbeddedValue(value)
437
438 if (embedded && typeof embedded.sql === 'string' && Array.isArray(embedded.values)) {
439 sql += embedded.sql + (strings[i + 1] || '')
440 params.push(...embedded.values)
402441 }
403442 // Handle raw SQL markers (from sql('identifier') or sql.raw())
404 else if (value && typeof value === 'object' && (value.__raw || value.raw)) {
405 const rawValue = value.__raw ? value.value : (typeof value.raw === 'string' ? value.raw : value.raw())
443 else if (embedded && (embedded.__raw || embedded.raw)) {
444 const raw = embedded.raw
445 const rawValue = embedded.__raw ? embedded.value : (typeof raw === 'string' ? raw : (raw as () => string)())
406446 sql += rawValue + (strings[i + 1] || '')
407447 }
408448 // Handle arrays - expand to placeholders
@@ -432,11 +472,11 @@ function createSQLiteSQL(filename: string): SQL {
432472 // caller silently loses its data.
433473 const trimmed = sql.trim().toUpperCase()
434474 if (trimmed.startsWith('SELECT') || trimmed.startsWith('PRAGMA') || /\bRETURNING\b/.test(trimmed)) {
435 const result = wrapper.query(sql, params)
475 const result = wrapper.query(sql, toBindings(params))
436476 return Promise.resolve(result)
437477 }
438478 else {
439 const result = wrapper.run(sql, params)
479 const result = wrapper.run(sql, toBindings(params))
440480 return Promise.resolve(result)
441481 }
442482 }
@@ -455,7 +495,7 @@ function createSQLiteSQL(filename: string): SQL {
455495 * - Tagged template literals: sql`SELECT * FROM users`
456496 * - Function calls for raw identifiers: sql('column_name')
457497 */
458 function sqlFunction(stringsOrValue: TemplateStringsArray | string, ...values: any[]): any {
498 function sqlFunction(stringsOrValue: TemplateStringsArray | string, ...values: unknown[]): any {
459499 // If called with a template literal (array of strings)
460500 if (Array.isArray(stringsOrValue) && 'raw' in stringsOrValue) {
461501 return processTaggedTemplate(stringsOrValue as TemplateStringsArray, ...values)
@@ -475,7 +515,7 @@ function createSQLiteSQL(filename: string): SQL {
475515 sqlFunction.raw = (str: string) => createRawMarker(str)
476516
477517 // Add .unsafe() method for raw SQL with parameters (like Bun's SQL.unsafe)
478 sqlFunction.unsafe = (sql: string, params: any[] = []) => {
518 sqlFunction.unsafe = (sql: string, params: unknown[] = []) => {
479519 const execute = (): Promise<any> => {
480520 try {
481521 // A RETURNING clause turns an INSERT/UPDATE/DELETE into a row producer;
@@ -483,11 +523,11 @@ function createSQLiteSQL(filename: string): SQL {
483523 // `.run()`'s { changes, lastInsertRowid }. See the tagged-template path.
484524 const trimmed = sql.trim().toUpperCase()
485525 if (trimmed.startsWith('SELECT') || trimmed.startsWith('PRAGMA') || /\bRETURNING\b/.test(trimmed)) {
486 const result = wrapper.query(sql, params)
526 const result = wrapper.query(sql, toBindings(params))
487527 return Promise.resolve(result)
488528 }
489529 else {
490 const result = wrapper.run(sql, params)
530 const result = wrapper.run(sql, toBindings(params))
491531 return Promise.resolve(result)
492532 }
493533 }
@@ -522,9 +562,9 @@ function createSQLiteSQL(filename: string): SQL {
522562 }
523563
524564 // Add .query() method for direct SQL execution
525 sqlFunction.query = (sql: string, params?: any[]) => {
565 sqlFunction.query = (sql: string, params?: unknown[]) => {
526566 try {
527 const result = wrapper.query(sql, params || [])
567 const result = wrapper.query(sql, toBindings(params || []))
528568 return Promise.resolve(result)
529569 }
530570 catch (error) {
@@ -533,7 +573,7 @@ function createSQLiteSQL(filename: string): SQL {
533573 }
534574
535575 // Add .file() method for executing SQL from a file
536 sqlFunction.file = async (filePath: string, _params?: any[]) => {
576 sqlFunction.file = async (filePath: string, _params?: unknown[]) => {
537577 try {
538578 const { readFileSync } = await import('node:fs')
539579 const sqlContent = readFileSync(filePath, 'utf-8')
packages/bun-query-builder/src/dynamodb-client.tsmodified+63-18
Changes to packages/bun-query-builder/src/dynamodb-client.ts
@@ -23,13 +23,58 @@ import type {
2323} from './drivers/dynamodb'
2424import { createDynamoDBDriver } from './drivers/dynamodb'
2525
26/**
27 * A value DynamoDB can store or compare against.
28 *
29 * DynamoDB's type system is small and closed - scalars, binary, lists and
30 * maps - so the attribute surface below says that rather than `any`. A value
31 * outside this set could never have round-tripped through the wire format.
32 */
33export type DynamoDBValue =
34 | string
35 | number
36 | boolean
37 | null
38 | Uint8Array
39 | DynamoDBValue[]
40 | { [key: string]: DynamoDBValue }
41
42/**
43 * The AWS SDK client, described by the one method this package uses.
44 *
45 * `@aws-sdk/client-dynamodb` is not a dependency (callers bring their own),
46 * so the shape is structural: anything with a `send` satisfies it, including
47 * the real `DynamoDBClient` and a test double.
48 */
49export interface DynamoDBClientLike {
50 send: (command: unknown) => Promise<unknown>
51 [key: string]: unknown
52}
53
54/** What DynamoDB reports about the capacity a call consumed. */
55export interface DynamoDBConsumedCapacity {
56 TableName?: string
57 CapacityUnits?: number
58 ReadCapacityUnits?: number
59 WriteCapacityUnits?: number
60 [key: string]: unknown
61}
62
63/** Every operator a DynamoDB filter expression accepts. */
64export type DynamoDBFilterOperator =
65 | DynamoDBComparisonOperator
66 | 'contains'
67 | 'attribute_exists'
68 | 'attribute_not_exists'
69 | 'IN'
70
2671/**
2772 * DynamoDB Query Builder Options
2873 */
2974export interface DynamoDBQueryBuilderOptions {
3075 config: DynamoDBConfig
3176 /** Optional DynamoDB client instance (e.g., from @aws-sdk/client-dynamodb) */
32 client?: any
77 client?: DynamoDBClientLike
3378}
3479
3580/**
@@ -41,7 +86,7 @@ export interface DynamoDBResult<T = any> {
4186 count?: number
4287 scannedCount?: number
4388 lastEvaluatedKey?: Record<string, any>
44 consumedCapacity?: any
89 consumedCapacity?: DynamoDBConsumedCapacity
4590}
4691
4792/**
@@ -97,7 +142,7 @@ export class DynamoDBQueryBuilder<T = any> {
97142 * Add a key condition (for Query operations)
98143 * Key conditions can only be applied to partition key and sort key
99144 */
100 whereKey(attribute: string, operator: DynamoDBComparisonOperator, value: any): this {
145 whereKey(attribute: string, operator: DynamoDBComparisonOperator, value: DynamoDBValue): this {
101146 this.keyConditions.push({ attribute, operator, value })
102147 return this
103148 }
@@ -105,14 +150,14 @@ export class DynamoDBQueryBuilder<T = any> { renamed throughout
105150 /**
106151 * Shorthand for partition key equals
107152 */
108 wherePartitionKey(attribute: string, value: any): this {
153 wherePartitionKey(attribute: string, value: DynamoDBValue): this {
109154 return this.whereKey(attribute, '=', value)
110155 }
111156
112157 /**
113158 * Shorthand for sort key equals
114159 */
115 whereSortKey(attribute: string, value: any): this {
160 whereSortKey(attribute: string, value: DynamoDBValue): this {
116161 return this.whereKey(attribute, '=', value)
117162 }
118163
@@ -127,7 +172,7 @@ export class DynamoDBQueryBuilder<T = any> {
127172 /**
128173 * Sort key between two values
129174 */
130 whereSortKeyBetween(attribute: string, start: any, end: any): this {
175 whereSortKeyBetween(attribute: string, start: DynamoDBValue, end: DynamoDBValue): this {
131176 this.keyConditions.push({ attribute, operator: 'BETWEEN', values: [start, end] })
132177 return this
133178 }
@@ -135,7 +180,7 @@ export class DynamoDBQueryBuilder<T = any> {
135180 /**
136181 * Add a filter condition (applied after Query/Scan)
137182 */
138 where(attribute: string, operator: DynamoDBComparisonOperator | 'contains' | 'attribute_exists' | 'attribute_not_exists' | 'IN', value?: any): this {
183 where(attribute: string, operator: DynamoDBFilterOperator, value?: DynamoDBValue): this {
139184 this.filterConditions.push({ attribute, operator, value })
140185 return this
141186 }
@@ -143,42 +188,42 @@ export class DynamoDBQueryBuilder<T = any> {
143188 /**
144189 * Filter where attribute equals value
145190 */
146 whereEquals(attribute: string, value: any): this {
191 whereEquals(attribute: string, value: DynamoDBValue): this {
147192 return this.where(attribute, '=', value)
148193 }
149194
150195 /**
151196 * Filter where attribute is less than value
152197 */
153 whereLessThan(attribute: string, value: any): this {
198 whereLessThan(attribute: string, value: DynamoDBValue): this {
154199 return this.where(attribute, '<', value)
155200 }
156201
157202 /**
158203 * Filter where attribute is less than or equal to value
159204 */
160 whereLessThanOrEqual(attribute: string, value: any): this {
205 whereLessThanOrEqual(attribute: string, value: DynamoDBValue): this {
161206 return this.where(attribute, '<=', value)
162207 }
163208
164209 /**
165210 * Filter where attribute is greater than value
166211 */
167 whereGreaterThan(attribute: string, value: any): this {
212 whereGreaterThan(attribute: string, value: DynamoDBValue): this {
168213 return this.where(attribute, '>', value)
169214 }
170215
171216 /**
172217 * Filter where attribute is greater than or equal to value
173218 */
174 whereGreaterThanOrEqual(attribute: string, value: any): this {
219 whereGreaterThanOrEqual(attribute: string, value: DynamoDBValue): this {
175220 return this.where(attribute, '>=', value)
176221 }
177222
178223 /**
179224 * Filter where attribute is between two values
180225 */
181 whereBetween(attribute: string, start: any, end: any): this {
226 whereBetween(attribute: string, start: DynamoDBValue, end: DynamoDBValue): this {
182227 this.filterConditions.push({ attribute, operator: 'BETWEEN', values: [start, end] })
183228 return this
184229 }
@@ -194,7 +239,7 @@ export class DynamoDBQueryBuilder<T = any> {
194239 /**
195240 * Filter where attribute contains value (for strings and sets)
196241 */
197 whereContains(attribute: string, value: any): this {
242 whereContains(attribute: string, value: DynamoDBValue): this {
198243 return this.where(attribute, 'contains', value)
199244 }
200245
@@ -215,7 +260,7 @@ export class DynamoDBQueryBuilder<T = any> {
215260 /**
216261 * Filter where attribute is in a list of values
217262 */
218 whereIn(attribute: string, values: any[]): this {
263 whereIn(attribute: string, values: DynamoDBValue[]): this {
219264 this.filterConditions.push({ attribute, operator: 'IN', values })
220265 return this
221266 }
@@ -633,7 +678,7 @@ export class DynamoDBItemBuilder<T = any> {
633678 /**
634679 * Set attribute value (for Update)
635680 */
636 set(attribute: string, value: any): this {
681 set(attribute: string, value: DynamoDBValue): this {
637682 if (!this.updateExpressions.set) {
638683 this.updateExpressions.set = {}
639684 }
@@ -666,7 +711,7 @@ export class DynamoDBItemBuilder<T = any> {
666711 /**
667712 * Add to a number or set (for Update)
668713 */
669 add(attribute: string, value: any): this {
714 add(attribute: string, value: DynamoDBValue): this {
670715 if (!this.updateExpressions.add) {
671716 this.updateExpressions.add = {}
672717 }
@@ -677,7 +722,7 @@ export class DynamoDBItemBuilder<T = any> {
677722 /**
678723 * Delete from a set (for Update)
679724 */
680 deleteFromSet(attribute: string, values: any): this {
725 deleteFromSet(attribute: string, values: DynamoDBValue): this {
681726 if (!this.updateExpressions.delete) {
682727 this.updateExpressions.delete = {}
683728 }
packages/bun-query-builder/src/dynamodb-single-table.tsmodified+2-2
Changes to packages/bun-query-builder/src/dynamodb-single-table.ts
@@ -20,7 +20,7 @@ import type {
2020 SingleTableEntityMapping,
2121} from './drivers/dynamodb'
2222import { createDynamoDBDriver } from './drivers/dynamodb'
23import type { DynamoDBQueryBuilderOptions } from './dynamodb-client'
23import type { DynamoDBFilterOperator, DynamoDBQueryBuilderOptions, DynamoDBValue } from './dynamodb-client'
2424import { DynamoDBItemBuilder, DynamoDBQueryBuilder } from './dynamodb-client'
2525
2626/**
@@ -498,7 +498,7 @@ export class SingleTableQueryBuilder<T> {
498498 /**
499499 * Add a filter condition
500500 */
501 where(attribute: string, operator: any, value?: any): this {
501 where(attribute: string, operator: DynamoDBFilterOperator, value?: DynamoDBValue): this {
502502 this.builder.where(attribute, operator, value)
503503 return this
504504 }
packages/bun-query-builder/src/dynamodb-tooling-adapter.tsmodified+7-7
Changes to packages/bun-query-builder/src/dynamodb-tooling-adapter.ts
@@ -41,7 +41,7 @@ export interface ParsedAttribute {
4141 nullable: boolean
4242 unique: boolean
4343 hidden: boolean
44 defaultValue?: any
44 defaultValue?: unknown
4545 cast?: string
4646 dynamoDbType: 'S' | 'N' | 'B' | 'BOOL' | 'NULL' | 'M' | 'L' | 'SS' | 'NS' | 'BS'
4747}
@@ -109,7 +109,7 @@ export interface StacksModel {
109109 nullable?: boolean
110110 unique?: boolean
111111 hidden?: boolean
112 default?: any
112 default?: unknown
113113 cast?: string
114114 validation?: { rule?: string }
115115 }>
@@ -150,7 +150,7 @@ export function resolveKeyPattern(keyPatterns: KeyPatterns, data: Record<string,
150150/**
151151 * Parse models from configuration (stub implementation)
152152 */
153export async function parseStacksModels(_config: any): Promise<{ models: Map<string, ParsedModel> }> {
153export async function parseStacksModels(_config: unknown): Promise<{ models: Map<string, ParsedModel> }> {
154154 // This is a stub - actual implementation would parse model files
155155 return { models: new Map() }
156156}
@@ -186,8 +186,8 @@ export function unmarshallItem(item: Record<string, any>): Record<string, any> {
186186// ============================================================================
187187// Imports from bun-query-builder (fluent API layer)
188188// ============================================================================
189import type { DynamoDBConfig, DynamoDBDriver, SingleTableEntityMapping } from './drivers/dynamodb'
190import type { DynamoDBQueryBuilderOptions } from './dynamodb-client'
189import type { DynamoDBConfig, DynamoDBDriver, DynamoDBTableDefinition, SingleTableEntityMapping } from './drivers/dynamodb'
190import type { DynamoDBClientLike, DynamoDBQueryBuilderOptions } from './dynamodb-client'
191191import type { SingleTableConfig, SingleTableEntity } from './dynamodb-single-table'
192192import { createDynamoDBDriver } from './drivers/dynamodb'
193193import { DynamoDBItemBuilder, DynamoDBQueryBuilder } from './dynamodb-client'
@@ -282,7 +282,7 @@ export class DynamoDBToolingAdapter {
282282 /**
283283 * Set the DynamoDB client instance
284284 */
285 setClient(client: any): this {
285 setClient(client: DynamoDBClientLike): this {
286286 this.queryBuilderOptions.client = client
287287 return this
288288 }
@@ -498,7 +498,7 @@ export class DynamoDBToolingAdapter {
498498 /**
499499 * Generate table definition for all registered models
500500 */
501 generateTableDefinition(): any {
501 generateTableDefinition(): DynamoDBTableDefinition {
502502 return this.singleTableManager.generateTableDefinition()
503503 }
504504
packages/bun-query-builder/src/meta.tsmodified+9-1
Changes to packages/bun-query-builder/src/meta.ts
@@ -24,7 +24,15 @@ export interface SchemaMeta {
2424 morphToMany?: Record<string, string>
2525 morphedByMany?: Record<string, string>
2626 }>
27 scopes?: Record<string, Record<string, (qb: any, value?: any) => any>>
27 /**
28 * Named query scopes, keyed by model then by scope name.
29 *
30 * The builder parameter stays `any`: a scope receives the fully-typed
31 * builder for its own model, which this erased metadata table cannot name
32 * without making every model's scopes mutually unassignable. The value and
33 * the return are `unknown`, so nothing downstream inherits the looseness.
34 */
35 scopes?: Record<string, Record<string, (qb: any, value?: unknown) => unknown>>
2836 /**
2937 * Original models record passed to `buildSchemaMeta`, retained so downstream
3038 * consumers (e.g. the pivot resolver) can read through-model attributes
packages/bun-query-builder/src/orm.tsmodified+26-4
Changes to packages/bun-query-builder/src/orm.ts
@@ -286,8 +286,8 @@ export interface ModelDefinition {
286286 readonly attributes: {
287287 readonly [key: string]: TypedAttribute<unknown>
288288 }
289 readonly get?: Record<string, (attributes: any) => unknown>
290 readonly set?: Record<string, (attributes: any) => unknown>
289 readonly get?: Record<string, (attributes: Record<string, unknown>) => unknown>
290 readonly set?: Record<string, (attributes: Record<string, unknown>) => unknown>
291291 readonly scopes?: Record<string, (value: unknown) => unknown>
292292 readonly indexes?: readonly object[]
293293 readonly dashboard?: { readonly enabled?: boolean; readonly highlight?: boolean | number }
@@ -617,7 +617,29 @@ function toPostgresPlaceholders(sql: string): string {
617617 * fallback (which would be 0) — see stacksjs/bun-query-builder#1032. MySQL
618618 * surfaces `affectedRows` instead.
619619 */
620export function extractChanges(res: any): number {
620/**
621 * What a driver hands back after a write.
622 *
623 * Each one reports the affected count under its own name - and Postgres
624 * returns an empty array carrying `count`, so the shape is genuinely a union
625 * rather than one thing. Naming the fields beats `any`: a caller reading this
626 * type learns which drivers report what.
627 */
628export interface WriteResultLike {
629 /** MySQL. */
630 affectedRows?: number
631 /** Postgres command tag. */
632 count?: number
633 /** SQLite (bun:sqlite). */
634 changes?: number
635 lastInsertRowid?: number | bigint
636 insertId?: number | bigint
637 command?: string
638 rows?: unknown[]
639 [key: string]: unknown
640}
641
642export function extractChanges(res: WriteResultLike | null | undefined): number {
621643 if (res == null)
622644 return 0
623645 if (typeof res.affectedRows === 'number') // MySQL
@@ -630,7 +652,7 @@ export function extractChanges(res: any): number {
630652}
631653
632654/** Extract a generated primary key from a Bun SQL driver result. */
633export function extractInsertId(res: any): number | bigint | null {
655export function extractInsertId(res: WriteResultLike | null | undefined): number | bigint | null {
634656 if (res == null || typeof res !== 'object')
635657 return null
636658 if ('insertId' in res && res.insertId != null) // MySQL
packages/bun-query-builder/src/schema.tsmodified+28-3
Changes to packages/bun-query-builder/src/schema.ts
@@ -221,14 +221,23 @@ export interface ModelOptions extends Base {
221221 morphTo?: MorphTo
222222 morphToMany?: MorphToMany<ModelNames>
223223 morphedByMany?: MorphedByMany<ModelNames>
224 /**
225 * Scopes, accessors and mutators a model declares.
226 *
227 * The parameter stays `any` on purpose. A real model writes
228 * `get: { name: (value: string) => value.toUpperCase() }`, and under
229 * `strictFunctionTypes` that is NOT assignable to `(value: unknown) => ...`
230 * - narrowing here would reject every correctly-written model. The RETURN
231 * is `unknown`, so nothing downstream inherits the looseness.
232 */
224233 scopes?: {
225 [key: string]: (value: any) => any
234 [key: string]: (value: any) => unknown
226235 }
227236 get?: {
228 [key: string]: (value: any) => any
237 [key: string]: (value: any) => unknown
229238 }
230239 set?: {
231 [key: string]: (value: any) => any
240 [key: string]: (value: any) => unknown
232241 }
233242}
234243
@@ -410,6 +419,22 @@ export type InferTableRelations<M, MRecord extends ModelRecord> = {
410419 * type Schema = DatabaseSchema<typeof models>
411420 * ```
412421 */
422/**
423 * Any database schema, described by its shape rather than by `any`.
424 *
425 * Generic constraints throughout the client read `DB extends DatabaseSchema<any>`,
426 * which says "a schema of anything" and drags `any` into every published
427 * signature that mentions one. This says the same thing accurately: a schema
428 * is tables, each with columns, a primary key, and optionally relations.
429 */
430export interface AnySchemaTable {
431 columns: Record<string, unknown>
432 primaryKey?: unknown
433 relations?: Record<string, string> | undefined
434}
435
436export type AnyDatabaseSchema = Record<string, AnySchemaTable>
437
413438export type DatabaseSchema<MRecord extends ModelRecord> = {
414439 [MName in keyof MRecord & string as InferTableName<UnwrapModelDefinition<MRecord[MName]>>]: {
415440 columns: InferAttributes<UnwrapModelDefinition<MRecord[MName]>>
packages/bun-query-builder/src/seeder.tsmodified+12-1
Changes to packages/bun-query-builder/src/seeder.ts
@@ -1,3 +1,14 @@
1/**
2 * The query builder handed to a seeder.
3 *
4 * A seeder is written against the app's own schema, which this base class
5 * cannot know, so the handle stays `any` deliberately - narrowing it here
6 * would reject every real seeder's typed table access. Named rather than
7 * inline so the reason travels with it.
8 */
9// eslint-disable-next-line ts/no-explicit-any
10export type SeederQueryBuilder = any
11
112/**
213 * Base seeder class that all seeders should extend.
314 * Provides access to query builder and faker for generating test data.
@@ -9,7 +20,7 @@ export abstract class Seeder {
920 *
1021 * @param qb - The query builder instance to use for database operations
1122 */
12 abstract run(qb: any): Promise<void>
23 abstract run(qb: SeederQueryBuilder): Promise<void>
1324
1425 /**
1526 * Optional method to specify the order in which seeders should run.
packages/bun-query-builder/src/sql-fragments.tsmodified+1-1
Changes to packages/bun-query-builder/src/sql-fragments.ts
@@ -93,7 +93,7 @@ export function renderWhereTerms(terms: readonly WhereTerm[]): string {
9393 * happened to come back empty — the same shape of silent, widening-or-narrowing
9494 * failure as the `IN ()` bug itself.
9595 */
96export function renderInPredicate(column: string, values: any[], negated: boolean, placeholders: string): string {
96export function renderInPredicate(column: string, values: readonly unknown[], negated: boolean, placeholders: string): string {
9797 if (values.length === 0)
9898 return negated ? TRUE_PREDICATE : FALSE_PREDICATE
9999 return `${column} ${negated ? 'NOT IN' : 'IN'} (${placeholders})`
packages/bun-query-builder/src/types.tsmodified+34-14
Changes to packages/bun-query-builder/src/types.ts
@@ -240,15 +240,34 @@ export interface SqlConfig {
240240 * Optional lifecycle hooks around query execution. These are invoked for any
241241 * statement executed through the builder (select/insert/update/delete/raw).
242242 */
243/** A bound statement parameter: whatever a driver will accept as a value. */
244export type QueryParam = string | number | bigint | boolean | Date | Uint8Array | null
245
246/** The kind of statement a hook is reporting on. */
247export type QueryKind = 'select' | 'insert' | 'update' | 'delete' | 'raw'
248
249/** The row shape a hook sees. Columns are unknown until the caller narrows. */
250export type QueryRecord = Record<string, unknown>
251
252/** A `where` description as the mutation hooks receive it. */
253export type QueryWhere = Record<string, unknown>
254
255/** Common fields on every query lifecycle event. */
256export interface QueryEvent {
257 sql: string
258 params?: QueryParam[]
259 kind?: QueryKind
260}
261
243262export interface QueryHooks {
244263 /** Called right before a query executes. */
245 onQueryStart?: (event: { sql: string, params?: any[], kind?: 'select' | 'insert' | 'update' | 'delete' | 'raw' }) => void
264 onQueryStart?: (event: QueryEvent) => void
246265 /** Called after a query succeeds. */
247 onQueryEnd?: (event: { sql: string, params?: any[], durationMs: number, rowCount?: number, kind?: 'select' | 'insert' | 'update' | 'delete' | 'raw' }) => void
266 onQueryEnd?: (event: QueryEvent & { durationMs: number, rowCount?: number }) => void
248267 /** Called after a query fails. */
249 onQueryError?: (event: { sql: string, params?: any[], error: any, durationMs: number, kind?: 'select' | 'insert' | 'update' | 'delete' | 'raw' }) => void
268 onQueryError?: (event: QueryEvent & { error: unknown, durationMs: number }) => void
250269 /** Optional tracer integration. Return an object with end() to finish a span. */
251 startSpan?: (event: { sql: string, params?: any[], kind?: 'select' | 'insert' | 'update' | 'delete' | 'raw' }) => { end: (error?: any) => void }
270 startSpan?: (event: QueryEvent) => { end: (error?: unknown) => void }
252271 /**
253272 * When set, a query whose duration meets/exceeds this many milliseconds is
254273 * reported as slow (via `onSlowQuery`, or a `console.warn` if no handler is
@@ -256,19 +275,19 @@ export interface QueryHooks {
256275 */
257276 slowQueryThresholdMs?: number
258277 /** Called when a query's duration meets/exceeds `slowQueryThresholdMs`. */
259 onSlowQuery?: (event: { sql: string, params?: any[], durationMs: number, kind?: 'select' | 'insert' | 'update' | 'delete' | 'raw' }) => void
278 onSlowQuery?: (event: QueryEvent & { durationMs: number }) => void
260279 /** Called before creating a record. Can modify data or throw to prevent creation. */
261 beforeCreate?: (event: { table: string, data: any }) => void | Promise<void>
280 beforeCreate?: (event: { table: string, data: QueryRecord }) => void | Promise<void>
262281 /** Called after creating a record. */
263 afterCreate?: (event: { table: string, data: any, result: any }) => void | Promise<void>
282 afterCreate?: (event: { table: string, data: QueryRecord, result: unknown }) => void | Promise<void>
264283 /** Called before updating a record. Can modify data or throw to prevent update. */
265 beforeUpdate?: (event: { table: string, data: any, where?: any }) => void | Promise<void>
284 beforeUpdate?: (event: { table: string, data: QueryRecord, where?: QueryWhere }) => void | Promise<void>
266285 /** Called after updating a record. */
267 afterUpdate?: (event: { table: string, data: any, where?: any, result: any }) => void | Promise<void>
286 afterUpdate?: (event: { table: string, data: QueryRecord, where?: QueryWhere, result: unknown }) => void | Promise<void>
268287 /** Called before deleting a record. Can throw to prevent deletion. */
269 beforeDelete?: (event: { table: string, where?: any }) => void | Promise<void>
288 beforeDelete?: (event: { table: string, where?: QueryWhere }) => void | Promise<void>
270289 /** Called after deleting a record. */
271 afterDelete?: (event: { table: string, where?: any, result: any }) => void | Promise<void>
290 afterDelete?: (event: { table: string, where?: QueryWhere, result: unknown }) => void | Promise<void>
272291}
273292
274293/**
@@ -362,7 +381,7 @@ export interface BrowserConfig {
362381 /** Request timeout in milliseconds (default: 30000) */
363382 timeout?: number
364383 /** Transform response data before returning (e.g., unwrap { data: [...] }) */
365 transformResponse?: <T>(response: any) => T
384 transformResponse?: <T>(response: unknown) => T
366385 /** Transform request data before sending */
367386 transformRequest?: <T>(data: T) => any
368387}
@@ -483,7 +502,7 @@ export interface QueryBuilderConfig {
483502 * signature is not a property. `keyof (() => void)` is `never`, so the
484503 * hook collapses to an empty object type: no longer callable, and loose
485504 * enough to accept `42`. It also erases type parameters, which would turn
486 * `BrowserConfig.transformResponse` (`<T>(response: any) => T`) into a
505 * `BrowserConfig.transformResponse` (`<T>(response: unknown) => T`) into a
487506 * shape with no signature at all. A hook or a token getter is a leaf.
488507 *
489508 * 2. ARRAYS SECOND, because the merge replaces them rather than merging them.
@@ -603,7 +622,8 @@ export interface GenerateMigrationResult {
603622 sql: string
604623 sqlStatements: string[]
605624 hasChanges: boolean
606 plan: any
625 /** The diff the SQL was generated from, as `migrations` describes it. */
626 plan: import('./migrations').MigrationPlan
607627 /**
608628 * Structured description of each change (drop/rename/modify/rebuild/...), so
609629 * callers can gate destructive ops and report renames without parsing SQL.
packages/bun-query-builder/test/public-surface-types.test.tsadded+66-0
Changes to packages/bun-query-builder/test/public-surface-types.test.ts
@@ -0,0 +1,66 @@
1import { describe, expect, it } from 'bun:test'
2import { existsSync, readdirSync, readFileSync } from 'node:fs'
3import { join } from 'node:path'
4
5/**
6 * A guard on what the published types actually say.
7 *
8 * `any` in a `.d.ts` is not a local shortcut - it is a hole punched through
9 * every consumer's typechecker. An app that writes `db.selectFrom('users')`
10 * gets no help from the compiler if the builder it received was typed `any`
11 * three files upstream, and that is exactly how the ORM ended up reporting
12 * models as `any` in installed Stacks apps.
13 *
14 * So the count is pinned. The survivors are deliberate and each carries its
15 * reason in the source:
16 *
17 * - driver escape hatches in `db.ts` (`execute(): Promise<any>`, the
18 * `[key: string]: any` index signatures) - narrowing them would force a
19 * cast at every `await sql.unsafe(...)` call site, which is the same hole
20 * moved somewhere less visible
21 * - `(...args: any[]) => any` in `DeepPartial` - the standard "is this a
22 * function" idiom; `unknown[]` does not match under contravariance
23 * - accessor/mutator parameters in `schema.ts` - a model's own
24 * `(value: string) => ...` would be REJECTED against `unknown`
25 * - the faker proxy's dynamic members
26 *
27 * Adding one means either a real justification in a comment next to it, or a
28 * better type. Raising this number without the former is the regression.
29 */
30
31const DIST = join(import.meta.dir, '..', 'dist')
32const BASELINE = 18
33
34function anyCount(source: string): number {
35 return (source.match(/:\s*any\b|<any>|any\[\]/g) ?? []).length
36}
37
38describe('the published type surface', () => {
39 it('does not leak more `any` than the documented survivors', () => {
40 if (!existsSync(DIST)) {
41 // Types are a build output; nothing to check before `bun run build`.
42 return
43 }
44
45 const declarations = readdirSync(DIST).filter(file => file.endsWith('.d.ts'))
46 expect(declarations.length).toBeGreaterThan(0)
47
48 const total = declarations.reduce(
49 (sum, file) => sum + anyCount(readFileSync(join(DIST, file), 'utf8')),
50 0,
51 )
52
53 expect(total).toBeLessThanOrEqual(BASELINE)
54 })
55
56 it('keeps the query builder itself free of them', () => {
57 if (!existsSync(join(DIST, 'client.d.ts')))
58 return
59
60 // client.d.ts is the type an app touches on every single query. It went
61 // from 29 to 0 by naming what a schema IS (`AnyDatabaseSchema`) instead
62 // of writing `DatabaseSchema<any>` in 21 generic constraints.
63 expect(anyCount(readFileSync(join(DIST, 'client.d.ts'), 'utf8'))).toBe(0)
64 expect(anyCount(readFileSync(join(DIST, 'orm.d.ts'), 'utf8'))).toBe(0)
65 })
66})