@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,204 @@
|
|
|
1
|
+
import type { PoolOptions } from '../pool'
|
|
2
|
+
import type { SQLAdapter } from './base'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Every driver name the ORM knows, as an interface so a third-party adapter can
|
|
6
|
+
* add its own.
|
|
7
|
+
*
|
|
8
|
+
* A union type cannot be extended from outside the package, and widening it to
|
|
9
|
+
* `string` would give up every place the compiler currently catches a typo — so
|
|
10
|
+
* this is an interface and a new name arrives by declaration merging, the same
|
|
11
|
+
* mechanism `DBSchema` already uses for an app's tables:
|
|
12
|
+
*
|
|
13
|
+
* ```ts no-check — a third-party package's own declaration file
|
|
14
|
+
* declare module '@bakery-framework/orm/adapters' {
|
|
15
|
+
* interface DriverRegistry {
|
|
16
|
+
* mssql: true
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* `@bakery-framework/orm/adapters` — the public subpath — and not this file, which is
|
|
22
|
+
* private and does not resolve from outside the package. The merge reaches
|
|
23
|
+
* through the barrel's `export *`; aiming it at an unresolvable specifier
|
|
24
|
+
* instead declares a second, unrelated interface and leaves every driver name
|
|
25
|
+
* rejected with nothing pointing at the cause.
|
|
26
|
+
*
|
|
27
|
+
* The value type is `true` and carries nothing: only the *keys* are read. It
|
|
28
|
+
* is a set spelled as an interface, because interfaces are what TypeScript
|
|
29
|
+
* lets you merge into.
|
|
30
|
+
*/
|
|
31
|
+
export interface DriverRegistry {
|
|
32
|
+
sqlite: true
|
|
33
|
+
postgres: true
|
|
34
|
+
mysql: true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A registered driver name. Extend {@link DriverRegistry} to add one. */
|
|
38
|
+
export type Driver = keyof DriverRegistry & string
|
|
39
|
+
|
|
40
|
+
export interface AdapterSpec {
|
|
41
|
+
/**
|
|
42
|
+
* The driver name, which is also the registry key: registering twice under
|
|
43
|
+
* one name replaces the earlier entry rather than adding a second.
|
|
44
|
+
*/
|
|
45
|
+
driver: Driver
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* URL schemes this adapter answers to, without `://`.
|
|
49
|
+
*
|
|
50
|
+
* Checked first and exactly — `mysql://…` reaches the adapter that listed
|
|
51
|
+
* `mysql`, and nothing else is consulted. List every spelling you accept;
|
|
52
|
+
* the built-in MySQL adapter lists four.
|
|
53
|
+
*/
|
|
54
|
+
protocols?: readonly string[]
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Last resort, for a target no scheme matched — a bare file path, say.
|
|
58
|
+
*
|
|
59
|
+
* Consulted in reverse registration order, so an adapter registered later can
|
|
60
|
+
* claim a target one of the built-ins would otherwise have taken. Return
|
|
61
|
+
* `false` (or omit the hook) to pass.
|
|
62
|
+
*/
|
|
63
|
+
matches?(target: string): boolean
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Open a connection. `target` is the raw `DB_URL`, or `undefined` when none
|
|
67
|
+
* was set and this adapter is the default.
|
|
68
|
+
*
|
|
69
|
+
* Async so the implementation can `await import()` its own driver module —
|
|
70
|
+
* which is how the three built-ins stay lazy: registering all of them costs
|
|
71
|
+
* three object literals, and only the one that wins is ever loaded.
|
|
72
|
+
*/
|
|
73
|
+
open(
|
|
74
|
+
target: string | undefined,
|
|
75
|
+
pool: PoolOptions,
|
|
76
|
+
): SQLAdapter | Promise<SQLAdapter>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* What to fall back to when the target names no adapter — split in two because
|
|
81
|
+
* the two cases have never had the same answer. See {@link resolveDriver}.
|
|
82
|
+
*/
|
|
83
|
+
export interface DriverFallback {
|
|
84
|
+
/** No `DB_URL` at all. */
|
|
85
|
+
empty: Driver
|
|
86
|
+
/** A target that matched no scheme and no `matches()` hook. */
|
|
87
|
+
unknown: Driver
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const DEFAULT_FALLBACK: DriverFallback = {
|
|
91
|
+
empty: 'sqlite',
|
|
92
|
+
unknown: 'postgres',
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A stack per driver name, not one entry per name.
|
|
97
|
+
*
|
|
98
|
+
* Registering over an existing driver is temporary far more often than it is
|
|
99
|
+
* permanent — a test swapping in a fake, an app overriding a built-in for one
|
|
100
|
+
* environment — so "undo" has to give back exactly what was displaced. One
|
|
101
|
+
* entry per name cannot: release two registrations out of order and the second
|
|
102
|
+
* disposer restores the first's spec, quietly leaving a stub adapter installed
|
|
103
|
+
* for the rest of the process. A stack makes the order irrelevant, which is the
|
|
104
|
+
* only version that is safe to hand to a test.
|
|
105
|
+
*
|
|
106
|
+
* The Map's own order is first-registration order of each *name*, which is what
|
|
107
|
+
* `listAdapters` and the reverse scans in `resolveDriver` read.
|
|
108
|
+
*/
|
|
109
|
+
const stacks = new Map<string, AdapterSpec[]>()
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Add an adapter. An existing one under the same driver name is pushed down,
|
|
113
|
+
* not discarded.
|
|
114
|
+
*
|
|
115
|
+
* Returns a function that removes exactly this registration, whenever it is
|
|
116
|
+
* called and in any order relative to other disposers. A registry with no way
|
|
117
|
+
* out is a leak by construction.
|
|
118
|
+
*/
|
|
119
|
+
export function registerAdapter(spec: AdapterSpec): () => void {
|
|
120
|
+
const stack = stacks.get(spec.driver) ?? []
|
|
121
|
+
stack.push(spec)
|
|
122
|
+
stacks.set(spec.driver, stack)
|
|
123
|
+
return () => {
|
|
124
|
+
const live = stacks.get(spec.driver)
|
|
125
|
+
if (!live) return
|
|
126
|
+
const at = live.lastIndexOf(spec)
|
|
127
|
+
if (at === -1) return
|
|
128
|
+
live.splice(at, 1)
|
|
129
|
+
if (!live.length) stacks.delete(spec.driver)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The registered adapter for a driver name, or `undefined`. */
|
|
134
|
+
export function getAdapter(driver: string): AdapterSpec | undefined {
|
|
135
|
+
return stacks.get(driver)?.at(-1)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Every registered adapter, one per driver, in registration order. */
|
|
139
|
+
export function listAdapters(): AdapterSpec[] {
|
|
140
|
+
return [...stacks.keys()].map(d => getAdapter(d)!).filter(Boolean)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Which adapter should open `target`.
|
|
145
|
+
*
|
|
146
|
+
* The order is the whole contract, so it is stated rather than left to be read
|
|
147
|
+
* out of the code:
|
|
148
|
+
*
|
|
149
|
+
* 1. **No target at all** → `fallback` (`sqlite`), so a fresh app with no
|
|
150
|
+
* `DB_URL` gets a file database rather than an error.
|
|
151
|
+
* 2. **A URL scheme** that some adapter declared → that adapter, exactly.
|
|
152
|
+
* 3. **`matches()`**, in reverse registration order — later registrations get
|
|
153
|
+
* first refusal, which is what makes overriding a built-in possible.
|
|
154
|
+
* 4. **`fallback`** otherwise, which for a target that looked like a host is
|
|
155
|
+
* `postgres`. Long-standing behaviour, kept deliberately: a bare
|
|
156
|
+
* `db.internal:5432/app` has been read as Postgres since before the
|
|
157
|
+
* registry existed.
|
|
158
|
+
*
|
|
159
|
+
* Throws only if the resolved driver has no adapter registered — which means
|
|
160
|
+
* something registered a `protocols` entry and then unregistered itself, not
|
|
161
|
+
* anything a user can reach by typing a URL.
|
|
162
|
+
*/
|
|
163
|
+
export function resolveAdapter(
|
|
164
|
+
target: string,
|
|
165
|
+
fallback: DriverFallback = DEFAULT_FALLBACK,
|
|
166
|
+
): AdapterSpec {
|
|
167
|
+
const driver = resolveDriver(target, fallback)
|
|
168
|
+
const spec = getAdapter(driver)
|
|
169
|
+
if (!spec) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`No adapter registered for driver '${driver}'. ` +
|
|
172
|
+
`Registered: ${[...stacks.keys()].join(', ') || '(none)'}.`,
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
return spec
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Step 1–4 of {@link resolveAdapter}, without the lookup. */
|
|
179
|
+
export function resolveDriver(
|
|
180
|
+
target: string,
|
|
181
|
+
fallback: DriverFallback = DEFAULT_FALLBACK,
|
|
182
|
+
): Driver {
|
|
183
|
+
const trimmed = target.trim()
|
|
184
|
+
if (!trimmed) return fallback.empty
|
|
185
|
+
|
|
186
|
+
// Newest first in both loops, so registering over a built-in is enough to
|
|
187
|
+
// take its protocols as well as its heuristics. Registration order alone
|
|
188
|
+
// would make an override depend on which module happened to load first.
|
|
189
|
+
const newestFirst = listAdapters().reverse()
|
|
190
|
+
|
|
191
|
+
const scheme = /^([a-z][a-z0-9+.-]*):\/\//i.exec(trimmed)?.[1]?.toLowerCase()
|
|
192
|
+
if (scheme) {
|
|
193
|
+
for (const spec of newestFirst) {
|
|
194
|
+
if (spec.protocols?.some(p => p.toLowerCase() === scheme))
|
|
195
|
+
return spec.driver
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const spec of newestFirst) {
|
|
200
|
+
if (spec.matches?.(trimmed)) return spec.driver
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return fallback.unknown
|
|
204
|
+
}
|