feat(inquiry): redesign status lifecycle, add handler registry, fix runtime bugs - #4
Merged
Merged
Conversation
…untime bugs Schema: - Replace `resolved`/`acknowledged` with explicit `approved`, `denied`, `changesRequested` statuses - Status now encodes the terminal decision directly — no redundant outcome field Handler registry (apps/api/src/modules/inquiry/handlers/): - Per-type folders with contentSchema, resolutionSchema, handleApprove, validate - Registry maps InquiryType → InquiryHandler, eliminating switch statements - inviteOrganizationUser: fully implemented with membership guard and unique enforcement - createSpace / updateSpace / transferSpace: typed stubs with unique flags New utils: - assertUniqueInquiry: 409 if open inquiry already exists between same parties - resolveContent: strips resolution metadata keys, merges overrides onto content Bug fixes: - resolution.ts: wrong type names (memberInvitation/memberApplication), no transaction, canceled as resolve outcome — all fixed; now uses db.txn() and delegates to handler - inquiryCreate.ts: targetModel never set, @ts-nocheck — fixed - inquiryUpdate.ts: @ts-nocheck removed - inquiryCancel.ts: resolved guard updated for approved/denied statuses - inquiryResolve.ts: canceled removed from outcomes, changesRequested added to resolvable statuses - adminInquiryReadMany route: nonexistent searchableFields removed New endpoint: POST /inquiries/:id/request-changes (target only) Factory: inquiryFactory with sourceOrganization + targetUser dependencies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ution output - Replace all string literals with InquiryStatus/InquiryType/InquiryResourceModel/Role enums - handleApprove now returns Promise<Record<string, unknown> | void> — output data (e.g. spaceId from createSpace) gets merged into resolution automatically - createSpace: spaceId belongs in resolution (output of approval), not content - TERMINAL_STATUSES constant in assertUniqueInquiry for clarity Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…olve rules
InquiryAction: send, resolve, requestChanges, cancel, update, read
inquiry rebac rules:
- send: org admin+ for inviteOrganizationUser, org member+ for createSpace,
space admin+ for updateSpace/transferSpace (uses type field rule)
- resolve: self (targetUserId) for user-targeted; superadmin bypass for admin-targeted
- requestChanges: delegates to resolve
- cancel/update: source-side mirror of send
- read: any source or target participant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add assertInquiryPermission utility (hydrate + rebac check)
- Wire all inquiry controllers to use rebac actions (read, send, update,
cancel, resolve, requestChanges)
- inquiryCreate: replace manual membership DB check with rebac send check
on partial record; generalize source field assignment for all 4 types
- inquiryRead: use getResource + validatePermission middleware instead of
manual fetch + access check
- inquirySent/inquiryReceived: replace getUserOrganizationIds DB query
with c.get('organizationUsers') context (already loaded in request)
- Delete access.ts (fully replaced by rebac)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add validatePermission(action) to each route's middleware array so resourceContextMiddleware + rebac check runs before the controller. Controllers now use getResource<'inquiry'>(c) — no manual fetch or permission check inline. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ermission Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- assertInquiryMutable + TERMINAL_STATUSES → validations/assertInquiryMutable.ts - assertUniqueInquiry → validations/assertUniqueInquiry.ts (imports TERMINAL_STATUSES) - Delete services/utils/assertUniqueInquiry.ts - inquiryCancel: replace inline terminal status check with assertInquiryMutable Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ntion Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…idation - Remove redundant validatePermission middleware from org/space create inquiry routes - Remove organizationId from inviteOrganizationUser content schema (resolved from context) - Split resolutionSchema into resolutionInputSchema (submittable) + resolutionSchema (full stored shape) - Export baseResolutionInputSchema/BaseResolution from schemas; default InquiryHandler generics to it - Make InquiryHandler generic <TContent, TResolution, TResolutionInput> with BaseResolution defaults - handleApprove now receives typed TContent instead of Record<string, unknown> - resolution.ts parses content through handler.contentSchema instead of casting - resolveContent uses resolutionInputSchema to determine allowed override keys - createSpace: add slug to content, remove redundant organizationId, export spaceContentSchema/SpaceContent - createSpace: validate checks existing space + open inquiry by slug (Promise.all) - updateSpace: content is Partial<SpaceContent> (name/slug optional), validate checks slug collision excluding self - transferSpace: content is empty object (source/target are FKs from context) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ssignability Replaces property function type with method shorthand syntax so InquiryHandler<TContent> is bivariant and assignable to InquiryHandler without casts or bivarianceHack. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, camelCase constants - InquiryHandler.validate now receives typed TContent as 3rd arg (no more inquiry.content casts) - validateInquiryPreCreate service centralises unique check + handler.validate call for all 4 create controllers - Deleted validateInquiryMutable.ts; moved inquiryTerminalStatuses into validateInquiryStatus.ts - Renamed assert* → validate* for status guard functions (validateInquiryIsEditable, etc.) - inquirySend controller now uses validateInquiryIsDraft instead of inline check - RESOLUTION_METADATA_KEYS simplified to just explanation - All CAPS_CASE constants renamed to camelCase (inquirySearchableFields, inquiryCreateSanitizeKeys, cacheReference) - getValidatedBody/getValidatedQuery utils replace repeated c.req cast pattern - INQUIRIES.md and CONTEXT.md updated to reflect all new patterns Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pre-create calls synthesize only source+target fields, not a full DB record. Partial<Inquiry> is honest about what's available; real Inquiry from update is still assignable, so no call sites break. Removes the as Inquiry cast in validateInquiryPreCreate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consistent with resolveInquiry which already uses db.txn. Ensures future hooks (audit logs, notifications) fire atomically with the status change. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Single-update operations are already atomic — no transaction needed. Only resolve requires txn (handleApprove + inquiry.update). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Contributor
Author
Code reviewFound 3 issues:
template/apps/api/src/modules/inquiry/controllers/inquiryUpdate.ts Lines 13 to 18 in 0de4246
🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
…stale FK fields
- Collapse InquirySourceMeta/InquiryTargetMeta to { sourceModel/targetModel: InquiryResourceModel } — only field read at runtime
- Remove dead string content-key fields from all handler sources/targets declarations
- Fix resolveInquiryTarget to read body.targetOrganizationId/targetSpaceId directly (not via content key indirection) — fixes transferSpace always 404ing
- Bake null resets into resolveInquirySource/resolveInquiryTarget returns via nullSourceFields/nullTargetFields spread — prevents stale body FK fields leaking into DB on create
- Cast body target IDs to branded types (OrganizationId, SpaceId, UserId) at point of use
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents role elevation via PATCH — content.role is evaluated by the rebac send rule, so updating content without re-checking allowed escalating to owner/admin without own permission. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix transferSpace handler: source-only uniqueness (one open transfer per space regardless of target org) via custom validate instead of validateUniqueInquiry which scoped by target too - Fix updateSpace test: use fresh space for unique-slug test to avoid open inquiry from previous test blocking the unique check - Fix updateSpace/createSpace handler tests: correct factory pattern (createSpace second-arg relations, ouCtx for createSpaceUser) - Fix spaceInquiries POST tests: upgrade to owner role (updateSpace requires sourceSpace.own, transferSpace requires sourceSpace.org.own) - Fix organizationInquiries createSpace tests: add required slug to content - Fix inquiryUpdate role-elevation test: use admin user (not owner) since owner has own permission and can legitimately invite admin role - Add target-can-read and unrelated-user-forbidden tests to inquiryRead Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eld, clean up tests and docs - Implement handleApprove for createSpace, updateSpace, transferSpace handlers - Change unique: boolean → 'targeted' | 'untargeted' for explicit uniqueness semantics - Fix Space.organizationId immutable field override so transferSpace can update org - Trim InquiryAction to read/send/resolve (remove unused actions) - Add superadmin bypass in createTestApp (platformRole=superadmin sets permix.setSuperadmin) - Add handler approve tests: createSpace, updateSpace, transferSpace - Fix --concurrency=1 → --max-concurrency=1 across all DB-backed packages - Switch root test script from --filter to --cwd to eliminate log spam - Fix navigation slice null guards on navigatePreservingContext/Spoof/All - Fix ui.test.ts appName expectation to match slice default - Update INQUIRIES.md, HOOKS.md, TESTING.md docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, resolve permissions - resolveInquiryTarget: remove handler param, use body.targetModel directly; add org slug + space slug alt lookups - inquiryCreateBodySchema: add targetOrganizationSlug, targetSpaceSlug - inviteOrganizationUser: consolidate 4 files into single index.ts - Move validateInquiryStatus → validations/, resolveContent → services/ (delete utils/) - ReBAC resolve: transferSpace now requires target org own (not manage) - Update all callers to drop handler arg from resolveInquiryTarget - Ticket and docs updated to reflect completed state Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.
Summary
resolved/acknowledgedwith explicitapproved,denied,changesRequestedstatuses — terminal decisions are now first-class enum values, not hidden inside a JSONoutcomefieldhandlers/folder with per-type subfolders (contentSchema,resolutionSchema,handleApprove,validate) and a central registry — adding a new inquiry type is additive with no changes to shared coderesolution.tswas silently no-oping on every approved invite (wrong type names),targetModelwas never set on create (breaking resolve access checks),canceledwas incorrectly accepted as a resolve outcome,searchableFieldsreferenced non-existent schema fieldsNew structure
New endpoint
POST /inquiries/:id/request-changes— target only, transitions tochangesRequested, stores explanation in resolution JSONTest plan
inviteOrganizationUserinquiry → statussentapproved→OrganizationUsercreated, statusapprovedapproved+ modifiedrolein body → override appliedcanceledchangesRequested🤖 Generated with Claude Code