feat(api,gateway): add all-or-nothing bulk remote assignments - #1521
Merged
Conversation
Backend for group-scoped bulk remote assignments (#1500), following the review on that issue: - N subjects x M timepoints: every (instrument, expiry) pair applies to every selected subject. - All-or-nothing. Preflight throws with every issue attached rather than reporting per-row outcomes, and create re-runs the same checks so a conflict appearing between review and submit cannot slip through. Any failure deletes every row staged by the call. - One gateway request per batch, carrying each instrument bundle once rather than once per assignment. - Conflicts block by default and are waived only by an explicit allowDuplicates. Extracts a dynamic future-date schema: z.coerce.date().min(new Date()) froze the bound at module load, so a long-running process kept accepting expiries that had since passed. Adds the gateway vitest project, which did not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A staged assignment was returned straight from the model, so the private key that decrypts it was serialized into the HTTP response and posted to the gateway. Assignment does not declare the field, but structural typing does not remove it at runtime, and the gateway's schema only discards it after it has crossed the wire. Also answers preflight with 200 rather than the 201 a POST defaults to, since it creates nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drives the API rather than the UI, so it needs no page object and no regenerated route tree. Covers what the unit tier structurally cannot: real Mongo scoping, the real gateway round trip, and the all-or-nothing guarantee across both. Verified to fail without the behaviour it asserts: breaking the conflict query's status filter turns the repeat preflight from 422 into 200 and fails the suite. Also drops a dead userId binding found while reviewing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joshunrau
approved these changes
Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backend for group-scoped bulk remote assignments (#1500), following the review on that issue.
A front-end PR follows once this is approved. It builds directly on these contracts, so it is kept separate to let the backend be reviewed and tested on its own.
What it does
POST /v1/assignments/bulk/preflightandPOST /v1/assignments/bulk, both group-scoped.A batch is N subjects × M timepoints: each
(instrument, expiry)pair applies to every selected subject, so 50 subjects and 3 instruments create 150 assignments in one operation.Following the review comments
CONFLICT,INSTRUMENT_UNAVAILABLE,SUBJECT_UNAVAILABLE.OUTSTANDING, unexpired assignment for that subject and instrument in that group. It blocks by default and is waived only by an explicitallowDuplicates.SUBJECT_UNAVAILABLEdeliberately does not distinguish a subject that does not exist from one outside the group, so the endpoint cannot be used to probe for subjects elsewhere.Also in here
z.coerce.date().min(new Date())froze its bound at module load, so a long-running API kept accepting expiries that had since passed. Now used by the single-assignment contract too.encryptionKeyPair. It was being serialized into the response and posted to the gateway;Assignmentdoes not declare the field, but structural typing does not remove it at runtime.Verification
446 unit tests plus 4 API-level Playwright tests covering real Mongo scoping, the real gateway round trip, and the all-or-nothing guarantee.
The e2e tests were checked against a mutant rather than trusted for being green: breaking the conflict query's status filter turns the repeat preflight from 422 into 200 and fails the suite.
Known gaps, deliberately left for discussion
timepointshas no upper bound.subjectIdsis capped at 500, but N×M means a group with many instruments can still make one request very large, and every assignment needs a keypair.🤖 Generated with Claude Code