@stacksjs/database 0.72.22 → 0.72.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
package/dist/index.d.ts CHANGED
@@ -10,6 +10,8 @@ export type {
10
10
  PostgresConfig,
11
11
  SqliteConfig,
12
12
  } from './driver-config';
13
+ // Core database utilities and default instance
14
+ export type { FrameworkSchema } from './framework-schema';
13
15
  export type { SchemaDriftColumn, SchemaDriftReport } from './schema-drift';
14
16
  export type { DeclaredFK, FkAuditResult, FkOrphan, FkOrphanReport, LiveFK } from './fk-audit';
15
17
  export type { DeclaredUnique, LiveUniqueIndex, UniqueAuditResult } from './unique-audit';
@@ -74,7 +76,6 @@ export {
74
76
  mergeWithDefaults,
75
77
  validateDriverConfig,
76
78
  } from './driver-config';
77
- // Core database utilities and default instance
78
79
  export * from './utils';
79
80
  // Types (compatibility layer for Kysely types)
80
81
  export * from './types';
package/dist/utils.d.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  import { createQueryBuilder, setConfig } from '@stacksjs/query-builder';
2
+ import type { FrameworkSchema } from './framework-schema';
2
3
  import type { QueryHooks } from '@stacksjs/query-builder';
3
4
  export declare function acquireDbConfigLock(): Promise<() => void>;
4
5
  // Function to initialize the config when it's available
5
6
  export declare function initializeDbConfig(config: any): void;
6
7
  export declare function createDatabaseQueryHooks(dispatch: (event: DatabaseQueryLogEvent) => void | Promise<void>): QueryHooks;
7
- declare function ensureConfigLoaded(): Promise<void>;
8
8
  export declare function ensureDatabaseConfigLoaded(): Promise<void>;
9
- declare function getDb(): ReturnType<typeof createQueryBuilder>;
10
9
  /**
11
10
  * Discard every cached database client after the underlying query-builder
12
11
  * connection is reset.
@@ -19,24 +18,46 @@ declare function getDb(): ReturnType<typeof createQueryBuilder>;
19
18
  */
20
19
  export declare function resetDatabaseConnection(): void;
21
20
  /**
22
- * The builder a read should use right now.
21
+ * Fluent chain returned by entry-point methods like `selectFrom`/`updateTable`.
22
+ *
23
+ * bun-query-builder marks legacy chain methods (e.g. `selectAll`, `whereILike`,
24
+ * `selectAllRelations`) as optional in its declarations even though they're
25
+ * always present at runtime. Re-typing them here avoids forcing every call
26
+ * site to use `?.()` or `!` on the chain.
27
+ *
28
+ * **The chain carries its row type.** `TRow` comes from the augmented
29
+ * `DatabaseSchema` that `buddy generate:db-types` writes, so
30
+ * `db.selectFrom('users').executeTakeFirst()` answers a `users` row rather than
31
+ * `any` - and `select(['id', 'handle'])` narrows it to those two columns.
32
+ *
33
+ * Every terminal used to be `Promise<any>`, and the cost was not theoretical:
34
+ * an application on top of this ends up annotating every result `any` to say
35
+ * what it already knows, which is a thousand places the compiler has been told
36
+ * to stop looking. A row it cannot type is `Record<string, unknown>` instead -
37
+ * still a value the caller has to narrow, but one narrowing it is checked.
23
38
  *
24
- * Falls back to the primary whenever routing is not permitted — no
25
- * replicas configured, auto-routing off, inside a transaction, or after a
26
- * write in this async context. See `./replicas` for why each of those
27
- * carve-outs exists.
39
+ * A join or an aliased select list widens the row to
40
+ * `Record<string, unknown>`: the shape then depends on the aliases rather than
41
+ * on any one table, and claiming otherwise would be worse than not knowing.
28
42
  */
29
- declare function getReadDb(): ReturnType<typeof createQueryBuilder>;
30
43
  /**
31
- * Explicitly replica-routed handle: `db.read.selectFrom('users')`.
44
+ * State the shape of rows this package knows and the query cannot.
45
+ *
46
+ * A raw query answers `Record<string, unknown>` when the table is not in the
47
+ * generated `DatabaseSchema` - which is always true *inside* the framework,
48
+ * because an application's schema does not exist at framework build time. The
49
+ * package that ships the model does know the shape, and this is where that
50
+ * knowledge is written down: once, at the boundary, named and greppable.
51
+ *
52
+ * It is an assertion, and deliberately an obvious one. What it replaces is a
53
+ * `Promise<any>` that spread the same claim silently through every caller.
32
54
  *
33
- * Unlike automatic routing this ignores `reads.autoRoute` — asking for
34
- * `db.read` IS the statement that this particular query tolerates a stale
35
- * result. It still respects the transaction carve-out, because a read
36
- * inside a transaction must see that transaction's own writes no matter
37
- * how it was requested.
55
+ * Application code should not need it: `buddy generate:db-types` gives `db` the
56
+ * real column types, and a row that arrives typed does not want asserting.
38
57
  */
39
- declare function getExplicitReadDb(): ReturnType<typeof createQueryBuilder>;
58
+ export declare function asRows<TRow>(rows: ReadonlyArray<Record<string, unknown>>): TRow[];
59
+ /** The single-row form of {@link asRows}. */
60
+ export declare function asRow<TRow>(row: Record<string, unknown> | undefined): TRow | undefined;
40
61
  /**
41
62
  * Update bun-query-builder configuration
42
63
  */
@@ -64,15 +85,11 @@ export declare const RAW_QUERY_SOFT_DELETE_CONFIG: {
64
85
  column: 'deleted_at';
65
86
  defaultFilter: true
66
87
  };
67
- /** Statements that mutate, for the read-your-writes tracking in `./replicas`. */
68
- declare const WRITE_ENTRY_POINTS: Set<any>;
69
- /** Reads that are candidates for replica routing. */
70
- declare const READ_ENTRY_POINTS: Set<any>;
71
88
  /**
72
89
  * Lazy proxy for the query builder - connection is only made when first used.
73
90
  * This is the main entry point for database operations.
74
91
  */
