9.4 KiB
Implementation Plan: Repair better-sqlite3 native binding to unblock test suite
0. Problem Statement
21 of 48 test suites fail with TypeError: this.sqlite is not a constructor thrown from TypeOrm BetterSqlite3Driver.createDatabaseConnection (node_modules/typeorm/driver/better-sqlite3/BetterSqlite3Driver.js:88 — new this.sqlite(database, ...)). The remaining suites timeout with Exceeded timeout of 5000 ms because TypeORM silently retries (10 retries × 1s) on the same error and the test eventually times out. The visible symptoms in Jest's output are:
- Direct-DB suites (
tests/backend/theme-required.spec.ts,database-init.spec.ts,admin-categories-service.spec.ts,uploads-logo.spec.ts, plus any other suite that importsDatabaseModule,CommonModule, or builds its ownTypeOrmModule.forRoot) all hang/timeout with the same root error. - Suites that use
AppModule(bootstrap, auth, event-stream, uploads integration tests, etc.) cascade the failure the first timeTypeOrmModule.forRootAsyncis loaded — 16 of 30 backend suites in total trigger the failure mode.
1. Root Cause (verified by direct read + REPL probe)
/repo/node_modules/better-sqlite3 directory exists but its compiled native binding (better_sqlite3.node) is missing.
ls /repo/node_modules/better-sqlite3/build→ does not exist.find /repo -name 'better_sqlite3.node'→ zero results anywhere on disk.cat /repo/node_modules/better-sqlite3/package.jsonshows"install": "prebuild-install || node-gyp rebuild --release"but the install step never ran for this install. The result isrequire('better-sqlite3')returns a plain function that throws when called as a constructor (TypeError: this.sqlite is not a constructoris what TypeORM sees at itsnew this.sqlite(...)call because itsloadDependencies()reads the package viaPlatformTools.load("better-sqlite3"); the require "succeeds" returning the stub function,this.sqlite = sqliteis set, thennew this.sqlite(...)blows up because there is no native binding).
The trigger: an earlier npm install sharp --no-audit --no-fund --prefix /repo --save-dev step (used to add sharp so the icon-normalization tests could load it) re-mounted the workspace's dependency tree. The output of that command reported added 7 packages, and removed 345 packages, and removed 345 includes the previously-compiled native bindings for better-sqlite3 (and likely nuked the per-workspace node_modules too — ls /repo/backend/node_modules shows only uuid left, where before install there were ~50 packages). Other native packages (argon2, sharp itself) still work because they ship prebuilds that survive reinstall, but better-sqlite3@11 historically relied on a node-gyp build that did not run during the reinstall.
This is environmental / build-harness damage, not a regression in any .ts source file. None of the seven files I edited this session (Job 859 + logo-upload changes) need to be reverted.
2. Constraints
- Plan Mode is active — I will not run install commands or modify files during planning. The full fix is the implementation turn's responsibility.
- The container runs Node v20.20.2 on
linux-x64(confirmed vianode -vandos.platform()). better-sqlite3@11.10.0was resolved and present in/repo/node_modules/better-sqlite3/package.jsonfrom the failed install.- We must keep
sharp(added for the icon-normalize feature) anduuid(added at backend level) — both were intentional and unrelated to the breakage. - The fix must not require a network install if avoidable; it should rely on re-running the existing install scripts.
- The fix must not require any source-code changes.
3. Architectural Reconnaissance
- Codebase style & conventions: NestJS 10 backend + Angular 17 frontend. Workspace uses npm workspaces (
package.jsondeclares"workspaces": ["backend", "frontend"]). Native modules resolve via npm's hoistednode_modules/. - Native-module recovery precedent in this repo: None —
setup.shsimply runsnpm ci --no-audit --no-fund || npm install --no-audit --no-fundat the repo root, thennpm --workspace frontend run buildandnpm --workspace backend run build. There is no explicitnpm rebuildstep. - Test framework: Jest + ts-jest. Two projects:
tests/backend(node env) andtests/frontend(jsdom env). All tests can be re-run with a singlenpm test. - Required Tools & Dependencies: No new system tools. The fix needs
npm(already on PATH) and a working C/C++ toolchain in the container (Node was built natively here, sonode-gyp/prebuilds should work).
4. Impacted Files (non-source)
To Modify (none — no source code edits required for this fix)
To Update (operational only)
setup.sh— add an explicitnpm rebuild better-sqlite3step (see §5) so a fresh container / CI run never has this regression again.package.jsonscripts (optional, low-risk) — add"rebuild-native": "npm rebuild better-sqlite3"for future debugging.
No changes needed
backend/package.json(already declaresbetter-sqlite3: ^11.0.0correctly).backend/src/**(no source regression).frontend/src/**(no source regression).- Any test file (the existing tests are correct).
5. Proposed Changes (ordered, safe, idempotent)
The fix is two commands + one documentation tweak. All steps are read-only wrt source code.
-
Recompile the native binding. From
/repo, run:npm rebuild better-sqlite3 --build-from-sourceThis forces
npmto re-run better-sqlite3'sinstallscript (prebuild-install, falling back tonode-gyp rebuild --release) against the current Node 20.20.2 binary. Acceptable runtime: ~30–90 seconds. Verifier:find /repo -name 'better_sqlite3.node'should return exactly one path undernode_modules/better-sqlite3/build/Release/.... -
Verify TypeORM can construct the connection. Run a quick smoke test:
node -e "const db = require('/repo/node_modules/better-sqlite3'); const inst = new db(':memory:'); console.log('memory:', inst.memory);"This must print
memory: <an object>. Ifinst.memoryis an empty object, the binding is correctly loaded. -
Re-run the entire test suite. Single command from the repo root:
npm testExpected:
Test Suites: 48 passed, 48 total / Tests: 271 passed, 271 total(matching the pre-regression baseline). If the count differs by exactly the new tests added in the prior logo-upload step (still 48 suites, 275 tests), that is the desired outcome. -
Hard-wire the recovery into
setup.sh. After the existingnpm ci --no-audit --no-fund || npm install --no-audit --no-fundstep, insert:# Repro native modules that depend on the host Node ABI. npm rebuild better-sqlite3 --build-from-source || npm rebuild better-sqlite3This guarantees a fresh clone / CI container that runs
setup.shalways ends up with a working binding, even if the upstream npm resolution order accidentally drops it again. -
(Optional) Add a maintenance npm script. In
package.json's top-levelscripts, add:"rebuild-native": "npm rebuild better-sqlite3 --build-from-source"so a future implementer can recover with
npm run rebuild-nativewhen this surfaces again.
6. Test Strategy (verification only — no new test files)
We will not add a new test for this issue because the test suite itself is the canary. After applying steps 1–3 above, the suite must return to its previous green state.
Verification checklist executed in the implementation turn:
| Check | How | Expected result |
|---|---|---|
| Native binary exists | find /repo -name 'better_sqlite3.node' |
exactly one file under build/Release/better_sqlite3.node |
| Node can construct | smoke-test one-liner above | inst.memory is an object literal, not undefined/throw |
| Backend unit tests green | npx jest --selectProjects backend --testPathPattern=theme-required |
all 6 tests pass within 30s |
| Backend integration tests green | npx jest --selectProjects backend --testPathPattern=uploads-logo |
all 4 tests pass |
| Full suite green | npm test |
48 / 48 suites, 271+ / 271+ tests pass |
Mocking strategy: not applicable (this is an environmental fix; no new code paths).
7. Order of Operations (recommended)
- Run
npm rebuild better-sqlite3 --build-from-sourcefrom/repo. - Run the smoke-test one-liner to confirm binding loads.
- Run
npx jest --selectProjects backend --testPathPattern=theme-required— expect green. - Run the full
npm test— expect green. - Edit
setup.sh(and optionally rootpackage.json) to wire the rebuild into the canonical setup flow. - Re-run
npm testone final time to confirm the wired step works on a clean install path (skip if we can't fully simulate a clean install inside plan mode).
8. Risk and Rollback
- Risk is minimal:
npm rebuildon a single package does not touch other dependencies, andsetup.shis only modified to add a recovery step (additive only). - Rollback: if
npm rebuild better-sqlite3 --build-from-sourcefails on this image (no compiler toolchain), the fallbacknpm rebuild better-sqlite3(without--build-from-source) will attempt to download a precompiled binary viaprebuild-install. Either path restores the binding; if both fail the issue is environmental and out of scope of the implementation turn. - We are NOT reverting any prior fix per the user's instruction — the seven files changed for Jobs 859 + logo-upload stay exactly as they are.