also looking at this
refactor(types): stop publishing `any` across the query builder's surface
#1117
13 files
+359
-110
| @@ -372,7 +372,9 @@ export class BrowserQueryBuilder<T = any> { | ||
| 372 | 372 | /** |
| 373 | 373 | * Add a where clause |
| 374 | 374 | */ |
| 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 { | |
| 376 | 378 | if (value === undefined) { |
| 377 | 379 | // where('column', value) shorthand for equality |
| 378 | 380 | this.state.wheres.push({ |
| @@ -397,7 +399,9 @@ export class BrowserQueryBuilder<T = any> { | ||
| 397 | 399 | /** |
| 398 | 400 | * Add an OR where clause |
| 399 | 401 | */ |
| 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 { | |
| 401 | 405 | if (value === undefined) { |
| 402 | 406 | this.state.wheres.push({ |
| 403 | 407 | column, |
| @@ -420,8 +424,15 @@ export class BrowserQueryBuilder<T = any> { | ||
| 420 | 424 | /** |
| 421 | 425 | * Add an AND where clause (alias for where) |
| 422 | 426 | */ |
| 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) | |
| 425 | 436 | } |
| 426 | 437 | |
| 427 | 438 | /** |
| @@ -441,14 +452,14 @@ export class BrowserQueryBuilder<T = any> { | ||
| 441 | 452 | /** |
| 442 | 453 | * Where column is in array |
| 443 | 454 | */ |
| 444 | whereIn(column: string, values: any[]): this { | |
| 455 | whereIn(column: string, values: readonly unknown[]): this { | |
| 445 | 456 | return this.where(column, 'in', values) |
| 446 | 457 | } |
| 447 | 458 | |
| 448 | 459 | /** |
| 449 | 460 | * Where column is not in array |
| 450 | 461 | */ |
| 451 | whereNotIn(column: string, values: any[]): this { | |
| 462 | whereNotIn(column: string, values: readonly unknown[]): this { | |
| 452 | 463 | return this.where(column, 'not in', values) |
| 453 | 464 | } |
| 454 | 465 | |
| @@ -3,7 +3,7 @@ | ||
| 3 | 3 | /* eslint-disable no-useless-catch */ |
| 4 | 4 | import type { SchemaMeta } from './meta' |
| 5 | 5 | import type { ResolvedPivot } from './pivot' |
| 6 | import type { DatabaseSchema } from './schema' | |
| 6 | import type { DatabaseSchema, AnyDatabaseSchema } from './schema' | |
| 7 | 7 | import type { QueryBuilderOptions, QueryHooks, SupportedDialect} from './types' |
| 8 | 8 | import { config, getPlaceholder, getPlaceholders, isMysqlLike, setConfig } from './config' |
| 9 | 9 | import type { DriverConnection } from './db' |
| @@ -400,7 +400,7 @@ class QueryCache { | ||
| 400 | 400 | private cache = new Map<string, CacheEntry>() |
| 401 | 401 | private maxSize = 100 |
| 402 | 402 | |
| 403 | get(key: string): any | null { | |
| 403 | get<T = unknown>(key: string): T | null { | |
| 404 | 404 | const entry = this.cache.get(key) |
| 405 | 405 | if (!entry) |
| 406 | 406 | return null |
| @@ -420,7 +420,7 @@ class QueryCache { | ||
| 420 | 420 | return entry.data |
| 421 | 421 | } |
| 422 | 422 | |
| 423 | set(key: string, data: any, ttlMs: number): void { | |
| 423 | set(key: string, data: unknown, ttlMs: number): void { | |
| 424 | 424 | // Re-inserted keys must move to the back or they'd be evicted as if old. |
| 425 | 425 | if (this.cache.has(key)) |
| 426 | 426 | this.cache.delete(key) |
| @@ -526,15 +526,15 @@ export type SortDirection = 'asc' | 'desc' | ||
| 526 | 526 | * |
| 527 | 527 | * Helper type extracting a string union of column names for a given table. |
| 528 | 528 | */ |
| 529 | export type ColumnName<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> = keyof DB[TTable]['columns'] & string | |
| 529 | export type ColumnName<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> = keyof DB[TTable]['columns'] & string | |
| 530 | 530 | // Named row alias to improve IDE hover readability |
| 531 | 531 | export type SelectedRow< |
| 532 | DB extends DatabaseSchema<any>, | |
| 532 | DB extends AnyDatabaseSchema, | |
| 533 | 533 | _TTable extends keyof DB & string, |
| 534 | 534 | TSelected, |
| 535 | 535 | > = Readonly<TSelected> |
| 536 | 536 | |
| 537 | type PrimaryKeyValue<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> = | |
| 537 | type PrimaryKeyValue<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> = | |
| 538 | 538 | DB[TTable]['primaryKey'] extends keyof DB[TTable]['columns'] |
| 539 | 539 | ? DB[TTable]['columns'][DB[TTable]['primaryKey']] |
| 540 | 540 | : unknown |
| @@ -545,7 +545,7 @@ type NumericColumnName<Columns> = { | ||
| 545 | 545 | : Exclude<Columns[K], null | undefined> extends number ? K : never |
| 546 | 546 | }[keyof Columns & string] |
| 547 | 547 | |
| 548 | type JoinColumn<DB extends DatabaseSchema<any>, TTables extends string> = TTables extends any | |
| 548 | type JoinColumn<DB extends AnyDatabaseSchema, TTables extends string> = TTables extends any | |
| 549 | 549 | ? `${TTables}.${keyof DB[TTables]['columns'] & string}` |
| 550 | 550 | : never |
| 551 | 551 | |
| @@ -559,13 +559,13 @@ type JoinColumn<DB extends DatabaseSchema<any>, TTables extends string> = TTable | ||
| 559 | 559 | * untyped schemas keep compiling. A table that declares ZERO relations |
| 560 | 560 | * yields `never` — every relation name is rejected. |
| 561 | 561 | */ |
| 562 | export type TableRelationName<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> = | |
| 562 | export type TableRelationName<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> = | |
| 563 | 563 | DB[TTable] extends { relations?: infer R } |
| 564 | 564 | ? unknown extends R ? string : keyof NonNullable<R> & string |
| 565 | 565 | : string |
| 566 | 566 | |
| 567 | 567 | type RelatedTableName< |
| 568 | DB extends DatabaseSchema<any>, | |
| 568 | DB extends AnyDatabaseSchema, | |
| 569 | 569 | TTable extends keyof DB & string, |
| 570 | 570 | TRelation extends TableRelationName<DB, TTable>, |
| 571 | 571 | > = DB[TTable] extends { relations?: infer Relations } |
| @@ -575,7 +575,7 @@ type RelatedTableName< | ||
| 575 | 575 | : keyof DB & string |
| 576 | 576 | |
| 577 | 577 | type RelationQueryBuilder< |
| 578 | DB extends DatabaseSchema<any>, | |
| 578 | DB extends AnyDatabaseSchema, | |
| 579 | 579 | TTable extends keyof DB & string, |
| 580 | 580 | TRelation extends TableRelationName<DB, TTable>, |
| 581 | 581 | > = SelectQueryBuilder< |
| @@ -584,7 +584,7 @@ type RelationQueryBuilder< | ||
| 584 | 584 | DB[RelatedTableName<DB, TTable, TRelation>]['columns'] |
| 585 | 585 | > |
| 586 | 586 | |
| 587 | type RelationConstraintRecord<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> = { | |
| 587 | type RelationConstraintRecord<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> = { | |
| 588 | 588 | [R in TableRelationName<DB, TTable>]: Partial<Record<R, (qb: RelationQueryBuilder<DB, TTable, R>) => unknown>> |
| 589 | 589 | }[TableRelationName<DB, TTable>] |
| 590 | 590 | |
| @@ -597,7 +597,7 @@ type RelationConstraintRecord<DB extends DatabaseSchema<any>, TTable extends key | ||
| 597 | 597 | * relations accepts nothing (the bare `Partial<Record<never, ...>>` would be |
| 598 | 598 | * `{}`, which strings are assignable to — hence the explicit never guard). |
| 599 | 599 | */ |
| 600 | export type WithRelationArg<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> = | |
| 600 | export type WithRelationArg<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> = | |
| 601 | 601 | [TableRelationName<DB, TTable>] extends [never] |
| 602 | 602 | ? never |
| 603 | 603 | : |
| @@ -614,7 +614,7 @@ type SnakeToPascal<S extends string> = S extends `${infer H}_${infer T}` | ||
| 614 | 614 | // thread a phantom TSql string through method signatures so hovers can show the |
| 615 | 615 | // composed SQL at compile-time for common operations. |
| 616 | 616 | type _TypedDynamicWhereMethods< |
| 617 | DB extends DatabaseSchema<any>, | |
| 617 | DB extends AnyDatabaseSchema, | |
| 618 | 618 | TTable extends keyof DB & string, |
| 619 | 619 | TSelected, |
| 620 | 620 | TJoined extends string, |
| @@ -639,7 +639,7 @@ type _TypedDynamicWhereMethods< | ||
| 639 | 639 | // resolution, silently downgrading `toSQL()` from the composed literal SQL |
| 640 | 640 | // type back to `string` after any dynamic-where call. |
| 641 | 641 | export type TypedSelectQueryBuilder< |
| 642 | DB extends DatabaseSchema<any>, | |
| 642 | DB extends AnyDatabaseSchema, | |
| 643 | 643 | TTable extends keyof DB & string, |
| 644 | 644 | TSelected, |
| 645 | 645 | TJoined extends string = TTable, |
| @@ -685,7 +685,7 @@ export type TypedSelectQueryBuilder< | ||
| 685 | 685 | } |
| 686 | 686 | |
| 687 | 687 | type DynamicWhereMethods< |
| 688 | DB extends DatabaseSchema<any>, | |
| 688 | DB extends AnyDatabaseSchema, | |
| 689 | 689 | TTable extends keyof DB & string, |
| 690 | 690 | TSelected, |
| 691 | 691 | TJoined extends string = TTable, |
| @@ -698,7 +698,7 @@ type DynamicWhereMethods< | ||
| 698 | 698 | } |
| 699 | 699 | |
| 700 | 700 | export interface BaseSelectQueryBuilder< |
| 701 | DB extends DatabaseSchema<any>, | |
| 701 | DB extends AnyDatabaseSchema, | |
| 702 | 702 | TTable extends keyof DB & string, |
| 703 | 703 | TSelected, |
| 704 | 704 | TJoined extends string = TTable, |
| @@ -926,7 +926,7 @@ export interface BaseSelectQueryBuilder< | ||
| 926 | 926 | /** OR-connected counterpart of `whereNotNull`. */ |
| 927 | 927 | orWhereNotNull: (column: keyof DB[TTable]['columns'] & string) => SelectQueryBuilder<DB, TTable, TSelected, TJoined> |
| 928 | 928 | /** 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> | |
| 930 | 930 | /** OR-connected counterpart of `whereExists`. */ |
| 931 | 931 | orWhereExists: (subquery: { toSQL: () => any }) => SelectQueryBuilder<DB, TTable, TSelected, TJoined> |
| 932 | 932 | /** |
| @@ -1906,13 +1906,13 @@ export interface BaseSelectQueryBuilder< | ||
| 1906 | 1906 | } |
| 1907 | 1907 | |
| 1908 | 1908 | export type SelectQueryBuilder< |
| 1909 | DB extends DatabaseSchema<any>, | |
| 1909 | DB extends AnyDatabaseSchema, | |
| 1910 | 1910 | TTable extends keyof DB & string, |
| 1911 | 1911 | TSelected, |
| 1912 | 1912 | TJoined extends string = TTable, |
| 1913 | 1913 | > = BaseSelectQueryBuilder<DB, TTable, TSelected, TJoined> & DynamicWhereMethods<DB, TTable, TSelected, TJoined> |
| 1914 | 1914 | |
| 1915 | export interface InsertQueryBuilder<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> { | |
| 1915 | export interface InsertQueryBuilder<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> { | |
| 1916 | 1916 | /** |
| 1917 | 1917 | * # `values` |
| 1918 | 1918 | * |
| @@ -1965,7 +1965,7 @@ export interface InsertQueryBuilder<DB extends DatabaseSchema<any>, TTable exten | ||
| 1965 | 1965 | executeTakeFirstOrThrow: () => Promise<DB[TTable]['columns']> |
| 1966 | 1966 | } |
| 1967 | 1967 | |
| 1968 | export interface UpdateQueryBuilder<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> { | |
| 1968 | export interface UpdateQueryBuilder<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> { | |
| 1969 | 1969 | /** |
| 1970 | 1970 | * # `set` |
| 1971 | 1971 | * |
| @@ -2064,7 +2064,7 @@ export interface UpdateQueryBuilder<DB extends DatabaseSchema<any>, TTable exten | ||
| 2064 | 2064 | executeTakeFirstOrThrow: () => Promise<{ numUpdatedRows: number }> |
| 2065 | 2065 | } |
| 2066 | 2066 | |
| 2067 | export interface DeleteQueryBuilder<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> { | |
| 2067 | export interface DeleteQueryBuilder<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> { | |
| 2068 | 2068 | /** |
| 2069 | 2069 | * # `where` |
| 2070 | 2070 | * |
| @@ -2144,7 +2144,7 @@ export interface DeleteQueryBuilder<DB extends DatabaseSchema<any>, TTable exten | ||
| 2144 | 2144 | executeTakeFirstOrThrow: () => Promise<{ numDeletedRows: number }> |
| 2145 | 2145 | } |
| 2146 | 2146 | |
| 2147 | export interface TableQueryBuilder<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> { | |
| 2147 | export interface TableQueryBuilder<DB extends AnyDatabaseSchema, TTable extends keyof DB & string> { | |
| 2148 | 2148 | /** |
| 2149 | 2149 | * # `insert` |
| 2150 | 2150 | * |
| @@ -2192,7 +2192,7 @@ export interface TableQueryBuilder<DB extends DatabaseSchema<any>, TTable extend | ||
| 2192 | 2192 | select: <K extends keyof DB[TTable]['columns'] & string>(...columns: K[]) => SelectQueryBuilder<DB, TTable, Pick<DB[TTable]['columns'], K>> |
| 2193 | 2193 | } |
| 2194 | 2194 | |
| 2195 | export interface QueryBuilder<DB extends DatabaseSchema<any>> { | |
| 2195 | export interface QueryBuilder<DB extends AnyDatabaseSchema> { | |
| 2196 | 2196 | // typed select list (column names or raw aliases) |
| 2197 | 2197 | /** |
| 2198 | 2198 | * # `select` |
| @@ -2660,7 +2660,7 @@ finally { reserved.release() } | ||
| 2660 | 2660 | |
| 2661 | 2661 | // Typed INSERT builder to expose a structured SQL literal in hovers |
| 2662 | 2662 | export type TypedInsertQueryBuilder< |
| 2663 | DB extends DatabaseSchema<any>, | |
| 2663 | DB extends AnyDatabaseSchema, | |
| 2664 | 2664 | TTable extends keyof DB & string, |
| 2665 | 2665 | TSql extends string = `INSERT INTO ${TTable}`, |
| 2666 | 2666 | > = Omit<InsertQueryBuilder<DB, TTable>, 'toSQL' | 'values' | 'returning'> & { |
| @@ -2680,9 +2680,10 @@ export type TypedInsertQueryBuilder< | ||
| 2680 | 2680 | } |
| 2681 | 2681 | |
| 2682 | 2682 | interface InternalState { |
| 2683 | sql: any | |
| 2683 | /** The driver handle - Bun's `SQL` or our SQLite wrapper, both `DriverConnection`. */ | |
| 2684 | sql: DriverConnection | |
| 2684 | 2685 | meta?: SchemaMeta |
| 2685 | schema?: any | |
| 2686 | schema?: AnyDatabaseSchema | |
| 2686 | 2687 | txDefaults?: TransactionOptions |
| 2687 | 2688 | /** |
| 2688 | 2689 | * Lifecycle hooks for this builder specifically. |
renamed throughout · 13 hidden lines
| @@ -2725,11 +2726,11 @@ function isRetriableTxError(err: any): boolean { renamed throughout | ||
| 2725 | 2726 | |
| 2726 | 2727 | type TransactionIsolation = 'read committed' | 'repeatable read' | 'serializable' |
| 2727 | 2728 | interface TxBackoff { baseMs?: number, maxMs?: number, factor?: number, jitter?: boolean } |
| 2728 | interface TxLoggerEvent { type: 'start' | 'retry' | 'commit' | 'rollback' | 'error', attempt: number, error?: any, durationMs?: number } | |
| 2729 | interface TxLoggerEvent { type: 'start' | 'retry' | 'commit' | 'rollback' | 'error', attempt: number, error?: unknown, durationMs?: number } | |
| 2729 | 2730 | export interface TransactionOptions { |
| 2730 | 2731 | retries?: number |
| 2731 | 2732 | isolation?: TransactionIsolation |
| 2732 | onRetry?: (attempt: number, error: any) => void | |
| 2733 | onRetry?: (attempt: number, error: unknown) => void | |
| 2733 | 2734 | afterCommit?: () => void |
| 2734 | 2735 | sqlStates?: string[] |
| 2735 | 2736 | backoff?: TxBackoff |
| @@ -2737,7 +2738,7 @@ export interface TransactionOptions { | ||
| 2737 | 2738 | /** When true, executes transaction in read-only mode (where supported). */ |
| 2738 | 2739 | readOnly?: boolean |
| 2739 | 2740 | /** Called when a transaction is rolled back. */ |
| 2740 | onRollback?: (error: any) => void | |
| 2741 | onRollback?: (error: unknown) => void | |
| 2741 | 2742 | /** Called after rollback completes. */ |
| 2742 | 2743 | afterRollback?: () => void |
| 2743 | 2744 | } |
| @@ -2903,7 +2904,7 @@ function computeBackoffMs(attempt: number, cfg?: TxBackoff): number { | ||
| 2903 | 2904 | } |
| 2904 | 2905 | |
| 2905 | 2906 | // eslint-disable-next-line pickier/no-unused-vars |
| 2906 | export function createQueryBuilder<DB extends DatabaseSchema<any>>(state?: Partial<InternalState>): QueryBuilder<DB> { | |
| 2907 | export function createQueryBuilder<DB extends AnyDatabaseSchema>(state?: Partial<InternalState>): QueryBuilder<DB> { | |
| 2907 | 2908 | // Single boundary cast: `state.sql` is `any` (allows mock/tx injection) and |
| 2908 | 2909 | // getOrCreateBunSql() returns Bun's `SQL`; both satisfy DriverConnection. With |
| 2909 | 2910 | // `_sql` typed, the downstream `.unsafe(...)` calls no longer need casts (#1044). |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | import type { DatabaseConfig, PoolConfig, SupportedDialect } from './types' |
| 2 | import type { SQLQueryBindings } from 'bun:sqlite' | |
| 2 | 3 | import { SQL } from 'bun' |
| 3 | 4 | |
| 4 | 5 | /** |
| @@ -6,6 +7,42 @@ import { SQL } from 'bun' | ||
| 6 | 7 | * Both Bun's native `SQL` query and our `createSQLiteSQL` wrapper satisfy this. |
| 7 | 8 | * See stacksjs/bun-query-builder#1044. |
| 8 | 9 | */ |
| 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 | */ | |
| 19 | export 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 | */ | |
| 37 | export 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. */ | |
| 42 | export function asEmbeddedValue(value: unknown): EmbeddedValue | null { | |
| 43 | return value !== null && typeof value === 'object' ? value as EmbeddedValue : null | |
| 44 | } | |
| 45 | ||
| 9 | 46 | export interface DriverQuery { |
| 10 | 47 | execute: () => Promise<any> |
| 11 | 48 | values?: () => any |
renamed throughout · 14 hidden lines
| @@ -30,11 +67,11 @@ export type AwaitableDriverQuery = DriverQuery & PromiseLike<any> renamed throughout | ||
| 30 | 67 | */ |
| 31 | 68 | export interface DriverConnection { |
| 32 | 69 | /** Tagged-template form: `` sql`SELECT 1` ``. */ |
| 33 | (strings: TemplateStringsArray, ...values: any[]): DriverQuery | |
| 70 | (strings: TemplateStringsArray, ...values: unknown[]): DriverQuery | |
| 34 | 71 | /** Function form for raw identifiers / value helpers: `sql('col')`. */ |
| 35 | 72 | (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 | |
| 38 | 75 | close?: () => Promise<void> | void |
| 39 | 76 | _prepareStatement?: (sql: string) => any |
| 40 | 77 | [key: string]: any |
| @@ -74,11 +111,11 @@ import { applySqliteBootstrapPragmas } from './sqlite-pragmas' | ||
| 74 | 111 | * numbered placeholders is returned untouched, so `?` queries and Bun's own |
| 75 | 112 | * tagged-template output are unaffected. |
| 76 | 113 | */ |
| 77 | export function bindNumberedPlaceholders(sql: string, params: any[]): { sql: string, params: any[] } { | |
| 114 | export function bindNumberedPlaceholders<T>(sql: string, params: T[]): { sql: string, params: T[] } { | |
| 78 | 115 | if (params.length === 0 || !/\$\d/.test(sql)) |
| 79 | 116 | return { sql, params } |
| 80 | 117 | |
| 81 | const out: any[] = [] | |
| 118 | const out: T[] = [] | |
| 82 | 119 | let rewritten = '' |
| 83 | 120 | let highest = 0 |
| 84 | 121 | |
| @@ -185,7 +222,7 @@ class SQLiteWrapper { | ||
| 185 | 222 | * Execute a query with parameters and return results. |
| 186 | 223 | * This mimics the SQL tagged template literal behavior. |
| 187 | 224 | */ |
| 188 | query(sql: string, params: any[] = []): any[] { | |
| 225 | query(sql: string, params: SQLQueryBindings[] = []): any[] { | |
| 189 | 226 | const bound = bindNumberedPlaceholders(sql, params) |
| 190 | 227 | const stmt = this.db.prepare(bound.sql) |
| 191 | 228 | return stmt.all(...bound.params) |
| @@ -194,7 +231,7 @@ class SQLiteWrapper { | ||
| 194 | 231 | /** |
| 195 | 232 | * Execute a query that doesn't return results (INSERT, UPDATE, DELETE). |
| 196 | 233 | */ |
| 197 | run(sql: string, params: any[] = []): any { | |
| 234 | run(sql: string, params: SQLQueryBindings[] = []): any { | |
| 198 | 235 | const bound = bindNumberedPlaceholders(sql, params) |
| 199 | 236 | const stmt = this.db.prepare(bound.sql) |
| 200 | 237 | return stmt.run(...bound.params) |
renamed throughout · 12 hidden lines
| @@ -364,10 +401,10 @@ function createSQLiteSQL(filename: string): SQL { renamed throughout | ||
| 364 | 401 | /** |
| 365 | 402 | * Process a tagged template literal and return a query object |
| 366 | 403 | */ |
| 367 | function processTaggedTemplate(strings: TemplateStringsArray, ...values: any[]): any { | |
| 404 | function processTaggedTemplate(strings: TemplateStringsArray, ...values: unknown[]): any { | |
| 368 | 405 | // Build the SQL string with placeholders |
| 369 | 406 | let sql = strings[0] |
| 370 | const params: any[] = [] | |
| 407 | const params: unknown[] = [] | |
| 371 | 408 | |
| 372 | 409 | for (let i = 0; i < values.length; i++) { |
| 373 | 410 | const value = values[i] |
| @@ -396,13 +433,16 @@ function createSQLiteSQL(filename: string): SQL { | ||
| 396 | 433 | // The discriminator is `values` being an ARRAY: raw markers carry |
| 397 | 434 | // `__raw` and no `values`, and Bun's native query objects expose `values` |
| 398 | 435 | // 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) | |
| 402 | 441 | } |
| 403 | 442 | // 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)()) | |
| 406 | 446 | sql += rawValue + (strings[i + 1] || '') |
| 407 | 447 | } |
| 408 | 448 | // Handle arrays - expand to placeholders |
| @@ -432,11 +472,11 @@ function createSQLiteSQL(filename: string): SQL { | ||
| 432 | 472 | // caller silently loses its data. |
| 433 | 473 | const trimmed = sql.trim().toUpperCase() |
| 434 | 474 | 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)) | |
| 436 | 476 | return Promise.resolve(result) |
| 437 | 477 | } |
| 438 | 478 | else { |
| 439 | const result = wrapper.run(sql, params) | |
| 479 | const result = wrapper.run(sql, toBindings(params)) | |
| 440 | 480 | return Promise.resolve(result) |
| 441 | 481 | } |
| 442 | 482 | } |
| @@ -455,7 +495,7 @@ function createSQLiteSQL(filename: string): SQL { | ||
| 455 | 495 | * - Tagged template literals: sql`SELECT * FROM users` |
| 456 | 496 | * - Function calls for raw identifiers: sql('column_name') |
| 457 | 497 | */ |
| 458 | function sqlFunction(stringsOrValue: TemplateStringsArray | string, ...values: any[]): any { | |
| 498 | function sqlFunction(stringsOrValue: TemplateStringsArray | string, ...values: unknown[]): any { | |
| 459 | 499 | // If called with a template literal (array of strings) |
| 460 | 500 | if (Array.isArray(stringsOrValue) && 'raw' in stringsOrValue) { |
| 461 | 501 | return processTaggedTemplate(stringsOrValue as TemplateStringsArray, ...values) |
| @@ -475,7 +515,7 @@ function createSQLiteSQL(filename: string): SQL { | ||
| 475 | 515 | sqlFunction.raw = (str: string) => createRawMarker(str) |
| 476 | 516 | |
| 477 | 517 | // 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[] = []) => { | |
| 479 | 519 | const execute = (): Promise<any> => { |
| 480 | 520 | try { |
| 481 | 521 | // A RETURNING clause turns an INSERT/UPDATE/DELETE into a row producer; |
| @@ -483,11 +523,11 @@ function createSQLiteSQL(filename: string): SQL { | ||
| 483 | 523 | // `.run()`'s { changes, lastInsertRowid }. See the tagged-template path. |
| 484 | 524 | const trimmed = sql.trim().toUpperCase() |
| 485 | 525 | 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)) | |
| 487 | 527 | return Promise.resolve(result) |
| 488 | 528 | } |
| 489 | 529 | else { |
| 490 | const result = wrapper.run(sql, params) | |
| 530 | const result = wrapper.run(sql, toBindings(params)) | |
| 491 | 531 | return Promise.resolve(result) |
| 492 | 532 | } |
| 493 | 533 | } |
| @@ -522,9 +562,9 @@ function createSQLiteSQL(filename: string): SQL { | ||
| 522 | 562 | } |
| 523 | 563 | |
| 524 | 564 | // Add .query() method for direct SQL execution |
| 525 | sqlFunction.query = (sql: string, params?: any[]) => { | |
| 565 | sqlFunction.query = (sql: string, params?: unknown[]) => { | |
| 526 | 566 | try { |
| 527 | const result = wrapper.query(sql, params || []) | |
| 567 | const result = wrapper.query(sql, toBindings(params || [])) | |
| 528 | 568 | return Promise.resolve(result) |
| 529 | 569 | } |
| 530 | 570 | catch (error) { |
| @@ -533,7 +573,7 @@ function createSQLiteSQL(filename: string): SQL { | ||
| 533 | 573 | } |
| 534 | 574 | |
| 535 | 575 | // 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[]) => { | |
| 537 | 577 | try { |
| 538 | 578 | const { readFileSync } = await import('node:fs') |
| 539 | 579 | const sqlContent = readFileSync(filePath, 'utf-8') |
| @@ -23,13 +23,58 @@ import type { | ||
| 23 | 23 | } from './drivers/dynamodb' |
| 24 | 24 | import { createDynamoDBDriver } from './drivers/dynamodb' |
| 25 | 25 | |
| 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 | */ | |
| 33 | export 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 | */ | |
| 49 | export interface DynamoDBClientLike { | |
| 50 | send: (command: unknown) => Promise<unknown> | |
| 51 | [key: string]: unknown | |
| 52 | } | |
| 53 | ||
| 54 | /** What DynamoDB reports about the capacity a call consumed. */ | |
| 55 | export 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. */ | |
| 64 | export type DynamoDBFilterOperator = | |
| 65 | | DynamoDBComparisonOperator | |
| 66 | | 'contains' | |
| 67 | | 'attribute_exists' | |
| 68 | | 'attribute_not_exists' | |
| 69 | | 'IN' | |
| 70 | ||
| 26 | 71 | /** |
| 27 | 72 | * DynamoDB Query Builder Options |
| 28 | 73 | */ |
| 29 | 74 | export interface DynamoDBQueryBuilderOptions { |
| 30 | 75 | config: DynamoDBConfig |
| 31 | 76 | /** Optional DynamoDB client instance (e.g., from @aws-sdk/client-dynamodb) */ |
| 32 | client?: any | |
| 77 | client?: DynamoDBClientLike | |
| 33 | 78 | } |
| 34 | 79 | |
| 35 | 80 | /** |
| @@ -41,7 +86,7 @@ export interface DynamoDBResult<T = any> { | ||
| 41 | 86 | count?: number |
| 42 | 87 | scannedCount?: number |
| 43 | 88 | lastEvaluatedKey?: Record<string, any> |
| 44 | consumedCapacity?: any | |
| 89 | consumedCapacity?: DynamoDBConsumedCapacity | |
| 45 | 90 | } |
| 46 | 91 | |
| 47 | 92 | /** |
| @@ -97,7 +142,7 @@ export class DynamoDBQueryBuilder<T = any> { | ||
| 97 | 142 | * Add a key condition (for Query operations) |
| 98 | 143 | * Key conditions can only be applied to partition key and sort key |
| 99 | 144 | */ |
| 100 | whereKey(attribute: string, operator: DynamoDBComparisonOperator, value: any): this { | |
| 145 | whereKey(attribute: string, operator: DynamoDBComparisonOperator, value: DynamoDBValue): this { | |
| 101 | 146 | this.keyConditions.push({ attribute, operator, value }) |
| 102 | 147 | return this |
| 103 | 148 | } |
renamed throughout · 16 hidden lines
| @@ -105,14 +150,14 @@ export class DynamoDBQueryBuilder<T = any> { renamed throughout | ||
| 105 | 150 | /** |
| 106 | 151 | * Shorthand for partition key equals |
| 107 | 152 | */ |
| 108 | wherePartitionKey(attribute: string, value: any): this { | |
| 153 | wherePartitionKey(attribute: string, value: DynamoDBValue): this { | |
| 109 | 154 | return this.whereKey(attribute, '=', value) |
| 110 | 155 | } |
| 111 | 156 | |
| 112 | 157 | /** |
| 113 | 158 | * Shorthand for sort key equals |
| 114 | 159 | */ |
| 115 | whereSortKey(attribute: string, value: any): this { | |
| 160 | whereSortKey(attribute: string, value: DynamoDBValue): this { | |
| 116 | 161 | return this.whereKey(attribute, '=', value) |
| 117 | 162 | } |
| 118 | 163 | |
| @@ -127,7 +172,7 @@ export class DynamoDBQueryBuilder<T = any> { | ||
| 127 | 172 | /** |
| 128 | 173 | * Sort key between two values |
| 129 | 174 | */ |
| 130 | whereSortKeyBetween(attribute: string, start: any, end: any): this { | |
| 175 | whereSortKeyBetween(attribute: string, start: DynamoDBValue, end: DynamoDBValue): this { | |
| 131 | 176 | this.keyConditions.push({ attribute, operator: 'BETWEEN', values: [start, end] }) |
| 132 | 177 | return this |
| 133 | 178 | } |
| @@ -135,7 +180,7 @@ export class DynamoDBQueryBuilder<T = any> { | ||
| 135 | 180 | /** |
| 136 | 181 | * Add a filter condition (applied after Query/Scan) |
| 137 | 182 | */ |
| 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 { | |
| 139 | 184 | this.filterConditions.push({ attribute, operator, value }) |
| 140 | 185 | return this |
| 141 | 186 | } |
| @@ -143,42 +188,42 @@ export class DynamoDBQueryBuilder<T = any> { | ||
| 143 | 188 | /** |
| 144 | 189 | * Filter where attribute equals value |
| 145 | 190 | */ |
| 146 | whereEquals(attribute: string, value: any): this { | |
| 191 | whereEquals(attribute: string, value: DynamoDBValue): this { | |
| 147 | 192 | return this.where(attribute, '=', value) |
| 148 | 193 | } |
| 149 | 194 | |
| 150 | 195 | /** |
| 151 | 196 | * Filter where attribute is less than value |
| 152 | 197 | */ |
| 153 | whereLessThan(attribute: string, value: any): this { | |
| 198 | whereLessThan(attribute: string, value: DynamoDBValue): this { | |
| 154 | 199 | return this.where(attribute, '<', value) |
| 155 | 200 | } |
| 156 | 201 | |
| 157 | 202 | /** |
| 158 | 203 | * Filter where attribute is less than or equal to value |
| 159 | 204 | */ |
| 160 | whereLessThanOrEqual(attribute: string, value: any): this { | |
| 205 | whereLessThanOrEqual(attribute: string, value: DynamoDBValue): this { | |
| 161 | 206 | return this.where(attribute, '<=', value) |
| 162 | 207 | } |
| 163 | 208 | |
| 164 | 209 | /** |
| 165 | 210 | * Filter where attribute is greater than value |
| 166 | 211 | */ |
| 167 | whereGreaterThan(attribute: string, value: any): this { | |
| 212 | whereGreaterThan(attribute: string, value: DynamoDBValue): this { | |
| 168 | 213 | return this.where(attribute, '>', value) |
| 169 | 214 | } |
| 170 | 215 | |
| 171 | 216 | /** |
| 172 | 217 | * Filter where attribute is greater than or equal to value |
| 173 | 218 | */ |
| 174 | whereGreaterThanOrEqual(attribute: string, value: any): this { | |
| 219 | whereGreaterThanOrEqual(attribute: string, value: DynamoDBValue): this { | |
| 175 | 220 | return this.where(attribute, '>=', value) |
| 176 | 221 | } |
| 177 | 222 | |
| 178 | 223 | /** |
| 179 | 224 | * Filter where attribute is between two values |
| 180 | 225 | */ |
| 181 | whereBetween(attribute: string, start: any, end: any): this { | |
| 226 | whereBetween(attribute: string, start: DynamoDBValue, end: DynamoDBValue): this { | |
| 182 | 227 | this.filterConditions.push({ attribute, operator: 'BETWEEN', values: [start, end] }) |
| 183 | 228 | return this |
| 184 | 229 | } |
| @@ -194,7 +239,7 @@ export class DynamoDBQueryBuilder<T = any> { | ||
| 194 | 239 | /** |
| 195 | 240 | * Filter where attribute contains value (for strings and sets) |
| 196 | 241 | */ |
| 197 | whereContains(attribute: string, value: any): this { | |
| 242 | whereContains(attribute: string, value: DynamoDBValue): this { | |
| 198 | 243 | return this.where(attribute, 'contains', value) |
| 199 | 244 | } |
| 200 | 245 | |
| @@ -215,7 +260,7 @@ export class DynamoDBQueryBuilder<T = any> { | ||
| 215 | 260 | /** |
| 216 | 261 | * Filter where attribute is in a list of values |
| 217 | 262 | */ |
| 218 | whereIn(attribute: string, values: any[]): this { | |
| 263 | whereIn(attribute: string, values: DynamoDBValue[]): this { | |
| 219 | 264 | this.filterConditions.push({ attribute, operator: 'IN', values }) |
| 220 | 265 | return this |
| 221 | 266 | } |
| @@ -633,7 +678,7 @@ export class DynamoDBItemBuilder<T = any> { | ||
| 633 | 678 | /** |
| 634 | 679 | * Set attribute value (for Update) |
| 635 | 680 | */ |
| 636 | set(attribute: string, value: any): this { | |
| 681 | set(attribute: string, value: DynamoDBValue): this { | |
| 637 | 682 | if (!this.updateExpressions.set) { |
| 638 | 683 | this.updateExpressions.set = {} |
| 639 | 684 | } |
| @@ -666,7 +711,7 @@ export class DynamoDBItemBuilder<T = any> { | ||
| 666 | 711 | /** |
| 667 | 712 | * Add to a number or set (for Update) |
| 668 | 713 | */ |
| 669 | add(attribute: string, value: any): this { | |
| 714 | add(attribute: string, value: DynamoDBValue): this { | |
| 670 | 715 | if (!this.updateExpressions.add) { |
| 671 | 716 | this.updateExpressions.add = {} |
| 672 | 717 | } |
| @@ -677,7 +722,7 @@ export class DynamoDBItemBuilder<T = any> { | ||
| 677 | 722 | /** |
| 678 | 723 | * Delete from a set (for Update) |
| 679 | 724 | */ |
| 680 | deleteFromSet(attribute: string, values: any): this { | |
| 725 | deleteFromSet(attribute: string, values: DynamoDBValue): this { | |
| 681 | 726 | if (!this.updateExpressions.delete) { |
| 682 | 727 | this.updateExpressions.delete = {} |
| 683 | 728 | } |
| @@ -20,7 +20,7 @@ import type { | ||
| 20 | 20 | SingleTableEntityMapping, |
| 21 | 21 | } from './drivers/dynamodb' |
| 22 | 22 | import { createDynamoDBDriver } from './drivers/dynamodb' |
| 23 | import type { DynamoDBQueryBuilderOptions } from './dynamodb-client' | |
| 23 | import type { DynamoDBFilterOperator, DynamoDBQueryBuilderOptions, DynamoDBValue } from './dynamodb-client' | |
| 24 | 24 | import { DynamoDBItemBuilder, DynamoDBQueryBuilder } from './dynamodb-client' |
| 25 | 25 | |
| 26 | 26 | /** |
| @@ -498,7 +498,7 @@ export class SingleTableQueryBuilder<T> { | ||
| 498 | 498 | /** |
| 499 | 499 | * Add a filter condition |
| 500 | 500 | */ |
| 501 | where(attribute: string, operator: any, value?: any): this { | |
| 501 | where(attribute: string, operator: DynamoDBFilterOperator, value?: DynamoDBValue): this { | |
| 502 | 502 | this.builder.where(attribute, operator, value) |
| 503 | 503 | return this |
| 504 | 504 | } |
| @@ -41,7 +41,7 @@ export interface ParsedAttribute { | ||
| 41 | 41 | nullable: boolean |
| 42 | 42 | unique: boolean |
| 43 | 43 | hidden: boolean |
| 44 | defaultValue?: any | |
| 44 | defaultValue?: unknown | |
| 45 | 45 | cast?: string |
| 46 | 46 | dynamoDbType: 'S' | 'N' | 'B' | 'BOOL' | 'NULL' | 'M' | 'L' | 'SS' | 'NS' | 'BS' |
| 47 | 47 | } |
| @@ -109,7 +109,7 @@ export interface StacksModel { | ||
| 109 | 109 | nullable?: boolean |
| 110 | 110 | unique?: boolean |
| 111 | 111 | hidden?: boolean |
| 112 | default?: any | |
| 112 | default?: unknown | |
| 113 | 113 | cast?: string |
| 114 | 114 | validation?: { rule?: string } |
| 115 | 115 | }> |
| @@ -150,7 +150,7 @@ export function resolveKeyPattern(keyPatterns: KeyPatterns, data: Record<string, | ||
| 150 | 150 | /** |
| 151 | 151 | * Parse models from configuration (stub implementation) |
| 152 | 152 | */ |
| 153 | export async function parseStacksModels(_config: any): Promise<{ models: Map<string, ParsedModel> }> { | |
| 153 | export async function parseStacksModels(_config: unknown): Promise<{ models: Map<string, ParsedModel> }> { | |
| 154 | 154 | // This is a stub - actual implementation would parse model files |
| 155 | 155 | return { models: new Map() } |
| 156 | 156 | } |
| @@ -186,8 +186,8 @@ export function unmarshallItem(item: Record<string, any>): Record<string, any> { | ||
| 186 | 186 | // ============================================================================ |
| 187 | 187 | // Imports from bun-query-builder (fluent API layer) |
| 188 | 188 | // ============================================================================ |
| 189 | import type { DynamoDBConfig, DynamoDBDriver, SingleTableEntityMapping } from './drivers/dynamodb' | |
| 190 | import type { DynamoDBQueryBuilderOptions } from './dynamodb-client' | |
| 189 | import type { DynamoDBConfig, DynamoDBDriver, DynamoDBTableDefinition, SingleTableEntityMapping } from './drivers/dynamodb' | |
| 190 | import type { DynamoDBClientLike, DynamoDBQueryBuilderOptions } from './dynamodb-client' | |
| 191 | 191 | import type { SingleTableConfig, SingleTableEntity } from './dynamodb-single-table' |
| 192 | 192 | import { createDynamoDBDriver } from './drivers/dynamodb' |
| 193 | 193 | import { DynamoDBItemBuilder, DynamoDBQueryBuilder } from './dynamodb-client' |
| @@ -282,7 +282,7 @@ export class DynamoDBToolingAdapter { | ||
| 282 | 282 | /** |
| 283 | 283 | * Set the DynamoDB client instance |
| 284 | 284 | */ |
| 285 | setClient(client: any): this { | |
| 285 | setClient(client: DynamoDBClientLike): this { | |
| 286 | 286 | this.queryBuilderOptions.client = client |
| 287 | 287 | return this |
| 288 | 288 | } |
| @@ -498,7 +498,7 @@ export class DynamoDBToolingAdapter { | ||
| 498 | 498 | /** |
| 499 | 499 | * Generate table definition for all registered models |
| 500 | 500 | */ |
| 501 | generateTableDefinition(): any { | |
| 501 | generateTableDefinition(): DynamoDBTableDefinition { | |
| 502 | 502 | return this.singleTableManager.generateTableDefinition() |
| 503 | 503 | } |
| 504 | 504 | |
| @@ -24,7 +24,15 @@ export interface SchemaMeta { | ||
| 24 | 24 | morphToMany?: Record<string, string> |
| 25 | 25 | morphedByMany?: Record<string, string> |
| 26 | 26 | }> |
| 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>> | |
| 28 | 36 | /** |
| 29 | 37 | * Original models record passed to `buildSchemaMeta`, retained so downstream |
| 30 | 38 | * consumers (e.g. the pivot resolver) can read through-model attributes |
| @@ -286,8 +286,8 @@ export interface ModelDefinition { | ||
| 286 | 286 | readonly attributes: { |
| 287 | 287 | readonly [key: string]: TypedAttribute<unknown> |
| 288 | 288 | } |
| 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> | |
| 291 | 291 | readonly scopes?: Record<string, (value: unknown) => unknown> |
| 292 | 292 | readonly indexes?: readonly object[] |
| 293 | 293 | readonly dashboard?: { readonly enabled?: boolean; readonly highlight?: boolean | number } |
| @@ -617,7 +617,29 @@ function toPostgresPlaceholders(sql: string): string { | ||
| 617 | 617 | * fallback (which would be 0) — see stacksjs/bun-query-builder#1032. MySQL |
| 618 | 618 | * surfaces `affectedRows` instead. |
| 619 | 619 | */ |
| 620 | export 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 | */ | |
| 628 | export 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 | ||
| 642 | export function extractChanges(res: WriteResultLike | null | undefined): number { | |
| 621 | 643 | if (res == null) |
| 622 | 644 | return 0 |
| 623 | 645 | if (typeof res.affectedRows === 'number') // MySQL |
| @@ -630,7 +652,7 @@ export function extractChanges(res: any): number { | ||
| 630 | 652 | } |
| 631 | 653 | |
| 632 | 654 | /** Extract a generated primary key from a Bun SQL driver result. */ |
| 633 | export function extractInsertId(res: any): number | bigint | null { | |
| 655 | export function extractInsertId(res: WriteResultLike | null | undefined): number | bigint | null { | |
| 634 | 656 | if (res == null || typeof res !== 'object') |
| 635 | 657 | return null |
| 636 | 658 | if ('insertId' in res && res.insertId != null) // MySQL |
| @@ -221,14 +221,23 @@ export interface ModelOptions extends Base { | ||
| 221 | 221 | morphTo?: MorphTo |
| 222 | 222 | morphToMany?: MorphToMany<ModelNames> |
| 223 | 223 | 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 | */ | |
| 224 | 233 | scopes?: { |
| 225 | [key: string]: (value: any) => any | |
| 234 | [key: string]: (value: any) => unknown | |
| 226 | 235 | } |
| 227 | 236 | get?: { |
| 228 | [key: string]: (value: any) => any | |
| 237 | [key: string]: (value: any) => unknown | |
| 229 | 238 | } |
| 230 | 239 | set?: { |
| 231 | [key: string]: (value: any) => any | |
| 240 | [key: string]: (value: any) => unknown | |
| 232 | 241 | } |
| 233 | 242 | } |
| 234 | 243 | |
| @@ -410,6 +419,22 @@ export type InferTableRelations<M, MRecord extends ModelRecord> = { | ||
| 410 | 419 | * type Schema = DatabaseSchema<typeof models> |
| 411 | 420 | * ``` |
| 412 | 421 | */ |
| 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 | */ | |
| 430 | export interface AnySchemaTable { | |
| 431 | columns: Record<string, unknown> | |
| 432 | primaryKey?: unknown | |
| 433 | relations?: Record<string, string> | undefined | |
| 434 | } | |
| 435 | ||
| 436 | export type AnyDatabaseSchema = Record<string, AnySchemaTable> | |
| 437 | ||
| 413 | 438 | export type DatabaseSchema<MRecord extends ModelRecord> = { |
| 414 | 439 | [MName in keyof MRecord & string as InferTableName<UnwrapModelDefinition<MRecord[MName]>>]: { |
| 415 | 440 | columns: InferAttributes<UnwrapModelDefinition<MRecord[MName]>> |
| @@ -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 | |
| 10 | export type SeederQueryBuilder = any | |
| 11 | ||
| 1 | 12 | /** |
| 2 | 13 | * Base seeder class that all seeders should extend. |
| 3 | 14 | * Provides access to query builder and faker for generating test data. |
| @@ -9,7 +20,7 @@ export abstract class Seeder { | ||
| 9 | 20 | * |
| 10 | 21 | * @param qb - The query builder instance to use for database operations |
| 11 | 22 | */ |
| 12 | abstract run(qb: any): Promise<void> | |
| 23 | abstract run(qb: SeederQueryBuilder): Promise<void> | |
| 13 | 24 | |
| 14 | 25 | /** |
| 15 | 26 | * Optional method to specify the order in which seeders should run. |
| @@ -93,7 +93,7 @@ export function renderWhereTerms(terms: readonly WhereTerm[]): string { | ||
| 93 | 93 | * happened to come back empty — the same shape of silent, widening-or-narrowing |
| 94 | 94 | * failure as the `IN ()` bug itself. |
| 95 | 95 | */ |
| 96 | export function renderInPredicate(column: string, values: any[], negated: boolean, placeholders: string): string { | |
| 96 | export function renderInPredicate(column: string, values: readonly unknown[], negated: boolean, placeholders: string): string { | |
| 97 | 97 | if (values.length === 0) |
| 98 | 98 | return negated ? TRUE_PREDICATE : FALSE_PREDICATE |
| 99 | 99 | return `${column} ${negated ? 'NOT IN' : 'IN'} (${placeholders})` |
| @@ -240,15 +240,34 @@ export interface SqlConfig { | ||
| 240 | 240 | * Optional lifecycle hooks around query execution. These are invoked for any |
| 241 | 241 | * statement executed through the builder (select/insert/update/delete/raw). |
| 242 | 242 | */ |
| 243 | /** A bound statement parameter: whatever a driver will accept as a value. */ | |
| 244 | export type QueryParam = string | number | bigint | boolean | Date | Uint8Array | null | |
| 245 | ||
| 246 | /** The kind of statement a hook is reporting on. */ | |
| 247 | export type QueryKind = 'select' | 'insert' | 'update' | 'delete' | 'raw' | |
| 248 | ||
| 249 | /** The row shape a hook sees. Columns are unknown until the caller narrows. */ | |
| 250 | export type QueryRecord = Record<string, unknown> | |
| 251 | ||
| 252 | /** A `where` description as the mutation hooks receive it. */ | |
| 253 | export type QueryWhere = Record<string, unknown> | |
| 254 | ||
| 255 | /** Common fields on every query lifecycle event. */ | |
| 256 | export interface QueryEvent { | |
| 257 | sql: string | |
| 258 | params?: QueryParam[] | |
| 259 | kind?: QueryKind | |
| 260 | } | |
| 261 | ||
| 243 | 262 | export interface QueryHooks { |
| 244 | 263 | /** 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 | |
| 246 | 265 | /** 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 | |
| 248 | 267 | /** 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 | |
| 250 | 269 | /** 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 } | |
| 252 | 271 | /** |
| 253 | 272 | * When set, a query whose duration meets/exceeds this many milliseconds is |
| 254 | 273 | * reported as slow (via `onSlowQuery`, or a `console.warn` if no handler is |
| @@ -256,19 +275,19 @@ export interface QueryHooks { | ||
| 256 | 275 | */ |
| 257 | 276 | slowQueryThresholdMs?: number |
| 258 | 277 | /** 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 | |
| 260 | 279 | /** 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> | |
| 262 | 281 | /** 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> | |
| 264 | 283 | /** 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> | |
| 266 | 285 | /** 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> | |
| 268 | 287 | /** 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> | |
| 270 | 289 | /** 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> | |
| 272 | 291 | } |
| 273 | 292 | |
| 274 | 293 | /** |
| @@ -362,7 +381,7 @@ export interface BrowserConfig { | ||
| 362 | 381 | /** Request timeout in milliseconds (default: 30000) */ |
| 363 | 382 | timeout?: number |
| 364 | 383 | /** Transform response data before returning (e.g., unwrap { data: [...] }) */ |
| 365 | transformResponse?: <T>(response: any) => T | |
| 384 | transformResponse?: <T>(response: unknown) => T | |
| 366 | 385 | /** Transform request data before sending */ |
| 367 | 386 | transformRequest?: <T>(data: T) => any |
| 368 | 387 | } |
| @@ -483,7 +502,7 @@ export interface QueryBuilderConfig { | ||
| 483 | 502 | * signature is not a property. `keyof (() => void)` is `never`, so the |
| 484 | 503 | * hook collapses to an empty object type: no longer callable, and loose |
| 485 | 504 | * 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 | |
| 487 | 506 | * shape with no signature at all. A hook or a token getter is a leaf. |
| 488 | 507 | * |
| 489 | 508 | * 2. ARRAYS SECOND, because the merge replaces them rather than merging them. |
| @@ -603,7 +622,8 @@ export interface GenerateMigrationResult { | ||
| 603 | 622 | sql: string |
| 604 | 623 | sqlStatements: string[] |
| 605 | 624 | hasChanges: boolean |
| 606 | plan: any | |
| 625 | /** The diff the SQL was generated from, as `migrations` describes it. */ | |
| 626 | plan: import('./migrations').MigrationPlan | |
| 607 | 627 | /** |
| 608 | 628 | * Structured description of each change (drop/rename/modify/rebuild/...), so |
| 609 | 629 | * callers can gate destructive ops and report renames without parsing SQL. |
| @@ -0,0 +1,66 @@ | ||
| 1 | import { describe, expect, it } from 'bun:test' | |
| 2 | import { existsSync, readdirSync, readFileSync } from 'node:fs' | |
| 3 | import { 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 | ||
| 31 | const DIST = join(import.meta.dir, '..', 'dist') | |
| 32 | const BASELINE = 18 | |
| 33 | ||
| 34 | function anyCount(source: string): number { | |
| 35 | return (source.match(/:\s*any\b|<any>|any\[\]/g) ?? []).length | |
| 36 | } | |
| 37 | ||
| 38 | describe('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 | }) | |