75
- export declare const db: Proxy;
92
+ export declare const db: Db;
76
93
  /**
77
94
  * Replica-routed handle exposed as `db.read`.
78
95
  *
@@ -80,7 +97,7 @@ export declare const db: Proxy;
80
97
  * available behind it (`db.read.selectFrom(...).where(...)`) without
81
98
  * re-declaring every chain entry point.
82
99
  */
83
- export declare const readDb: Proxy;
100
+ export declare const readDb: Omit<Db, 'read'>;
84
101
  export declare interface DatabaseQueryLogEvent {
85
102
  query: {
86
103
  sql: string
@@ -90,90 +107,97 @@ export declare interface DatabaseQueryLogEvent {
90
107
  error?: unknown
91
108
  }
92
109
  /**
93
- * Fluent chain returned by entry-point methods like `selectFrom`/`updateTable`.
110
+ * What an insert reports when it was not asked to return rows.
94
111
  *
95
- * bun-query-builder marks legacy chain methods (e.g. `selectAll`, `whereILike`,
96
- * `selectAllRelations`) as optional in its declarations even though they're
97
- * always present at runtime. Re-typing them here avoids forcing every call
98
- * site to use `?.()` or `!` on the chain.
112
+ * Every field optional and every one differently named, because drivers
113
+ * disagree: Postgres answers a count, SQLite a `changes`, MySQL an `insertId`.
114
+ * Typing this as the row - which is what the query builder's own declarations
115
+ * do - is how framework code came to read `insertId` off a value that is not a
116
+ * row and cannot have one.
99
117
  *
100
- * Returns are typed as `any` deliberately — typing each variant precisely
101
- * would re-introduce the optional methods, and we already lose strict column
102
- * typing one step into a chain (the underlying query builder is constructed
103
- * with no schema). Tests cover the runtime semantics.
118
+ * `returning(...)` is the way to get rows out of an insert, and it changes the
119
+ * chain's kind so the types follow.
104
120
  */
105
- export declare interface FluentChain {
106
- where(callback: (eb: import('./types').StacksExpressionBuilder) => unknown): FluentChain
107
- where(...args: any[]): FluentChain
108
- whereNull: (...args: any[]) => FluentChain
109
- whereNotNull: (...args: any[]) => FluentChain
110
- whereIn: (...args: any[]) => FluentChain
111
- whereNotIn: (...args: any[]) => FluentChain
112
- whereLike: (...args: any[]) => FluentChain
113
- whereNotLike: (...args: any[]) => FluentChain
114
- whereILike: (...args: any[]) => FluentChain
115
- whereNotILike: (...args: any[]) => FluentChain
116
- whereBetween: (...args: any[]) => FluentChain
117
- whereNotBetween: (...args: any[]) => FluentChain
118
- whereRaw: (...args: any[]) => FluentChain
119
- whereColumn: (...args: any[]) => FluentChain
120
- orWhere: (...args: any[]) => FluentChain
121
- orWhereNull: (...args: any[]) => FluentChain
122
- orWhereNotNull: (...args: any[]) => FluentChain
123
- orWhereIn: (...args: any[]) => FluentChain
124
- orWhereNotIn: (...args: any[]) => FluentChain
125
- orWhereLike: (...args: any[]) => FluentChain
126
- orWhereNotLike: (...args: any[]) => FluentChain
127
- orWhereILike: (...args: any[]) => FluentChain
128
- orWhereColumn: (...args: any[]) => FluentChain
129
- andWhere: (...args: any[]) => FluentChain
130
- whereAny: (...args: any[]) => FluentChain
131
- whereAll: (...args: any[]) => FluentChain
132
- whereNone: (...args: any[]) => FluentChain
133
- having: (...args: any[]) => FluentChain
134
- groupBy: (...args: any[]) => FluentChain
135
- orderBy: (...args: any[]) => FluentChain
136
- limit: (...args: any[]) => FluentChain
137
- offset: (...args: any[]) => FluentChain
138
- select(selection: ((eb: import('./types').StacksExpressionBuilder) => unknown) | ReadonlyArray<string | ((eb: import('./types').StacksExpressionBuilder) => unknown) | unknown>): FluentChain
139
- select(...args: any[]): FluentChain
140
- selectAll: () => FluentChain
141
- selectAllRelations: () => FluentChain
142
- selectRaw: (...args: any[]) => FluentChain
143
- distinct: () => FluentChain
144
- distinctOn: (...args: any[]) => FluentChain
145
- innerJoin: (...args: any[]) => FluentChain
146
- leftJoin: (...args: any[]) => FluentChain
147
- rightJoin: (...args: any[]) => FluentChain
148
- fullJoin: (...args: any[]) => FluentChain
149
- crossJoin: (...args: any[]) => FluentChain
150
- with: (...args: any[]) => FluentChain
151
- union: (...args: any[]) => FluentChain
152
- unionAll: (...args: any[]) => FluentChain
153
- values: (...args: any[]) => FluentChain
154
- set: (...args: any[]) => FluentChain
155
- returning: (...args: any[]) => FluentChain
156
- returningAll: () => FluentChain
157
- onConflict: (...args: any[]) => FluentChain
158
- onDuplicateKeyUpdate: (...args: any[]) => FluentChain
159
- onConflictDoNothing: (...args: any[]) => FluentChain
160
- onDuplicateKeyIgnore: () => FluentChain
161
- forUpdate: () => FluentChain
162
- forShare: () => FluentChain
121
+ export declare interface InsertReceipt {
122
+ insertId?: number | bigint
123
+ numInsertedOrUpdatedRows?: number | bigint
124
+ numAffectedRows?: number | bigint
125
+ affectedRows?: number
126
+ changes?: number
127
+ }
128
+ export declare interface BaseFluentChain<TRow = Record<string, unknown>, TKind extends ChainKind = 'select'> {
129
+ where(callback: (eb: import('./types').StacksExpressionBuilder) => unknown): FluentChain<TRow, TKind>
130
+ where(...args: unknown[]): FluentChain<TRow, TKind>
131
+ whereNull: (...args: unknown[]) => FluentChain<TRow, TKind>
132
+ whereNotNull: (...args: unknown[]) => FluentChain<TRow, TKind>
133
+ whereIn: (...args: unknown[]) => FluentChain<TRow, TKind>
134
+ whereNotIn: (...args: unknown[]) => FluentChain<TRow, TKind>
135
+ whereLike: (...args: unknown[]) => FluentChain<TRow, TKind>
136
+ whereNotLike: (...args: unknown[]) => FluentChain<TRow, TKind>
137
+ whereILike: (...args: unknown[]) => FluentChain<TRow, TKind>
138
+ whereNotILike: (...args: unknown[]) => FluentChain<TRow, TKind>
139
+ whereBetween: (...args: unknown[]) => FluentChain<TRow, TKind>
140
+ whereNotBetween: (...args: unknown[]) => FluentChain<TRow, TKind>
141
+ whereRaw: (...args: unknown[]) => FluentChain<TRow, TKind>
142
+ whereColumn: (...args: unknown[]) => FluentChain<TRow, TKind>
143
+ orWhere: (...args: unknown[]) => FluentChain<TRow, TKind>
144
+ orWhereNull: (...args: unknown[]) => FluentChain<TRow, TKind>
145
+ orWhereNotNull: (...args: unknown[]) => FluentChain<TRow, TKind>
146
+ orWhereIn: (...args: unknown[]) => FluentChain<TRow, TKind>
147
+ orWhereNotIn: (...args: unknown[]) => FluentChain<TRow, TKind>
148
+ orWhereLike: (...args: unknown[]) => FluentChain<TRow, TKind>
149
+ orWhereNotLike: (...args: unknown[]) => FluentChain<TRow, TKind>
150
+ orWhereILike: (...args: unknown[]) => FluentChain<TRow, TKind>
151
+ orWhereColumn: (...args: unknown[]) => FluentChain<TRow, TKind>
152
+ andWhere: (...args: unknown[]) => FluentChain<TRow, TKind>
153
+ whereAny: (...args: unknown[]) => FluentChain<TRow, TKind>
154
+ whereAll: (...args: unknown[]) => FluentChain<TRow, TKind>
155
+ whereNone: (...args: unknown[]) => FluentChain<TRow, TKind>
156
+ having: (...args: unknown[]) => FluentChain<TRow, TKind>
157
+ groupBy: (...args: unknown[]) => FluentChain<TRow, TKind>
158
+ orderBy: (...args: unknown[]) => FluentChain<TRow, TKind>
159
+ limit: (...args: unknown[]) => FluentChain<TRow, TKind>
160
+ offset: (...args: unknown[]) => FluentChain<TRow, TKind>
161
+ select<K extends KnownKeys<TRow> & string>(columns: readonly K[]): FluentChain<Pick<TRow, K>, TKind>
162
+ select(selection: ((eb: import('./types').StacksExpressionBuilder) => unknown) | ReadonlyArray<string | ((eb: import('./types').StacksExpressionBuilder) => unknown) | unknown>): FluentChain<Record<string, unknown>, TKind>
163
+ select(...args: unknown[]): FluentChain<Record<string, unknown>, TKind>
164
+ selectAll: () => FluentChain<TRow, TKind>
165
+ selectAllRelations: () => FluentChain<Record<string, unknown>, TKind>
166
+ selectRaw: (...args: unknown[]) => FluentChain<Record<string, unknown>, TKind>
167
+ distinct: () => FluentChain<TRow, TKind>
168
+ distinctOn: (...args: unknown[]) => FluentChain<TRow, TKind>
169
+ innerJoin: (...args: unknown[]) => FluentChain<Record<string, unknown>, TKind>
170
+ leftJoin: (...args: unknown[]) => FluentChain<Record<string, unknown>, TKind>
171
+ rightJoin: (...args: unknown[]) => FluentChain<Record<string, unknown>, TKind>
172
+ fullJoin: (...args: unknown[]) => FluentChain<Record<string, unknown>, TKind>
173
+ crossJoin: (...args: unknown[]) => FluentChain<Record<string, unknown>, TKind>
174
+ with: (...args: unknown[]) => FluentChain<Record<string, unknown>, TKind>
175
+ union: (...args: unknown[]) => FluentChain<Record<string, unknown>, TKind>
176
+ unionAll: (...args: unknown[]) => FluentChain<Record<string, unknown>, TKind>
177
+ values: (values: Partial<TRow> | ReadonlyArray<Partial<TRow>> | object | readonly object[]) => FluentChain<TRow, TKind>
178
+ set: (values: Partial<TRow> | object) => FluentChain<TRow, TKind>
179
+ returning<K extends KnownKeys<TRow> & string>(columns: readonly K[]): FluentChain<Pick<TRow, K>, 'returning'>
180
+ returning(...args: unknown[]): FluentChain<Record<string, unknown>, 'returning'>
181
+ returningAll: () => FluentChain<TRow, 'returning'>
182
+ onConflict: (...args: unknown[]) => FluentChain<TRow, TKind>
183
+ onDuplicateKeyUpdate: (...args: unknown[]) => FluentChain<TRow, TKind>
184
+ onConflictDoNothing: (...args: unknown[]) => FluentChain<TRow, TKind>
185
+ onDuplicateKeyIgnore: () => FluentChain<TRow, TKind>
186
+ forUpdate: () => FluentChain<TRow, TKind>
187
+ forShare: () => FluentChain<TRow, TKind>
163
188
  toSQL: () => string
164
- execute: () => Promise<any>
165
- executeTakeFirst: () => Promise<any>
166
- executeTakeFirstOrThrow: () => Promise<any>
167
- pluck: (...args: any[]) => Promise<any>
168
- count: (...args: any[]) => Promise<number>
169
- sum: (...args: any[]) => Promise<number>
170
- avg: (...args: any[]) => Promise<number>
171
- min: (...args: any[]) => Promise<any>
172
- max: (...args: any[]) => Promise<any>
189
+ execute: () => Promise<ResultOf<TRow, TKind>>
190
+ executeTakeFirst: () => Promise<FirstOf<TRow, TKind>>
191
+ executeTakeFirstOrThrow: () => Promise<NonNullable<FirstOf<TRow, TKind>>>
192
+ pluck: (...args: unknown[]) => Promise<unknown[]>
193
+ count: (...args: unknown[]) => Promise<number>
194
+ sum: (...args: unknown[]) => Promise<number>
195
+ avg: (...args: unknown[]) => Promise<number>
196
+ min: (...args: unknown[]) => Promise<unknown>
197
+ max: (...args: unknown[]) => Promise<unknown>
173
198
  exists: () => Promise<boolean>
174
199
  doesntExist: () => Promise<boolean>
175
- $call: (callback: (query: FluentChain) => FluentChain) => FluentChain
176
- [key: string]: any
200
+ $call: (callback: (query: FluentChain<TRow, TKind>) => FluentChain<TRow, TKind>) => FluentChain<TRow, TKind>
177
201
  }
178
202
  /*.ts` and
179
203
  * emits `database/types.d.ts` containing:
@@ -197,20 +221,83 @@ export declare interface FluentChain {
197
221
  export declare interface DatabaseSchema {}
198
222
  declare interface Db extends Pick<Required<RawQueryBuilder>, GenericPassthroughKeys> {
199
223
  fn: import('./types').ExpressionFunctions
200
- selectFrom: (table: TableName) => FluentChain
201
- insertInto: (table: TableName) => FluentChain
202
- updateTable: (table: TableName) => FluentChain
203
- deleteFrom: (table: TableName) => FluentChain
204
- table: (table: TableName) => FluentChain
205
- selectFromSub: (sub: any, alias: string) => FluentChain
206
- select: (table: TableName, ...columns: string[]) => FluentChain
207
- unsafe: (query: string, params?: any[]) => UnsafeReturn
224
+ selectFrom: <T extends TableName>(table: T) => FluentChain<RowOf<T>, 'select'>
225
+ insertInto: <T extends TableName>(table: T) => FluentChain<RowOf<T>, 'insert'>
226
+ updateTable: <T extends TableName>(table: T) => FluentChain<RowOf<T>, 'update'>
227
+ deleteFrom: <T extends TableName>(table: T) => FluentChain<RowOf<T>, 'delete'>
228
+ table: <T extends TableName>(table: T) => FluentChain<RowOf<T>, 'select'>
229
+ selectFromSub: (sub: unknown, alias: string) => FluentChain<Record<string, unknown>>
230
+ select: <T extends TableName>(table: T, ...columns: string[]) => FluentChain<Record<string, unknown>>
231
+ unsafe: (query: string, params?: unknown[]) => UnsafeReturn
208
232
  read: Omit<Db, 'read'>
209
233
  }
210
234
  // The bun-query-builder types `unsafe()` as returning `Promise<any>`, but at
211
235
  // runtime it returns a Bun SQL Statement that has `.execute()`. This interface
212
236
  // corrects the return type so callers can chain `.execute()` without type errors.
213
237
  declare type UnsafeReturn = Promise<any> & { execute: () => Promise<any> }
238
+ /**
239
+ * The keys a row type actually declares, or `never` for a loose record.
240
+ *
241
+ * `keyof Record<string, unknown>` is `string`, so a narrowing overload written
242
+ * against it will happily accept *anything* as a column - including
243
+ * `'menu_items.id as id'`, which then becomes a property name in the result
244
+ * type. That is worse than not narrowing: the row type looks specific and every
245
+ * key in it is fiction.
246
+ */
247
+ export type KnownKeys<T> = string extends keyof T ? never : keyof T;
248
+ /**
249
+ * Which verb started a chain, so its terminals can answer the right thing.
250
+ *
251
+ * `returning` is its own kind rather than a flag on the others: a mutation with
252
+ * `RETURNING` answers rows, and that is the difference between reading
253
+ * `rows[0].id` and reading a count.
254
+ */
255
+ export type ChainKind = 'select' | 'insert' | 'update' | 'delete' | 'returning';
256
+ /** What `execute()` resolves to for each verb. */
257
+ export type ResultOf<TRow, TKind extends ChainKind> = TKind extends 'select' | 'returning'
258
+ ? TRow[]
259
+ : number;
260
+ /**
261
+ * What `executeTakeFirst()` resolves to for each verb.
262
+ *
263
+ * The counts are *required*, because the runtime always sets them: an update
264
+ * that changed nothing answers `{ numUpdatedRows: 0 }`. Declaring them optional
265
+ * would make every caller write `?? 0` for a case that cannot happen.
266
+ */
267
+ export type FirstOf<TRow, TKind extends ChainKind> = TKind extends 'select' | 'returning'
268
+ ? TRow | undefined
269
+ : TKind extends 'insert'
270
+ ? InsertReceipt | undefined
271
+ : TKind extends 'update'
272
+ ? { numUpdatedRows: number }
273
+ : { numDeletedRows: number }
274
+ /** `created_at` -> `CreatedAt`, for the dynamic helper names below. */
275
+ declare type SnakeToPascal<S extends string> = S extends `${infer Head}_${infer Tail}`
276
+ ? `${Capitalize<Head>}${SnakeToPascal<Tail>}`
277
+ : Capitalize<S>;
278
+ /**
279
+ * The dynamic `where<Column>` helpers bun-query-builder generates.
280
+ *
281
+ * Derived from the row type rather than allowed by an index signature. The
282
+ * index signature that used to be here (`[key: string]: any`) made every
283
+ * misspelling legal and every result `any`: `whereHndle('a')` compiled, and so
284
+ * did reading a property that does not exist.
285
+ */
286
+ export type DynamicWhereMethods<TRow, TKind extends ChainKind = 'select'> = {
287
+ [K in keyof TRow & string as `where${SnakeToPascal<K>}`]: (value: TRow[K]) => FluentChain<TRow, TKind>
288
+ } & {
289
+ [K in keyof TRow & string as `orWhere${SnakeToPascal<K>}`]: (value: TRow[K]) => FluentChain<TRow, TKind>
290
+ } & {
291
+ [K in keyof TRow & string as `andWhere${SnakeToPascal<K>}`]: (value: TRow[K]) => FluentChain<TRow, TKind>
292
+ }
293
+ /**
294
+ * A chain over one table's rows: the methods, plus the generated helpers.
295
+ *
296
+ * A type alias rather than an interface because the helper half is a mapped
297
+ * type over `TRow`, and an interface can only extend members TypeScript knows
298
+ * statically.
299
+ */
300
+ export type FluentChain<TRow = Record<string, unknown>, TKind extends ChainKind = 'select'> = BaseFluentChain<TRow, TKind> & DynamicWhereMethods<TRow, TKind>;
214
301
  /**
215
302
  * Top-level surface of the lazy `db` proxy. Methods that return a chainable
216
303
  * builder are typed as `FluentChain` to flatten the optional-method noise
@@ -261,7 +348,28 @@ declare type GenericPassthroughKeys = | 'transaction'
261
348
  * well-documented LiteralUnion trick.
262
349
  */
263
350
  // eslint-disable-next-line ts/no-empty-object-type
264
- export type TableName = (keyof DatabaseSchema & string) | (string & {});
351
+ export type TableName = (keyof DatabaseSchema & string) | (keyof FrameworkSchema & string) | (string & {});
352
+ /**
353
+ * The row type of a registered table, or an unknown-valued record.
354
+ *
355
+ * A table the generated `DatabaseSchema` knows answers its own columns. One it
356
+ * does not - an app that has never run `buddy generate:db-types`, or a table
357
+ * that lives outside a model - answers `Record<string, unknown>`: still a value
358
+ * the caller narrows, but narrowing it is checked rather than waved through.
359
+ */
360
+ export type RowOf<T extends TableName> = T extends keyof DatabaseSchema
361
+ ? Shape<DatabaseSchema[T]>
362
+ : T extends keyof FrameworkSchema
363
+ ? Shape<FrameworkSchema[T]>
364
+ : Record<string, unknown>;
365
+ /**
366
+ * A generated entry, whichever of the two shapes it was written in.
367
+ *
368
+ * The app generator has emitted a flat column record for a while; the
369
+ * `{ columns }` form is what the query builder's own schema type uses. Both are
370
+ * accepted so an app does not have to regenerate to keep compiling.
371
+ */
372
+ declare type Shape<T> = T extends { columns: infer C } ? C : T;
265
373
  // SQLite bootstrap pragmas (stacksjs/stacks#1951) now live in
266
374
  // @stacksjs/query-builder — the one chokepoint every framework
267
375
  // query-builder instance is created through — so EVERY fresh sqlite
package/dist/utils.js CHANGED
@@ -1 +1 @@
1
- import{AsyncLocalStorage}from"node:async_hooks";import{createQueryBuilder,registerPersistentQueryHooks,resetConnection as resetQueryBuilderConnection,setConfig}from"@stacksjs/query-builder";import{SQL}from"bun";import{env as envVars}from"@stacksjs/env";import{getConnectionDefaults}from"./defaults";import{isMysqlWire,isVitessSharded,toQueryBuilderDialect}from"./dialect";import{relativeMigrationDirectory,resolveMigrationDirectory}from"./migration-path";import{contextInTransaction,markContextWrote,resolveReplicaConnection,selectReplica,shouldRouteToReplica,withTransactionContext}from"./replicas";import{aggregateFunctions}from"./types";const sqliteDefaults=getConnectionDefaults("sqlite",envVars),mysqlDefaults=getConnectionDefaults("mysql",envVars),postgresDefaults=getConnectionDefaults("postgres",envVars);let appEnv=envVars.APP_ENV||"local",dbDriver=envVars.DB_CONNECTION||"sqlite",dbConfig={connections:{sqlite:{database:sqliteDefaults.database,prefix:""},mysql:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},singlestore:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},vitess:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:15306,prefix:"",sharded:isVitessSharded()},postgres:{name:postgresDefaults.database,host:postgresDefaults.host,username:postgresDefaults.username,password:postgresDefaults.password,port:postgresDefaults.port,prefix:""}}},dbConfigLockTail=Promise.resolve();export function acquireDbConfigLock(){let release=()=>{};const held=new Promise((resolve)=>{release=resolve}),acquired=dbConfigLockTail.then(()=>release);dbConfigLockTail=dbConfigLockTail.then(()=>held);return acquired}export function initializeDbConfig(config){if(config?.app?.env)appEnv=config.app.env;if(config?.database?.default)dbDriver=config.database.default;if(config?.database)dbConfig=config.database;updateQueryBuilderConfig();_dbInstance=null;_replicaInstances=new Map}function getEnv(){return appEnv}function getDriver(){return dbDriver}function getDatabaseConfig(){return dbConfig}function getDialect(){return toQueryBuilderDialect(getDriver())}function getDbConfig(){const driver=getDriver(),database=getDatabaseConfig(),env=getEnv();if(driver==="sqlite"){const defaultName=env!=="testing"?"database/stacks.sqlite":"database/stacks_testing.sqlite";return{database:database.connections?.sqlite?.database??defaultName}}if(driver==="mysql")return{database:database.connections?.mysql?.name||"stacks",host:database.connections?.mysql?.host??"127.0.0.1",username:database.connections?.mysql?.username??"root",password:database.connections?.mysql?.password??"",port:database.connections?.mysql?.port??3306};if(driver==="singlestore")return{database:database.connections?.singlestore?.name||"stacks",host:database.connections?.singlestore?.host??"127.0.0.1",username:database.connections?.singlestore?.username??"root",password:database.connections?.singlestore?.password??"",port:database.connections?.singlestore?.port??3306};if(driver==="vitess")return{database:database.connections?.vitess?.name||"stacks",host:database.connections?.vitess?.host??"127.0.0.1",username:database.connections?.vitess?.username??"root",password:database.connections?.vitess?.password??"",port:database.connections?.vitess?.port??15306};if(driver==="postgres"){const dbName=database.connections?.postgres?.name??"stacks";return{database:env==="testing"?`${dbName}_testing`:dbName,host:database.connections?.postgres?.host??"127.0.0.1",username:database.connections?.postgres?.username??"",password:database.connections?.postgres?.password??"",port:database.connections?.postgres?.port??5432}}return{database:":memory:"}}export const QB_SNAPSHOT_DIR="storage/framework/database",RAW_QUERY_SOFT_DELETE_CONFIG={enabled:!1,column:"deleted_at",defaultFilter:!0};export function createDatabaseQueryHooks(dispatch){function forward(event){try{Promise.resolve(dispatch(event)).catch(()=>{})}catch{}}return{onQueryEnd:(event)=>forward({query:{sql:event.sql,parameters:event.params},queryDurationMillis:event.durationMs}),onQueryError:(event)=>forward({query:{sql:event.sql,parameters:event.params},queryDurationMillis:event.durationMs,error:event.error})}}function forwardDatabaseQuery(event){import("./query-logger").then(({logQuery})=>logQuery(event)).catch(()=>{})}registerPersistentQueryHooks(createDatabaseQueryHooks(forwardDatabaseQuery));function getPoolConfig(){const driver=getDriver();if(driver==="sqlite")return;return getDatabaseConfig().connections?.[driver]?.pool}function getReplicas(){const driver=getDriver();if(driver==="sqlite")return[];return getDatabaseConfig().connections?.[driver]?.replicas??[]}function getReadPolicy(){return getDatabaseConfig().reads??{}}function toBunPoolOptions(pool){if(!pool)return{};const options={};if(pool.max!==void 0)options.max=pool.max;if(pool.idleTimeoutMs!==void 0)options.idleTimeout=Math.round(pool.idleTimeoutMs/1000);if(pool.acquireTimeoutMs!==void 0)options.connectionTimeout=Math.round(pool.acquireTimeoutMs/1000);if(pool.maxLifetimeMs!==void 0)options.maxLifetime=Math.round(pool.maxLifetimeMs/1000);return options}function updateQueryBuilderConfig(){const dialect=getDialect(),dbConfigForQb=getDbConfig(),pool=getPoolConfig();setConfig({dialect,vitess:{sharded:isVitessSharded(dbConfig.connections.vitess?.sharded)},database:pool?{...dbConfigForQb,pool}:dbConfigForQb,verbose:getEnv()!=="production",snapshotDir:QB_SNAPSHOT_DIR,migrationDir:relativeMigrationDirectory(resolveMigrationDirectory(toQueryBuilderDialect(dialect),{snapshotDir:QB_SNAPSHOT_DIR})),timestamps:{createdAt:"created_at",updatedAt:"updated_at",defaultOrderColumn:"created_at"},softDeletes:RAW_QUERY_SOFT_DELETE_CONFIG})}updateQueryBuilderConfig();let _dbInstance=null,_configInitPromise=null;function ensureConfigLoaded(){if(!_configInitPromise)_configInitPromise=(async()=>{try{const{config,overridesReady}=await import("@stacksjs/config");await overridesReady;if(config){initializeDbConfig(config);_dbInstance=null}}catch{}})();return _configInitPromise}export async function ensureDatabaseConfigLoaded(){await ensureConfigLoaded()}export{applySqlitePragmas,SQLITE_BOOTSTRAP_PRAGMAS}from"@stacksjs/query-builder";const sqliteTxOwner=new AsyncLocalStorage;let sqliteTxTail=Promise.resolve();function serializeSqliteTransaction(run){if(sqliteTxOwner.getStore())return run();const result=sqliteTxTail.then(()=>sqliteTxOwner.run(!0,run));sqliteTxTail=result.then(()=>{return},()=>{return});return result}function applySqliteTransactionSerialization(instance){const original=instance.transaction.bind(instance);instance.transaction=(...args)=>serializeSqliteTransaction(()=>original(...args))}function applyTransactionRoutingContext(instance){const original=instance.transaction.bind(instance);instance.transaction=(...args)=>withTransactionContext(()=>original(...args))}function getDb(){if(!_dbInstance){updateQueryBuilderConfig();_dbInstance=createQueryBuilder();if(getDialect()==="sqlite")applySqliteTransactionSerialization(_dbInstance);applyTransactionRoutingContext(_dbInstance)}return _dbInstance}let _replicaInstances=new Map;export function resetDatabaseConnection(){resetQueryBuilderConnection();_dbInstance=null;_replicaInstances=new Map}function getReplicaDb(replica){const primary=getDbConfig(),resolved=resolveReplicaConnection(replica,primary),key=`${resolved.host}:${resolved.port??""}`,cached=_replicaInstances.get(key);if(cached)return cached;const scheme=isMysqlWire(getDriver())?"mysql":"postgres",auth=resolved.username?`${encodeURIComponent(resolved.username)}:${encodeURIComponent(resolved.password??"")}@`:"",url=`${scheme}://${auth}${resolved.host}:${resolved.port}/${resolved.database}`,sql=new SQL({url,...toBunPoolOptions(getPoolConfig())}),instance=createQueryBuilder({sql});_replicaInstances.set(key,instance);return instance}function getReadDb(){const replicas=getReplicas(),policy=getReadPolicy();if(!shouldRouteToReplica({policy,replicas}))return getDb();const replica=selectReplica(replicas,policy.strategy);return replica?getReplicaDb(replica):getDb()}function getExplicitReadDb(){const replicas=getReplicas();if(!replicas.length||contextInTransaction())return getDb();const replica=selectReplica(replicas,getReadPolicy().strategy);return replica?getReplicaDb(replica):getDb()}const WRITE_ENTRY_POINTS=new Set(["insertInto","updateTable","deleteFrom","create","createMany","insertOrIgnore","insertGetId","updateOrInsert","upsert"]),READ_ENTRY_POINTS=new Set(["selectFrom","selectFromSub","select"]);ensureConfigLoaded();export const db=new Proxy({},{get(_target,prop){if(prop==="fn")return aggregateFunctions;if(prop==="read")return readDb;if(typeof prop==="string"&&WRITE_ENTRY_POINTS.has(prop))markContextWrote();const instance=typeof prop==="string"&&READ_ENTRY_POINTS.has(prop)?getReadDb():getDb(),value=instance[prop];if(typeof value==="function")return value.bind(instance);return value}}),readDb=new Proxy({},{get(_target,prop){if(prop==="fn")return aggregateFunctions;const instance=getExplicitReadDb(),value=instance[prop];if(typeof value==="function")return value.bind(instance);return value}});export{setConfig};
1
+ import{AsyncLocalStorage}from"node:async_hooks";import{createQueryBuilder,registerPersistentQueryHooks,resetConnection as resetQueryBuilderConnection,setConfig}from"@stacksjs/query-builder";import{SQL}from"bun";import{env as envVars}from"@stacksjs/env";import{getConnectionDefaults}from"./defaults";import{isMysqlWire,isVitessSharded,toQueryBuilderDialect}from"./dialect";import{relativeMigrationDirectory,resolveMigrationDirectory}from"./migration-path";import{contextInTransaction,markContextWrote,resolveReplicaConnection,selectReplica,shouldRouteToReplica,withTransactionContext}from"./replicas";import{aggregateFunctions}from"./types";const sqliteDefaults=getConnectionDefaults("sqlite",envVars),mysqlDefaults=getConnectionDefaults("mysql",envVars),postgresDefaults=getConnectionDefaults("postgres",envVars);let appEnv=envVars.APP_ENV||"local",dbDriver=envVars.DB_CONNECTION||"sqlite",dbConfig={connections:{sqlite:{database:sqliteDefaults.database,prefix:""},mysql:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},singlestore:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},vitess:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:15306,prefix:"",sharded:isVitessSharded()},postgres:{name:postgresDefaults.database,host:postgresDefaults.host,username:postgresDefaults.username,password:postgresDefaults.password,port:postgresDefaults.port,prefix:""}}},dbConfigLockTail=Promise.resolve();export function acquireDbConfigLock(){let release=()=>{};const held=new Promise((resolve)=>{release=resolve}),acquired=dbConfigLockTail.then(()=>release);dbConfigLockTail=dbConfigLockTail.then(()=>held);return acquired}export function initializeDbConfig(config){if(config?.app?.env)appEnv=config.app.env;if(config?.database?.default)dbDriver=config.database.default;if(config?.database)dbConfig=config.database;updateQueryBuilderConfig();_dbInstance=null;_replicaInstances=new Map}function getEnv(){return appEnv}function getDriver(){return dbDriver}function getDatabaseConfig(){return dbConfig}function getDialect(){return toQueryBuilderDialect(getDriver())}function getDbConfig(){const driver=getDriver(),database=getDatabaseConfig(),env=getEnv();if(driver==="sqlite"){const defaultName=env!=="testing"?"database/stacks.sqlite":"database/stacks_testing.sqlite";return{database:database.connections?.sqlite?.database??defaultName}}if(driver==="mysql")return{database:database.connections?.mysql?.name||"stacks",host:database.connections?.mysql?.host??"127.0.0.1",username:database.connections?.mysql?.username??"root",password:database.connections?.mysql?.password??"",port:database.connections?.mysql?.port??3306};if(driver==="singlestore")return{database:database.connections?.singlestore?.name||"stacks",host:database.connections?.singlestore?.host??"127.0.0.1",username:database.connections?.singlestore?.username??"root",password:database.connections?.singlestore?.password??"",port:database.connections?.singlestore?.port??3306};if(driver==="vitess")return{database:database.connections?.vitess?.name||"stacks",host:database.connections?.vitess?.host??"127.0.0.1",username:database.connections?.vitess?.username??"root",password:database.connections?.vitess?.password??"",port:database.connections?.vitess?.port??15306};if(driver==="postgres"){const dbName=database.connections?.postgres?.name??"stacks";return{database:env==="testing"?`${dbName}_testing`:dbName,host:database.connections?.postgres?.host??"127.0.0.1",username:database.connections?.postgres?.username??"",password:database.connections?.postgres?.password??"",port:database.connections?.postgres?.port??5432}}return{database:":memory:"}}export const QB_SNAPSHOT_DIR="storage/framework/database",RAW_QUERY_SOFT_DELETE_CONFIG={enabled:!1,column:"deleted_at",defaultFilter:!0};export function createDatabaseQueryHooks(dispatch){function forward(event){try{Promise.resolve(dispatch(event)).catch(()=>{})}catch{}}return{onQueryEnd:(event)=>forward({query:{sql:event.sql,parameters:event.params},queryDurationMillis:event.durationMs}),onQueryError:(event)=>forward({query:{sql:event.sql,parameters:event.params},queryDurationMillis:event.durationMs,error:event.error})}}function forwardDatabaseQuery(event){import("./query-logger").then(({logQuery})=>logQuery(event)).catch(()=>{})}registerPersistentQueryHooks(createDatabaseQueryHooks(forwardDatabaseQuery));function getPoolConfig(){const driver=getDriver();if(driver==="sqlite")return;return getDatabaseConfig().connections?.[driver]?.pool}function getReplicas(){const driver=getDriver();if(driver==="sqlite")return[];return getDatabaseConfig().connections?.[driver]?.replicas??[]}function getReadPolicy(){return getDatabaseConfig().reads??{}}function toBunPoolOptions(pool){if(!pool)return{};const options={};if(pool.max!==void 0)options.max=pool.max;if(pool.idleTimeoutMs!==void 0)options.idleTimeout=Math.round(pool.idleTimeoutMs/1000);if(pool.acquireTimeoutMs!==void 0)options.connectionTimeout=Math.round(pool.acquireTimeoutMs/1000);if(pool.maxLifetimeMs!==void 0)options.maxLifetime=Math.round(pool.maxLifetimeMs/1000);return options}function updateQueryBuilderConfig(){const dialect=getDialect(),dbConfigForQb=getDbConfig(),pool=getPoolConfig();setConfig({dialect,vitess:{sharded:isVitessSharded(dbConfig.connections.vitess?.sharded)},database:pool?{...dbConfigForQb,pool}:dbConfigForQb,verbose:getEnv()!=="production",snapshotDir:QB_SNAPSHOT_DIR,migrationDir:relativeMigrationDirectory(resolveMigrationDirectory(toQueryBuilderDialect(dialect),{snapshotDir:QB_SNAPSHOT_DIR})),timestamps:{createdAt:"created_at",updatedAt:"updated_at",defaultOrderColumn:"created_at"},softDeletes:RAW_QUERY_SOFT_DELETE_CONFIG})}updateQueryBuilderConfig();let _dbInstance=null,_configInitPromise=null;function ensureConfigLoaded(){if(!_configInitPromise)_configInitPromise=(async()=>{try{const{config,overridesReady}=await import("@stacksjs/config");await overridesReady;if(config){initializeDbConfig(config);_dbInstance=null}}catch{}})();return _configInitPromise}export async function ensureDatabaseConfigLoaded(){await ensureConfigLoaded()}export{applySqlitePragmas,SQLITE_BOOTSTRAP_PRAGMAS}from"@stacksjs/query-builder";const sqliteTxOwner=new AsyncLocalStorage;let sqliteTxTail=Promise.resolve();function serializeSqliteTransaction(run){if(sqliteTxOwner.getStore())return run();const result=sqliteTxTail.then(()=>sqliteTxOwner.run(!0,run));sqliteTxTail=result.then(()=>{return},()=>{return});return result}function applySqliteTransactionSerialization(instance){const original=instance.transaction.bind(instance);instance.transaction=(...args)=>serializeSqliteTransaction(()=>original(...args))}function applyTransactionRoutingContext(instance){const original=instance.transaction.bind(instance);instance.transaction=(...args)=>withTransactionContext(()=>original(...args))}function getDb(){if(!_dbInstance){updateQueryBuilderConfig();_dbInstance=createQueryBuilder();if(getDialect()==="sqlite")applySqliteTransactionSerialization(_dbInstance);applyTransactionRoutingContext(_dbInstance)}return _dbInstance}let _replicaInstances=new Map;export function resetDatabaseConnection(){resetQueryBuilderConnection();_dbInstance=null;_replicaInstances=new Map}function getReplicaDb(replica){const primary=getDbConfig(),resolved=resolveReplicaConnection(replica,primary),key=`${resolved.host}:${resolved.port??""}`,cached=_replicaInstances.get(key);if(cached)return cached;const scheme=isMysqlWire(getDriver())?"mysql":"postgres",auth=resolved.username?`${encodeURIComponent(resolved.username)}:${encodeURIComponent(resolved.password??"")}@`:"",url=`${scheme}://${auth}${resolved.host}:${resolved.port}/${resolved.database}`,sql=new SQL({url,...toBunPoolOptions(getPoolConfig())}),instance=createQueryBuilder({sql});_replicaInstances.set(key,instance);return instance}function getReadDb(){const replicas=getReplicas(),policy=getReadPolicy();if(!shouldRouteToReplica({policy,replicas}))return getDb();const replica=selectReplica(replicas,policy.strategy);return replica?getReplicaDb(replica):getDb()}function getExplicitReadDb(){const replicas=getReplicas();if(!replicas.length||contextInTransaction())return getDb();const replica=selectReplica(replicas,getReadPolicy().strategy);return replica?getReplicaDb(replica):getDb()}const WRITE_ENTRY_POINTS=new Set(["insertInto","updateTable","deleteFrom","create","createMany","insertOrIgnore","insertGetId","updateOrInsert","upsert"]),READ_ENTRY_POINTS=new Set(["selectFrom","selectFromSub","select"]);ensureConfigLoaded();export function asRows(rows){return rows}export function asRow(row){return row}export const db=new Proxy({},{get(_target,prop){if(prop==="fn")return aggregateFunctions;if(prop==="read")return readDb;if(typeof prop==="string"&&WRITE_ENTRY_POINTS.has(prop))markContextWrote();const instance=typeof prop==="string"&&READ_ENTRY_POINTS.has(prop)?getReadDb():getDb(),value=instance[prop];if(typeof value==="function")return value.bind(instance);return value}}),readDb=new Proxy({},{get(_target,prop){if(prop==="fn")return aggregateFunctions;const instance=getExplicitReadDb(),value=instance[prop];if(typeof value==="function")return value.bind(instance);return value}});export{setConfig};
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/database",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.22",
5
+ "version": "0.72.24",
6
6
  "description": "The Stacks database integration.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -65,15 +65,15 @@
65
65
  "dynamodb-tooling": "^0.3.2"
66
66
  },
67
67
  "devDependencies": {
68
- "@stacksjs/cli": "0.72.22",
69
- "@stacksjs/config": "0.72.22",
70
- "@stacksjs/logging": "0.72.22",
71
- "@stacksjs/router": "0.72.22",
68
+ "@stacksjs/cli": "0.72.24",
69
+ "@stacksjs/config": "0.72.24",
70
+ "@stacksjs/logging": "0.72.24",
71
+ "@stacksjs/router": "0.72.24",
72
72
  "better-dx": "^0.2.23",
73
- "@stacksjs/path": "0.72.22",
74
- "@stacksjs/query-builder": "0.72.22",
75
- "@stacksjs/storage": "0.72.22",
76
- "@stacksjs/strings": "0.72.22",
77
- "@stacksjs/utils": "0.72.22"
73
+ "@stacksjs/path": "0.72.24",
74
+ "@stacksjs/query-builder": "0.72.24",
75
+ "@stacksjs/storage": "0.72.24",
76
+ "@stacksjs/strings": "0.72.24",
77
+ "@stacksjs/utils": "0.72.24"
78
78
  }
79
79
  }