@bakery-framework/orm 1.0.0

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.
@@ -0,0 +1,69 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import { Try } from '@bakery-framework/core/utils'
3
+ import { createDbAdapter, type SQLAdapter } from './adapters'
4
+
5
+ let dbCache: any = null
6
+ let dbPromise: Promise<any> | null = null
7
+ let testDb: any = null
8
+
9
+ /**
10
+ * Test seam. Unit tests that only need a stub adapter should call this instead
11
+ * of `mock.module('./connection', …)` — Bun's module mocks are process-global
12
+ * and are never restored, so one mocking test file silently breaks every later
13
+ * test that needs a real connection.
14
+ */
15
+ export function __setTestDb(db: unknown): void {
16
+ testDb = db
17
+ }
18
+
19
+ export function __resetTestDb(): void {
20
+ testDb = null
21
+ }
22
+
23
+ export function initDB(): Promise<SQLAdapter> {
24
+ if (dbPromise) return dbPromise
25
+
26
+ dbPromise = (async () => {
27
+ dbCache = await createDbAdapter()
28
+ return dbCache
29
+ })().catch(error => {
30
+ // Don't cache the rejection: a transient failure at boot would otherwise
31
+ // make every later initDB() return the same error for the process lifetime,
32
+ // with no way to reconnect short of a restart.
33
+ dbPromise = null
34
+ throw error
35
+ })
36
+
37
+ return dbPromise
38
+ }
39
+
40
+ export async function closeDB(): Promise<void> {
41
+ if (dbCache) {
42
+ await Try.catch(() => dbCache.close())
43
+ dbCache = null
44
+ dbPromise = null
45
+ }
46
+ }
47
+
48
+ function getConn() {
49
+ if (testDb) return testDb
50
+ if (!dbCache) {
51
+ throw new Error("DB not initialized. Call 'await initDB()' first.")
52
+ }
53
+ return dbCache
54
+ }
55
+
56
+ export const connection: SQLAdapter = new Proxy({} as SQLAdapter, {
57
+ // Resolve once per access: this is the hottest path in the ORM, and calling
58
+ // getConn() for both the target and the receiver doubled the work.
59
+ get(_, prop) {
60
+ const conn = getConn()
61
+ return Reflect.get(conn, prop, conn)
62
+ },
63
+ })
64
+
65
+ export const txStorage = new AsyncLocalStorage<any>()
66
+
67
+ export function getActiveDb(): SQLAdapter {
68
+ return txStorage.getStore() ?? connection
69
+ }
package/src/define.ts ADDED
@@ -0,0 +1,380 @@
1
+ import type {
2
+ ExtractOptionals,
3
+ ExtractTableTypes,
4
+ ExtractViews,
5
+ } from './schema-util'
6
+
7
+ /**
8
+ * Prototype: tables as values.
9
+ *
10
+ * Today a schema is one `constraints` object plus a hand-written `DBSchema`
11
+ * type, and everything that references a table — indexes, foreign keys, query
12
+ * columns — does so by string. Typos surface at sync time, or not at all.
13
+ *
14
+ * Here a table is a value carrying its own name and columns, so `indexes.ts`
15
+ * and `foreign.ts` can import it, rename-symbol works across files, and an
16
+ * identifier reaching SQL is one the framework constructed rather than one a
17
+ * caller typed.
18
+ *
19
+ * The column descriptors are unchanged — the `Field` builders and the
20
+ * `ExtractTableTypes` mapping are reused as-is, so this is a restructuring of
21
+ * how tables are *declared*, not of how their types are computed. That is what
22
+ * keeps `DB.table('users')` autocompleting exactly as it does now: the derived
23
+ * `DBSchema` has the same shape the hand-written one had.
24
+ */
25
+
26
+ /** Columns as declared: `{ id: Field.Primary(), name: Field.Text(true) }`. */
27
+ export type ColumnMap = Record<string, unknown>
28
+
29
+ /**
30
+ * A declared table: the name columns qualify against, the table actually read
31
+ * from, and the columns themselves.
32
+ *
33
+ * **Was `TableDef`, and the rename is the point.** `schema-util.ts` exports a
34
+ * `TableDef<T, N, O>` describing a *column* — and that is the one every
35
+ * internal call site, every test and every schema file means. Both were public,
36
+ * and the root barrel re-exported *this* one, so `import type { TableDef } from
37
+ * '@bakery-framework/orm'` handed you a table where you asked for a column. Nothing
38
+ * errored at the import, and nothing errored until you tried to use it.
39
+ *
40
+ * `TableRef` also says what it is, and pairs with `TableColumn` below.
41
+ */
42
+ export interface TableRef<
43
+ N extends string = string,
44
+ C extends ColumnMap = ColumnMap,
45
+ > {
46
+ /** The name columns qualify against — the alias, when aliased. */
47
+ readonly __table: N
48
+ /** The real table to read FROM. Differs from `__table` only for an alias. */
49
+ readonly __source: string
50
+ readonly __columns: C
51
+ }
52
+
53
+ /**
54
+ * A reference to one column, carrying its table. This is the value that
55
+ * replaces the `'users.id'` string — `qId` receives structured data rather
56
+ * than text that has to be validated before it can be trusted.
57
+ */
58
+ export interface TableColumn<
59
+ N extends string = string,
60
+ K extends string = string,
61
+ > {
62
+ readonly __table: N
63
+ readonly __column: K
64
+ }
65
+
66
+ /**
67
+ * Declare a table. The name is explicit rather than inferred from the export
68
+ * binding: deriving it would need a build step or a Proxy, and neither is
69
+ * worth the indirection for one string.
70
+ */
71
+ export function table<N extends string, C extends ColumnMap>(
72
+ name: N,
73
+ columns: C,
74
+ ): TableRef<N, C> & { readonly [K in keyof C]: TableColumn<N, K & string> } {
75
+ const refs = Object.fromEntries(
76
+ Object.keys(columns).map(key => [key, { __table: name, __column: key }]),
77
+ )
78
+
79
+ return Object.assign(Object.create(null), refs, {
80
+ __table: name,
81
+ __source: name,
82
+ __columns: columns,
83
+ })
84
+ }
85
+
86
+ /**
87
+ * Declare a view — a stored `SELECT` the database treats as a table.
88
+ *
89
+ * export const activeUsers = view(
90
+ * 'active_users',
91
+ * 'SELECT id, name FROM users WHERE active = 1',
92
+ * { id: Field.Primary(), name: Field.Varchar(64) },
93
+ * )
94
+ *
95
+ * The columns are the shape the `SELECT` returns. They are declared rather than
96
+ * inferred because nothing here parses SQL, and they are what gives the view a
97
+ * row type — reading from it is typed exactly like reading a table.
98
+ *
99
+ * **Writes are rejected at compile time.** `InferViews` collects these names and
100
+ * `Mutation.Tables` excludes them, so `DB.Insert.into('active_users')` does not
101
+ * typecheck. A view is a `SELECT`; the database would refuse the write anyway,
102
+ * and refusing it earlier is strictly better.
103
+ *
104
+ * `db:sync` emits `CREATE VIEW`, diffs the body as normalised text, and drops
105
+ * and recreates the view when it changes — views hold no data, so recreating is
106
+ * free and there is no migration to plan.
107
+ *
108
+ * Previously declarable only in the older `DBInfo` layout, or here by writing
109
+ * `_view` into a `table()` call and casting. The key is the same; this just
110
+ * types it.
111
+ */
112
+ export function view<N extends string, C extends ColumnMap>(
113
+ name: N,
114
+ body: string,
115
+ columns: C,
116
+ ): TableRef<N, C & { _view: string }> & {
117
+ readonly [K in keyof C]: TableColumn<N, K & string>
118
+ }
119
+ /**
120
+ * A view over one table, borrowing its columns.
121
+ *
122
+ * export const activeUsers = view('active_users', users, 'SELECT * FROM users WHERE active = 1')
123
+ *
124
+ * The filtered-view case, which is the common one: the shape is the source
125
+ * table's, so restating it is duplication that nothing checks — declare a
126
+ * column the `SELECT` does not return and you find out at query time.
127
+ *
128
+ * The source is a **value**, not a type argument, and that is forced rather
129
+ * than chosen. `view<typeof users>(name, body)` cannot work: TypeScript stops
130
+ * inferring the *remaining* type parameters as soon as one is supplied
131
+ * explicitly, so `N` would fall back to `string` and the view's name would stop
132
+ * being a literal. `__table` is what `InferConstraints` keys the schema map on,
133
+ * so that name degrading takes `InferViews` with it — and since
134
+ * `Mutation.Tables` now excludes views, `Exclude<…, string>` is `never` and
135
+ * *every* mutation stops compiling. Passing the table keeps both inferred.
136
+ *
137
+ * Projecting a subset? Use the three-argument form and name the columns.
138
+ */
139
+ export function view<N extends string, C extends ColumnMap>(
140
+ name: N,
141
+ source: TableRef<string, C>,
142
+ body: string,
143
+ ): TableRef<N, C & { _view: string }> & {
144
+ readonly [K in keyof C]: TableColumn<N, K & string>
145
+ }
146
+ /**
147
+ * A view described by an interface rather than by column builders.
148
+ *
149
+ * export interface ActiveUsersView {
150
+ * id: number
151
+ * name: string
152
+ * }
153
+ *
154
+ * export const activeUsers = view<'active_users', ActiveUsersView>(
155
+ * 'active_users',
156
+ * 'SELECT id, name FROM users WHERE active = 1',
157
+ * )
158
+ *
159
+ * This is what `db:sync --choose=db` writes into `orm/views.ts`, and it is the
160
+ * honest shape for a view: **a view has no column DDL.** `CREATE VIEW x AS
161
+ * SELECT …` declares no types, and the sync engine only ever reads the body —
162
+ * `createView(name, sql)` takes nothing else, and the diff compares the two
163
+ * bodies as text. So a view's columns exist purely to give it a row type, and
164
+ * writing `Field.Varchar(64)` there would imply a width the database neither
165
+ * stores nor enforces.
166
+ *
167
+ * **Both type arguments are given, and that is forced.** TypeScript stops
168
+ * inferring the remaining type parameters as soon as one is supplied, so
169
+ * `view<ActiveUsersView>(name, body)` would leave `N` as `string` — and `N` is
170
+ * what `TablesOf` re-keys the schema map on, so the whole map collapses to an
171
+ * index signature and every mutation stops compiling. Naming both keeps it a
172
+ * literal. In generated code the repetition costs nothing.
173
+ *
174
+ * Column references still work — `activeUsers.id` — even though the keys are
175
+ * known only to the type. See the implementation.
176
+ */
177
+ export function view<N extends string, T>(
178
+ name: N,
179
+ body: string,
180
+ ): TableRef<N, ViewColumns<T>> & {
181
+ readonly [K in keyof T]: TableColumn<N, K & string>
182
+ }
183
+ export function view(
184
+ name: string,
185
+ bodyOrSource: string | TableRef,
186
+ columnsOrBody?: ColumnMap | string,
187
+ ): unknown {
188
+ // Two arguments means the interface form: the columns are type-only, so the
189
+ // runtime object carries the body and nothing else.
190
+ if (columnsOrBody === undefined && typeof bodyOrSource === 'string') {
191
+ return viewFromType(name, bodyOrSource)
192
+ }
193
+ // The second argument tells the two forms apart: a `SELECT` string, or the
194
+ // table to borrow columns from.
195
+ const derived = typeof bodyOrSource !== 'string'
196
+ const body = derived ? (columnsOrBody as string) : bodyOrSource
197
+ const columns = derived
198
+ ? (bodyOrSource as TableRef).__columns
199
+ : (columnsOrBody as ColumnMap)
200
+ return viewImpl(name, body, columns)
201
+ }
202
+
203
+ /**
204
+ * A row interface, as the descriptor map `ExtractTableTypes` reads.
205
+ *
206
+ * One `{ type: T[K] }` per property — which is all a descriptor needs now that
207
+ * `type` carries the row type — plus the `_view` marker that makes
208
+ * `ExtractViews` classify it as a view.
209
+ */
210
+ type ViewColumns<T> = { [K in keyof T]-?: { type: T[K] } } & { _view: string }
211
+
212
+ /**
213
+ * The interface form's runtime value.
214
+ *
215
+ * The column keys live only in the type, so the refs cannot be enumerated the
216
+ * way `table()` enumerates them. A `Proxy` answers for any property instead,
217
+ * which is exactly as correct here: a ref is `{ __table, __column }` computed
218
+ * from the key, and the key is whatever was asked for. The type is what
219
+ * restricts *which* keys are askable.
220
+ *
221
+ * `__table`, `__source` and `__columns` are answered from the real object so
222
+ * `collectConstraints` and the sync engine see what they expect.
223
+ */
224
+ function viewFromType(name: string, body: string): unknown {
225
+ const base: Record<string, unknown> = {
226
+ __table: name,
227
+ __source: name,
228
+ __columns: { _view: body },
229
+ }
230
+ return new Proxy(base, {
231
+ get(target, prop) {
232
+ if (typeof prop !== 'string' || prop in target) {
233
+ return Reflect.get(target, prop)
234
+ }
235
+ return { __table: name, __column: prop }
236
+ },
237
+ // Without this the sync engine's `Object.keys`/spread would see the three
238
+ // internals as ordinary columns.
239
+ ownKeys: target => Reflect.ownKeys(target),
240
+ })
241
+ }
242
+
243
+ function viewImpl<N extends string, C extends ColumnMap>(
244
+ name: N,
245
+ body: string,
246
+ columns: C,
247
+ ): TableRef<N, C & { _view: string }> & {
248
+ readonly [K in keyof C]: TableColumn<N, K & string>
249
+ } {
250
+ // `_view` rides inside `__columns` because that is the object the sync engine
251
+ // receives, and it is where `ExtractViews` and the adapters already look for
252
+ // it. Adding a sibling field would mean teaching `collectConstraints`, the
253
+ // diff and three adapters about a second place to check.
254
+ //
255
+ // It has to be in `__columns`'s *declared type* too, not only at runtime:
256
+ // `ExtractViews` is what `InferViews` reads and what `Mutation.Tables`
257
+ // excludes, so erasing `_view` from the type left writes to a view
258
+ // compiling — the exact thing declaring one is supposed to prevent.
259
+ // `ExtractTableTypes` filters the key out of the row type separately.
260
+ return Object.assign(table(name, columns), {
261
+ __columns: { _view: body, ...columns },
262
+ }) as any
263
+ }
264
+
265
+ /**
266
+ * Alias a table for a join, so the same table can appear twice in one query.
267
+ *
268
+ * This is the object-form answer to `join('users.id', ..., 'author')` followed
269
+ * by `'author.username'`. It is not a downgrade: with strings the alias is
270
+ * declared in one place and referenced as text everywhere else, and nothing
271
+ * checks that they agree. Here the alias *is* the value you use, so a typo is
272
+ * a compile error and rename-symbol reaches every usage.
273
+ *
274
+ * Columns re-qualify against the alias while `__source` remembers the real
275
+ * table, which is what lets the builder emit `FROM users AS author`.
276
+ */
277
+ export function alias<N extends string, C extends ColumnMap, A extends string>(
278
+ base: TableRef<N, C>,
279
+ name: A,
280
+ ): TableRef<A, C> & { readonly [K in keyof C]: TableColumn<A, K & string> } {
281
+ const refs = Object.fromEntries(
282
+ Object.keys(base.__columns).map(key => [
283
+ key,
284
+ { __table: name, __column: key },
285
+ ]),
286
+ )
287
+
288
+ return Object.assign(Object.create(null), refs, {
289
+ __table: name,
290
+ __source: base.__source,
291
+ __columns: base.__columns,
292
+ })
293
+ }
294
+
295
+ /** Every `TableRef` exported by a module, keyed by its declared table name. */
296
+ type TablesOf<M> = {
297
+ [K in keyof M as M[K] extends TableRef<infer N, any> ? N : never]: M[K]
298
+ }
299
+
300
+ /**
301
+ * The `constraints` shape the sync engine already consumes, rebuilt from
302
+ * table values. Keeping this identical is what lets the diff engine, the
303
+ * generator and the query builder stay untouched.
304
+ */
305
+ export type InferConstraints<M> = {
306
+ [N in keyof TablesOf<M>]: TablesOf<M>[N] extends TableRef<any, infer C>
307
+ ? C
308
+ : never
309
+ }
310
+
311
+ /** Row types — the derived replacement for a hand-written `DBSchema`. */
312
+ export type InferSchema<M> = {
313
+ [N in keyof InferConstraints<M>]: ExtractTableTypes<InferConstraints<M>, N>
314
+ }
315
+
316
+ /** Columns optional on insert, derived the same way. */
317
+ export type InferOptionals<M> = {
318
+ [N in keyof InferConstraints<M>]: ExtractOptionals<InferConstraints<M>, N>
319
+ }
320
+
321
+ /** View names, for exclusion from mutation targets. */
322
+ export type InferViews<M> = ExtractViews<InferConstraints<M>>
323
+
324
+ /** Collect the runtime constraints object the sync engine loads. */
325
+ export function collectConstraints(module: Record<string, unknown>) {
326
+ const constraints: Record<string, unknown> = {}
327
+
328
+ for (const exported of Object.values(module)) {
329
+ if (!exported || typeof exported !== 'object') continue
330
+ const def = exported as TableRef
331
+ if (typeof def.__table !== 'string' || !def.__columns) continue
332
+
333
+ // Skip aliases. An alias is a query-time view of an existing table, and
334
+ // collecting one would tell the sync engine to CREATE a table named after
335
+ // it — so `alias(users, 'author')` would try to build an `author` table.
336
+ if (def.__source !== def.__table) continue
337
+
338
+ constraints[def.__table] = def.__columns
339
+ }
340
+
341
+ return constraints
342
+ }
343
+
344
+ /**
345
+ * The row type of a `table()` or `view()`, for naming.
346
+ *
347
+ * export type ActiveUsersView = RowOf<typeof activeUsers>
348
+ * // ^ { id: number; name: string }
349
+ *
350
+ * TypeScript cannot mint a *named* interface from a value — a name has to be
351
+ * written somewhere — so this is the one line that does it, and it stays
352
+ * correct when the declaration changes because it is derived rather than
353
+ * copied. A hand-written `interface ActiveUsersView` would be a second source
354
+ * of truth that nothing checks against the first.
355
+ *
356
+ * `_view` is filtered out by `ExtractTableTypes`, so a view's row type is its
357
+ * columns and nothing else.
358
+ */
359
+ export type RowOf<T extends TableRef> = ExtractTableTypes<
360
+ { t: T['__columns'] },
361
+ 't'
362
+ >
363
+
364
+ /**
365
+ * What an `INSERT` into it accepts: {@link RowOf} with the optional columns
366
+ * made optional.
367
+ *
368
+ * export type NewUser = InsertOf<typeof users>
369
+ * // ^ { name: string; id?: number; createdAt?: number }
370
+ */
371
+ export type InsertOf<T extends TableRef> = Omit<
372
+ RowOf<T>,
373
+ ExtractOptionals<{ t: T['__columns'] }, 't'> & keyof RowOf<T>
374
+ > &
375
+ Partial<
376
+ Pick<
377
+ RowOf<T>,
378
+ ExtractOptionals<{ t: T['__columns'] }, 't'> & keyof RowOf<T>
379
+ >
380
+ >