mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 16:41:08 +02:00
chore(db): enforce pgSchema isolation with a lint script
The "every Drizzle table uses pgSchema" rule was documented in
.claude/guidelines/database.md (added yesterday as part of Concern 5)
but enforced only by convention. A new service could slip a raw
\`pgTable()\` past review and collide in the default \`public\` schema
of \`mana_platform\`, and nothing would surface the mistake until a
production migration failed.
- \`scripts/validate-pg-schema-isolation.mjs\` scans every tracked
TypeScript file under services/, apps/api/, packages/ for call sites
of \`pgTable(\` (not imports — imports can still be useful for types).
Strips comments before matching so doc-examples like "use \`pgTable()\`"
don't trigger false positives.
- Wired as \`pnpm run validate:pg-schema\` and a new CI step in the
validate job (right after the turbo-recursion check). 721 files
scan clean today.
- Removed an unused \`pgTable\` import in mana-subscriptions that would
have been the only import of the symbol remaining after this change.
- Updated .claude/guidelines/database.md — the old verification blurb
said "no automated lint rule yet", now points at the enforcer.
Drift verified: injecting a synthetic \`pgTable('bad', {})\` into
subscriptions.ts failed with a clear file:line violation pointing at
the database guideline.
Closes the "no automated lint rule" gap noted in the database guideline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1eda3f5395
commit
5ec1dfc747
5 changed files with 114 additions and 8 deletions
|
|
@ -131,13 +131,7 @@ New services: pick a short, unambiguous name (`auth`, not `mana_auth_schema`), a
|
|||
|
||||
### Verification
|
||||
|
||||
Before merging a change that adds a new Drizzle schema file, confirm with:
|
||||
|
||||
```bash
|
||||
rg "pgTable\(" services/ apps/api/ packages/ --type ts
|
||||
```
|
||||
|
||||
Any hit that's not inside `mana-sync` is a violation. There's no automated lint rule yet — adding one is tracked in the architecture audit.
|
||||
Enforced by `pnpm run validate:pg-schema` (`scripts/validate-pg-schema-isolation.mjs`), wired into the CI `validate` job. Scans every TypeScript file under `services/`, `apps/api/`, and `packages/` for raw `pgTable(` call sites and fails the PR if any are found. Imports of the symbol are ignored — only actual call sites are violations.
|
||||
|
||||
## Schema Design
|
||||
|
||||
|
|
|
|||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
|
@ -443,6 +443,9 @@ jobs:
|
|||
- name: Validate no recursive turbo calls
|
||||
run: pnpm run validate:turbo
|
||||
|
||||
- name: Validate pgSchema isolation (no raw pgTable)
|
||||
run: pnpm run validate:pg-schema
|
||||
|
||||
- name: Audit crypto registry (Dexie ↔ registry ↔ allowlist)
|
||||
run: pnpm run check:crypto
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"check:status": "bash scripts/check-status.sh",
|
||||
"validate:dockerfiles": "node scripts/validate-dockerfiles.mjs",
|
||||
"validate:turbo": "node scripts/validate-no-recursive-turbo.mjs",
|
||||
"validate:pg-schema": "node scripts/validate-pg-schema-isolation.mjs",
|
||||
"check:crypto": "node scripts/audit-crypto-registry.mjs",
|
||||
"check:crypto:seed": "node scripts/audit-crypto-registry.mjs --seed",
|
||||
"audit:deps": "node scripts/audit-workspace-deps.mjs",
|
||||
|
|
|
|||
109
scripts/validate-pg-schema-isolation.mjs
Executable file
109
scripts/validate-pg-schema-isolation.mjs
Executable file
|
|
@ -0,0 +1,109 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Validate that every Drizzle table declaration uses `pgSchema('x').table(...)`
|
||||
* instead of raw `pgTable(...)`.
|
||||
*
|
||||
* Why: `mana_platform` is one shared Postgres database for every service.
|
||||
* Without schema namespacing, a `users` table in one service collides with
|
||||
* `users` in another. The rule is in `.claude/guidelines/database.md` but
|
||||
* was enforced only by convention until now — a new service could slip
|
||||
* a raw `pgTable()` past review and pollute the default `public` schema.
|
||||
*
|
||||
* Rule: no call-site of `pgTable(` may appear in TypeScript under
|
||||
* `services/`, `apps/api/`, or `packages/`. Imports of the symbol are
|
||||
* ignored (they can still be useful for types), only actual calls are
|
||||
* violations.
|
||||
*
|
||||
* Exception list: none. `mana-sync` is Go; it has no .ts schema files
|
||||
* to begin with. Projection tables on top of `mana_sync` (e.g.
|
||||
* `mana-ai`'s mission_snapshots) use `pgSchema('mana_ai').table(...)`
|
||||
* to stay out of the core sync namespace.
|
||||
*
|
||||
* Zero deps — plain Node ESM. Uses `git ls-files` so node_modules and
|
||||
* build output are auto-excluded.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = join(__dirname, '..');
|
||||
|
||||
/** Directories we scan for schema files. */
|
||||
const SCAN_GLOBS = ['services/**/*.ts', 'apps/api/**/*.ts', 'packages/**/*.ts'];
|
||||
|
||||
/** Paths we never flag. `node_modules` and `dist` are already filtered by
|
||||
* `git ls-files`; this is for in-tree exceptions. */
|
||||
const ALLOWLIST_PATHS = [
|
||||
// `dist/` directories that slipped into git. Defensive — shouldn't exist.
|
||||
/\/dist\//,
|
||||
];
|
||||
|
||||
function listTsFiles() {
|
||||
const out = execSync(`git ls-files ${SCAN_GLOBS.map((g) => `"${g}"`).join(' ')}`, {
|
||||
cwd: REPO_ROOT,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
return out
|
||||
.split('\n')
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean)
|
||||
.filter((p) => !ALLOWLIST_PATHS.some((re) => re.test(p)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip // line comments and /* block comments *\/ so a doc-comment
|
||||
* mentioning `pgTable()` doesn't trigger a false positive.
|
||||
*/
|
||||
function stripComments(source) {
|
||||
return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
|
||||
}
|
||||
|
||||
function validate() {
|
||||
const files = listTsFiles();
|
||||
const violations = [];
|
||||
let scanned = 0;
|
||||
|
||||
for (const rel of files) {
|
||||
scanned++;
|
||||
const abs = join(REPO_ROOT, rel);
|
||||
let source;
|
||||
try {
|
||||
source = readFileSync(abs, 'utf8');
|
||||
} catch {
|
||||
continue; // deleted between ls-files and read
|
||||
}
|
||||
const stripped = stripComments(source);
|
||||
// Find each line that calls `pgTable(`. Import lines look like
|
||||
// `import { pgTable } from ...` — they never have `pgTable(` so
|
||||
// they're auto-excluded by this regex.
|
||||
const lines = stripped.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (/\bpgTable\s*\(/.test(lines[i])) {
|
||||
violations.push(
|
||||
`${rel}:${i + 1}: raw \`pgTable(\` call — use \`pgSchema('<name>').table(...)\` instead. ` +
|
||||
`See .claude/guidelines/database.md §"Schema Isolation".`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error(`\n✗ pgSchema isolation check FAILED (${violations.length} violation(s)):\n`);
|
||||
for (const v of violations) console.error(` • ${v}`);
|
||||
console.error(
|
||||
`\nEvery Drizzle table in this monorepo must live under its own Postgres schema. ` +
|
||||
`A raw \`pgTable()\` call drops the table into the default \`public\` schema and ` +
|
||||
`risks colliding with other services sharing \`mana_platform\`.\n`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✓ No raw pgTable() calls: scanned ${scanned} TypeScript files under services/, apps/api/, packages/.`
|
||||
);
|
||||
}
|
||||
|
||||
validate();
|
||||
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import {
|
||||
pgSchema,
|
||||
pgTable,
|
||||
uuid,
|
||||
integer,
|
||||
text,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue