@@ -8,6 +8,7 @@ import chalk from 'chalk';
88import { bundleRequire } from 'bundle-require' ;
99import { loadConfig , BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js' ;
1010import { isHostConfig , shouldBootWithLibrary } from '../utils/plugin-detection.js' ;
11+ import { resolveDriverType , createStorageDriver } from '../utils/storage-driver.js' ;
1112import { readEnvWithDeprecation , resolveMultiOrgEnabled , resolveAllowDegradedTenancy , isMcpServerEnabled , resolveSearchPinyinEnabled , isModuleNotFoundError } from '@objectstack/types' ;
1213import { PLATFORM_CAPABILITY_TOKENS , canonicalizePlatformCapability } from '@objectstack/spec/kernel' ;
1314import { resolveObjectStackHome } from '@objectstack/runtime' ;
@@ -872,71 +873,48 @@ export default class Serve extends Command {
872873 // postgres://, postgresql:// → postgres
873874 // mysql://, mysql2:// → mysql
874875 // libsql://, http(s):// + .turso. → turso
875- // file:, sqlite:, *.db, :memory: → sqlite
876- // 3. Default: InMemoryDriver in dev mode
876+ // wasm-sqlite://, *.wasm.db → sqlite-wasm
877+ // memory://, mingo:// → memory (mingo InMemoryDriver)
878+ // file:, sqlite:, *.db, :memory: → sqlite (SQLite's own in-memory mode)
879+ // 3. Default: dev SQLite (native → wasm → in-memory step-down); prod none
880+ //
881+ // Kind-resolution and construction live in utils/storage-driver.ts so the
882+ // whole dispatch is unit-testable (storage-driver.test.ts). #3276: the
883+ // `memory` kind now maps to the mingo InMemoryDriver instead of silently
884+ // falling through to the dev SQLite `:memory:` default.
877885 const hasDriver = plugins . some ( ( p : any ) => p . name ?. includes ( 'driver' ) || p . constructor ?. name ?. includes ( 'Driver' ) ) ;
878886 if ( ! hasDriver && config . objects ) {
879- const explicitDriver = ( process . env . OS_DATABASE_DRIVER ?? '' ) . toLowerCase ( ) . trim ( ) ;
880887 const databaseUrl = process . env . OS_DATABASE_URL ;
881-
882- const inferDriverFromUrl = ( url : string | undefined ) : string => {
883- if ( ! url ) return '' ;
884- const u = url . trim ( ) ;
885- if ( / ^ m o n g o d b ( \+ s r v ) ? : \/ \/ / i. test ( u ) ) return 'mongodb' ;
886- if ( / ^ p o s t g r e s ( q l ) ? : \/ \/ / i. test ( u ) ) return 'postgres' ;
887- if ( / ^ m y s q l 2 ? : \/ \/ / i. test ( u ) ) return 'mysql' ;
888- if ( / ^ l i b s q l : \/ \/ / i. test ( u ) ) return 'turso' ;
889- if ( / ^ h t t p s ? : \/ \/ / i. test ( u ) && / \. t u r s o \. / i. test ( u ) ) return 'turso' ;
890- if ( / ^ w a s m - s q l i t e : \/ \/ / i. test ( u ) || / \. w a s m \. d b $ / i. test ( u ) ) return 'sqlite-wasm' ;
891- if ( / ^ f i l e : / i. test ( u ) || / ^ s q l i t e : / i. test ( u ) || u === ':memory:' || / \. ( d b | s q l i t e | s q l i t e 3 ) $ / i. test ( u ) ) return 'sqlite' ;
892- return '' ;
893- } ;
894-
895- const driverType = explicitDriver || inferDriverFromUrl ( databaseUrl ) ;
888+ const driverType = resolveDriverType ( process . env . OS_DATABASE_DRIVER , databaseUrl ) ;
896889
897890 try {
898891 const { DriverPlugin } = await import ( '@objectstack/runtime' ) ;
899-
900- if ( driverType === 'mongodb' || driverType === 'mongo' ) {
901- const { MongoDBDriver } = await import ( '@objectstack/driver-mongodb' ) ;
902- await kernel . use ( new DriverPlugin ( new MongoDBDriver ( {
903- url : databaseUrl ?? 'mongodb://localhost:27017/objectstack' ,
904- } ) as any ) ) ;
905- trackPlugin ( 'MongoDBDriver' ) ;
906- resolvedDriverLabel = 'MongoDBDriver' ;
907- resolvedDatabaseUrl = databaseUrl ?? 'mongodb://localhost:27017/objectstack' ;
908- } else if ( driverType === 'sqlite' || driverType === 'sql' ) {
909- const filePath = ( databaseUrl ?? ':memory:' ) . replace ( / ^ f i l e : / , '' ) . replace ( / ^ s q l i t e : / , '' ) . replace ( / ^ s q l : \/ \/ / , '' ) ;
910- // Probe-by-connect with a dev-only native → wasm → in-memory
911- // step-down (#2229). better-sqlite3 loads its native addon lazily
912- // (first query), so an ABI mismatch is invisible here and would
913- // otherwise surface much later as a runtime crash. resolveSqliteDriver
914- // forces the load and degrades gracefully in dev / fails loudly in prod.
915- const { resolveSqliteDriver } = await import ( '@objectstack/service-datasource' ) ;
916- const resolved = await resolveSqliteDriver ( {
917- filename : filePath ,
918- dev : isDev ,
919- // #2186: in dev, self-heal a persisted DB when a metadata change
920- // relaxes a constraint (loosen-only; never destructive / never in prod).
921- autoMigrate : isDev ? 'safe' : undefined ,
922- warn : ( m ) => console . warn ( chalk . yellow ( m ) ) ,
923- } ) ;
924- await kernel . use ( new DriverPlugin ( resolved . driver ) ) ;
925- trackPlugin ( resolved . engine === 'memory' ? 'MemoryDriver' : resolved . engine === 'sqlite-wasm' ? 'SqliteWasmDriver' : 'SqlDriver' ) ;
926- resolvedDriverLabel = resolved . label ;
927- resolvedDatabaseUrl = resolved . engine === 'memory' ? '(in-memory)' : ( databaseUrl ?? ':memory:' ) ;
892+ const resolution = await createStorageDriver ( driverType , {
893+ databaseUrl,
894+ isDev,
895+ warn : ( m ) => console . warn ( chalk . yellow ( m ) ) ,
896+ } ) ;
897+ if ( resolution ) {
898+ await kernel . use ( new DriverPlugin ( resolution . driver as any ) ) ;
899+ trackPlugin ( resolution . trackName ) ;
900+ resolvedDriverLabel = resolution . label ;
901+ resolvedDatabaseUrl = resolution . displayUrl ;
928902
929903 // ADR-0057 §3.6 (#2834 ②): provision the dedicated `telemetry`
930904 // datasource — a sibling SQLite file the engine routes every
931905 // telemetry/event/audit-classed object to, so platform-generated
932906 // growth can never again bloat the business DB. Dev default-on
933907 // for file-backed primaries; `OS_TELEMETRY_DB=0` opts out,
934- // `OS_TELEMETRY_DB=<path>` opts in anywhere (incl. serve).
935- if ( resolved . engine !== 'memory' ) {
908+ // `OS_TELEMETRY_DB=<path>` opts in anywhere (incl. serve). Gated on
909+ // an explicit SQLite primary (`sqliteFilePath`, unset for the mingo
910+ // memory driver AND the dev-default `:memory:` store) whose resolved
911+ // engine is real SQLite — never mingo in-memory.
912+ if ( resolution . sqliteFilePath && resolution . engine !== 'memory' ) {
936913 const { resolveTelemetryDbPath } = await import ( '../utils/telemetry-datasource.js' ) ;
937- const telemetryPath = resolveTelemetryDbPath ( { primaryPath : filePath , env : process . env , dev : isDev } ) ;
914+ const telemetryPath = resolveTelemetryDbPath ( { primaryPath : resolution . sqliteFilePath , env : process . env , dev : isDev } ) ;
938915 if ( telemetryPath ) {
939916 try {
917+ const { resolveSqliteDriver } = await import ( '@objectstack/service-datasource' ) ;
940918 const telemetry = await resolveSqliteDriver ( {
941919 filename : telemetryPath ,
942920 dev : isDev ,
@@ -957,59 +935,6 @@ export default class Serve extends Command {
957935 }
958936 }
959937 }
960- } else if ( driverType === 'sqlite-wasm' || driverType === 'wasm-sqlite' || driverType === 'wasm' ) {
961- const { SqliteWasmDriver } = await import ( '@objectstack/driver-sqlite-wasm' ) ;
962- const filePath = ( databaseUrl ?? ':memory:' ) . replace ( / ^ f i l e : / , '' ) . replace ( / ^ w a s m - s q l i t e : \/ \/ / , '' ) . replace ( / ^ s q l i t e : / , '' ) ;
963- await kernel . use ( new DriverPlugin ( new SqliteWasmDriver ( {
964- filename : filePath ,
965- persist : 'on-disconnect' ,
966- } ) as any ) ) ;
967- trackPlugin ( 'SqliteWasmDriver' ) ;
968- resolvedDriverLabel = 'SqliteWasmDriver' ;
969- resolvedDatabaseUrl = databaseUrl ?? ':memory:' ;
970- } else if ( driverType === 'postgres' || driverType === 'postgresql' || driverType === 'pg' ) {
971- const { SqlDriver } = await import ( '@objectstack/driver-sql' ) ;
972- await kernel . use ( new DriverPlugin ( new SqlDriver ( {
973- client : 'pg' ,
974- connection : databaseUrl ,
975- pool : { min : 0 , max : 5 } ,
976- autoMigrate : isDev ? 'safe' : undefined , // #2186 dev loosen-only self-heal
977- } ) as any ) ) ;
978- trackPlugin ( 'PostgresDriver' ) ;
979- resolvedDriverLabel = 'SqlDriver(pg)' ;
980- resolvedDatabaseUrl = databaseUrl ;
981- } else if ( driverType === 'mysql' || driverType === 'mysql2' ) {
982- const { SqlDriver } = await import ( '@objectstack/driver-sql' ) ;
983- await kernel . use ( new DriverPlugin ( new SqlDriver ( {
984- client : 'mysql2' ,
985- connection : databaseUrl ,
986- pool : { min : 0 , max : 5 } ,
987- autoMigrate : isDev ? 'safe' : undefined , // #2186 dev loosen-only self-heal
988- } ) as any ) ) ;
989- trackPlugin ( 'MySQLDriver' ) ;
990- resolvedDriverLabel = 'SqlDriver(mysql2)' ;
991- resolvedDatabaseUrl = databaseUrl ;
992- } else if ( isDev ) {
993- // Default in dev (no DB configured): prefer native SQLite for
994- // production-like SQL at native speed, with a graceful step-down to
995- // wasm SQLite (real SQL + on-disk persistence) then in-memory when the
996- // native better-sqlite3 binary is unavailable — not built, ABI mismatch
997- // after a Node upgrade (e.g. NODE_MODULE_VERSION change), or a blocked
998- // prebuild download. Shared with the explicit-file branch and the
999- // datasource factory via resolveSqliteDriver (#2229), which probes by
1000- // actually opening a connection + running SELECT 1 (better-sqlite3 loads
1001- // its native addon lazily at first query, not at construction).
1002- const { resolveSqliteDriver } = await import ( '@objectstack/service-datasource' ) ;
1003- const resolved = await resolveSqliteDriver ( {
1004- filename : ':memory:' ,
1005- dev : true ,
1006- autoMigrate : 'safe' , // #2186 dev loosen-only self-heal
1007- warn : ( m ) => console . warn ( chalk . yellow ( m ) ) ,
1008- } ) ;
1009- await kernel . use ( new DriverPlugin ( resolved . driver ) ) ;
1010- trackPlugin ( resolved . engine === 'memory' ? 'MemoryDriver' : resolved . engine === 'sqlite-wasm' ? 'SqliteWasmDriver' : 'SqlDriver' ) ;
1011- resolvedDriverLabel = resolved . label ;
1012- resolvedDatabaseUrl = resolved . engine === 'memory' ? '(in-memory)' : ':memory:' ;
1013938 }
1014939 } catch ( e : any ) {
1015940 // silent
0 commit comments