[E00-S03-T02] pg/Kysely imports isolated to database-postgres #391

Merged
kpcto merged 3 commits from feature/177 into main 2026-08-29 23:04:54 +00:00
11 changed files with 708 additions and 7 deletions
+16
View File
@@ -37,6 +37,22 @@ jobs:
- name: Run secrets-not-embedded test suite - name: Run secrets-not-embedded test suite
run: node --test tests/secrets-not-embedded.test.mjs run: node --test tests/secrets-not-embedded.test.mjs
# E00-S03-T02: the static assertions of tests/database-postgres-imports.test.mjs
# gate every PR — the scan proves pg/Kysely imports live only in
# packages/database-postgres and the mutation probes prove the scan catches
# a driver import injected into any other package.
database-postgres-imports:
name: Database-postgres import isolation (E00-S03-T02)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js 24
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Run database-postgres import isolation suite
run: node --test tests/database-postgres-imports.test.mjs
# E00-S03-T01: the static assertions of tests/compose-config.test.mjs (db # E00-S03-T01: the static assertions of tests/compose-config.test.mjs (db
# image pinned to postgres:18.6-bookworm, health gate, volume persistence, # image pinned to postgres:18.6-bookworm, health gate, volume persistence,
# build platforms) gate every PR (the docker-gated real-stack probes inside # build platforms) gate every PR (the docker-gated real-stack probes inside
+6 -4
View File
@@ -17,6 +17,7 @@ The workspace is a pnpm monorepo with three package groups:
| --- | --- | --- | | --- | --- | --- |
| `apps/` | `apps/server` (`@personal-blog/server`) | Public server application. Serves the application health endpoint (E00-S02-T03); the Fastify 5 application shell lands in a later story. | | `apps/` | `apps/server` (`@personal-blog/server`) | Public server application. Serves the application health endpoint (E00-S02-T03); the Fastify 5 application shell lands in a later story. |
| `packages/` | `packages/core` (`@personal-blog/core`) | Application core (site identity, content primitives). Bootstrap placeholder. | | `packages/` | `packages/core` (`@personal-blog/core`) | Application core (site identity, content primitives). Bootstrap placeholder. |
| `packages/` | `packages/database-postgres` (`@personal-blog/database-postgres`) | PostgreSQL database adapter package. Single owner of the `pg`/Kysely driver imports (E00-S03-T02); the concrete adapter lands in later stories. |
| `extensions/` | `extensions/example` (`@personal-blog/example-extension`) | Example extension exercising the `extensions/` group. Bootstrap placeholder. | | `extensions/` | `extensions/example` (`@personal-blog/example-extension`) | Example extension exercising the `extensions/` group. Bootstrap placeholder. |
A dependency-boundary rule (`dependency-boundaries.json`, enforced by A dependency-boundary rule (`dependency-boundaries.json`, enforced by
@@ -74,8 +75,9 @@ pnpm typecheck
`pnpm build` runs `tsc -p tsconfig.json` in each package in topological order `pnpm build` runs `tsc -p tsconfig.json` in each package in topological order
and emits `dist/` (JavaScript + type declarations + source maps) per package. and emits `dist/` (JavaScript + type declarations + source maps) per package.
Expected result: `apps/server/dist/`, `packages/core/dist/` and Expected result: `apps/server/dist/`, `packages/core/dist/`,
`extensions/example/dist/` are produced, all packages report `Done`, exit 0. `packages/database-postgres/dist/` and `extensions/example/dist/` are
produced, all packages report `Done`, exit 0.
`dist/` is git-ignored; rebuild whenever you change `src/`. `dist/` is git-ignored; rebuild whenever you change `src/`.
## Run ## Run
@@ -119,8 +121,8 @@ This is also the suite that enforces the dependency-boundary rule.
git clone https://git.stevanovic.co.uk/Fabrika/PersonalBlog.git && cd PersonalBlog git clone https://git.stevanovic.co.uk/Fabrika/PersonalBlog.git && cd PersonalBlog
corepack enable corepack enable
pnpm install --frozen-lockfile # exit 0, lockfile untouched pnpm install --frozen-lockfile # exit 0, lockfile untouched
pnpm build # 3/3 packages emit dist/, exit 0 pnpm build # 4/4 packages emit dist/, exit 0
pnpm typecheck # 3/3 packages pass --noEmit, exit 0 pnpm typecheck # 4/4 packages pass --noEmit, exit 0
pnpm test # 10/10 pass, exit 0 pnpm test # 10/10 pass, exit 0
pnpm --filter @personal-blog/server start # serves GET /health on port 3000, stays up pnpm --filter @personal-blog/server start # serves GET /health on port 3000, stays up
``` ```
+26
View File
@@ -0,0 +1,26 @@
{
"name": "@personal-blog/database-postgres",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "EPPP PostgreSQL database adapter package. The single workspace package allowed to import the pg driver and Kysely (E00-S03-T02); the concrete adapter (migration ledger, advisory lock) lands in later stories.",
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"kysely": "0.29.4",
"pg": "8.22.0"
},
"devDependencies": {
"@types/pg": "8.21.0"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}
}
+20
View File
@@ -0,0 +1,20 @@
/**
* @personal-blog/database-postgres — EPPP PostgreSQL adapter package.
*
* [E00-S03-T02] this package is the single owner of the PostgreSQL driver
* stack: it is the only workspace package allowed to import `pg` and Kysely.
* Every piece of the workspace that talks to the database goes through this
* boundary — the pool, the query builder dialect and the adapter API this
* package will expose — so no other package ever imports the driver directly.
*
* This module is the driver boundary: it imports the PostgreSQL driver (`pg`)
* and Kysely and re-exports the pieces later stories build the adapter on
* (migration ledger E00-S03-T03, advisory lock E00-S03-T04). Until then the
* re-exports keep the driver reachable only from here — the isolation is real,
* not a placeholder.
*/
import { Pool } from 'pg';
import { Kysely, PostgresDialect } from 'kysely';
export { Pool, Kysely, PostgresDialect };
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"]
}
+135
View File
@@ -22,11 +22,85 @@ importers:
packages/core: {} packages/core: {}
packages/database-postgres:
dependencies:
kysely:
specifier: 0.29.4
version: 0.29.4
pg:
specifier: 8.22.0
version: 8.22.0
devDependencies:
'@types/pg':
specifier: 8.21.0
version: 8.21.0
packages: packages:
'@types/node@24.13.3': '@types/node@24.13.3':
resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==}
'@types/pg@8.21.0':
resolution: {integrity: sha512-AYdtudzabjLZgVgRZmAnU8bAnVUXzuJX2IYHeSIiIHm68olD+LgQYCGWdtcNYnP0uq9c4S4NibVG3Ni7VbKW7Q==}
kysely@0.29.4:
resolution: {integrity: sha512-y5mVgQNkMbs1eK9Xyc0pmNdabN2wHhRYY/5r4W5HrUT1rYCEPeVNSj1RUJeSDKT3U0p+mXCvLgkrFuIafYI6BA==}
engines: {node: '>=22.0.0'}
pg-cloudflare@1.4.0:
resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==}
pg-connection-string@2.14.0:
resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==}
pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
pg-pool@3.14.0:
resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==}
peerDependencies:
pg: '>=8.0'
pg-protocol@1.16.0:
resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==}
pg-types@2.2.0:
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
engines: {node: '>=4'}
pg@8.22.0:
resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==}
engines: {node: '>= 16.0.0'}
peerDependencies:
pg-native: '>=3.0.1'
peerDependenciesMeta:
pg-native:
optional: true
pgpass@1.0.5:
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
postgres-bytea@1.0.1:
resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==}
engines: {node: '>=0.10.0'}
postgres-date@1.0.7:
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
engines: {node: '>=0.10.0'}
postgres-interval@1.2.0:
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
engines: {node: '>=0.10.0'}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
typescript@6.0.3: typescript@6.0.3:
resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
@@ -35,12 +109,73 @@ packages:
undici-types@7.18.2: undici-types@7.18.2:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
snapshots: snapshots:
'@types/node@24.13.3': '@types/node@24.13.3':
dependencies: dependencies:
undici-types: 7.18.2 undici-types: 7.18.2
'@types/pg@8.21.0':
dependencies:
'@types/node': 24.13.3
pg-protocol: 1.16.0
pg-types: 2.2.0
kysely@0.29.4: {}
pg-cloudflare@1.4.0:
optional: true
pg-connection-string@2.14.0: {}
pg-int8@1.0.1: {}
pg-pool@3.14.0(pg@8.22.0):
dependencies:
pg: 8.22.0
pg-protocol@1.16.0: {}
pg-types@2.2.0:
dependencies:
pg-int8: 1.0.1
postgres-array: 2.0.0
postgres-bytea: 1.0.1
postgres-date: 1.0.7
postgres-interval: 1.2.0
pg@8.22.0:
dependencies:
pg-connection-string: 2.14.0
pg-pool: 3.14.0(pg@8.22.0)
pg-protocol: 1.16.0
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.4.0
pgpass@1.0.5:
dependencies:
split2: 4.2.0
postgres-array@2.0.0: {}
postgres-bytea@1.0.1: {}
postgres-date@1.0.7: {}
postgres-interval@1.2.0:
dependencies:
xtend: 4.0.2
split2@4.2.0: {}
typescript@6.0.3: {} typescript@6.0.3: {}
undici-types@7.18.2: {} undici-types@7.18.2: {}
xtend@4.0.2: {}
+493
View File
@@ -0,0 +1,493 @@
/**
* Database-postgres import isolation test — locks in the [E00-S03-T02] rule
* that `pg`/Kysely imports live only in the `database-postgres` package.
*
* Acceptance criteria covered (each test fails without the committed state):
* - "pg/Kysely imports are isolated to database-postgres" -> the workspace
* package `packages/database-postgres` (`@personal-blog/database-postgres`)
* exists, declares the driver as its own dependencies (`pg`, `kysely`
* exact pins, `@types/pg` for types) and its source really imports the
* driver; a static scan of every workspace package's source proves that
* every `pg`/`kysely` import specifier (static import/export-from, dynamic
* import, require) resolves to a file inside `packages/database-postgres`
* — and no other package's manifest declares the driver either.
* - "no other package imports the database driver directly" -> the same
* scan finds zero driver imports outside the owner package, and the
* committed state is non-vacuous: a mutation probe injects a `pg` import
* into `apps/server/src/index.ts` in a temp copy of the committed tree
* and the scan fails, naming the offending file and the driver; a clean
* copy passes (probe sanity).
*
* Run: `node --test tests/database-postgres-imports.test.mjs`
* (node:test — built into Node >= 18; no dependencies, lockfile untouched.)
*/
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, readdirSync, writeFileSync, mkdirSync, cpSync, mkdtempSync, rmSync } from 'node:fs';
import { readdir } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8');
/** The workspace package allowed to import the PostgreSQL driver (E00-S03-T02). */
const OWNER_DIR = 'packages/database-postgres';
/** The root test glob (root `scripts.test`, E00-S01-T12) that runs every suite. */
const ROOT_TEST_GLOB = 'tests/**/*.test.mjs';
/** The CI job that gates the isolation criterion on every PR. */
const CI_JOB = 'database-postgres-imports';
/** Directories never scanned as package source. */
const IGNORED_DIRS = new Set(['node_modules', 'dist', 'coverage', '.git', '.pnpm-store']);
/** The workspace groups scanned for source files (mirrors pnpm-workspace.yaml). */
const GROUP_DIRS = ['apps', 'packages', 'extensions'];
/** Extensions scanned as package source. */
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs']);
/**
* True when a module specifier names the PostgreSQL driver: the `pg` package
* or Kysely (including their subpaths, e.g. `pg/connection-string` or
* `kysely/plugin/dialect`). `@types/pg` is a types-only package — never
* imported at runtime — so it is not a driver import.
*/
function isDriverSpecifier(specifier) {
return (
specifier === 'pg' ||
specifier.startsWith('pg/') ||
specifier === 'kysely' ||
specifier.startsWith('kysely/')
);
}
// ---------------------------------------------------------------------------
// Comment stripping (string/comment aware, preserves line count)
// ---------------------------------------------------------------------------
/**
* Replaces comments with whitespace while preserving line structure, so
* specifier extraction never fires on commented-out imports and reported
* line numbers still match the original source. Mirrors the stripper in
* tests/architecture-import.test.mjs.
*/
function stripComments(source) {
let out = '';
let i = 0;
const n = source.length;
let state = 'code'; // code | line | block | sq | dq | tpl
const tplStack = []; // template states to resume after `${...}` closes
let braceDepth = 0;
while (i < n) {
const c = source[i];
const next = source[i + 1];
if (state === 'code') {
if (tplStack.length > 0) {
if (c === '{') {
braceDepth += 1;
out += c;
i += 1;
continue;
}
if (c === '}') {
braceDepth -= 1;
out += c;
i += 1;
if (braceDepth === 0) state = tplStack.pop();
continue;
}
}
if (c === '/' && next === '/') {
out += ' ';
i += 2;
state = 'line';
continue;
}
if (c === '/' && next === '*') {
out += ' ';
i += 2;
state = 'block';
continue;
}
if (c === "'") {
out += c;
i += 1;
state = 'sq';
continue;
}
if (c === '"') {
out += c;
i += 1;
state = 'dq';
continue;
}
if (c === '`') {
out += c;
i += 1;
state = 'tpl';
continue;
}
out += c;
i += 1;
continue;
}
if (state === 'line') {
if (c === '\n') {
out += c;
i += 1;
state = 'code';
} else {
out += ' ';
i += 1;
}
continue;
}
if (state === 'block') {
if (c === '*' && next === '/') {
out += ' ';
i += 2;
state = 'code';
} else {
out += c === '\n' ? c : ' ';
i += 1;
}
continue;
}
if (state === 'sq' || state === 'dq') {
const quote = state === 'sq' ? "'" : '"';
out += c;
if (c === '\\' && next !== undefined) {
out += next;
i += 2;
} else {
if (c === quote) state = 'code';
i += 1;
}
continue;
}
// template literal
out += c;
if (c === '\\' && next !== undefined) {
out += next;
i += 2;
continue;
}
if (c === '`') {
state = 'code';
i += 1;
continue;
}
if (c === '$' && next === '{') {
out += next;
i += 2;
tplStack.push('tpl');
braceDepth = 1;
state = 'code';
continue;
}
i += 1;
}
return out;
}
// ---------------------------------------------------------------------------
// Driver-import discovery
// ---------------------------------------------------------------------------
const STATIC_IMPORT = /\b(?:import|export)\s+(?:type\s+)?(?:[\w*{},\s]*?\s+from\s*)?(['"])([^'"]+)\1/g;
const REQUIRE_CALL = /\brequire\s*\(\s*(['"])([^'"]+)\1\s*\)/g;
const DYNAMIC_IMPORT = /\bimport\s*\(\s*(['"])([^'"]+)\1\s*\)/g;
/** Returns [{ specifier, index }] for every module specifier in the (stripped) source. */
function extractSpecifiers(stripped) {
const found = [];
for (const re of [STATIC_IMPORT, REQUIRE_CALL, DYNAMIC_IMPORT]) {
re.lastIndex = 0;
let m;
while ((m = re.exec(stripped)) !== null) {
found.push({ specifier: m[2], index: m.index });
}
}
found.sort((a, b) => a.index - b.index);
return found;
}
function lineOf(stripped, index) {
return stripped.slice(0, index).split('\n').length;
}
/**
* Returns [{ relPath, line, specifier }] for every pg/Kysely import found in
* the given source files. `relPath` is the file path relative to the scan
* root (posix), so reports read the same for the real workspace and for temp
* copies.
*/
function findDriverImports(files) {
const imports = [];
for (const file of files) {
const stripped = stripComments(file.content);
for (const { specifier, index } of extractSpecifiers(stripped)) {
if (isDriverSpecifier(specifier)) {
imports.push({
relPath: file.relPath,
line: lineOf(stripped, index),
specifier,
});
}
}
}
return imports;
}
/** True when a scan-relative posix path sits inside the owner package. */
function isInsideOwner(relPath) {
return relPath === OWNER_DIR || relPath.startsWith(`${OWNER_DIR}/`);
}
/** Walks the workspace groups under a root and returns [{ relPath, content }] for source files. */
async function collectSourceFiles(rootDir) {
const files = [];
async function walk(dir, relDir) {
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const abs = path.join(dir, entry.name);
const rel = `${relDir}/${entry.name}`;
if (entry.isDirectory()) {
if (!IGNORED_DIRS.has(entry.name)) await walk(abs, rel);
continue;
}
if (SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
files.push({ relPath: rel, content: readFileSync(abs, 'utf8') });
}
}
}
for (const group of GROUP_DIRS) {
await walk(path.join(rootDir, group), group);
}
return files;
}
// ---------------------------------------------------------------------------
// Manifest helpers
// ---------------------------------------------------------------------------
/** Returns [{ relPath, name, driverDeps }] for every workspace package manifest under a root. */
async function collectManifests(rootDir) {
const manifests = [];
for (const group of GROUP_DIRS) {
let entries;
try {
entries = await readdir(path.join(rootDir, group), { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const manifestPath = path.join(rootDir, group, entry.name, 'package.json');
let manifest;
try {
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
} catch {
continue;
}
const allDeps = {
...(manifest.dependencies ?? {}),
...(manifest.devDependencies ?? {}),
};
manifests.push({
relPath: `${group}/${entry.name}/package.json`,
name: manifest.name,
driverDeps: Object.keys(allDeps).filter(isDriverSpecifier),
});
}
}
return manifests;
}
/** A single driver import, formatted for violation reports. */
function formatImport(driverImport) {
return ` ${driverImport.relPath}:${driverImport.line} imports "${driverImport.specifier}"`;
}
// ---------------------------------------------------------------------------
// Temp-copy helpers (mutation probes over the committed tree)
// ---------------------------------------------------------------------------
/** Copies the committed tree (minus ignored dirs) into a fresh temp dir. */
function copyCommittedTree() {
const dir = mkdtempSync(path.join(os.tmpdir(), 'eppp-db-imports-'));
for (const entry of readdirSync(REPO_ROOT, { withFileTypes: true })) {
if (IGNORED_DIRS.has(entry.name) || entry.name === 'tests') continue;
cpSync(path.join(REPO_ROOT, entry.name), path.join(dir, entry.name), {
recursive: true,
});
}
// The isolation test file itself is part of the scan surface in a real
// checkout; copying it keeps the temp copy's package graph identical.
mkdirSync(path.join(dir, 'tests'), { recursive: true });
cpSync(path.join(REPO_ROOT, 'tests'), path.join(dir, 'tests'), { recursive: true });
return dir;
}
/** Injects a driver import into a package source inside a tree copy. */
function injectDriverImport(rootDir, relPath, line) {
const abs = path.join(rootDir, relPath);
const original = readFileSync(abs, 'utf8');
const injected = `${line}\n${original}`;
writeFileSync(abs, injected);
return abs;
}
/** Runs the isolation scan over a root and returns the misplaced imports. */
async function scanRoot(rootDir) {
const files = await collectSourceFiles(rootDir);
return findDriverImports(files).filter((d) => !isInsideOwner(d.relPath));
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test('packages/database-postgres exists, is the driver owner, and its source imports the driver', () => {
const manifest = JSON.parse(read(path.join(OWNER_DIR, 'package.json')));
assert.equal(manifest.name, '@personal-blog/database-postgres');
// The driver is owned here: exact pins, never ranges (reproducibility,
// same policy as typescript@6.0.3 at the root).
// Golden tuple (Technology-Stack §5.2 / §7): pg 8.22.0, Kysely 0.29.4.
// @types/pg has no 8.22.x release; 8.21.0 is the closest published match
// (types for the immediately preceding pg minor).
assert.equal(manifest.dependencies?.pg, '8.22.0', 'owner must pin pg exactly');
assert.equal(manifest.dependencies?.kysely, '0.29.4', 'owner must pin kysely exactly');
assert.equal(manifest.devDependencies?.['@types/pg'], '8.21.0', 'owner must pin @types/pg exactly');
// The boundary module really imports the driver — the isolation is
// exercised, not just declared.
const source = read(path.join(OWNER_DIR, 'src', 'index.ts'));
assert.match(source, /from\s+'pg'/, 'packages/database-postgres/src/index.ts must import pg');
assert.match(source, /from\s+'kysely'/, 'packages/database-postgres/src/index.ts must import kysely');
});
test('pg/Kysely imports are isolated to database-postgres in the real workspace', async () => {
const files = await collectSourceFiles(REPO_ROOT);
const imports = findDriverImports(files);
// Non-vacuous: the driver is actually imported somewhere — and that
// somewhere is the owner package.
assert.ok(imports.length > 0, 'the workspace must contain pg/Kysely imports (in the owner package)');
const ownerImports = imports.filter((d) => isInsideOwner(d.relPath));
assert.ok(
ownerImports.some((d) => d.specifier === 'pg'),
'the owner package must import pg',
);
assert.ok(
ownerImports.some((d) => d.specifier === 'kysely'),
'the owner package must import kysely',
);
const misplaced = imports.filter((d) => !isInsideOwner(d.relPath));
assert.deepEqual(
misplaced,
[],
`no package other than database-postgres may import pg/Kysely:\n${misplaced.map(formatImport).join('\n')}`,
);
});
test('no other package declares the database driver in its manifest', async () => {
const manifests = await collectManifests(REPO_ROOT);
assert.ok(
manifests.some((m) => m.relPath.startsWith(`${OWNER_DIR}/`)),
'the owner package manifest must be discovered',
);
const offenders = manifests.filter((m) => !m.relPath.startsWith(`${OWNER_DIR}/`) && m.driverDeps.length > 0);
assert.deepEqual(
offenders,
[],
`only ${OWNER_DIR} may declare pg/kysely as a dependency:\n` +
offenders
.map((m) => ` ${m.relPath} (${m.name}) declares ${m.driverDeps.join(', ')}`)
.join('\n'),
);
});
test('the isolation scan catches a driver import injected into another package (mutation probe)', async () => {
const dir = copyCommittedTree();
try {
injectDriverImport(dir, 'apps/server/src/index.ts', "import { Pool } from 'pg';");
const misplaced = await scanRoot(dir);
assert.equal(misplaced.length, 1, `expected exactly one violation, got:\n${misplaced.map(formatImport).join('\n')}`);
assert.equal(misplaced[0].relPath, 'apps/server/src/index.ts');
assert.equal(misplaced[0].specifier, 'pg');
assert.equal(misplaced[0].line, 1);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('a copy of the committed tree without misplaced driver imports passes the scan (probe sanity)', async () => {
const dir = copyCommittedTree();
try {
const misplaced = await scanRoot(dir);
assert.deepEqual(
misplaced,
[],
`a clean copy of the committed tree must pass the isolation scan:\n${misplaced.map(formatImport).join('\n')}`,
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('the specifier matcher flags pg/kysely imports and ignores other packages (unit probe)', () => {
for (const spec of ['pg', 'pg/connection-string', 'kysely', 'kysely/plugin/dialect']) {
assert.ok(isDriverSpecifier(spec), `"${spec}" must be flagged as a driver import`);
}
for (const spec of ['fastify', 'node:http', '@types/pg', 'pg-native', 'pg-pool', 'postgres-array', './internal']) {
assert.equal(isDriverSpecifier(spec), false, `"${spec}" must not be flagged as a driver import`);
}
});
test('comment stripping ignores commented-out driver imports (unit probe)', () => {
const src = [
'// import { Pool } from \'pg\';',
'/* import { Kysely } from "kysely"; */',
"import { Pool } from 'pg';",
'',
].join('\n');
const imports = findDriverImports([{ relPath: 'apps/server/src/index.ts', content: src }]);
assert.equal(imports.length, 1);
assert.equal(imports[0].specifier, 'pg');
assert.equal(imports[0].line, 3, 'the stripped line number must match the original source');
});
test('the isolation criterion is enforced in CI', () => {
// Picked up by the root test command (root `scripts.test` glob).
const scripts = JSON.parse(read('package.json')).scripts ?? {};
assert.equal(
scripts.test,
`node --test "${ROOT_TEST_GLOB}"`,
`root scripts.test must run the "${ROOT_TEST_GLOB}" glob so this suite runs with the rest`,
);
// And a dedicated CI job gates it on every PR.
const workflow = read('.gitea/workflows/ci.yml');
assert.ok(
workflow.includes(`node --test tests/database-postgres-imports.test.mjs`),
`CI must run the isolation suite (job "${CI_JOB}") on every PR`,
);
});
+1 -1
View File
@@ -37,7 +37,7 @@ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..
const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8'); const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8');
/** The workspace packages that must compile under the strict base config. */ /** The workspace packages that must compile under the strict base config. */
const WORKSPACE_PACKAGES = ['apps/server', 'packages/core', 'extensions/example']; const WORKSPACE_PACKAGES = ['apps/server', 'packages/core', 'packages/database-postgres', 'extensions/example'];
/** The strict-family flags the committed base config must set. */ /** The strict-family flags the committed base config must set. */
const STRICT_FAMILY_FLAGS = [ const STRICT_FAMILY_FLAGS = [
+1 -1
View File
@@ -33,7 +33,7 @@ const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8');
const PINNED_TYPESCRIPT = '6.0.3'; const PINNED_TYPESCRIPT = '6.0.3';
/** Every workspace package that must resolve the pinned TypeScript version. */ /** Every workspace package that must resolve the pinned TypeScript version. */
const WORKSPACE_PACKAGES = ['apps/server', 'packages/core', 'extensions/example']; const WORKSPACE_PACKAGES = ['apps/server', 'packages/core', 'packages/database-postgres', 'extensions/example'];
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Tests // Tests
+1 -1
View File
@@ -28,7 +28,7 @@ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..
const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8'); const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8');
const WORKSPACE_GROUPS = ['apps/*', 'packages/*', 'extensions/*']; const WORKSPACE_GROUPS = ['apps/*', 'packages/*', 'extensions/*'];
const WORKSPACE_PACKAGES = ['.', 'apps/server', 'packages/core', 'extensions/example']; const WORKSPACE_PACKAGES = ['.', 'apps/server', 'packages/core', 'packages/database-postgres', 'extensions/example'];
const PINNED_PNPM = '11.23.0'; const PINNED_PNPM = '11.23.0';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+1
View File
@@ -38,6 +38,7 @@ const TOP_LEVEL_GROUPS = ['apps', 'packages', 'extensions'];
const EXPECTED_PACKAGES = { const EXPECTED_PACKAGES = {
'@personal-blog/server': 'apps/server', '@personal-blog/server': 'apps/server',
'@personal-blog/core': 'packages/core', '@personal-blog/core': 'packages/core',
'@personal-blog/database-postgres': 'packages/database-postgres',
'@personal-blog/example-extension': 'extensions/example', '@personal-blog/example-extension': 'extensions/example',
}; };