chore: update dependencies

This commit is contained in:
Amruth Pillai
2026-07-08 18:54:00 +02:00
parent f9eafdf5cf
commit af5c7dde82
114 changed files with 7209 additions and 2056 deletions
+238
View File
@@ -0,0 +1,238 @@
# Plan 003: Fix silent-success mutations and bound bulk-operation inputs
> **Executor instructions**: Follow step by step. Run every verification
> command and confirm the expected result before moving on. Honor "STOP
> conditions". When done, update the status row in `plans/README.md`.
>
> **Drift check (run first)**:
> `git diff --stat 73daf22b2..HEAD -- packages/api/src/features/resume/service.ts packages/api/src/dto/application.ts packages/api/src/features/applications/service.ts`
> If any changed since this plan was written, compare "Current state" excerpts
> against live code; on mismatch, STOP.
## Status
- **Priority**: P1
- **Effort**: S
- **Risk**: LOW
- **Depends on**: 001 (its Step 3 tests characterize the current silent-return
behavior this plan changes — update them here). Not a hard blocker, but if
001 is DONE you must update its assertions in the same PR.
- **Category**: bug
- **Planned at**: commit `73daf22b2`, 2026-07-08
## Why this matters
Two small, independent correctness/robustness fixes on the API layer:
1. **Silent-success mutations.** `setLocked`, `setPassword`, and
`removePassword` in `packages/api/src/features/resume/service.ts` run an
`UPDATE ... WHERE id = ? AND userId = ? RETURNING`, and when no row matches
they do `if (!resume) return;` — returning HTTP 200 success. Every other
mutation in this file (`update`, `delete`) throws `ORPCError("NOT_FOUND")` in
the same situation. The inconsistency means a client calling these on a
resume that doesn't exist (or isn't theirs) is told it succeeded, so the UI
updates its state as if the lock/password change took effect. Ownership is
still enforced (the `WHERE` includes `userId`), so this is not an auth
bypass — it's a misleading false-success that hides not-found/not-owned.
2. **Unbounded bulk inputs.** `bulkUpdate` and `bulkDelete` DTOs in
`packages/api/src/dto/application.ts` accept `ids: z.array(z.string()).min(1)`
with **no `.max()`**. `bulkDelete`'s service loads all target rows into
memory before deleting. A single API call with a very large `ids` array is
unbounded memory/DB work. A `.max()` cap is a one-line defensive fix.
## Current state
### Part A — silent-success (resume/service.ts)
`setLocked` (~663679), `setPassword` (~681699), `removePassword` (~726742)
each look like this (setLocked shown; the other two differ only in the `.set`
and the `mutation` label):
```ts
setLocked: async (input: { id: string; userId: string; isLocked: boolean }) => {
const [resume] = await db
.update(schema.resume)
.set({ isLocked: input.isLocked })
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)))
.returning({ id: schema.resume.id, updatedAt: schema.resume.updatedAt });
if (!resume) return; // <-- silent no-op; should throw NOT_FOUND
await notifyResumeUpdated({ /* ... mutation: "lock" ... */ });
},
```
The reference behavior to match is `update` (~603) and `delete` (~751):
`if (!resume) throw new ORPCError("NOT_FOUND");`. `ORPCError` is already imported
at the top of `service.ts` (used throughout).
The routers that call these (for context; **do not change them**):
- `crud.ts:187` `setLocked``protectedProcedure`, does not currently declare a
`NOT_FOUND` error in `.errors({...})`.
- `sharing.ts:29` `setPassword`, `sharing.ts:80` `removePassword` — likewise.
oRPC will surface a thrown `ORPCError("NOT_FOUND")` as a 404 regardless of
whether it's declared in `.errors()`, matching how `update`/`delete` already
behave (they throw the same without special router declarations). So no router
change is required.
### Part B — unbounded bulk inputs (dto/application.ts)
`packages/api/src/dto/application.ts:163-176`:
```ts
bulkUpdate: {
input: z.object({
ids: z.array(z.string()).min(1),
status: applicationStatusSchema.optional(),
archived: z.boolean().optional(),
addTags: z.array(z.string()).optional(),
}),
output: z.object({ updated: z.number() }),
},
bulkDelete: {
input: z.object({ ids: z.array(z.string()).min(1) }),
output: z.object({ deleted: z.number() }),
},
```
The consuming service is `packages/api/src/features/applications/service.ts`
(`bulkUpdate` ~340381, `bulkDelete` ~383396; `bulkDelete` fetches all target
rows into memory before deleting).
## Commands you will need
| Purpose | Command | Expected |
|-----------|-------------------------------------------------------------------------|--------------------|
| Typecheck | `pnpm --filter @reactive-resume/api typecheck` | exit 0 |
| Test A | `pnpm --filter @reactive-resume/api test -- resume/service.test.ts` | all pass |
| Test B | `pnpm --filter @reactive-resume/api test -- applications` | all pass |
| All api | `pnpm --filter @reactive-resume/api test` | all pass |
(Do NOT run `pnpm check`.)
## Scope
**In scope**:
- `packages/api/src/features/resume/service.ts` (Part A)
- `packages/api/src/features/resume/service.test.ts` (update if 001 is DONE; else
the tests may not exist yet — see Step 3)
- `packages/api/src/dto/application.ts` (Part B)
- `packages/api/src/dto/application.test.ts` if one exists, else add a focused
test near the applications service tests (Step 4)
- `plans/README.md` (status row)
**Out of scope**:
- `crud.ts` / `sharing.ts` routers — no change needed (see Current state).
- The applications service body — a `.max()` on input is sufficient; do not
also rewrite the in-memory `bulkDelete` scan in this plan (that's a separate
perf concern tracked elsewhere).
- Any other service method.
## Git workflow
- Branch: `advisor/003-api-quick-fixes`
- Commit style: conventional commits, e.g.
`fix(api): throw NOT_FOUND on lock/password mutations for missing resume` and
`fix(api): cap bulk application operation id arrays`.
- Do NOT push or open a PR unless instructed.
## Steps
### Step 1: Throw NOT_FOUND in the three mutations
In `resume/service.ts`, change `if (!resume) return;` to
`if (!resume) throw new ORPCError("NOT_FOUND");` in **all three** of `setLocked`,
`setPassword`, `removePassword`. Match the exact form used by `update`/`delete`
in the same file.
**Verify**: `pnpm --filter @reactive-resume/api typecheck` → exit 0.
### Step 2: Cap the bulk id arrays
In `dto/application.ts`, add `.max(200, "Too many items in a single bulk
operation")` to the `ids` array in **both** `bulkUpdate` and `bulkDelete`
inputs (i.e. `z.array(z.string()).min(1).max(200)`). 200 comfortably exceeds
the UI's page size (~25) while bounding abuse; if a comment nearby documents a
different intended cap, use that number and note it.
**Verify**: `pnpm --filter @reactive-resume/api typecheck` → exit 0.
### Step 3: Update / add tests for Part A
- If Plan 001 is DONE (`resume/service.test.ts` exists): update its `setLocked`
/ `setPassword` / `removePassword` "not-found path" cases to now assert the
method **rejects with `NOT_FOUND`** (previously they asserted a silent
resolve). This test diff is the intended proof the behavior changed.
- If Plan 001 is NOT done (no test file): add a minimal
`resume/service.test.ts` covering just these three methods' not-found →
`NOT_FOUND` behavior and success path, using
`packages/api/src/features/applications/service.test.ts` as the mock pattern.
**Verify**: `pnpm --filter @reactive-resume/api test -- resume/service.test.ts`
→ all pass; the three not-found cases assert `NOT_FOUND`.
### Step 4: Test for Part B
Add a small test that `bulkUpdate`/`bulkDelete` input schemas reject an `ids`
array longer than the cap and accept one at the cap. If
`packages/api/src/dto/application.test.ts` exists, add there; otherwise create
it. Example shape:
```ts
import { describe, expect, it } from "vitest";
import { applicationDto } from "./application"; // confirm the real export name
it("rejects oversized bulk id arrays", () => {
const ids = Array.from({ length: 201 }, (_, i) => String(i));
expect(applicationDto.bulkDelete.input.safeParse({ ids }).success).toBe(false);
});
```
Confirm the actual export name/shape by reading the top and bottom of
`dto/application.ts` before writing the import.
**Verify**: `pnpm --filter @reactive-resume/api test -- applications` and
`pnpm --filter @reactive-resume/api test` → all pass.
## Test plan
- Part A: three methods now reject with `NOT_FOUND` on no-match (updated or new
cases in `resume/service.test.ts`).
- Part B: bulk input schemas reject arrays over the cap, accept at the cap.
- Verification: `pnpm --filter @reactive-resume/api test` → all pass.
## Done criteria
Machine-checkable. ALL must hold:
- [ ] `grep -n "if (!resume) return;" packages/api/src/features/resume/service.ts` returns **no** matches
- [ ] `grep -c "throw new ORPCError(\"NOT_FOUND\")" packages/api/src/features/resume/service.ts` is ≥ 5 (update, delete, + the 3 new)
- [ ] `grep -n ".max(" packages/api/src/dto/application.ts` shows the cap on both bulk inputs
- [ ] `pnpm --filter @reactive-resume/api typecheck` exits 0
- [ ] `pnpm --filter @reactive-resume/api test` exits 0
- [ ] `git status --porcelain` lists only in-scope files + `plans/README.md`
- [ ] `plans/README.md` status row for 003 updated
## STOP conditions
Stop and report if:
- The three methods no longer contain `if (!resume) return;` (already fixed by
someone else, or drifted) — reconcile against live code before editing.
- Throwing `NOT_FOUND` breaks an e2e or unit test that *depended on* the silent
success (a client that fires these against unknown ids and expects 200) —
report the caller; do not revert the fix without confirming intent.
- The applications DTO export name differs from `applicationDto` — read the file
and use the real name; do not guess.
## Maintenance notes
- The `.max(200)` cap is an input guard, not a fix for `bulkDelete`'s O(n)
in-memory scan — that remains a known, separate perf item. If bulk operations
ever need to exceed the cap, revisit both the cap and the scan together.
- If the routers later add explicit `.errors({ NOT_FOUND: ... })` declarations
for these procedures (for nicer OpenAPI docs), that's compatible with this
change but not required by it.