From de4e4a9fd77837a7f96d5370d6cb71bdd51eb541 Mon Sep 17 00:00:00 2001 From: GermanBluefox Date: Sat, 3 Oct 2026 20:04:19 +0300 Subject: [PATCH] Build without the optional mysql2 and sqlite3 drivers The drivers are optionalDependencies and are import()ed lazily, so a missing one is supposed to degrade to a log message. The build was stricter than the runtime: `import type { Database } from 'sqlite3'` makes TypeScript resolve the package while type checking, and so does `import('sqlite3')`, even though that specifier is only evaluated at runtime. Both fail with TS2307 when the driver is absent. That is not hypothetical - adapter-tests-sqlite (24.x, macos-latest) failed exactly this way on #578: npm skipped 38 optional packages on that runner (sqlite3, cpu-features, diskusage, unix-dgram and their trees) with nothing but a warning, and `npm run build:ts` then died on sqlite3-client.ts lines 5 and 16. Measured, one driver removed at a time: without sqlite3 2 errors (sqlite3-client.ts:5, :16) without mysql2 3 errors (connection-options.ts:1, mysql-client.ts:5, :13) without mssql ok without pg ok Only the two drivers that ship their types inside the package break; `pg` and `mssql` are covered by @types/pg and @types/mssql, which are regular devDependencies and therefore always installed. src/lib/optional-drivers.ts now declares the sliver of mysql2 and sqlite3 this adapter actually touches, and importDriver() routes the specifier through a `string` so the type checker cannot resolve it either. The emitted code is unchanged in substance - `import(name)` instead of `import('sqlite3')` - and tsc now succeeds with all four drivers absent. Verified at runtime too: opening an in-memory SQLite database, querying it and closing it still works, and a removed driver still reports `Node.js DB driver "sqlite3" could not be loaded` instead of crashing. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 1 + build/lib/mysql-client.js | 3 +- build/lib/mysql-client.js.map | 2 +- build/lib/optional-drivers.js | 37 ++++++++++++++++ build/lib/optional-drivers.js.map | 1 + build/lib/sqlite3-client.js | 3 +- build/lib/sqlite3-client.js.map | 2 +- src/lib/connection-options.ts | 2 +- src/lib/mysql-client.ts | 8 ++-- src/lib/optional-drivers.ts | 72 +++++++++++++++++++++++++++++++ src/lib/sqlite3-client.ts | 17 +++++--- 11 files changed, 133 insertions(+), 15 deletions(-) create mode 100644 build/lib/optional-drivers.js create mode 100644 build/lib/optional-drivers.js.map create mode 100644 src/lib/optional-drivers.ts diff --git a/README.md b/README.md index f4433634..f112d3f5 100644 --- a/README.md +++ b/README.md @@ -621,6 +621,7 @@ sendTo('sql.0', 'getEnabledDPs', {}, function (result) { ## Changelog ### **WORK IN PROGRESS** +* (@GermanBluefox) `npm run build:ts` no longer needs the optional `mysql2` and `sqlite3` drivers to be installed * (@GermanBluefox) Fixed buffered values being lost when the adapter stops, and unload no longer hangs when there is nothing else to write (#577) * (@GermanBluefox) `getCounter` no longer reads the NULL boundary marker that `writeNulls` stores on start and stop as a zero counter value (#577) * (@GermanBluefox) Fixed "Test connection" reporting a failure for a working configuration: the request was built as a text template that could produce invalid JSON, and it left out `dbname`, `doNotCreateDatabase` and the docker settings (#355) diff --git a/build/lib/mysql-client.js b/build/lib/mysql-client.js index ba3b7970..f8d1c85f 100644 --- a/build/lib/mysql-client.js +++ b/build/lib/mysql-client.js @@ -7,11 +7,12 @@ exports.MySQL2ClientPool = exports.MySQL2Client = void 0; const connection_factory_1 = require("./connection-factory"); const sql_client_1 = __importDefault(require("./sql-client")); const sql_client_pool_1 = require("./sql-client-pool"); +const optional_drivers_1 = require("./optional-drivers"); class MySQL2ConnectionFactory extends connection_factory_1.ConnectionFactory { createConnection; openConnection(options, callback) { if (!this.createConnection) { - void import('mysql2').then(mysql2 => { + void (0, optional_drivers_1.importDriver)('mysql2').then(mysql2 => { this.createConnection = mysql2.default.createConnection; this.openConnection(options, callback); }, diff --git a/build/lib/mysql-client.js.map b/build/lib/mysql-client.js.map index c782c6ac..39914b76 100644 --- a/build/lib/mysql-client.js.map +++ b/build/lib/mysql-client.js.map @@ -1 +1 @@ -{"version":3,"file":"mysql-client.js","sourceRoot":"","sources":["../../src/lib/mysql-client.ts"],"names":[],"mappings":";;;;;;AAAA,6DAA6E;AAC7E,8DAAqC;AACrC,uDAAmE;AAMnE,MAAM,uBAAwB,SAAQ,sCAAiB;IAC3C,gBAAgB,CAAM;IAC9B,cAAc,CAAC,OAAqB,EAAE,QAA8D;QAChG,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACzB,KAAK,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CACtB,MAAM,CAAC,EAAE;gBACL,IAAI,CAAC,gBAAgB,GAAG,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC;gBACxD,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAC3C,CAAC;YACD,0EAA0E;YAC1E,iEAAiE;YACjE,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,mDAAmD,CAAC,EAAE,CAAC,CAAC,CACnF,CAAC;YACF,OAAO;QACX,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAClD,UAAU,CAAC,OAAO,CAAC,CAAC,GAAiB,EAAQ,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC;IAC/E,CAAC;IAED,eAAe,CAAC,UAAyC,EAAE,QAAwC;QAC/F,IAAI,UAAU,EAAE,CAAC;YACb,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,CAAC;aAAM,CAAC;YACJ,QAAQ,EAAE,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;IACL,CAAC;IAED,OAAO,CACH,UAAyB,EACzB,GAAW,EACX,QAAqE;QAErE,gGAAgG;QAChG,iGAAiG;QACjG,oGAAoG;QACpG,oGAAoG;QACpG,mGAAmG;QACnG,cAAc;QACd,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,GAA6B,EAAE,OAAiB,EAAE,EAAE;YACvE,IAAI,GAAG,EAAE,CAAC;gBACN,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC;YACzB,CAAC;YACD,OAAO,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;IACP,CAAC;CACJ;AAED,MAAa,YAAa,SAAQ,oBAAS;IACvC,YAAY,aAA2B;QACnC,KAAK,CAAC,aAAa,EAAE,IAAI,uBAAuB,EAAE,CAAC,CAAC;IACxD,CAAC;CACJ;AAJD,oCAIC;AAED,MAAa,gBAAiB,SAAQ,+BAAa;IAC/C,YAAY,WAAuB,EAAE,UAAwB;QACzD,KAAK,CAAC,WAAW,EAAE,UAAU,EAAE,IAAI,uBAAuB,EAAE,CAAC,CAAC;IAClE,CAAC;CACJ;AAJD,4CAIC"} \ No newline at end of file +{"version":3,"file":"mysql-client.js","sourceRoot":"","sources":["../../src/lib/mysql-client.ts"],"names":[],"mappings":";;;;;;AAAA,6DAA6E;AAC7E,8DAAqC;AACrC,uDAAmE;AAEnE,yDAA6G;AAI7G,MAAM,uBAAwB,SAAQ,sCAAiB;IAC3C,gBAAgB,CAAM;IAC9B,cAAc,CAAC,OAAqB,EAAE,QAAmE;QACrG,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACzB,KAAK,IAAA,+BAAY,EAAc,QAAQ,CAAC,CAAC,IAAI,CACzC,MAAM,CAAC,EAAE;gBACL,IAAI,CAAC,gBAAgB,GAAG,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC;gBACxD,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAC3C,CAAC;YACD,0EAA0E;YAC1E,iEAAiE;YACjE,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,mDAAmD,CAAC,EAAE,CAAC,CAAC,CACnF,CAAC;YACF,OAAO;QACX,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAClD,UAAU,CAAC,OAAO,CAAC,CAAC,GAAiB,EAAQ,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC;IAC/E,CAAC;IAED,eAAe,CAAC,UAA8C,EAAE,QAAwC;QACpG,IAAI,UAAU,EAAE,CAAC;YACb,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,CAAC;aAAM,CAAC;YACJ,QAAQ,EAAE,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;IACL,CAAC;IAED,OAAO,CACH,UAAyB,EACzB,GAAW,EACX,QAAqE;QAErE,gGAAgG;QAChG,iGAAiG;QACjG,oGAAoG;QACpG,oGAAoG;QACpG,mGAAmG;QACnG,cAAc;QACd,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,GAA6B,EAAE,OAAiB,EAAE,EAAE;YACvE,IAAI,GAAG,EAAE,CAAC;gBACN,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC;YACzB,CAAC;YACD,OAAO,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;IACP,CAAC;CACJ;AAED,MAAa,YAAa,SAAQ,oBAAS;IACvC,YAAY,aAA2B;QACnC,KAAK,CAAC,aAAa,EAAE,IAAI,uBAAuB,EAAE,CAAC,CAAC;IACxD,CAAC;CACJ;AAJD,oCAIC;AAED,MAAa,gBAAiB,SAAQ,+BAAa;IAC/C,YAAY,WAAuB,EAAE,UAAwB;QACzD,KAAK,CAAC,WAAW,EAAE,UAAU,EAAE,IAAI,uBAAuB,EAAE,CAAC,CAAC;IAClE,CAAC;CACJ;AAJD,4CAIC"} \ No newline at end of file diff --git a/build/lib/optional-drivers.js b/build/lib/optional-drivers.js new file mode 100644 index 00000000..4622c0d0 --- /dev/null +++ b/build/lib/optional-drivers.js @@ -0,0 +1,37 @@ +"use strict"; +/** + * Types and the loader for the optional database drivers. + * + * `mysql2`, `pg`, `mssql` and `sqlite3` are **optionalDependencies**: npm skips a driver whose + * installation fails and says so only in a warning, so a checkout can legitimately be missing one. + * At runtime that is handled - the drivers are `import()`ed lazily and a missing one degrades to a + * log message. The *build* used to be stricter than the runtime: + * + * - `import type { Database } from 'sqlite3'` makes TypeScript resolve the package while type + * checking, and + * - so does `import('sqlite3')`, even though the specifier is only ever evaluated at runtime. + * + * Both fail with TS2307 when the driver is absent, so `npm run build:ts` broke on a CI runner that + * had skipped the optional dependency. `pg` and `mssql` survive that because `@types/pg` and + * `@types/mssql` are regular devDependencies and are therefore always installed; `mysql2` and + * `sqlite3` ship their types inside the driver package and have no `@types` fallback. + * + * Hence the declarations below: they describe only the sliver of each driver this adapter actually + * touches, which keeps the build independent of whether the driver is installed. When a new driver + * call is needed, add it here rather than reaching back into the driver's own types. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.importDriver = importDriver; +/** + * Import an optional driver at runtime without making the build depend on it. + * + * TypeScript resolves `import('')` while type checking, so a literal specifier would + * reintroduce the TS2307 this module exists to avoid. Passing the name through a `string`-typed + * parameter keeps the import opaque to the type checker; Node resolves it exactly as before. + * + * @param name the driver's package name + */ +function importDriver(name) { + return import(name); +} +//# sourceMappingURL=optional-drivers.js.map \ No newline at end of file diff --git a/build/lib/optional-drivers.js.map b/build/lib/optional-drivers.js.map new file mode 100644 index 00000000..be920335 --- /dev/null +++ b/build/lib/optional-drivers.js.map @@ -0,0 +1 @@ +{"version":3,"file":"optional-drivers.js","sourceRoot":"","sources":["../../src/lib/optional-drivers.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;AAiDH,oCAEC;AAXD;;;;;;;;GAQG;AACH,SAAgB,YAAY,CAAI,IAAY;IACxC,OAAO,MAAM,CAAC,IAAI,CAAe,CAAC;AACtC,CAAC"} \ No newline at end of file diff --git a/build/lib/sqlite3-client.js b/build/lib/sqlite3-client.js index d12cbe81..cd475e40 100644 --- a/build/lib/sqlite3-client.js +++ b/build/lib/sqlite3-client.js @@ -7,11 +7,12 @@ exports.SQLite3ClientPool = exports.SQLite3Client = exports.SQLite3ConnectionFac const connection_factory_1 = require("./connection-factory"); const sql_client_1 = __importDefault(require("./sql-client")); const sql_client_pool_1 = require("./sql-client-pool"); +const optional_drivers_1 = require("./optional-drivers"); class SQLite3ConnectionFactory extends connection_factory_1.ConnectionFactory { Database; openConnection(options, callback) { if (!this.Database) { - void import('sqlite3').then(sqlite3 => { + void (0, optional_drivers_1.importDriver)('sqlite3').then(sqlite3 => { this.Database = sqlite3.default.Database; this.openConnection(options, callback); }, diff --git a/build/lib/sqlite3-client.js.map b/build/lib/sqlite3-client.js.map index 76370a7c..804f9be8 100644 --- a/build/lib/sqlite3-client.js.map +++ b/build/lib/sqlite3-client.js.map @@ -1 +1 @@ -{"version":3,"file":"sqlite3-client.js","sourceRoot":"","sources":["../../src/lib/sqlite3-client.ts"],"names":[],"mappings":";;;;;;AAAA,6DAAyD;AACzD,8DAAqC;AACrC,uDAAmE;AAQnE,MAAa,wBAAyB,SAAQ,sCAAiB;IACnD,QAAQ,CAA8B;IAE9C,cAAc,CAAC,OAAuB,EAAE,QAA4D;QAChG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACjB,KAAK,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CACvB,OAAO,CAAC,EAAE;gBACN,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;gBACzC,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAC3C,CAAC;YACD,2EAA2E;YAC3E,iEAAiE;YACjE,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,oDAAoD,CAAC,EAAE,CAAC,CAAC,CACpF,CAAC;YACF,OAAO;QACX,CAAC;QAED,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YACf,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,GAAiB,EAAQ,EAAE;gBACrF,IAAI,GAAG,EAAE,CAAC;oBACN,QAAQ,CAAC,GAAG,CAAC,CAAC;gBAClB,CAAC;qBAAM,CAAC;oBACJ,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;gBACvB,CAAC;YACL,CAAC,CAAC,CAAC;YACH,OAAO;QACX,CAAC;QACD,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,GAAiB,EAAQ,EAAE;YACvE,IAAI,GAAG,EAAE,CAAC;gBACN,QAAQ,CAAC,GAAG,CAAC,CAAC;YAClB,CAAC;iBAAM,CAAC;gBACJ,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACvB,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC;IAED,eAAe,CAAC,EAAY,EAAE,QAAuC;QACjE,IAAI,EAAE,EAAE,CAAC;YACL,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACvB,CAAC;aAAM,CAAC;YACJ,QAAQ,EAAE,EAAE,CAAC;QACjB,CAAC;IACL,CAAC;IAED,OAAO,CAAI,EAAY,EAAE,GAAW,EAAE,QAAwD;QAC1F,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;IAC9B,CAAC;CACJ;AA/CD,4DA+CC;AAED,MAAa,aAAc,SAAQ,oBAAS;IACxC,YAAY,aAA6B;QACrC,KAAK,CAAC,aAAa,EAAE,IAAI,wBAAwB,EAAE,CAAC,CAAC;IACzD,CAAC;CACJ;AAJD,sCAIC;AAED,MAAa,iBAAkB,SAAQ,+BAAa;IAChD,YAAY,WAAuB,EAAE,aAA6B;QAC9D,KAAK,CAAC,WAAW,EAAE,aAAa,EAAE,IAAI,wBAAwB,EAAE,CAAC,CAAC;IACtE,CAAC;CACJ;AAJD,8CAIC"} \ No newline at end of file +{"version":3,"file":"sqlite3-client.js","sourceRoot":"","sources":["../../src/lib/sqlite3-client.ts"],"names":[],"mappings":";;;;;;AAAA,6DAAyD;AACzD,8DAAqC;AACrC,uDAAmE;AAEnE,yDAK4B;AAM5B,MAAa,wBAAyB,SAAQ,sCAAiB;IACnD,QAAQ,CAAyC;IAEzD,cAAc,CAAC,OAAuB,EAAE,QAAmE;QACvG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACjB,KAAK,IAAA,+BAAY,EAAgB,SAAS,CAAC,CAAC,IAAI,CAC5C,OAAO,CAAC,EAAE;gBACN,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;gBACzC,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAC3C,CAAC;YACD,2EAA2E;YAC3E,iEAAiE;YACjE,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,oDAAoD,CAAC,EAAE,CAAC,CAAC,CACpF,CAAC;YACF,OAAO;QACX,CAAC;QAED,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YACf,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,GAAiB,EAAQ,EAAE;gBACrF,IAAI,GAAG,EAAE,CAAC;oBACN,QAAQ,CAAC,GAAG,CAAC,CAAC;gBAClB,CAAC;qBAAM,CAAC;oBACJ,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;gBACvB,CAAC;YACL,CAAC,CAAC,CAAC;YACH,OAAO;QACX,CAAC;QACD,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,GAAiB,EAAQ,EAAE;YACvE,IAAI,GAAG,EAAE,CAAC;gBACN,QAAQ,CAAC,GAAG,CAAC,CAAC;YAClB,CAAC;iBAAM,CAAC;gBACJ,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACvB,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC;IAED,eAAe,CAAC,EAAmB,EAAE,QAAuC;QACxE,IAAI,EAAE,EAAE,CAAC;YACL,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACvB,CAAC;aAAM,CAAC;YACJ,QAAQ,EAAE,EAAE,CAAC;QACjB,CAAC;IACL,CAAC;IAED,OAAO,CAAI,EAAmB,EAAE,GAAW,EAAE,QAAwD;QACjG,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;IAC9B,CAAC;CACJ;AA/CD,4DA+CC;AAED,MAAa,aAAc,SAAQ,oBAAS;IACxC,YAAY,aAA6B;QACrC,KAAK,CAAC,aAAa,EAAE,IAAI,wBAAwB,EAAE,CAAC,CAAC;IACzD,CAAC;CACJ;AAJD,sCAIC;AAED,MAAa,iBAAkB,SAAQ,+BAAa;IAChD,YAAY,WAAuB,EAAE,aAA6B;QAC9D,KAAK,CAAC,WAAW,EAAE,aAAa,EAAE,IAAI,wBAAwB,EAAE,CAAC,CAAC;IACtE,CAAC;CACJ;AAJD,8CAIC"} \ No newline at end of file diff --git a/src/lib/connection-options.ts b/src/lib/connection-options.ts index bfb42244..0c18e88d 100644 --- a/src/lib/connection-options.ts +++ b/src/lib/connection-options.ts @@ -1,4 +1,4 @@ -import type { ConnectionOptions as MySQLOptions } from 'mysql2'; +import type { MySQLOptions } from './optional-drivers'; import type { SqlAdapterConfigTyped } from '../types'; export type { MySQLOptions }; diff --git a/src/lib/mysql-client.ts b/src/lib/mysql-client.ts index 83e5b06e..5f9ca635 100644 --- a/src/lib/mysql-client.ts +++ b/src/lib/mysql-client.ts @@ -2,15 +2,15 @@ import { ConnectionFactory, type SQLConnection } from './connection-factory'; import SQLClient from './sql-client'; import { SQLClientPool, type PoolConfig } from './sql-client-pool'; -import type { Connection, ConnectionOptions as MySQLOptions } from 'mysql2'; +import { importDriver, type MySQLConnection, type MySQLModule, type MySQLOptions } from './optional-drivers'; export type { MySQLOptions }; class MySQL2ConnectionFactory extends ConnectionFactory { private createConnection: any; - openConnection(options: MySQLOptions, callback: (err: Error | null, connection?: Connection) => void): void { + openConnection(options: MySQLOptions, callback: (err: Error | null, connection?: MySQLConnection) => void): void { if (!this.createConnection) { - void import('mysql2').then( + void importDriver('mysql2').then( mysql2 => { this.createConnection = mysql2.default.createConnection; this.openConnection(options, callback); @@ -26,7 +26,7 @@ class MySQL2ConnectionFactory extends ConnectionFactory { connection.connect((err: Error | null): void => callback(err, connection)); } - closeConnection(connection: Connection | null | undefined, callback: (error?: Error | null) => void): void { + closeConnection(connection: MySQLConnection | null | undefined, callback: (error?: Error | null) => void): void { if (connection) { connection.end(callback); } else { diff --git a/src/lib/optional-drivers.ts b/src/lib/optional-drivers.ts new file mode 100644 index 00000000..d10a871e --- /dev/null +++ b/src/lib/optional-drivers.ts @@ -0,0 +1,72 @@ +/** + * Types and the loader for the optional database drivers. + * + * `mysql2`, `pg`, `mssql` and `sqlite3` are **optionalDependencies**: npm skips a driver whose + * installation fails and says so only in a warning, so a checkout can legitimately be missing one. + * At runtime that is handled - the drivers are `import()`ed lazily and a missing one degrades to a + * log message. The *build* used to be stricter than the runtime: + * + * - `import type { Database } from 'sqlite3'` makes TypeScript resolve the package while type + * checking, and + * - so does `import('sqlite3')`, even though the specifier is only ever evaluated at runtime. + * + * Both fail with TS2307 when the driver is absent, so `npm run build:ts` broke on a CI runner that + * had skipped the optional dependency. `pg` and `mssql` survive that because `@types/pg` and + * `@types/mssql` are regular devDependencies and are therefore always installed; `mysql2` and + * `sqlite3` ship their types inside the driver package and have no `@types` fallback. + * + * Hence the declarations below: they describe only the sliver of each driver this adapter actually + * touches, which keeps the build independent of whether the driver is installed. When a new driver + * call is needed, add it here rather than reaching back into the driver's own types. + */ + +/** The `mysql2` connection options this adapter sets. */ +export interface MySQLOptions { + host?: string; + port?: number; + user?: string; + password?: string; + /** Takes precedence over host and port - mysql2 then ignores both. */ + socketPath?: string; + ssl?: { rejectUnauthorized: boolean }; +} + +/** The `mysql2` connection surface this adapter uses. */ +export interface MySQLConnection { + end(callback?: (err?: Error | null) => void): void; +} + +/** The `sqlite3` database surface this adapter uses. */ +export interface SQLite3Database { + close(callback?: (err: Error | null) => void): void; + all(sql: string, params: Array, callback: (err: Error | null, rows?: Array) => void): void; +} + +/** The two `new sqlite3.Database(...)` overloads this adapter calls. */ +export interface SQLite3DatabaseConstructor { + new (fileName: string, callback: (err: Error | null) => void): SQLite3Database; + new (fileName: string, mode: number, callback: (err: Error | null) => void): SQLite3Database; +} + +/** Shape of `await import('mysql2')`. */ +export interface MySQLModule { + default: { createConnection: (options: MySQLOptions) => MySQLConnection }; +} + +/** Shape of `await import('sqlite3')`. */ +export interface SQLite3Module { + default: { Database: SQLite3DatabaseConstructor }; +} + +/** + * Import an optional driver at runtime without making the build depend on it. + * + * TypeScript resolves `import('')` while type checking, so a literal specifier would + * reintroduce the TS2307 this module exists to avoid. Passing the name through a `string`-typed + * parameter keeps the import opaque to the type checker; Node resolves it exactly as before. + * + * @param name the driver's package name + */ +export function importDriver(name: string): Promise { + return import(name) as Promise; +} diff --git a/src/lib/sqlite3-client.ts b/src/lib/sqlite3-client.ts index b1176d29..83d35389 100644 --- a/src/lib/sqlite3-client.ts +++ b/src/lib/sqlite3-client.ts @@ -2,18 +2,23 @@ import { ConnectionFactory } from './connection-factory'; import SQLClient from './sql-client'; import { SQLClientPool, type PoolConfig } from './sql-client-pool'; -import type { Database } from 'sqlite3'; +import { + importDriver, + type SQLite3Database, + type SQLite3DatabaseConstructor, + type SQLite3Module, +} from './optional-drivers'; type SQLite3Options = { fileName: string; mode?: number }; export type { SQLite3Options }; export class SQLite3ConnectionFactory extends ConnectionFactory { - private Database: typeof Database | undefined; + private Database: SQLite3DatabaseConstructor | undefined; - openConnection(options: SQLite3Options, callback: (err: Error | null, connection?: Database) => void): void { + openConnection(options: SQLite3Options, callback: (err: Error | null, connection?: SQLite3Database) => void): void { if (!this.Database) { - void import('sqlite3').then( + void importDriver('sqlite3').then( sqlite3 => { this.Database = sqlite3.default.Database; this.openConnection(options, callback); @@ -44,7 +49,7 @@ export class SQLite3ConnectionFactory extends ConnectionFactory { }); } - closeConnection(db: Database, callback?: (err?: Error | null) => void): void { + closeConnection(db: SQLite3Database, callback?: (err?: Error | null) => void): void { if (db) { db.close(callback); } else { @@ -52,7 +57,7 @@ export class SQLite3ConnectionFactory extends ConnectionFactory { } } - execute(db: Database, sql: string, callback: (err: Error | null, result?: Array) => void): void { + execute(db: SQLite3Database, sql: string, callback: (err: Error | null, result?: Array) => void): void { db.all(sql, [], callback); } }