11 KiB
11 KiB
Backend Fastify / Prisma
Scope
Server work lives under server/. Prefer existing route, service, utility and
QA script patterns over new abstractions.
Rules
- Validate Prisma changes with
cd server && npm run prisma:validate. - For route/service edits, run
node --checkon the touched files and the focused regression listed in the current Trellis task. - Prisma migrations require rollback notes in the Trellis task design or PRD.
- Do not resolve migration drift, reset databases, or rewrite migration history without explicit approval.
- Workflow, approval and permission behavior must be verified at process level, not only syntax level.
Common Checks
cd server && node --check src/routes/reimbursements.jscd server && node --check src/routes/workflow.jscd server && ./scripts/regression-workflow-actions.shcd server && ./scripts/regression-all.sh
Scenario: Local Regression Migration Drift
1. Scope / Trigger
- Trigger: a local or attached E2E fails with Prisma
missing column,column does not exist, enum missing, or route 500 immediately after a schema-backed feature was added. - Applies to
server/prisma/schema.prisma,server/prisma/migrations/, local regression databases such ashyb_oa_regression, and Playwright tests that use real API/database flows.
2. Signatures
- Read-only drift check:
cd server && npx prisma migrate status. - Apply committed migrations after explicit confirmation:
cd server && npx prisma migrate deploy. - Local service smoke for attached E2E:
HOST=127.0.0.1 PORT=3000 JOBS_ENABLED=false SERVE_CLIENT_DIST=true npm run dev. - Server entrypoints that call
getConfig()beforebuildApp()must loaddotenv/configbefore reading config.
3. Contracts
migrate statusis safe and read-only; it may be run before asking the user.migrate deploywrites database schema state and requires explicit user confirmation unless the current task already approved that exact local DB.- Do not use
prisma migrate resetfor E2E recovery without separate backup and destructive-action approval. - Attached local E2E should disable background jobs with
JOBS_ENABLED=falseunless the test explicitly validates job behavior.
4. Validation & Error Matrix
migrate statusreports pending migrations -> record the migration names, inspect whether they are additive/destructive, then ask before applying.- Missing
JWT_SECRETduringnpm run devwhile.envexists -> verify the entrypoint loadsdotenv/configbeforegetConfig(). - Missing column after
migrate deploy-> inspect actual columns and migration history; do not patch tests to hide schema drift. - E2E selector failure after schema is fixed -> treat as page/test contract drift and update the focused E2E assertion, not the database.
5. Good/Base/Bad Cases
- Good: run
migrate status, apply only committed migrations after approval, seed dev users, then rerun the focused Playwright flow. - Base: if migrations are already current, skip DB writes and debug the test failure directly.
- Bad: use
migrate resetor manually add columns to unblock a test. - Bad: start the server with an ad hoc
node -r dotenv/configworkaround and leavenpm run devbroken.
6. Tests Required
cd server && npm run prisma:validate.bash scripts/qa/check-local-migration-drift.shwhen local DB drift was the cause.cd server && node --check src/index.jsif server bootstrap changed.- Focused Playwright flow that previously failed.
- Frontend
npm run buildwhen Playwright or client behavior changed.
7. Wrong vs Correct
Wrong
cd server && npx prisma migrate reset
Correct
cd server && npx prisma migrate status
# after explicit approval for the local regression database only
cd server && npx prisma migrate deploy
Scenario: Portal Announcement Acknowledgement
1. Scope / Trigger
- Trigger: extending
announcements_v1from read-only announcements into formal employee acknowledgement. - Applies to
server/prisma/schema.prisma,server/src/routes/announcements.js,client/src/api/announcements.ts, employee/announcements, and admin/system/announcements. - Use this only for lightweight policy acknowledgement; do not treat it as electronic signature, CA signing, contract signing, or full policy CMS.
2. Signatures
- DB:
PortalAnnouncementmay carrycategory,requires_acknowledgement,acknowledgement_statement,acknowledgement_due_at. - DB:
PortalAnnouncementAcknowledgementstores one receipt per(announcement_id, employee_id)with statement/title/summary snapshots andactor_id. - API:
GET /api/announcements?manage=0returns visible announcements and the current employee's acknowledgement status. - API:
GET /api/announcements?manage=1requiressystem.settingsand returns acknowledgement summary plus minimal pending employees. - API:
POST /api/announcements/:id/acknowledgeaccepts{ "confirmed": true }.
3. Contracts
- Required acknowledgement status values:
not_required,pending,acknowledged,overdue. - Employee response must not include other employees' acknowledgement rows or pending lists.
- Admin pending list may include only
id,employeeNo,name, andstatus; do not include phone, email, WeCom userid, account identifiers, or sensitive profile fields. - Acknowledgement is explicit only: opening an announcement or clicking its link must never create a receipt.
- Policy content changes after any acknowledgement require a new announcement version; do not mutate title, summary, category, required flag, or statement in place.
4. Validation & Error Matrix
announcements_v1disabled ->409 VALID_001.- policy acknowledgement without statement ->
400 VALID_001. manage=1withoutsystem.settings->403 PERM_001.- acknowledge invisible/missing announcement ->
404 ANN_404. - acknowledge normal announcement ->
409 VALID_001. - acknowledge without
{ confirmed: true }->400 VALID_001. - edit protected policy fields after acknowledgement exists ->
409 VALID_001.
5. Good/Base/Bad Cases
- Good: admin creates a policy announcement, employee checks the confirmation box, server creates one receipt and one audit log.
- Base: normal announcement remains compatible and returns
not_required. - Bad: frontend displays another employee's receipt status on
/announcements. - Bad: admin updates an acknowledged policy title instead of creating a new version.
6. Tests Required
- Prisma validate after schema/migration changes.
- Route focused tests for required statement, permission, idempotency, normal/invisible announcement rejection, admin summary, and protected edit rejection.
- Frontend typecheck and build.
- Playwright flow for admin create -> employee acknowledge -> admin summary update when UI changes.
7. Wrong vs Correct
Wrong
await prisma.portalAnnouncement.update({
where: { id },
data: { title: "制度 V2" },
});
Correct
const acknowledgementCount = await prisma.portalAnnouncementAcknowledgement.count({
where: { announcement_id: id },
});
if (acknowledgementCount > 0) {
throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 409 });
}
Scenario: Safe Development Seed
1. Scope / Trigger
- Trigger: changing or running
server/src/scripts/seed-dev.js, its safety guard, development account defaults or theseed:devnpm command.
2. Signatures
- Command:
SEED_DEV_ALLOW=1 SEED_DEV_DATABASE_CONFIRM=<db> npm run seed:dev. - Optional reset:
SEED_DEV_RESET_PASSWORDS=1. - Target:
DATABASE_URLparsed with the samepgsemantics used by Prisma's PostgreSQL adapter.
3. Contracts
NODE_ENV=productionis permanently refused.- The explicit allow switch and exact database-name confirmation are both required.
- The effective PostgreSQL host, including query-string overrides, must be loopback; checking only the URI authority is insufficient.
- Existing passwords are preserved by default. Newly created accounts and
explicitly rotated accounts set
must_change_password=true. - The guard and seed logs never print credentials, complete targets or account passwords.
4. Validation & Error Matrix
- Production ->
production_forbidden. - Missing allow switch ->
explicit_allow_required. - Invalid/non-PostgreSQL URL ->
database_url_invalidordatabase_protocol_invalid. - Effective non-loopback host, including
?host=...->database_host_not_local. - Missing/mismatched confirmation ->
database_confirmation_mismatch.
5. Good/Base/Bad Cases
- Good: loopback test database, exact confirmation, new accounts require a password change.
- Base: rerun without reset preserves existing password hashes.
- Bad: accept
postgresql://[email protected]/db?host=remote.internalbecause onlyURL.hostnamewas checked.
6. Tests Required
- Pure guard tests for production, missing allow, remote authority, host-query override, exact confirmation and redacted errors.
- Unit tests for existing-password preservation, explicit rotation and new account first-login change.
- Do not validate refusal paths by connecting to a real database.
7. Wrong vs Correct
Wrong
if (new URL(env.DATABASE_URL).hostname === "127.0.0.1") allowSeed();
Correct
const client = new Client({ connectionString: env.DATABASE_URL });
if (!LOOPBACK_HOSTS.has(client.host)) refuse("database_host_not_local");
Scenario: Database-Free App Contract Tests
1. Scope / Trigger
- Trigger: unit tests call the production
buildApp()factory to verify health, readiness, static serving or other app-level contracts in CI without a DB.
2. Signatures
- Production:
buildApp(). - Test seam:
buildApp({ prismaPlugin, startJobs }).
3. Contracts
- Omitted dependencies always use the production Prisma plugin and job starter.
- Unit tests inject a
fastify-plugin-wrapped Prisma decorator and a no-op job starter; they must not rely on ESMvi.mockintercepting a CJSrequire. - App-level test doubles expose only methods used by the tested requests.
DATABASE_URL='' JWT_SECRET=''must not make isolated app tests access a developer.envor local PostgreSQL instance.
4. Validation & Error Matrix
- Production call with missing DB target -> normal startup refusal.
- Test call with injected Prisma plugin -> no production client construction.
- Test double missing a method exercised by the request -> focused unit failure.
- Injected dependencies leaking into
buildApp()callers without options -> contract regression.
5. Good/Base/Bad Cases
- Good: health/static tests inject Prisma and pass with empty DB/JWT process variables after setting their own test JWT.
- Base: API/Worker call
buildApp()without options and retain production wiring. - Bad: a hoisted
vi.mock("../prismaClient")appears green only because a local.envdatabase is reachable.
6. Tests Required
- Run health and static tests with
DATABASE_URL='' JWT_SECRET=''. - Run the full
scripts/check-minimal.shunder the same empty environment. - Keep the production default path covered by normal startup/smoke checks.
7. Wrong vs Correct
Wrong
vi.mock("../../src/prismaClient", () => ({ createPrismaClient: fake }));
await buildApp(); // CJS plugin may still construct the real client
Correct
await buildApp({ prismaPlugin: testPrismaPlugin, startJobs: noOpStartJobs });