508 lines
22 KiB
Markdown
508 lines
22 KiB
Markdown
# 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 under `drizzle/`, and schema definitions under `modules/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(): AppDatabase`
|
|
- `checkDatabaseConnection(): 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`, and `DELETE` exports and return `Response`.
|
|
|
|
### 3. Contracts
|
|
|
|
- Environment key: `DATABASE_URL` is 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
|
|
- API response shape:
|
|
|
|
```ts
|
|
type ApiResult<T extends Record<string, unknown>> =
|
|
| ({ success: true; reason: string } & T)
|
|
| { success: false; reason: string };
|
|
```
|
|
|
|
- `modules/core/server/store.ts` is a compatibility read model:
|
|
- `readData()` may aggregate Drizzle rows for existing pages.
|
|
- `writeData()` and `updateData()` must remain unavailable after migration.
|
|
- New mutation code must use Drizzle queries or transactions.
|
|
|
|
### 4. Validation & Error Matrix
|
|
|
|
- Missing `DATABASE_URL` -> throw during `getDatabase()` and surface a clear database configuration failure.
|
|
- Missing/expired session -> `401` with `{ success: false, reason: "未登录或会话已过期" }`.
|
|
- Missing permission -> `403` with `{ success: false, reason: "权限不足" }` and a denied audit log.
|
|
- Invalid JSON body -> `400` with a Chinese user-facing `reason`.
|
|
- Missing record by ID -> `404` with `{ success: false, reason: "<entity>不存在" }`.
|
|
- Duplicate account email -> `409` with `{ 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 returns `ApiResult`.
|
|
- Base: Server Component reads data directly through focused server helpers or the temporary `readData()` compatibility model.
|
|
- Bad: New mutation code calls `writeData()` or `updateData()`.
|
|
- Bad: New code treats `.data/teatea.json` as the active persistence layer.
|
|
- Bad: API auth is enforced only by hidden UI controls.
|
|
|
|
### 6. Tests Required
|
|
|
|
- `pnpm lint`
|
|
- `pnpm type-check`
|
|
- `pnpm build`
|
|
- When schema changes are made: `pnpm db:generate`, then review generated SQL and run `pnpm db:migrate` against 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 `403` and writes an audit log
|
|
- admission mutation updates `admissions`, `beds`, and `elders` consistently
|
|
|
|
### 7. Wrong vs Correct
|
|
|
|
#### Wrong
|
|
|
|
```ts
|
|
await updateData((data) => {
|
|
data.beds.push(newBed);
|
|
});
|
|
```
|
|
|
|
#### Correct
|
|
|
|
```ts
|
|
const database = getDatabase();
|
|
await database.insert(beds).values({
|
|
organizationId,
|
|
roomId,
|
|
code,
|
|
status: "available",
|
|
});
|
|
```
|
|
|
|
#### Wrong
|
|
|
|
```ts
|
|
await database.insert(admissions).values({ organizationId, elderId, bedId });
|
|
await database.update(beds).set({ status: "occupied" }).where(eq(beds.id, bedId));
|
|
```
|
|
|
|
#### Correct
|
|
|
|
```ts
|
|
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));
|
|
});
|
|
```
|
|
|
|
## Scenario: Organization Invitation Limits
|
|
|
|
### 1. Scope / Trigger
|
|
|
|
- Trigger: changing organization invitation creation, registration-by-invite consumption, or the `organization_invitations` table.
|
|
- Applies because invitation links are persisted in PostgreSQL, created through a protected organization API, consumed inside account registration, and displayed in organization settings.
|
|
|
|
### 2. Signatures
|
|
|
|
- `POST /api/organizations/[id]/invitations`
|
|
- Request: `{ email?: string; roleId: string; validityDays?: number; maxUses?: number }`
|
|
- Response: `ApiResult<{ invitation: typeof organizationInvitations.$inferSelect }>`
|
|
- DB columns:
|
|
- `organization_invitations.expires_at timestamp with time zone not null`
|
|
- `organization_invitations.max_uses integer not null default 1`
|
|
- `organization_invitations.used_count integer not null default 0`
|
|
- Registration helper: `createRegistration({ name, email, password, organizationId?, invitationToken? })`
|
|
|
|
### 3. Contracts
|
|
|
|
- Invite creation requires `account:manage` and may only target the active organization when the session is organization-scoped.
|
|
- `roleId` must refer to an enabled role in the target organization.
|
|
- If invitation `email` is non-empty, registration must use the same normalized email address.
|
|
- `validityDays` defaults to `7` and must be a positive integer within the product limit.
|
|
- `maxUses` defaults to `1` and must be a positive integer within the product limit.
|
|
- "Use count" means successful invitation registrations, not page visits or token preview requests.
|
|
- A token is consumable only when `status = 'active'`, `expires_at >= now`, and `used_count < max_uses`.
|
|
- Successful registration inserts the account and membership in the same transaction before consuming the invitation.
|
|
- Consuming an invitation increments `used_count`; only when the returned count reaches `max_uses` should the row become `status = 'accepted'` and receive `accepted_by_account_id` / `accepted_at`.
|
|
|
|
### 4. Validation & Error Matrix
|
|
|
|
- Invalid JSON body -> `400` / `请求数据格式无效`.
|
|
- Missing `roleId` -> `400` / `请选择邀请角色`.
|
|
- Invalid `validityDays` -> `400` / `邀请有效期需为 ... 的整数`.
|
|
- Invalid `maxUses` -> `400` / `最大使用次数需为 ... 的整数`.
|
|
- Target organization missing -> `404` / `机构不存在`.
|
|
- Role missing, disabled, or cross-organization -> `404` / `角色不存在`.
|
|
- Unknown invitation token during registration -> `邀请链接无效`.
|
|
- Expired, non-active, or exhausted invitation token during registration -> `邀请链接已失效`.
|
|
- Registration email mismatch for an email-limited invitation -> `邀请邮箱与注册邮箱不一致`.
|
|
|
|
### 5. Good/Base/Bad Cases
|
|
|
|
- Good: keep invitation limit constants shared between the create API and invite dialog so UI bounds and backend validation stay aligned.
|
|
- Good: consume invitation links with an atomic `UPDATE ... WHERE used_count < max_uses` predicate and check that a row was returned.
|
|
- Good: derive invitation list UI from the persisted `used_count / max_uses` fields.
|
|
- Base: old one-use behavior remains the default by using `validityDays = 7` and `maxUses = 1`.
|
|
- Bad: count page loads as invitation usage; link previews, refreshes, and bots can exhaust a link without a registration.
|
|
- Bad: decide whether a link is exhausted only from a value read before registration; concurrent registrations can over-consume the link.
|
|
|
|
### 6. Tests Required
|
|
|
|
- `pnpm db:generate`, then review SQL for only additive invitation limit columns or intentional invitation changes.
|
|
- `pnpm lint`
|
|
- `pnpm type-check`
|
|
- `pnpm test`
|
|
- `pnpm build`
|
|
- Integration assertions when route-level tests cover auth registration:
|
|
- default invite creation returns a 7-day, one-use link
|
|
- custom `validityDays` and `maxUses` are persisted
|
|
- expired and exhausted links fail registration
|
|
- successful registration increments `used_count`
|
|
- the final allowed registration marks the invitation accepted
|
|
|
|
### 7. Wrong vs Correct
|
|
|
|
#### Wrong
|
|
|
|
```ts
|
|
if (invitation.usedCount >= invitation.maxUses) {
|
|
throw new Error("邀请链接已失效");
|
|
}
|
|
await transaction.update(organizationInvitations)
|
|
.set({ usedCount: invitation.usedCount + 1 })
|
|
.where(eq(organizationInvitations.id, invitation.id));
|
|
```
|
|
|
|
#### Correct
|
|
|
|
```ts
|
|
const consumedRows = await transaction
|
|
.update(organizationInvitations)
|
|
.set({ usedCount: sql`${organizationInvitations.usedCount} + 1` })
|
|
.where(and(
|
|
eq(organizationInvitations.id, invitation.id),
|
|
eq(organizationInvitations.status, "active"),
|
|
gte(organizationInvitations.expiresAt, now),
|
|
lt(organizationInvitations.usedCount, organizationInvitations.maxUses),
|
|
))
|
|
.returning({
|
|
id: organizationInvitations.id,
|
|
maxUses: organizationInvitations.maxUses,
|
|
usedCount: organizationInvitations.usedCount,
|
|
});
|
|
const consumed = consumedRows[0];
|
|
if (!consumed) {
|
|
throw new Error("邀请链接已失效");
|
|
}
|
|
```
|
|
|
|
## Scenario: Facility Room Creation Without Fabricated Hierarchy
|
|
|
|
### 1. Scope / Trigger
|
|
|
|
- Trigger: changing facility room creation APIs or any UI that creates rooms.
|
|
- Applies because `rooms.floorId` is required by schema, but the system must not create
|
|
fake campus/building/floor records to satisfy that foreign key.
|
|
|
|
### 2. Signatures
|
|
|
|
- `POST /api/facilities/rooms`
|
|
- Request: `{ name: string; code: string; floorId: string; capacity?: number }`
|
|
- Response: `ApiResult<{ room: typeof rooms.$inferSelect }>`
|
|
|
|
### 3. Contracts
|
|
|
|
- `floorId` must refer to an existing `floors.id` in the authenticated active organization.
|
|
- `capacity` is optional; omitted means schema/product default `1`.
|
|
- If `capacity` is supplied, it must be a positive integer.
|
|
- The handler may insert only the `rooms` row. It must not insert `campuses`,
|
|
`buildings`, or `floors` as placeholder hierarchy.
|
|
|
|
### 4. Validation & Error Matrix
|
|
|
|
- Missing active organization -> `400` / `请选择机构后维护房间`.
|
|
- Missing `name`, `code`, or `floorId` -> `400` / `房间名称、编号和楼层不能为空`.
|
|
- Invalid supplied `capacity` -> `400` / `房间容量需为正整数`.
|
|
- `floorId` not found in active organization -> `404` / `楼层不存在`.
|
|
- Insert returning no row -> `500` / `房间创建失败`.
|
|
|
|
### 5. Good/Base/Bad Cases
|
|
|
|
- Good: create UI first exposes real campus/building/floor selection, then submits the selected `floorId`.
|
|
- Base: if facility hierarchy management is not exposed yet, keep room creation UI hidden or disabled.
|
|
- Bad: auto-create `"默认院区"`, `"默认楼栋"`, or `"默认楼层"` inside the room API.
|
|
|
|
### 6. Tests Required
|
|
|
|
- `pnpm lint`
|
|
- `pnpm type-check`
|
|
- `pnpm build`
|
|
- Integration assertions:
|
|
- missing `floorId` returns `400`
|
|
- unknown or cross-organization `floorId` returns `404`
|
|
- valid `floorId` creates a room without inserting campus/building/floor rows
|
|
|
|
### 7. Wrong vs Correct
|
|
|
|
#### Wrong
|
|
|
|
```ts
|
|
const floor = existingFloor ?? await transaction.insert(floors).values({ name: "默认楼层" }).returning();
|
|
await transaction.insert(rooms).values({ floorId: floor.id, name, code });
|
|
```
|
|
|
|
#### Correct
|
|
|
|
```ts
|
|
const floorRows = await database
|
|
.select({ id: floors.id })
|
|
.from(floors)
|
|
.where(and(eq(floors.id, floorId), eq(floors.organizationId, organizationId)))
|
|
.limit(1);
|
|
const floor = floorRows[0];
|
|
if (!floor) {
|
|
return jsonFailure("楼层不存在", 404);
|
|
}
|
|
await database.insert(rooms).values({ organizationId, floorId: floor.id, name, code });
|
|
```
|
|
|
|
## Scenario: Health Admin Data Management
|
|
|
|
### 1. Scope / Trigger
|
|
|
|
- Trigger: adding or changing health profile, vital record, chronic condition, or health anomaly review behavior.
|
|
- Applies because health data is persisted in Drizzle tables and rendered through a settings management page, not fabricated in the operational `/app/health` placeholder.
|
|
|
|
### 2. Signatures
|
|
|
|
- `GET /api/health/admin`
|
|
- `PUT /api/health/profiles/[elderId]`
|
|
- `POST /api/health/vitals`
|
|
- `POST /api/health/chronic-conditions`
|
|
- `PATCH /api/health/reviews/[id]`
|
|
- `listHealthAdminData(organizationId: string): Promise<HealthAdminData>`
|
|
- Mutations return either the domain DTO or `{ success: false; reason: string; status: number }`.
|
|
|
|
### 3. Contracts
|
|
|
|
- Health APIs must call `requirePermission` before reading or mutating:
|
|
- reads use `health:read`
|
|
- mutations use `health:manage`
|
|
- All health rows are scoped by `organizationId`.
|
|
- Elder-owned health mutations must validate the elder exists in the active organization.
|
|
- `GET /api/health/admin` returns `{ success: true; reason: string; data: HealthAdminData }`; client refresh code must read `result.data`.
|
|
- Mutation success responses follow the standard shape:
|
|
- profile: `{ success: true; reason: string; profile }`
|
|
- vital: `{ success: true; reason: string; vital; review? }`
|
|
- chronic condition: `{ success: true; reason: string; condition }`
|
|
- review: `{ success: true; reason: string; review }`
|
|
- Mutations must write audit logs after successful persistence.
|
|
|
|
### 4. Validation & Error Matrix
|
|
|
|
- Missing active organization -> `400` with a Chinese reason instructing the user to select an organization.
|
|
- Missing `elderId` for vital/condition -> `400`.
|
|
- Invalid date/source/status/numeric field -> `400`.
|
|
- Elder not found or belongs to another organization -> `404` / `老人档案不存在`.
|
|
- Review not found or belongs to another organization -> `404` / `异常复核记录不存在`.
|
|
- Missing permission -> `403` from `requirePermission`; route must not call domain helpers.
|
|
- Insert/update returning no row -> `500` mutation failure.
|
|
|
|
### 5. Good/Base/Bad Cases
|
|
|
|
- Good: keep `/app/settings/health` as the backend management surface and leave `/app/health` operational until real operational data exists.
|
|
- Good: return the admin payload under a `data` key so route output and client refresh types match.
|
|
- Good: seed demo health rows only inside the default workspace seeding transaction and tie them to real seeded elders.
|
|
- Base: anomaly review rows may be created automatically from MVP vital thresholds.
|
|
- Bad: render UI-only health records or counters on static module pages.
|
|
- Bad: hide mutation buttons in the UI without enforcing `health:manage` in the Route Handler.
|
|
- Bad: update reviews by `id` alone without also filtering by active `organizationId`.
|
|
|
|
### 6. Tests Required
|
|
|
|
- `pnpm test` with API assertions for each health route.
|
|
- Route tests must cover at least: happy path, permission denial, missing active organization, invalid input, and missing/cross-organization IDs.
|
|
- `pnpm db:generate` after schema changes, then review the generated SQL for only additive health enum/table/index/FK changes.
|
|
- `pnpm lint`, `pnpm type-check`, and `pnpm build`.
|
|
|
|
### 7. Wrong vs Correct
|
|
|
|
#### Wrong
|
|
|
|
```ts
|
|
const data = await listHealthAdminData(organizationId);
|
|
return jsonSuccess("健康数据已加载", data);
|
|
```
|
|
|
|
#### Correct
|
|
|
|
## Scenario: Collaboration Module Data Management
|
|
|
|
### 1. Scope / Trigger
|
|
|
|
- Trigger: changing 设备运维, 公告通知, 规则预警, or 家属服务 pages, APIs, seed data, or schema.
|
|
- Applies because these modules are real Drizzle/PostgreSQL-backed collaboration workspaces, not reserved/static module pages.
|
|
|
|
### 2. Signatures
|
|
|
|
- `GET /api/devices/assets`, `POST /api/devices/assets`, `PATCH|DELETE /api/devices/assets/[id]`
|
|
- `POST /api/devices/tickets`, `PATCH|DELETE /api/devices/tickets/[id]`
|
|
- `GET|POST /api/notices`, `POST|PATCH|DELETE /api/notices/[id]`
|
|
- `GET|POST /api/alerts/rules`, `PATCH|DELETE /api/alerts/rules/[id]`
|
|
- `POST /api/alerts/triggers`, `PATCH|DELETE /api/alerts/triggers/[id]`
|
|
- `GET|POST /api/family/contacts`, `PATCH|DELETE /api/family/contacts/[id]`
|
|
- `POST /api/family/visits`, `PATCH|DELETE /api/family/visits/[id]`
|
|
- `POST /api/family/feedback`, `PATCH|DELETE /api/family/feedback/[id]`
|
|
|
|
### 3. Contracts
|
|
|
|
- Reads require module read permission: `device:read`, `notice:read`, `alert:read`, or `family:read`.
|
|
- Mutations require module manage permission: `device:manage`, `notice:manage`, `alert:manage`, or `family:manage`.
|
|
- All records must be scoped by active `organizationId`; updates/deletes must filter by both `id` and `organizationId`.
|
|
- Read APIs return `{ success: true, reason, data }` where `data` is the module workspace DTO.
|
|
- Mutation APIs return the changed resource under a resource-specific key and write an audit log after successful persistence.
|
|
- Seed demo rows only inside `seedDefaultWorkspaceData(organizationId)` and connect them to real seeded elders/devices/rules where applicable.
|
|
|
|
### 4. Validation & Error Matrix
|
|
|
|
- Missing session or permission -> response from `requirePermission`; domain helper must not be called.
|
|
- Missing active organization -> `400` with a Chinese reason asking the user to select an organization.
|
|
- Invalid enum, empty required title/name/content, invalid date -> `400`.
|
|
- Cross-organization or missing referenced record -> `404` with entity-specific reason.
|
|
- Insert/update returning no row -> structured mutation failure with status `500` or `404`.
|
|
|
|
### 5. Good/Base/Bad Cases
|
|
|
|
- Good: Server page checks read permission, loads persisted workspace data, and passes serializable DTOs to a Client Component.
|
|
- Good: Client refreshes by calling the module read API after mutations.
|
|
- Good: notice read receipts use `POST /api/notices/[id]` with read permission; create/update/delete use manage permission.
|
|
- Base: alert rules and triggers are manually managed in v1; no background rule engine is implied.
|
|
- Bad: rendering fake collaboration counters or rows on reserved pages after these modules have real tables.
|
|
- Bad: updating a record by `id` alone without the active organization filter.
|
|
|
|
### 6. Tests Required
|
|
|
|
- `pnpm db:generate`, then review SQL for additive collaboration enums/tables/indexes/FKs.
|
|
- `pnpm lint`, `pnpm type-check`, `pnpm test`, and `pnpm build`.
|
|
- API route tests should cover permission denial, missing active organization, successful create/update audit, and missing/cross-organization mutation failures.
|
|
|
|
### 7. Wrong vs Correct
|
|
|
|
#### Wrong
|
|
|
|
```ts
|
|
await database.update(alertTriggers).set({ status }).where(eq(alertTriggers.id, id));
|
|
```
|
|
|
|
#### Correct
|
|
|
|
```ts
|
|
await database
|
|
.update(alertTriggers)
|
|
.set({ status, updatedAt: new Date() })
|
|
.where(and(eq(alertTriggers.id, id), eq(alertTriggers.organizationId, organizationId)));
|
|
```
|
|
|
|
```ts
|
|
const data = await listHealthAdminData(organizationId);
|
|
return jsonSuccess("健康数据已加载", { data });
|
|
```
|
|
|
|
#### Wrong
|
|
|
|
```ts
|
|
await database.update(healthAnomalyReviews).set({ status }).where(eq(healthAnomalyReviews.id, id));
|
|
```
|
|
|
|
#### Correct
|
|
|
|
```ts
|
|
await database
|
|
.update(healthAnomalyReviews)
|
|
.set({ status, updatedAt: new Date() })
|
|
.where(and(eq(healthAnomalyReviews.id, id), eq(healthAnomalyReviews.organizationId, organizationId)));
|
|
```
|
|
|
|
## Scenario: Care Execution Workspace
|
|
|
|
### 1. Scope / Trigger
|
|
|
|
- Trigger: adding or changing persisted daily care task execution behavior.
|
|
- Applies when replacing reserved operational module placeholders with real care task data. Care records must be Drizzle-backed and organization-scoped.
|
|
|
|
### 2. Signatures
|
|
|
|
- `GET /api/care/tasks`
|
|
- `PATCH /api/care/tasks/[id]`
|
|
- `listCareExecutionData(organizationId: string): Promise<CareExecutionData>`
|
|
- `updateCareTaskStatus(input): Promise<CareTask | { success: false; reason: string; status: number }>`
|
|
- Drizzle table: `care_tasks`
|
|
|
|
### 3. Contracts
|
|
|
|
- `/app/care` and `/app/{organizationSlug}/care` render the real care workspace once `care_tasks` exists.
|
|
- Reads require `care:read`; mutations require `care:manage`.
|
|
- `GET /api/care/tasks` returns `{ success: true; reason: string; data: CareExecutionData }`; client refresh code must read `result.data`.
|
|
- `PATCH /api/care/tasks/[id]` request: `{ status: "pending" | "in_progress" | "completed" | "cancelled"; executionNotes?: string }`.
|
|
- Completing a task sets `completedAt` and `completedByAccountId`; moving a task away from `completed` clears completion metadata.
|
|
- Mutations update by both `id` and active `organizationId`, then write an audit log after success.
|
|
|
|
### 4. Validation & Error Matrix
|
|
|
|
- Missing active organization on list -> `400` / `请选择机构后查看护理任务`.
|
|
- Missing active organization on mutation -> `400` / `请选择机构后处理护理任务`.
|
|
- Invalid status -> `400` / `护理任务状态无效`.
|
|
- Missing or cross-organization task ID -> `404` / `护理任务不存在`.
|
|
- Missing permission -> `403` from `requirePermission`; route must not call domain helpers.
|
|
- Update returning no row -> structured mutation failure.
|
|
|
|
### 5. Good/Base/Bad Cases
|
|
|
|
- Good: keep care task examples in default workspace seed tied to real seeded elders.
|
|
- Good: use one additive `care_tasks` table for MVP execution records instead of building a full recurring care-plan engine.
|
|
- Good: preserve `/app/health` separation; care execution is operational and not the health admin settings page.
|
|
- Base: `elderId` can be nullable for future public-area checks, but seeded MVP examples should use real elders.
|
|
- Bad: render fake care metrics in a static placeholder after the module becomes real.
|
|
- Bad: update task status by ID alone without `organizationId`.
|
|
- Bad: only disable buttons in the UI while leaving Route Handlers on broad elder permissions.
|
|
|
|
### 6. Tests Required
|
|
|
|
- `pnpm test` with API assertions for list and status update routes.
|
|
- Route tests must cover happy path, permission denial, missing organization, invalid status, and missing/cross-organization task IDs.
|
|
- `pnpm db:generate` after schema changes and review generated SQL for only care enum/table/index/FK changes.
|
|
- `pnpm lint`, `pnpm type-check`, and `pnpm build`.
|
|
|
|
### 7. Wrong vs Correct
|
|
|
|
#### Wrong
|
|
|
|
```ts
|
|
await database.update(careTasks).set({ status }).where(eq(careTasks.id, id));
|
|
```
|
|
|
|
#### Correct
|
|
|
|
```ts
|
|
await database
|
|
.update(careTasks)
|
|
.set({ status, updatedAt: new Date() })
|
|
.where(and(eq(careTasks.id, id), eq(careTasks.organizationId, organizationId)));
|
|
```
|