4.4 KiB
4.4 KiB
Drizzle PostgreSQL Current Persistence Contract
Scenario: Current Project After PostgreSQL Migration
1. Scope / Trigger
- Trigger: implementing or modifying auth, RBAC, audit, elder, facility, admission, organization, settings, or dashboard data flows in this repository.
- Applies because this project already has
drizzle-orm,postgres, Drizzle migrations underdrizzle/, and schema definitions undermodules/core/server/schema.ts. - This supersedes local JSON persistence for current feature work. Local JSON guidance is only historical or for projects that have not adopted Drizzle yet.
2. Signatures
getDatabase(): AppDatabasecheckDatabaseConnection(): Promise<{ ok: boolean; reason: string }>readData(): Promise<AppData>recordAuditLog(input: AuditInput): Promise<AuditLog>requirePermission(permission: Permission, auditContext: DeniedAuditContext): Promise<PermissionCheckSuccess | PermissionCheckFailure>- Route Handlers use standard
GET,POST,PATCH, andDELETEexports and returnResponse.
3. Contracts
- Environment key:
DATABASE_URLis required for database-backed runtime behavior. - Drizzle source of truth:
modules/core/server/schema.ts. - Migration output:
drizzle/. - Database config:
drizzle.config.ts. - Session cookie:
- Name:
teatea_session - Flags:
httpOnly,sameSite: "lax",path: "/" - Lifetime: 7 days
- Name:
- API response shape:
type ApiResult<T extends Record<string, unknown>> =
| ({ success: true; reason: string } & T)
| { success: false; reason: string };
modules/core/server/store.tsis a compatibility read model:readData()may aggregate Drizzle rows for existing pages.writeData()andupdateData()must remain unavailable after migration.- New mutation code must use Drizzle queries or transactions.
4. Validation & Error Matrix
- Missing
DATABASE_URL-> throw duringgetDatabase()and surface a clear database configuration failure. - Missing/expired session ->
401with{ success: false, reason: "未登录或会话已过期" }. - Missing permission ->
403with{ success: false, reason: "权限不足" }and a denied audit log. - Invalid JSON body ->
400with a Chinese user-facingreason. - Missing record by ID ->
404with{ success: false, reason: "<entity>不存在" }. - Duplicate account email ->
409with{ success: false, reason: "账号已存在" }. - Bed/admission conflict -> structured failure response and no partial mutation.
5. Good/Base/Bad Cases
- Good: UI submits to a Route Handler, the handler validates input, calls
requirePermission, mutates Drizzle tables in a transaction when multiple tables are involved, records audit, and returnsApiResult. - Base: Server Component reads data directly through focused server helpers or the temporary
readData()compatibility model. - Bad: New mutation code calls
writeData()orupdateData(). - Bad: New code treats
.data/teatea.jsonas the active persistence layer. - Bad: API auth is enforced only by hidden UI controls.
6. Tests Required
pnpm lintpnpm type-checkpnpm build- When schema changes are made:
pnpm db:generate, then review generated SQL and runpnpm db:migrateagainst the configured development database. - Manual or automated integration assertions:
- first setup creates platform admin, organization, organization roles, membership, and cookie session
- protected app route redirects without cookie
- protected app route renders with a valid cookie
- CRUD mutation persists across reload/API list
- denied permission returns
403and writes an audit log - admission mutation updates
admissions,beds, andeldersconsistently
7. Wrong vs Correct
Wrong
await updateData((data) => {
data.beds.push(newBed);
});
Correct
const database = getDatabase();
await database.insert(beds).values({
organizationId,
roomId,
code,
status: "available",
});
Wrong
await database.insert(admissions).values({ organizationId, elderId, bedId });
await database.update(beds).set({ status: "occupied" }).where(eq(beds.id, bedId));
Correct
await database.transaction(async (transaction) => {
await transaction.insert(admissions).values({ organizationId, elderId, bedId });
await transaction.update(beds).set({ status: "occupied" }).where(eq(beds.id, bedId));
await transaction.update(elders).set({ status: "active" }).where(eq(elders.id, elderId));
});