@@ -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 } from '@objectstack/types' ;
1213import { resolveObjectStackHome } from '@objectstack/runtime' ;
1314import { LOG_LEVELS , resolveLogLevel , readLogLevelEnv } from '../utils/log-level.js' ;
@@ -631,71 +632,48 @@ export default class Serve extends Command {
631632 // postgres://, postgresql:// → postgres
632633 // mysql://, mysql2:// → mysql
633634 // libsql://, http(s):// + .turso. → turso
634- // file:, sqlite:, *.db, :memory: → sqlite
635- // 3. Default: InMemoryDriver in dev mode
635+ // wasm-sqlite://, *.wasm.db → sqlite-wasm
636+ // memory://, mingo:// → memory (mingo InMemoryDriver)
637+ // file:, sqlite:, *.db, :memory: → sqlite (SQLite's own in-memory mode)
638+ // 3. Default: dev SQLite (native → wasm → in-memory step-down); prod none
639+ //
640+ // Kind-resolution and construction live in utils/storage-driver.ts so the
641+ // whole dispatch is unit-testable (storage-driver.test.ts). #3276: the
642+ // `memory` kind now maps to the mingo InMemoryDriver instead of silently
643+ // falling through to the dev SQLite `:memory:` default.
636644 const hasDriver = plugins . some ( ( p : any ) => p . name ?. includes ( 'driver' ) || p . constructor ?. name ?. includes ( 'Driver' ) ) ;
637645 if ( ! hasDriver && config . objects ) {
638- const explicitDriver = ( process . env . OS_DATABASE_DRIVER ?? '' ) . toLowerCase ( ) . trim ( ) ;
639646 const databaseUrl = process . env . OS_DATABASE_URL ;
640-
641- const inferDriverFromUrl = ( url : string | undefined ) : string => {
642- if ( ! url ) return '' ;
643- const u = url . trim ( ) ;
644- if ( / ^ m o n g o d b ( \+ s r v ) ? : \/ \/ / i. test ( u ) ) return 'mongodb' ;
645- if ( / ^ p o s t g r e s ( q l ) ? : \/ \/ / i. test ( u ) ) return 'postgres' ;
646- if ( / ^ m y s q l 2 ? : \/ \/ / i. test ( u ) ) return 'mysql' ;
647- if ( / ^ l i b s q l : \/ \/ / i. test ( u ) ) return 'turso' ;
648- if ( / ^ h t t p s ? : \/ \/ / i. test ( u ) && / \. t u r s o \. / i. test ( u ) ) return 'turso' ;
649- 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' ;
650- 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' ;
651- return '' ;
652- } ;
653-
654- const driverType = explicitDriver || inferDriverFromUrl ( databaseUrl ) ;
647+ const driverType = resolveDriverType ( process . env . OS_DATABASE_DRIVER , databaseUrl ) ;
655648
656649 try {
657650 const { DriverPlugin } = await import ( '@objectstack/runtime' ) ;
658-
659- if ( driverType === 'mongodb' || driverType === 'mongo' ) {
660- const { MongoDBDriver } = await import ( '@objectstack/driver-mongodb' ) ;
661- await kernel . use ( new DriverPlugin ( new MongoDBDriver ( {
662- url : databaseUrl ?? 'mongodb://localhost:27017/objectstack' ,
663- } ) as any ) ) ;
664- trackPlugin ( 'MongoDBDriver' ) ;
665- resolvedDriverLabel = 'MongoDBDriver' ;
666- resolvedDatabaseUrl = databaseUrl ?? 'mongodb://localhost:27017/objectstack' ;
667- } else if ( driverType === 'sqlite' || driverType === 'sql' ) {
668- const filePath = ( databaseUrl ?? ':memory:' ) . replace ( / ^ f i l e : / , '' ) . replace ( / ^ s q l i t e : / , '' ) . replace ( / ^ s q l : \/ \/ / , '' ) ;
669- // Probe-by-connect with a dev-only native → wasm → in-memory
670- // step-down (#2229). better-sqlite3 loads its native addon lazily
671- // (first query), so an ABI mismatch is invisible here and would
672- // otherwise surface much later as a runtime crash. resolveSqliteDriver
673- // forces the load and degrades gracefully in dev / fails loudly in prod.
674- const { resolveSqliteDriver } = await import ( '@objectstack/service-datasource' ) ;
675- const resolved = await resolveSqliteDriver ( {
676- filename : filePath ,
677- dev : isDev ,
678- // #2186: in dev, self-heal a persisted DB when a metadata change
679- // relaxes a constraint (loosen-only; never destructive / never in prod).
680- autoMigrate : isDev ? 'safe' : undefined ,
681- warn : ( m ) => console . warn ( chalk . yellow ( m ) ) ,
682- } ) ;
683- await kernel . use ( new DriverPlugin ( resolved . driver ) ) ;
684- trackPlugin ( resolved . engine === 'memory' ? 'MemoryDriver' : resolved . engine === 'sqlite-wasm' ? 'SqliteWasmDriver' : 'SqlDriver' ) ;
685- resolvedDriverLabel = resolved . label ;
686- resolvedDatabaseUrl = resolved . engine === 'memory' ? '(in-memory)' : ( databaseUrl ?? ':memory:' ) ;
651+ const resolution = await createStorageDriver ( driverType , {
652+ databaseUrl,
653+ isDev,
654+ warn : ( m ) => console . warn ( chalk . yellow ( m ) ) ,
655+ } ) ;
656+ if ( resolution ) {
657+ await kernel . use ( new DriverPlugin ( resolution . driver as any ) ) ;
658+ trackPlugin ( resolution . trackName ) ;
659+ resolvedDriverLabel = resolution . label ;
660+ resolvedDatabaseUrl = resolution . displayUrl ;
687661
688662 // ADR-0057 §3.6 (#2834 ②): provision the dedicated `telemetry`
689663 // datasource — a sibling SQLite file the engine routes every
690664 // telemetry/event/audit-classed object to, so platform-generated
691665 // growth can never again bloat the business DB. Dev default-on
692666 // for file-backed primaries; `OS_TELEMETRY_DB=0` opts out,
693- // `OS_TELEMETRY_DB=<path>` opts in anywhere (incl. serve).
694- if ( resolved . engine !== 'memory' ) {
667+ // `OS_TELEMETRY_DB=<path>` opts in anywhere (incl. serve). Gated on
668+ // an explicit SQLite primary (`sqliteFilePath`, unset for the mingo
669+ // memory driver AND the dev-default `:memory:` store) whose resolved
670+ // engine is real SQLite — never mingo in-memory.
671+ if ( resolution . sqliteFilePath && resolution . engine !== 'memory' ) {
695672 const { resolveTelemetryDbPath } = await import ( '../utils/telemetry-datasource.js' ) ;
696- const telemetryPath = resolveTelemetryDbPath ( { primaryPath : filePath , env : process . env , dev : isDev } ) ;
673+ const telemetryPath = resolveTelemetryDbPath ( { primaryPath : resolution . sqliteFilePath , env : process . env , dev : isDev } ) ;
697674 if ( telemetryPath ) {
698675 try {
676+ const { resolveSqliteDriver } = await import ( '@objectstack/service-datasource' ) ;
699677 const telemetry = await resolveSqliteDriver ( {
700678 filename : telemetryPath ,
701679 dev : isDev ,
@@ -716,59 +694,6 @@ export default class Serve extends Command {
716694 }
717695 }
718696 }
719- } else if ( driverType === 'sqlite-wasm' || driverType === 'wasm-sqlite' || driverType === 'wasm' ) {
720- const { SqliteWasmDriver } = await import ( '@objectstack/driver-sqlite-wasm' ) ;
721- 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 : / , '' ) ;
722- await kernel . use ( new DriverPlugin ( new SqliteWasmDriver ( {
723- filename : filePath ,
724- persist : 'on-disconnect' ,
725- } ) as any ) ) ;
726- trackPlugin ( 'SqliteWasmDriver' ) ;
727- resolvedDriverLabel = 'SqliteWasmDriver' ;
728- resolvedDatabaseUrl = databaseUrl ?? ':memory:' ;
729- } else if ( driverType === 'postgres' || driverType === 'postgresql' || driverType === 'pg' ) {
730- const { SqlDriver } = await import ( '@objectstack/driver-sql' ) ;
731- await kernel . use ( new DriverPlugin ( new SqlDriver ( {
732- client : 'pg' ,
733- connection : databaseUrl ,
734- pool : { min : 0 , max : 5 } ,
735- autoMigrate : isDev ? 'safe' : undefined , // #2186 dev loosen-only self-heal
736- } ) as any ) ) ;
737- trackPlugin ( 'PostgresDriver' ) ;
738- resolvedDriverLabel = 'SqlDriver(pg)' ;
739- resolvedDatabaseUrl = databaseUrl ;
740- } else if ( driverType === 'mysql' || driverType === 'mysql2' ) {
741- const { SqlDriver } = await import ( '@objectstack/driver-sql' ) ;
742- await kernel . use ( new DriverPlugin ( new SqlDriver ( {
743- client : 'mysql2' ,
744- connection : databaseUrl ,
745- pool : { min : 0 , max : 5 } ,
746- autoMigrate : isDev ? 'safe' : undefined , // #2186 dev loosen-only self-heal
747- } ) as any ) ) ;
748- trackPlugin ( 'MySQLDriver' ) ;
749- resolvedDriverLabel = 'SqlDriver(mysql2)' ;
750- resolvedDatabaseUrl = databaseUrl ;
751- } else if ( isDev ) {
752- // Default in dev (no DB configured): prefer native SQLite for
753- // production-like SQL at native speed, with a graceful step-down to
754- // wasm SQLite (real SQL + on-disk persistence) then in-memory when the
755- // native better-sqlite3 binary is unavailable — not built, ABI mismatch
756- // after a Node upgrade (e.g. NODE_MODULE_VERSION change), or a blocked
757- // prebuild download. Shared with the explicit-file branch and the
758- // datasource factory via resolveSqliteDriver (#2229), which probes by
759- // actually opening a connection + running SELECT 1 (better-sqlite3 loads
760- // its native addon lazily at first query, not at construction).
761- const { resolveSqliteDriver } = await import ( '@objectstack/service-datasource' ) ;
762- const resolved = await resolveSqliteDriver ( {
763- filename : ':memory:' ,
764- dev : true ,
765- autoMigrate : 'safe' , // #2186 dev loosen-only self-heal
766- warn : ( m ) => console . warn ( chalk . yellow ( m ) ) ,
767- } ) ;
768- await kernel . use ( new DriverPlugin ( resolved . driver ) ) ;
769- trackPlugin ( resolved . engine === 'memory' ? 'MemoryDriver' : resolved . engine === 'sqlite-wasm' ? 'SqliteWasmDriver' : 'SqlDriver' ) ;
770- resolvedDriverLabel = resolved . label ;
771- resolvedDatabaseUrl = resolved . engine === 'memory' ? '(in-memory)' : ':memory:' ;
772697 }
773698 } catch ( e : any ) {
774699 // silent
0 commit comments