@objectstack/service-datasource 10.0.0 → 10.3.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.
Files changed (37) hide show
  1. package/.turbo/turbo-build.log +28 -16
  2. package/CHANGELOG.md +100 -0
  3. package/dist/chunk-76HQ74MX.cjs +82 -0
  4. package/dist/chunk-76HQ74MX.cjs.map +1 -0
  5. package/dist/chunk-BI2SYWLC.cjs +9 -0
  6. package/dist/chunk-BI2SYWLC.cjs.map +1 -0
  7. package/dist/chunk-JRBGOCRJ.js +82 -0
  8. package/dist/chunk-JRBGOCRJ.js.map +1 -0
  9. package/dist/chunk-XLS4RP7B.js +9 -0
  10. package/dist/chunk-XLS4RP7B.js.map +1 -0
  11. package/dist/contracts/index.cjs +7 -1
  12. package/dist/contracts/index.cjs.map +1 -1
  13. package/dist/contracts/index.d.cts +59 -1
  14. package/dist/contracts/index.d.ts +59 -1
  15. package/dist/contracts/index.js +6 -0
  16. package/dist/index.cjs +284 -106
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +270 -5
  19. package/dist/index.d.ts +270 -5
  20. package/dist/index.js +216 -38
  21. package/dist/index.js.map +1 -1
  22. package/dist/sqlite-driver-fallback-BPFQYLX7.js +11 -0
  23. package/dist/sqlite-driver-fallback-BPFQYLX7.js.map +1 -0
  24. package/dist/sqlite-driver-fallback-JX4XOICD.cjs +11 -0
  25. package/dist/sqlite-driver-fallback-JX4XOICD.cjs.map +1 -0
  26. package/package.json +8 -7
  27. package/src/__tests__/datasource-connection-service.test.ts +294 -0
  28. package/src/contracts/connect-policy.ts +69 -0
  29. package/src/contracts/index.ts +11 -0
  30. package/src/datasource-admin-plugin.ts +37 -40
  31. package/src/datasource-admin-service.ts +2 -0
  32. package/src/datasource-connection-service.ts +364 -0
  33. package/src/default-datasource-driver-factory.ts +26 -9
  34. package/src/index.ts +29 -0
  35. package/src/logger.ts +2 -0
  36. package/src/sqlite-driver-fallback.test.ts +184 -0
  37. package/src/sqlite-driver-fallback.ts +195 -0
