@objectstack/service-datasource 10.2.0 → 11.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/.turbo/turbo-build.log +26 -18
- package/CHANGELOG.md +64 -0
- package/dist/chunk-76HQ74MX.cjs +82 -0
- package/dist/chunk-76HQ74MX.cjs.map +1 -0
- package/dist/chunk-JRBGOCRJ.js +82 -0
- package/dist/chunk-JRBGOCRJ.js.map +1 -0
- package/dist/index.cjs +16 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -2
- package/dist/index.d.ts +95 -2
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/sqlite-driver-fallback-BPFQYLX7.js +11 -0
- package/dist/sqlite-driver-fallback-BPFQYLX7.js.map +1 -0
- package/dist/sqlite-driver-fallback-JX4XOICD.cjs +11 -0
- package/dist/sqlite-driver-fallback-JX4XOICD.cjs.map +1 -0
- package/package.json +8 -7
- package/src/default-datasource-driver-factory.ts +26 -9
- package/src/index.ts +11 -0
- package/src/sqlite-driver-fallback.test.ts +184 -0
- package/src/sqlite-driver-fallback.ts +195 -0
- package/LICENSE.apache +0 -202
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NATIVE_SQLITE_MEMORY_FALLBACK_WARNING,
|
|
3
|
+
NATIVE_SQLITE_WASM_FALLBACK_WARNING,
|
|
4
|
+
resolveSqliteDriver
|
|
5
|
+
} from "./chunk-JRBGOCRJ.js";
|
|
6
|
+
export {
|
|
7
|
+
NATIVE_SQLITE_MEMORY_FALLBACK_WARNING,
|
|
8
|
+
NATIVE_SQLITE_WASM_FALLBACK_WARNING,
|
|
9
|
+
resolveSqliteDriver
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=sqlite-driver-fallback-BPFQYLX7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
var _chunk76HQ74MXcjs = require('./chunk-76HQ74MX.cjs');
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
exports.NATIVE_SQLITE_MEMORY_FALLBACK_WARNING = _chunk76HQ74MXcjs.NATIVE_SQLITE_MEMORY_FALLBACK_WARNING; exports.NATIVE_SQLITE_WASM_FALLBACK_WARNING = _chunk76HQ74MXcjs.NATIVE_SQLITE_WASM_FALLBACK_WARNING; exports.resolveSqliteDriver = _chunk76HQ74MXcjs.resolveSqliteDriver;
|
|
11
|
+
//# sourceMappingURL=sqlite-driver-fallback-JX4XOICD.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/framework/framework/packages/services/service-datasource/dist/sqlite-driver-fallback-JX4XOICD.cjs"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACF,kRAAC","file":"/home/runner/work/framework/framework/packages/services/service-datasource/dist/sqlite-driver-fallback-JX4XOICD.cjs"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/service-datasource",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "The datasource service (ADR-0015): external-table federation (introspect/draft/import/validate) + runtime UI datasource lifecycle (list/test/create/update/remove + REST routes). Open-source mechanism; the tier line falls on which ICryptoProvider / driver factory a host injects.",
|
|
6
6
|
"type": "module",
|
|
@@ -19,18 +19,19 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@objectstack/core": "
|
|
23
|
-
"@objectstack/spec": "
|
|
22
|
+
"@objectstack/core": "11.0.0",
|
|
23
|
+
"@objectstack/spec": "11.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/node": "^26.0.0",
|
|
27
27
|
"tsup": "^8.5.1",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
29
|
"vitest": "^4.1.9",
|
|
30
|
-
"@objectstack/driver-memory": "
|
|
31
|
-
"@objectstack/driver-sql": "
|
|
32
|
-
"@objectstack/
|
|
33
|
-
"@objectstack/
|
|
30
|
+
"@objectstack/driver-memory": "11.0.0",
|
|
31
|
+
"@objectstack/driver-sql": "11.0.0",
|
|
32
|
+
"@objectstack/driver-sqlite-wasm": "11.0.0",
|
|
33
|
+
"@objectstack/plugin-hono-server": "11.0.0",
|
|
34
|
+
"@objectstack/driver-mongodb": "11.0.0"
|
|
34
35
|
},
|
|
35
36
|
"keywords": [
|
|
36
37
|
"objectstack",
|
|
@@ -113,7 +113,19 @@ function buildMongoUrl(spec: DatasourceConnectionSpec): string {
|
|
|
113
113
|
* lazily so a host that never builds (e.g.) a mongo connection doesn't pay for
|
|
114
114
|
* the mongo SDK.
|
|
115
115
|
*/
|
|
116
|
-
export
|
|
116
|
+
export interface DefaultDatasourceDriverFactoryOptions {
|
|
117
|
+
/**
|
|
118
|
+
* Enables the dev-only native-`better-sqlite3` → wasm → in-memory step-down
|
|
119
|
+
* for sqlite construction (#2229). When omitted, defaults per call to
|
|
120
|
+
* `process.env.NODE_ENV === 'development'`. In production a native load
|
|
121
|
+
* failure is NOT silently swapped for a different engine (fail-closed).
|
|
122
|
+
*/
|
|
123
|
+
dev?: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function createDefaultDatasourceDriverFactory(
|
|
127
|
+
options: DefaultDatasourceDriverFactoryOptions = {},
|
|
128
|
+
): IDatasourceDriverFactory {
|
|
117
129
|
return {
|
|
118
130
|
supports(driverId: string): boolean {
|
|
119
131
|
return resolveKind(driverId) !== undefined;
|
|
@@ -140,14 +152,19 @@ export function createDefaultDatasourceDriverFactory(): IDatasourceDriverFactory
|
|
|
140
152
|
}
|
|
141
153
|
|
|
142
154
|
if (kind === 'sqlite') {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
155
|
+
// better-sqlite3 loads its native addon lazily (first query), so an ABI
|
|
156
|
+
// mismatch is invisible at construction and crashes later. resolveSqliteDriver
|
|
157
|
+
// probes up-front and, IN DEV ONLY, steps down to wasm SQLite (real SQL +
|
|
158
|
+
// on-disk persistence) then in-memory; in production it returns the native
|
|
159
|
+
// driver unprobed so a failure surfaces loudly (fail-closed). (#2229)
|
|
160
|
+
const conn = buildSqlConnection(spec, 'better-sqlite3') as { filename?: string };
|
|
161
|
+
const { resolveSqliteDriver } = await import('./sqlite-driver-fallback.js');
|
|
162
|
+
const resolved = await resolveSqliteDriver({
|
|
163
|
+
filename: conn.filename ?? ':memory:',
|
|
164
|
+
dev: options.dev,
|
|
165
|
+
...(schemaMode ? { schemaMode } : {}),
|
|
166
|
+
});
|
|
167
|
+
return toHandle(resolved.driver, () => sqlServerVersion(resolved.driver, 'sqlite'));
|
|
151
168
|
}
|
|
152
169
|
|
|
153
170
|
if (kind === 'mongodb') {
|
package/src/index.ts
CHANGED
|
@@ -76,6 +76,17 @@ export type {
|
|
|
76
76
|
|
|
77
77
|
// Host glue: dev driver factory + fail-closed secret binder.
|
|
78
78
|
export { createDefaultDatasourceDriverFactory } from './default-datasource-driver-factory.js';
|
|
79
|
+
// Shared native-better-sqlite3 → wasm → in-memory step-down (#2229).
|
|
80
|
+
export {
|
|
81
|
+
resolveSqliteDriver,
|
|
82
|
+
NATIVE_SQLITE_WASM_FALLBACK_WARNING,
|
|
83
|
+
NATIVE_SQLITE_MEMORY_FALLBACK_WARNING,
|
|
84
|
+
} from './sqlite-driver-fallback.js';
|
|
85
|
+
export type {
|
|
86
|
+
ResolveSqliteDriverOptions,
|
|
87
|
+
ResolvedSqliteDriver,
|
|
88
|
+
SqliteFallbackEngine,
|
|
89
|
+
} from './sqlite-driver-fallback.js';
|
|
79
90
|
export {
|
|
80
91
|
createDatasourceSecretBinder,
|
|
81
92
|
toCredentialsRef,
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeEach, afterAll, vi } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
resolveSqliteDriver,
|
|
6
|
+
NATIVE_SQLITE_WASM_FALLBACK_WARNING,
|
|
7
|
+
NATIVE_SQLITE_MEMORY_FALLBACK_WARNING,
|
|
8
|
+
} from './sqlite-driver-fallback.js';
|
|
9
|
+
|
|
10
|
+
// Shared, mutable test state read by the mocked driver constructors. `vi.hoisted`
|
|
11
|
+
// makes it available inside the hoisted `vi.mock` factories below.
|
|
12
|
+
const state = vi.hoisted(() => ({
|
|
13
|
+
/** Make the native better-sqlite3 driver throw a NODE_MODULE_VERSION-style error. */
|
|
14
|
+
nativeFails: false,
|
|
15
|
+
/** Make the wasm SQLite driver fail to connect (forces the in-memory last resort). */
|
|
16
|
+
wasmFails: false,
|
|
17
|
+
nativeConfigs: [] as any[],
|
|
18
|
+
wasmConfigs: [] as any[],
|
|
19
|
+
memoryCount: 0,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const ABI_ERROR_MESSAGE =
|
|
23
|
+
"The module '/x/better_sqlite3.node' was compiled against a different Node.js version " +
|
|
24
|
+
'using NODE_MODULE_VERSION 141. This version of Node.js requires NODE_MODULE_VERSION 127. ' +
|
|
25
|
+
'Please try re-compiling or re-installing the module.';
|
|
26
|
+
|
|
27
|
+
vi.mock('@objectstack/driver-sql', () => {
|
|
28
|
+
class SqlDriver {
|
|
29
|
+
public readonly name = 'com.objectstack.driver.sql';
|
|
30
|
+
constructor(public readonly config: any) {
|
|
31
|
+
state.nativeConfigs.push(config);
|
|
32
|
+
}
|
|
33
|
+
async connect(): Promise<void> {
|
|
34
|
+
// Mirrors the real driver: connect() runs mkdir + a PRAGMA whose error it
|
|
35
|
+
// swallows — so it is NOT where the ABI failure surfaces.
|
|
36
|
+
}
|
|
37
|
+
async execute(_sql: string): Promise<unknown> {
|
|
38
|
+
// better-sqlite3 loads its native addon lazily at the first query, so the
|
|
39
|
+
// ABI mismatch surfaces here (the SELECT 1 probe), not at construction.
|
|
40
|
+
if (state.nativeFails) throw new Error(ABI_ERROR_MESSAGE);
|
|
41
|
+
return [{ ok: 1 }];
|
|
42
|
+
}
|
|
43
|
+
async disconnect(): Promise<void> {}
|
|
44
|
+
}
|
|
45
|
+
return { SqlDriver };
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
vi.mock('@objectstack/driver-sqlite-wasm', () => {
|
|
49
|
+
class SqliteWasmDriver {
|
|
50
|
+
public readonly name = 'com.objectstack.driver.sqlite-wasm';
|
|
51
|
+
constructor(public readonly config: any) {
|
|
52
|
+
state.wasmConfigs.push(config);
|
|
53
|
+
}
|
|
54
|
+
async connect(): Promise<void> {
|
|
55
|
+
if (state.wasmFails) throw new Error('wasm sqlite failed to initialise');
|
|
56
|
+
}
|
|
57
|
+
async execute(): Promise<unknown> {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
async disconnect(): Promise<void> {}
|
|
61
|
+
}
|
|
62
|
+
return { SqliteWasmDriver };
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
vi.mock('@objectstack/driver-memory', () => {
|
|
66
|
+
class InMemoryDriver {
|
|
67
|
+
public readonly name = 'com.objectstack.driver.memory';
|
|
68
|
+
constructor() {
|
|
69
|
+
state.memoryCount += 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { InMemoryDriver };
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('resolveSqliteDriver — native better-sqlite3 → wasm → in-memory step-down (#2229)', () => {
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
state.nativeFails = false;
|
|
78
|
+
state.wasmFails = false;
|
|
79
|
+
state.nativeConfigs = [];
|
|
80
|
+
state.wasmConfigs = [];
|
|
81
|
+
state.memoryCount = 0;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('uses native better-sqlite3 on the happy path (no fallback, no warning)', async () => {
|
|
85
|
+
const warn = vi.fn();
|
|
86
|
+
const resolved = await resolveSqliteDriver({ filename: ':memory:', dev: true, warn });
|
|
87
|
+
|
|
88
|
+
expect(resolved.engine).toBe('better-sqlite3');
|
|
89
|
+
expect(resolved.label).toBe('SqlDriver(sqlite)');
|
|
90
|
+
expect(resolved.driver.name).toBe('com.objectstack.driver.sql');
|
|
91
|
+
expect(warn).not.toHaveBeenCalled();
|
|
92
|
+
expect(state.wasmConfigs).toHaveLength(0);
|
|
93
|
+
expect(state.memoryCount).toBe(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('falls back to wasm SQLite when the native addon fails to load, emitting the warning', async () => {
|
|
97
|
+
state.nativeFails = true;
|
|
98
|
+
const warn = vi.fn();
|
|
99
|
+
|
|
100
|
+
const resolved = await resolveSqliteDriver({
|
|
101
|
+
filename: '/tmp/proj/.objectstack/data/dev.db',
|
|
102
|
+
dev: true,
|
|
103
|
+
warn,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(resolved.engine).toBe('sqlite-wasm');
|
|
107
|
+
expect(resolved.label).toBe('SqliteWasmDriver');
|
|
108
|
+
expect(resolved.driver.name).toBe('com.objectstack.driver.sqlite-wasm');
|
|
109
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
110
|
+
expect(warn).toHaveBeenCalledWith(NATIVE_SQLITE_WASM_FALLBACK_WARNING);
|
|
111
|
+
// The persistent file path is preserved (real on-disk persistence via wasm).
|
|
112
|
+
expect(state.wasmConfigs[0].filename).toBe('/tmp/proj/.objectstack/data/dev.db');
|
|
113
|
+
expect(state.wasmConfigs[0].persist).toBe('on-write');
|
|
114
|
+
expect(state.memoryCount).toBe(0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('uses on-disconnect persistence for an ephemeral :memory: wasm fallback', async () => {
|
|
118
|
+
state.nativeFails = true;
|
|
119
|
+
const resolved = await resolveSqliteDriver({ filename: ':memory:', dev: true, warn: vi.fn() });
|
|
120
|
+
|
|
121
|
+
expect(resolved.engine).toBe('sqlite-wasm');
|
|
122
|
+
expect(state.wasmConfigs[0].filename).toBe(':memory:');
|
|
123
|
+
expect(state.wasmConfigs[0].persist).toBe('on-disconnect');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('drops to InMemoryDriver as a dev-only last resort when neither native nor wasm load', async () => {
|
|
127
|
+
state.nativeFails = true;
|
|
128
|
+
state.wasmFails = true;
|
|
129
|
+
const warn = vi.fn();
|
|
130
|
+
|
|
131
|
+
const resolved = await resolveSqliteDriver({ filename: ':memory:', dev: true, warn });
|
|
132
|
+
|
|
133
|
+
expect(resolved.engine).toBe('memory');
|
|
134
|
+
expect(resolved.label).toBe('InMemoryDriver');
|
|
135
|
+
expect(state.memoryCount).toBe(1);
|
|
136
|
+
expect(warn).toHaveBeenCalledWith(NATIVE_SQLITE_MEMORY_FALLBACK_WARNING);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('forwards autoMigrate / schemaMode to the native driver', async () => {
|
|
140
|
+
await resolveSqliteDriver({ filename: ':memory:', dev: true, autoMigrate: 'safe', schemaMode: 'managed' });
|
|
141
|
+
expect(state.nativeConfigs[0]).toMatchObject({
|
|
142
|
+
client: 'better-sqlite3',
|
|
143
|
+
useNullAsDefault: true,
|
|
144
|
+
autoMigrate: 'safe',
|
|
145
|
+
schemaMode: 'managed',
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('production (dev=false) is fail-closed — returns native unprobed, never degrades', async () => {
|
|
150
|
+
state.nativeFails = true;
|
|
151
|
+
const warn = vi.fn();
|
|
152
|
+
|
|
153
|
+
const resolved = await resolveSqliteDriver({ filename: '/tmp/prod.db', dev: false, warn });
|
|
154
|
+
|
|
155
|
+
// The native driver is handed back as-is so the ABI failure surfaces loudly
|
|
156
|
+
// at first use — we must NOT swap in wasm/mingo behind the operator's back.
|
|
157
|
+
expect(resolved.engine).toBe('better-sqlite3');
|
|
158
|
+
expect(resolved.label).toBe('SqlDriver(sqlite)');
|
|
159
|
+
expect(warn).not.toHaveBeenCalled();
|
|
160
|
+
expect(state.wasmConfigs).toHaveLength(0);
|
|
161
|
+
expect(state.memoryCount).toBe(0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('dev gate defaults to NODE_ENV when not passed explicitly', () => {
|
|
165
|
+
const originalNodeEnv = process.env.NODE_ENV;
|
|
166
|
+
afterAll(() => {
|
|
167
|
+
process.env.NODE_ENV = originalNodeEnv;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('falls back to wasm when dev is omitted and NODE_ENV=development', async () => {
|
|
171
|
+
state.nativeFails = true;
|
|
172
|
+
process.env.NODE_ENV = 'development';
|
|
173
|
+
const resolved = await resolveSqliteDriver({ filename: ':memory:', warn: vi.fn() });
|
|
174
|
+
expect(resolved.engine).toBe('sqlite-wasm');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('is fail-closed when dev is omitted and NODE_ENV=production', async () => {
|
|
178
|
+
state.nativeFails = true;
|
|
179
|
+
process.env.NODE_ENV = 'production';
|
|
180
|
+
const resolved = await resolveSqliteDriver({ filename: ':memory:', warn: vi.fn() });
|
|
181
|
+
expect(resolved.engine).toBe('better-sqlite3');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -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
|
+
}
|