@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.
- package/LICENSE +19 -0
- package/README.md +88 -0
- package/package.json +59 -0
- package/src/adapters/base.ts +1128 -0
- package/src/adapters/mysql.ts +619 -0
- package/src/adapters/observe.ts +261 -0
- package/src/adapters/pgsql.ts +611 -0
- package/src/adapters/registry.ts +204 -0
- package/src/adapters/sqlite.ts +588 -0
- package/src/adapters.ts +72 -0
- package/src/backup.ts +37 -0
- package/src/connection.ts +69 -0
- package/src/define.ts +380 -0
- package/src/field.ts +595 -0
- package/src/globals.d.ts +22 -0
- package/src/index.ts +63 -0
- package/src/orm/index.ts +24 -0
- package/src/orm/mutation.ts +692 -0
- package/src/orm/query.ts +1680 -0
- package/src/pool.ts +83 -0
- package/src/schema-registry.ts +75 -0
- package/src/schema-util.ts +467 -0
- package/src/sync/builder.ts +618 -0
- package/src/sync/engine.ts +399 -0
- package/src/sync/helpers.ts +1119 -0
- package/src/sync/history.ts +169 -0
- package/src/sync/index.ts +113 -0
- package/src/sync/ledger.ts +335 -0
- package/src/sync/load.ts +368 -0
- package/src/sync/rollback.ts +200 -0
- package/src/sync/types.ts +101 -0
- package/src/sync/view-sql.ts +160 -0
- package/templates/schema.example.ts +94 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { Try } from '@bakery-framework/core/utils'
|
|
2
|
+
import type { SQLAdapter } from './base'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Query observability: one callback, called once per executed statement.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately **not** an emitter and **not** a listener array. This sits in
|
|
8
|
+
* the hot path of every statement the ORM runs, so the cost of having the
|
|
9
|
+
* feature at all has to be a single null read when nobody is watching — see
|
|
10
|
+
* `observe()` below, which returns the driver's own value untouched in that
|
|
11
|
+
* case. An emitter would allocate an event object and walk a list per query
|
|
12
|
+
* whether or not anything subscribed.
|
|
13
|
+
*
|
|
14
|
+
* Process-wide rather than per-adapter, on purpose. Every adapter builds a
|
|
15
|
+
* *new* adapter instance per transaction (`SQLiteAdapter.transaction` wraps the
|
|
16
|
+
* transaction handle in a fresh `SQLiteAdapter`), so a hook attached to one
|
|
17
|
+
* instance would go silent for exactly the statements a slow-query panel most
|
|
18
|
+
* wants to see. `driver` is on the event instead, so a process talking to two
|
|
19
|
+
* databases can still tell them apart.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export type QueryMethod = 'all' | 'run' | 'get' | 'values' | 'iterate'
|
|
23
|
+
|
|
24
|
+
export interface QueryEvent {
|
|
25
|
+
/** The statement as handed to the driver, before dialect normalisation. */
|
|
26
|
+
sql: string
|
|
27
|
+
/** Wall-clock duration in milliseconds, fractional. */
|
|
28
|
+
ms: number
|
|
29
|
+
/**
|
|
30
|
+
* Rows the statement produced, or `null` when it cannot be known — a failed
|
|
31
|
+
* statement, or a result shape the adapter does not describe as an array.
|
|
32
|
+
* For `run` this is the *affected* row count, not a result set.
|
|
33
|
+
*/
|
|
34
|
+
rows: number | null
|
|
35
|
+
driver: SQLAdapter.Driver
|
|
36
|
+
/** Which executor entry point ran. See the note on `iterate` in `ms`. */
|
|
37
|
+
method: QueryMethod
|
|
38
|
+
/** `null` when the statement succeeded; the thrown value otherwise. */
|
|
39
|
+
error: unknown
|
|
40
|
+
/**
|
|
41
|
+
* Bound parameter **values**, present only when the observer was registered
|
|
42
|
+
* with `{ params: true }`.
|
|
43
|
+
*
|
|
44
|
+
* Security decision, and the reason this is opt-in with the default off:
|
|
45
|
+
* parameters are user data. A query's bindings routinely hold passwords,
|
|
46
|
+
* session tokens, API keys and personal information, and anything an
|
|
47
|
+
* observer receives is one `logger.info` away from a log file, an analytics
|
|
48
|
+
* table or a dashboard panel. Defaulting this on would turn "add a slow
|
|
49
|
+
* query panel" into a credential leak that nobody reviewed. Callers that
|
|
50
|
+
* genuinely need bindings — a local query profiler, say — must ask, and are
|
|
51
|
+
* then responsible for what they do with them.
|
|
52
|
+
*/
|
|
53
|
+
params?: readonly unknown[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type QueryObserver = (event: QueryEvent) => unknown
|
|
57
|
+
|
|
58
|
+
export interface QueryObserverOptions {
|
|
59
|
+
/**
|
|
60
|
+
* Include bound parameter values on every event. Defaults to `false`.
|
|
61
|
+
* Read the note on `QueryEvent.params` before turning this on.
|
|
62
|
+
*/
|
|
63
|
+
params?: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let observer: QueryObserver | null = null
|
|
67
|
+
let includeParams = false
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Install (or with `null`, remove) the process-wide query observer.
|
|
71
|
+
*
|
|
72
|
+
* Returns a disposer that clears the observer only if it is still the one this
|
|
73
|
+
* call installed, so a test that forgets to restore cannot silently unhook a
|
|
74
|
+
* later one.
|
|
75
|
+
*
|
|
76
|
+
* ```ts no-check — illustrative: the app decides where slow queries go
|
|
77
|
+
* import { setQueryObserver } from '@bakery-framework/orm'
|
|
78
|
+
*
|
|
79
|
+
* setQueryObserver(event => {
|
|
80
|
+
* if (event.ms > 100) slowQueries.push(event)
|
|
81
|
+
* })
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function setQueryObserver(
|
|
85
|
+
next: QueryObserver | null,
|
|
86
|
+
options: QueryObserverOptions = {},
|
|
87
|
+
): () => void {
|
|
88
|
+
observer = next
|
|
89
|
+
includeParams = next ? options.params === true : false
|
|
90
|
+
const installed = next
|
|
91
|
+
return () => {
|
|
92
|
+
if (observer !== installed) return
|
|
93
|
+
observer = null
|
|
94
|
+
includeParams = false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The observer currently installed, if any. For tests and diagnostics.
|
|
100
|
+
*/
|
|
101
|
+
export function getQueryObserver(): QueryObserver | null {
|
|
102
|
+
return observer
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function emit(event: QueryEvent, params: unknown[]): void {
|
|
106
|
+
const fn = observer
|
|
107
|
+
// Re-read rather than trusting the caller's check: an observer may be
|
|
108
|
+
// uninstalled while a query is in flight.
|
|
109
|
+
if (!fn) return
|
|
110
|
+
if (includeParams) event.params = params
|
|
111
|
+
// An observer is application code in the middle of somebody's query. A
|
|
112
|
+
// throwing or rejecting hook is the observer's bug, and it must not become
|
|
113
|
+
// the caller's failed write.
|
|
114
|
+
Try(() => fn(event))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type Exec<R> = (sqlText: string, params?: unknown[]) => Promise<R> | R
|
|
118
|
+
|
|
119
|
+
async function timed<R>(
|
|
120
|
+
driver: SQLAdapter.Driver,
|
|
121
|
+
method: QueryMethod,
|
|
122
|
+
fn: Exec<R>,
|
|
123
|
+
countRows: (result: R) => number | null,
|
|
124
|
+
sqlText: string,
|
|
125
|
+
params: unknown[],
|
|
126
|
+
): Promise<R> {
|
|
127
|
+
const started = performance.now()
|
|
128
|
+
try {
|
|
129
|
+
const result = await fn(sqlText, params)
|
|
130
|
+
emit(
|
|
131
|
+
{
|
|
132
|
+
sql: sqlText,
|
|
133
|
+
ms: performance.now() - started,
|
|
134
|
+
// Called bare, not through `Try`: every `countRows` passed in from
|
|
135
|
+
// `createExecutor` is total by construction, and wrapping it cost more
|
|
136
|
+
// per query than the guard was worth.
|
|
137
|
+
rows: countRows(result),
|
|
138
|
+
driver,
|
|
139
|
+
method,
|
|
140
|
+
error: null,
|
|
141
|
+
},
|
|
142
|
+
params,
|
|
143
|
+
)
|
|
144
|
+
return result
|
|
145
|
+
} catch (error) {
|
|
146
|
+
emit(
|
|
147
|
+
{
|
|
148
|
+
sql: sqlText,
|
|
149
|
+
ms: performance.now() - started,
|
|
150
|
+
rows: null,
|
|
151
|
+
driver,
|
|
152
|
+
method,
|
|
153
|
+
// Normalised so `error` is a reliable "did this throw" signal: a driver
|
|
154
|
+
// that rejects with `undefined` would otherwise be indistinguishable
|
|
155
|
+
// from success.
|
|
156
|
+
error: error ?? new Error('query failed with no error value'),
|
|
157
|
+
},
|
|
158
|
+
params,
|
|
159
|
+
)
|
|
160
|
+
throw error
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Wrap one executor entry point so it reports to the observer.
|
|
166
|
+
*
|
|
167
|
+
* The unobserved path is the `if` below and nothing else: no timer read, no
|
|
168
|
+
* event object, and the underlying call's own return value — which for `all`
|
|
169
|
+
* and `run` may legitimately be synchronous — passes straight through.
|
|
170
|
+
*/
|
|
171
|
+
export function observe<R>(
|
|
172
|
+
driver: SQLAdapter.Driver,
|
|
173
|
+
method: QueryMethod,
|
|
174
|
+
fn: (sqlText: string, params?: unknown[]) => Promise<R>,
|
|
175
|
+
countRows: (result: R) => number | null,
|
|
176
|
+
): (sqlText: string, params?: unknown[]) => Promise<R>
|
|
177
|
+
export function observe<R>(
|
|
178
|
+
driver: SQLAdapter.Driver,
|
|
179
|
+
method: QueryMethod,
|
|
180
|
+
fn: Exec<R>,
|
|
181
|
+
countRows: (result: R) => number | null,
|
|
182
|
+
): (sqlText: string, params?: unknown[]) => Promise<R> | R
|
|
183
|
+
// Two overloads because the executor's five entry points are not one shape.
|
|
184
|
+
// `get` and `values` are declared to return a promise unconditionally, while
|
|
185
|
+
// `all` and `run` may legitimately be synchronous — and the implementation
|
|
186
|
+
// signature, which has to admit both, cannot narrow to the former on its own.
|
|
187
|
+
// Without the first overload `createExecutor` fails to satisfy `Executor`.
|
|
188
|
+
export function observe<R>(
|
|
189
|
+
driver: SQLAdapter.Driver,
|
|
190
|
+
method: QueryMethod,
|
|
191
|
+
fn: Exec<R>,
|
|
192
|
+
countRows: (result: R) => number | null,
|
|
193
|
+
): (sqlText: string, params?: unknown[]) => Promise<R> | R {
|
|
194
|
+
return (sqlText: string, params: unknown[] = []) => {
|
|
195
|
+
if (!observer) return fn(sqlText, params)
|
|
196
|
+
return timed(driver, method, fn, countRows, sqlText, params)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* `iterate` is a stream, so its duration means something different from the
|
|
202
|
+
* other four, and the choice here is deliberate:
|
|
203
|
+
*
|
|
204
|
+
* - `ms` spans from the call to `iterate()` until iteration **ends** — the
|
|
205
|
+
* generator is exhausted, throws, or the consumer breaks out of the loop.
|
|
206
|
+
* That includes whatever the consumer did between yields, so an `iterate`
|
|
207
|
+
* event is not comparable with an `all` event and must not be averaged into
|
|
208
|
+
* the same "query time" number. It is still the useful measure: for a stream,
|
|
209
|
+
* the interesting quantity is how long the statement held a cursor open.
|
|
210
|
+
* - `rows` counts rows **consumed**, not rows matched. A consumer that breaks
|
|
211
|
+
* after ten rows of a million-row scan reports ten.
|
|
212
|
+
* - Exactly one event, emitted from `finally`, so an early `break` (which
|
|
213
|
+
* resumes the generator with a return completion, skipping `catch`) still
|
|
214
|
+
* reports.
|
|
215
|
+
*/
|
|
216
|
+
async function* drain(
|
|
217
|
+
driver: SQLAdapter.Driver,
|
|
218
|
+
source: AsyncIterable<SQLAdapter.RowRecord> | Iterable<SQLAdapter.RowRecord>,
|
|
219
|
+
sqlText: string,
|
|
220
|
+
params: unknown[],
|
|
221
|
+
started: number,
|
|
222
|
+
): AsyncIterable<SQLAdapter.RowRecord> {
|
|
223
|
+
let rows = 0
|
|
224
|
+
let error: unknown = null
|
|
225
|
+
try {
|
|
226
|
+
for await (const row of source) {
|
|
227
|
+
rows++
|
|
228
|
+
yield row
|
|
229
|
+
}
|
|
230
|
+
} catch (err) {
|
|
231
|
+
error = err ?? new Error('iteration failed with no error value')
|
|
232
|
+
throw err
|
|
233
|
+
} finally {
|
|
234
|
+
emit(
|
|
235
|
+
{
|
|
236
|
+
sql: sqlText,
|
|
237
|
+
ms: performance.now() - started,
|
|
238
|
+
rows,
|
|
239
|
+
driver,
|
|
240
|
+
method: 'iterate',
|
|
241
|
+
error,
|
|
242
|
+
},
|
|
243
|
+
params,
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function observeIterate(
|
|
249
|
+
driver: SQLAdapter.Driver,
|
|
250
|
+
iterate: SQLAdapter.Executor['iterate'],
|
|
251
|
+
): SQLAdapter.Executor['iterate'] {
|
|
252
|
+
return (sqlText: string, params: unknown[] = []) => {
|
|
253
|
+
if (!observer) return iterate(sqlText, params)
|
|
254
|
+
// The source is created here rather than inside `drain`, and `started` with
|
|
255
|
+
// it: an async generator body does not run until the first `next()`, so
|
|
256
|
+
// both would otherwise be deferred to whenever the consumer got round to
|
|
257
|
+
// pulling — and the statement would be issued later than it is today.
|
|
258
|
+
const started = performance.now()
|
|
259
|
+
return drain(driver, iterate(sqlText, params), sqlText, params, started)
|
|
260
|
+
}
|
|
261
|
+
}
|