@@ -0,0 +1,195 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ /**
4
+ * Shared native-`better-sqlite3` → wasm SQLite → in-memory step-down for any
5
+ * sqlite-via-`better-sqlite3` construction (issue #2229).
6
+ *
7
+ * ## Why a probe is necessary
8
+ *
9
+ * `better-sqlite3` loads its native `.node` addon LAZILY — not at
10
+ * `require('better-sqlite3')`, and not even at knex construction, but at the
11
+ * first pool-connection acquire (`new Database(file)`), i.e. the first query.
12
+ * So an ABI mismatch (a cached prebuilt binary built for a different Node
13
+ * version — `NODE_MODULE_VERSION` mismatch) is invisible at boot and only
14
+ * surfaces much later as a runtime `Find operation failed` on the first read.
15
+ *
16
+ * This helper makes the failure observable up-front by actively probing: it
17
+ * opens a connection and runs a cheap `SELECT 1`, which forces the native addon
18
+ * to load. (`connect()` alone is NOT a reliable probe: for SQLite it only runs
19
+ * `mkdir` + a `PRAGMA` whose error is swallowed internally — so we additionally
20
+ * issue a raw `SELECT 1`, which propagates the load error.) On failure it steps
21
+ * down:
22
+ *
23
+ * 1. native `better-sqlite3` — fast, real SQL
24
+ * 2. wasm SQLite — pure-JS, real SQL + on-disk persistence, slower [dev only]
25
+ * 3. in-memory (mingo) — neither real SQL nor persistent [dev only, last resort]
26
+ *
27
+ * ## Dev vs production
28
+ *
29
+ * The wasm + in-memory step-down is GATED to dev. In production a native load
30
+ * failure is NOT silently swapped for a different engine: the error is re-thrown
31
+ * so it surfaces loudly (fail-closed) instead of an operator unknowingly running
32
+ * on wasm/mingo. This mirrors the existing `serve.ts` default-dev fallback and
33
+ * hoists it into one place shared by every sqlite construction site.
34
+ */
35
+
36
+ /** Which engine the resolver ultimately produced. */
37
+ export type SqliteFallbackEngine = 'better-sqlite3' | 'sqlite-wasm' | 'memory';
38
+
39
+ export interface ResolveSqliteDriverOptions {
40
+ /**
41
+ * SQLite filename — `:memory:` for an ephemeral database, or an absolute /
42
+ * relative path for a persistent file. Preserved across the wasm fallback so
43
+ * a persistent `file:` database keeps its on-disk persistence through wasm.
44
+ * Pass the raw filename (callers strip any `file:` / `sqlite:` scheme first).
45
+ */
46
+ filename: string;
47
+ /**
48
+ * Gates the wasm + in-memory step-down. When `true` (dev) a native ABI/load
49
+ * failure steps down the chain with a warning. When `false` (production) the
50
+ * native driver is returned unprobed so a failure surfaces loudly at first use
51
+ * (fail-closed) — we never silently degrade behind the operator's back.
52
+ * Defaults to `process.env.NODE_ENV === 'development'`.
53
+ */
54
+ dev?: boolean;
55
+ /** Forwarded to the native SqlDriver (dev loosen-only self-heal, #2186). */
56
+ autoMigrate?: 'off' | 'safe';
57
+ /** Forwarded to the SQL drivers (external schema mode, ADR-0015). */
58
+ schemaMode?: string;
59
+ /**
60
+ * Warning sink for the step-down messages. Defaults to `console.warn`.
61
+ * `serve.ts` passes a `chalk.yellow` wrapper so the banner stays consistent.
62
+ */
63
+ warn?: (message: string) => void;
64
+ }
65
+
66
+ export interface ResolvedSqliteDriver {
67
+ /** The concrete engine driver to register (e.g. via `DriverPlugin`). */
68
+ driver: any;
69
+ /** Which engine actually resolved. */
70
+ engine: SqliteFallbackEngine;
71
+ /** Banner label, matching `serve.ts`'s existing strings. */
72
+ label: string;
73
+ }
74
+
75
+ /**
76
+ * Warning emitted when native `better-sqlite3` is unavailable but wasm SQLite
77
+ * loads. Kept byte-for-byte identical to the original `serve.ts` text so the
78
+ * dev experience is the same regardless of which construction site triggers it.
79
+ */
80
+ export const NATIVE_SQLITE_WASM_FALLBACK_WARNING =
81
+ ' ⚠ native better-sqlite3 unavailable (ABI mismatch or not built) — dev using wasm SQLite (real SQL, slower).\n' +
82
+ ' Rebuild better-sqlite3 for native speed, or set OS_DATABASE_DRIVER=sqlite-wasm to silence this.';
83
+
84
+ /** Warning emitted when neither native nor wasm SQLite loads (dev last resort). */
85
+ export const NATIVE_SQLITE_MEMORY_FALLBACK_WARNING =
86
+ ' ⚠ neither native nor wasm SQLite available — dev falling back to InMemoryDriver (mingo, not real SQL).\n' +
87
+ ' Rebuild better-sqlite3, or set OS_DATABASE_URL / OS_DATABASE_DRIVER for SQL fidelity.';
88
+
89
+ /** `:memory:` and other `:`-prefixed pseudo-filenames are never persisted. */
90
+ function isEphemeralFilename(filename: string): boolean {
91
+ return filename === ':memory:' || filename.startsWith(':');
92
+ }
93
+
94
+ /**
95
+ * Probe a `better-sqlite3` SQLite construction and, in dev, step down to wasm
96
+ * SQLite (then in-memory) when the native addon cannot load.
97
+ *
98
+ * @see {@link ResolveSqliteDriverOptions}
99
+ */
100
+ export async function resolveSqliteDriver(
101
+ opts: ResolveSqliteDriverOptions,
102
+ ): Promise<ResolvedSqliteDriver> {
103
+ const { filename } = opts;
104
+ const dev = opts.dev ?? process.env.NODE_ENV === 'development';
105
+ const warn =
106
+ opts.warn ??
107
+ ((message: string) => {
108
+ try {
109
+ // eslint-disable-next-line no-console
110
+ console.warn(message);
111
+ } catch {
112
+ /* ignore */
113
+ }
114
+ });
115
+
116
+ const { SqlDriver } = await import('@objectstack/driver-sql');
117
+
118
+ const buildNative = () =>
119
+ new SqlDriver({
120
+ client: 'better-sqlite3',
121
+ connection: { filename },
122
+ useNullAsDefault: true,
123
+ ...(opts.autoMigrate ? { autoMigrate: opts.autoMigrate } : {}),
124
+ ...(opts.schemaMode ? { schemaMode: opts.schemaMode } : {}),
125
+ } as any);
126
+
127
+ // Production: never silently swap engines. Construct the native driver and
128
+ // hand it back UNPROBED — exactly the historical behavior. A native load
129
+ // failure surfaces loudly at first use (fail-closed).
130
+ if (!dev) {
131
+ return { driver: buildNative(), engine: 'better-sqlite3', label: 'SqlDriver(sqlite)' };
132
+ }
133
+
134
+ // ── Dev: probe-by-connect, step down on native ABI/load failure. ──────────
135
+
136
+ // 1. Native better-sqlite3.
137
+ let nativeDriver: any;
138
+ let nativeOk = false;
139
+ try {
140
+ nativeDriver = buildNative();
141
+ // connect() runs mkdir (so a SELECT on a file DB whose dir is missing does
142
+ // not false-positive as an ABI failure) + a PRAGMA whose error it swallows;
143
+ // the raw SELECT 1 below is what reliably forces the native addon to load
144
+ // and PROPAGATES an ABI mismatch.
145
+ await nativeDriver.connect();
146
+ await nativeDriver.execute('SELECT 1');
147
+ nativeOk = true;
148
+ } catch {
149
+ nativeOk = false;
150
+ if (typeof nativeDriver?.disconnect === 'function') {
151
+ try {
152
+ await nativeDriver.disconnect();
153
+ } catch {
154
+ /* ignore */
155
+ }
156
+ }
157
+ }
158
+ if (nativeOk) {
159
+ return { driver: nativeDriver, engine: 'better-sqlite3', label: 'SqlDriver(sqlite)' };
160
+ }
161
+
162
+ // 2. wasm SQLite — real SQL semantics + on-disk persistence, no native build.
163
+ let wasmDriver: any;
164
+ let wasmOk = false;
165
+ try {
166
+ const { SqliteWasmDriver } = await import('@objectstack/driver-sqlite-wasm');
167
+ wasmDriver = new SqliteWasmDriver({
168
+ filename,
169
+ // Match the existing construction sites: ephemeral DBs flush on
170
+ // disconnect; a persistent file flushes on every write so AI-authored
171
+ // data survives an unclean dev-server kill.
172
+ persist: isEphemeralFilename(filename) ? 'on-disconnect' : 'on-write',
173
+ } as any);
174
+ await wasmDriver.connect();
175
+ wasmOk = true;
176
+ } catch {
177
+ wasmOk = false;
178
+ if (typeof wasmDriver?.disconnect === 'function') {
179
+ try {
180
+ await wasmDriver.disconnect();
181
+ } catch {
182
+ /* ignore */
183
+ }
184
+ }
185
+ }
186
+ if (wasmOk) {
187
+ warn(NATIVE_SQLITE_WASM_FALLBACK_WARNING);
188
+ return { driver: wasmDriver, engine: 'sqlite-wasm', label: 'SqliteWasmDriver' };
189
+ }
190
+
191
+ // 3. In-memory (mingo) — dev-only last resort. Not real SQL, not persistent.
192
+ const { InMemoryDriver } = await import('@objectstack/driver-memory');
193
+ warn(NATIVE_SQLITE_MEMORY_FALLBACK_WARNING);
194
+ return { driver: new InMemoryDriver(), engine: 'memory', label: 'InMemoryDriver' };
195
+ }