Skip to content

Add nested routes support in the RestApiHandler#2454

Open
lsmith77 wants to merge 1 commit intozenstackhq:devfrom
lsmith77:nested-routes
Open

Add nested routes support in the RestApiHandler#2454
lsmith77 wants to merge 1 commit intozenstackhq:devfrom
lsmith77:nested-routes

Conversation

@lsmith77
Copy link
Contributor

@lsmith77 lsmith77 commented Mar 5, 2026

fixes #2453

Note: this was largely AI developed. I wanted to offer it as a more concrete proposal than just the above referenced ticket. If you agree this feature has merit, I will do a more detailed analysis of the proposed code changes.

TOCTOU Race Condition in Nested Mutations

The nested update/delete operations verify child-parent membership using a merged filter, then perform the mutation using only the child ID. In a concurrent scenario where the parent relationship changes between check and mutation, the operation could target an orphaned or reassigned resource.

Options Explored:

  • Direct mutations with merged filter (ATTEMPTED, FAILED): Have processNestedUpdate/processNestedDelete build and execute mutations directly using the merged filter for atomicity.
    • ❌ Result: Passing the merged filter to the ORM's update() and delete() calls resulted in 422 (Unprocessable Entity) errors. Not viable with current ORM implementation.
  • Optional where parameter (NOT ATTEMPTED): Add optional where parameter to processUpdate/processDelete to allow merged filter injection from nested handlers.
    • ⚠️ Trade-off: Would modify widely-used public methods, affecting all CRUD operations globally—higher risk for unintended consequences across the codebase. Still theoretically viable but rejected for minimal impact principle.
  • Format adaptation (NOT ATTEMPTED): Investigate merged filter semantics and adapt the format for direct ORM use.
    • ⚠️ Trade-off: Would require deep ORM integration work and understanding internal filter representations. Still theoretically viable but high effort/risk ratio.

Why Not Fixed:

  • Option 1 definitively failed due to ORM incompatibility
  • Options 2-3 rejected to keep PR scope minimal and reduce cross-cutting impact
  • Can't easily expose the race condition in tests (requires concurrent modifications)

Mitigation:

The authorization check prevents unauthorized cross-parent access (the primary security concern). The window for this race is extremely narrow and requires concurrent parent-relationship changes—a rare operation pattern.

Summary by CodeRabbit

  • New Features

    • Added configurable nested REST routes for parent-child resources, scoped reads/writes, nested single resource URLs, path-aware link generation, and prevention of changing parent via nested paths.
    • Validation of nested-route configuration including optional orphan-protection checks.
  • Bug Fixes

    • Fixed range/filter parsing issue.
  • Tests

    • Added extensive tests for nested-route validation, routing semantics, orphan-protection, mappings, and CRUD behavior.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 5, 2026

📝 Walkthrough

Walkthrough

Adds explicit nested route support to RestApiHandler via a new nestedRoutes option, validates configs at construction, introduces UrlPatterns.NESTED_SINGLE, and implements nested-aware routing, validation, handlers, and tests for GET/POST/PATCH/DELETE with parent-child enforcement and linking.

Changes

Cohort / File(s) Summary
Nested Routes Implementation
packages/server/src/api/rest/index.ts
Adds nestedRoutes to RestApiHandlerOptions, UrlPatterns.NESTED_SINGLE, normalization and constructor-time validation of nested configs, nested URL matching/patterns, helpers and processors for nested collection/single read/create/update/delete, propagation of nested context into read/write helpers, link/path generation adjustments, and a minor Between filter split fix.
Nested Routes Validation Tests
packages/server/test/api/options-validation.test.ts
Adds tests covering shape and semantic validation of nestedRoutes: invalid types, missing models/relations, relation target correctness, and requireOrphanProtection enforcement across onDelete behaviors.
Nested Routes Integration Tests
packages/server/test/api/rest.test.ts
Adds end-to-end nested route tests: parent-scoped reads, 404s for missing parents or mismatched ownership, POST binding to parent and payload rejection when altering parent, scoped PATCH/DELETE, filtering/pagination, fallback for non-configured 3-segment paths, JSON:API nested self-links, and modelNameMapping cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #2374 — Implements hierarchical/nested REST endpoints (adds nestedRoutes, nestedSingle pattern, and nested read/write processors), which aligns with the objectives described in that issue.

Poem

🐰
I hop where parents meet their young,
I stitch each path so links stay strung.
No stray child lost along the way,
I guard the routes and keep them gay.
Hooray — nested paths now bright as day!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding nested routes support to RestApiHandler, which is the primary feature implemented in this PR.
Linked Issues check ✅ Passed The implementation addresses all key coding requirements from issue #2453: nested route configuration with parent-child validation, automatic parent binding on POST, 404 handling, orphan protection validation, URL pattern support, JSON:API self-links, model name mapping respect, and constructor-time validation.
Out of Scope Changes check ✅ Passed All changes are within scope: core nested routing implementation, comprehensive test coverage, and a minor data-path fix in Between filter construction are all directly related to delivering nested routes functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/server/test/api/rest.test.ts (1)

3932-3972: Strengthen mapped nested-route coverage with mutation + link assertions.

This block currently checks mapped GET only. Adding mapped POST/PATCH/DELETE assertions (plus nested links.self) would protect against mapped-segment dispatch regressions.

🧪 Example assertion to add
+            const created = await mappedHandler({
+                method: 'post',
+                path: '/users/u1/posts',
+                client: nestedClient,
+                requestBody: {
+                    data: { type: 'Post', attributes: { title: 'mapped-created' } },
+                },
+            });
+            expect(created.status).toBe(201);
+            expect(created.body.data.links.self).toContain('/users/u1/posts/');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/test/api/rest.test.ts` around lines 3932 - 3972, The test
only asserts mapped GET behavior; add assertions that exercise mapped
nested-route mutations and link metadata to prevent dispatch regressions: use
RestApiHandler with modelNameMapping and nestedRoutes (the mappedApi and
mappedHandler helpers) to send POST to /users/u1/posts (create a child), PATCH
to update that child via /users/u1/posts/{id}, and DELETE to remove it,
asserting status codes and that nested list changes accordingly; additionally
assert the response includes nested links.self pointing to the mapped nested URL
(e.g., /users/{parentId}/posts) after each relevant response to verify link
wiring.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/server/src/api/rest/index.ts`:
- Around line 1562-1578: The code currently only rejects setting the
parent-controlled relation via relationships but still allows scalar FK
attributes; update the validation inside the forcedParentRelation block to also
detect any scalar FK fields from forcedParentRelation.parentIdFields present in
createPayload.data and return the same makeError when found. In practice, in the
forcedParentRelation branch check createPayload.data for any keys matching each
entry in forcedParentRelation.parentIdFields (in addition to the existing
relationships check) and call this.makeError('invalidPayload', ...) if any are
present; otherwise proceed to set
createPayload.data[forcedParentRelation.relation] = { connect:
this.makeIdConnect(...) } and include the select via this.makeDefaultIdKey(...)
as before.
- Around line 1303-1311: After building the nested parent filter in the
nested-create flow, add an explicit existence check for the parent before
performing the mutation: use getModelInfo(parentType) to get the parent model
metadata, reuse the filter produced by buildNestedParentFilter(parentType,
parentId, ...) to query the underlying data store via your model/adapter read
method, and if the parent row is not found return a 404-style error (consistent
with your API) instead of proceeding; this ensures nested POST returns an
explicit "parent not found" response rather than relying on downstream ORM
errors.
- Around line 1366-1373: The current guard only checks body.data.relationships
and uses the `in` operator unsafely; update the nested-patch guard in the
handler that inspects requestBody to (1) ensure body?.data?.relationships is an
object before using `in` (e.g., typeof === 'object' && body.data.relationships
!== null) or use hasOwnProperty, and (2) also inspect body?.data?.attributes for
scalar FK fields that could re-parent (cover common patterns for the FK name
derived from nestedRouteConfig.relation such as
`${nestedRouteConfig.relation}Id` and `${nestedRouteConfig.relation}_id`) and
call this.makeError('invalidPayload', ...) if any of those are present;
reference nestedRouteConfig.relation, requestBody/body, body.data.relationships,
body.data.attributes, and this.makeError to locate and update the logic.

---

Nitpick comments:
In `@packages/server/test/api/rest.test.ts`:
- Around line 3932-3972: The test only asserts mapped GET behavior; add
assertions that exercise mapped nested-route mutations and link metadata to
prevent dispatch regressions: use RestApiHandler with modelNameMapping and
nestedRoutes (the mappedApi and mappedHandler helpers) to send POST to
/users/u1/posts (create a child), PATCH to update that child via
/users/u1/posts/{id}, and DELETE to remove it, asserting status codes and that
nested list changes accordingly; additionally assert the response includes
nested links.self pointing to the mapped nested URL (e.g.,
/users/{parentId}/posts) after each relevant response to verify link wiring.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 28be15c8-cc27-4592-8c39-1e2203f23b8d

📥 Commits

Reviewing files that changed from the base of the PR and between 75d77de and 228bd2e.

📒 Files selected for processing (3)
  • packages/server/src/api/rest/index.ts
  • packages/server/test/api/options-validation.test.ts
  • packages/server/test/api/rest.test.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/server/src/api/rest/index.ts (1)

1373-1390: ⚠️ Potential issue | 🟠 Major

Harden nested PATCH FK guard using schema-derived FK fields (not naming heuristics).

Line 1383 only checks authorId / author_id-style keys. Custom or compound FK fields can still re-parent through attributes, bypassing nested route constraints.

💡 Proposed fix
 const rel = nestedRouteConfig.relation;
@@
-const fkCandidates = [`${rel}Id`, `${rel}_id`];
+const fkCandidates = [
+    ...(this.schema.models[typeInfo.name]?.fields[rel]?.relation?.fields ?? []),
+    ...Object.values(typeInfo.fields)
+        .filter((f) => f.foreignKeyFor?.includes(rel))
+        .map((f) => f.name),
+];
 if (
     typeof body?.data?.attributes === 'object' &&
     body.data.attributes !== null &&
     fkCandidates.some((fk) => Object.prototype.hasOwnProperty.call(body.data.attributes, fk))
 ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/api/rest/index.ts` around lines 1373 - 1390, The nested
PATCH guard currently only checks heuristic FK names and can be bypassed; update
the check in the handler that uses requestBody/body to compute FK candidate
fields from the schema/nestedRouteConfig instead of hardcoded
`${rel}Id`/`${rel}_id`: call the schema or ORM helper that returns actual
foreign-key field names for nestedRouteConfig.relation (e.g., a method like
schema.getForeignKeyFieldsForRelation or nestedRouteConfig.foreignKeys), then
test those real FK field names against body.data.attributes and
body.data.relationships and return this.makeError('invalidPayload', ...) if any
are present; keep the existing early-return behavior and use
nestedRouteConfig.relation and makeError symbols to locate and update the logic.
🧹 Nitpick comments (1)
packages/server/test/api/rest.test.ts (1)

4022-4062: Add mapped nested-create coverage to prove mapped 3-segment nested routing.

This block currently validates mapped GET /users/:id/posts, which can still pass via non-nested fallback behavior. Add a mapped nested create assertion (POST /users/:id/posts) and verify nested self-links to ensure mapped nested routing is truly exercised.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/test/api/rest.test.ts` around lines 4022 - 4062, Add a mapped
nested-create assertion and self-link checks to ensure mapped 3-segment nested
routing is exercised: use the existing mappedHandler created from
RestApiHandler/mappedApi to POST to '/users/u1/posts' (create a post for u1),
assert the response status is 201 (or created) and the returned resource
contains the expected attributes (title 'mapped-post' or similar), then fetch
the collection via GET '/users/u1/posts' (or inspect the POST response) and
assert the nested self-links/relationships reference the mapped route (e.g.,
self or related links include '/users/u1/posts') and that the created post
appears in the collection; reference mappedApi, mappedHandler and nestedClient
when adding these assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/server/src/api/rest/index.ts`:
- Around line 1373-1390: The nested PATCH guard currently only checks heuristic
FK names and can be bypassed; update the check in the handler that uses
requestBody/body to compute FK candidate fields from the
schema/nestedRouteConfig instead of hardcoded `${rel}Id`/`${rel}_id`: call the
schema or ORM helper that returns actual foreign-key field names for
nestedRouteConfig.relation (e.g., a method like
schema.getForeignKeyFieldsForRelation or nestedRouteConfig.foreignKeys), then
test those real FK field names against body.data.attributes and
body.data.relationships and return this.makeError('invalidPayload', ...) if any
are present; keep the existing early-return behavior and use
nestedRouteConfig.relation and makeError symbols to locate and update the logic.

---

Nitpick comments:
In `@packages/server/test/api/rest.test.ts`:
- Around line 4022-4062: Add a mapped nested-create assertion and self-link
checks to ensure mapped 3-segment nested routing is exercised: use the existing
mappedHandler created from RestApiHandler/mappedApi to POST to '/users/u1/posts'
(create a post for u1), assert the response status is 201 (or created) and the
returned resource contains the expected attributes (title 'mapped-post' or
similar), then fetch the collection via GET '/users/u1/posts' (or inspect the
POST response) and assert the nested self-links/relationships reference the
mapped route (e.g., self or related links include '/users/u1/posts') and that
the created post appears in the collection; reference mappedApi, mappedHandler
and nestedClient when adding these assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 32ecfa26-53fe-43ca-944c-46d23f105cae

📥 Commits

Reviewing files that changed from the base of the PR and between 228bd2e and b1c1b54.

📒 Files selected for processing (3)
  • packages/server/src/api/rest/index.ts
  • packages/server/test/api/options-validation.test.ts
  • packages/server/test/api/rest.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/server/test/api/options-validation.test.ts

@lsmith77 lsmith77 force-pushed the nested-routes branch 2 times, most recently from 80c9dd6 to 8c9a4a1 Compare March 5, 2026 14:21
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/server/test/api/rest.test.ts (1)

3996-4036: Add a regression test for mapped child names without nestedRoutes.

This block validates mapped nested routes, but it doesn’t cover the flat-route fallback case when modelNameMapping is set and nestedRoutes is absent. Please add a case like GET /users/:id/posts (with Post -> posts mapping, no nested config) to ensure regular fetchRelated stays intact.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/test/api/rest.test.ts` around lines 3996 - 4036, Add a new
regression test in the same suite that verifies mapped child names work when
modelNameMapping is present but nestedRoutes is omitted: instantiate
RestApiHandler with schema: nestedClient.$schema, endpoint, and
modelNameMapping: { User: 'users', Post: 'posts' } but no nestedRoutes, create
users and a post via nestedClient.user.create, then call the handler (reuse
mappedHandler pattern) with method 'get' and path `/users/u1/posts` and assert
status 200 and that the returned collection contains the created post, and also
call `/users/u2/posts` and assert it returns an empty list; this ensures the
flat-route fallback (fetchRelated behavior) works for mapped child names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/server/src/api/rest/index.ts`:
- Around line 509-513: The code is mutating match.relationship with a model-name
alias which breaks flat 3-segment routes and the processFetchRelated fallback;
instead of rewriting match.relationship, compute a local mappedRelationship
(e.g. const mappedRelationship =
this.reverseModelNameMapping[match.relationship] || match.relationship) and use
that local value only when resolving nested-route model dispatch, leaving
match.relationship untouched so processFetchRelated still receives the original
route segment; apply the same non-mutating change to the other occurrences
around the blocks that reference reverseModelNameMapping (the spots analogous to
the current match.relationship rewrite).
- Line 594: The variable declaration uses let for values that are never
reassigned (e.g., nestedMatch created from this.matchUrlPattern(path,
UrlPatterns.FETCH_RELATIONSHIP)); change these to const. Update the three
occurrences in this module where a value is assigned from this.matchUrlPattern
or similar pattern-matching calls (including the nestedMatch declaration and the
two other let declarations reported) to use const instead of let so they satisfy
the prefer-const lint rule.

---

Nitpick comments:
In `@packages/server/test/api/rest.test.ts`:
- Around line 3996-4036: Add a new regression test in the same suite that
verifies mapped child names work when modelNameMapping is present but
nestedRoutes is omitted: instantiate RestApiHandler with schema:
nestedClient.$schema, endpoint, and modelNameMapping: { User: 'users', Post:
'posts' } but no nestedRoutes, create users and a post via
nestedClient.user.create, then call the handler (reuse mappedHandler pattern)
with method 'get' and path `/users/u1/posts` and assert status 200 and that the
returned collection contains the created post, and also call `/users/u2/posts`
and assert it returns an empty list; this ensures the flat-route fallback
(fetchRelated behavior) works for mapped child names.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5737dc7f-5d4d-47f4-bd8d-c5698cabdc0a

📥 Commits

Reviewing files that changed from the base of the PR and between b1c1b54 and 8c9a4a1.

📒 Files selected for processing (3)
  • packages/server/src/api/rest/index.ts
  • packages/server/test/api/options-validation.test.ts
  • packages/server/test/api/rest.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/server/test/api/options-validation.test.ts

if (!requestBody) {
return this.makeError('invalidPayload');
}
let nestedMatch = this.matchUrlPattern(path, UrlPatterns.FETCH_RELATIONSHIP);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

head -n 700 packages/server/src/api/rest/index.ts | tail -n 120 | cat -n

Repository: zenstackhq/zenstack

Length of output: 6416


🏁 Script executed:

sed -n '590,600p;640,650p;680,690p' packages/server/src/api/rest/index.ts | cat -n

Repository: zenstackhq/zenstack

Length of output: 1948


Change let to const for variables that are never reassigned.

Lines 594, 644, and 685 declare variables with let that are never reassigned after initialization. Change these to const to comply with the prefer-const linting rule.

Changes needed
-                    let nestedMatch = this.matchUrlPattern(path, UrlPatterns.FETCH_RELATIONSHIP);
+                    const nestedMatch = this.matchUrlPattern(path, UrlPatterns.FETCH_RELATIONSHIP);
@@
-                    let nestedPatchMatch = this.matchUrlPattern(path, UrlPatterns.NESTED_SINGLE);
+                    const nestedPatchMatch = this.matchUrlPattern(path, UrlPatterns.NESTED_SINGLE);
@@
-                    let nestedDeleteMatch = this.matchUrlPattern(path, UrlPatterns.NESTED_SINGLE);
+                    const nestedDeleteMatch = this.matchUrlPattern(path, UrlPatterns.NESTED_SINGLE);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let nestedMatch = this.matchUrlPattern(path, UrlPatterns.FETCH_RELATIONSHIP);
const nestedMatch = this.matchUrlPattern(path, UrlPatterns.FETCH_RELATIONSHIP);
🧰 Tools
🪛 GitHub Actions: Build and Test

[error] 594-594: ESLint: 'nestedMatch' is defined but never reassigned. Use 'const' instead. (prefer-const)

🪛 GitHub Check: build-test (22.x, mysql)

[failure] 594-594:
'nestedMatch' is never reassigned. Use 'const' instead

🪛 GitHub Check: build-test (22.x, postgresql)

[failure] 594-594:
'nestedMatch' is never reassigned. Use 'const' instead

🪛 GitHub Check: build-test (22.x, sqlite)

[failure] 594-594:
'nestedMatch' is never reassigned. Use 'const' instead

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/api/rest/index.ts` at line 594, The variable declaration
uses let for values that are never reassigned (e.g., nestedMatch created from
this.matchUrlPattern(path, UrlPatterns.FETCH_RELATIONSHIP)); change these to
const. Update the three occurrences in this module where a value is assigned
from this.matchUrlPattern or similar pattern-matching calls (including the
nestedMatch declaration and the two other let declarations reported) to use
const instead of let so they satisfy the prefer-const lint rule.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/server/src/api/rest/index.ts`:
- Around line 1369-1375: The existence check uses (client as
any)[childType].findFirst with where:
mergeFilters(makeIdFilter(typeInfo.idFields, childId), nestedFilter) but the
subsequent mutation (e.g., (client as any)[childType].update / delete) only uses
the child ID; to prevent TOCTOU, change the mutation's where clause to the same
merged filter (use this.mergeFilters(this.makeIdFilter(typeInfo.idFields,
childId), nestedFilter)) so the update/delete is atomically scoped to the parent
filter (apply the same change to the other occurrence around the update/delete
call referenced in the diff).
- Around line 1434-1442: The current nested DELETE checks membership with
findFirst then calls this.processDelete(client, childType, childId), which
introduces a TOCTOU race; instead delete using a parent-scoped where that
combines the child's id filter and the nestedFilter in a single operation.
Update the call site to pass a merged where (use
this.mergeFilters(this.makeIdFilter(typeInfo.idFields, childId), nestedFilter))
into the delete operation (or adjust processDelete to accept and use a where
object rather than just childId) so deletion is atomic and scoped to the parent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4e8f8d98-65fb-43b0-8e70-8ee926068b46

📥 Commits

Reviewing files that changed from the base of the PR and between 8c9a4a1 and 45402a3.

📒 Files selected for processing (3)
  • packages/server/src/api/rest/index.ts
  • packages/server/test/api/options-validation.test.ts
  • packages/server/test/api/rest.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/server/test/api/rest.test.ts
  • packages/server/test/api/options-validation.test.ts

Comment on lines +1369 to +1375
// Verify child belongs to parent before updating
const exists = await (client as any)[childType].findFirst({
where: this.mergeFilters(this.makeIdFilter(typeInfo.idFields, childId), nestedFilter),
});
if (!exists) {
return this.makeError('notFound');
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Avoid TOCTOU in nested PATCH by scoping mutation atomically to parent filter.

Line 1369 checks parent-scoped existence, but Line 1401 performs update by child ID only. A concurrent re-parent between those calls can let this nested endpoint update a child no longer under parentId.

💡 Suggested direction
- // Verify child belongs to parent before updating
- const exists = await (client as any)[childType].findFirst({ where: scopedWhere });
- if (!exists) return this.makeError('notFound');
- return this.processUpdate(client, childType, childId, query, requestBody, linkers);
+// Perform scoped mutation in a single parent-constrained operation
+// (e.g. scoped updateMany + fetch-for-response, or a dedicated scoped update helper)
+return this.processNestedScopedUpdate(
+  client,
+  childType,
+  childId,
+  scopedWhere,
+  query,
+  requestBody,
+  linkers
+);

The key is: mutation where must include both child ID and nestedFilter.

Also applies to: 1401-1404

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/api/rest/index.ts` around lines 1369 - 1375, The
existence check uses (client as any)[childType].findFirst with where:
mergeFilters(makeIdFilter(typeInfo.idFields, childId), nestedFilter) but the
subsequent mutation (e.g., (client as any)[childType].update / delete) only uses
the child ID; to prevent TOCTOU, change the mutation's where clause to the same
merged filter (use this.mergeFilters(this.makeIdFilter(typeInfo.idFields,
childId), nestedFilter)) so the update/delete is atomically scoped to the parent
filter (apply the same change to the other occurrence around the update/delete
call referenced in the diff).

Comment on lines +1434 to +1442
// Verify child belongs to parent before deleting
const exists = await (client as any)[childType].findFirst({
where: this.mergeFilters(this.makeIdFilter(typeInfo.idFields, childId), nestedFilter),
});
if (!exists) {
return this.makeError('notFound');
}

return this.processDelete(client, childType, childId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Avoid TOCTOU in nested DELETE by deleting with parent-scoped where.

Line 1434 validates membership first, then Line 1442 deletes by child ID only. This has the same race window as PATCH and can delete a child after it’s re-parented.

💡 Suggested fix
- // Verify child belongs to parent before deleting
- const exists = await (client as any)[childType].findFirst({
-     where: this.mergeFilters(this.makeIdFilter(typeInfo.idFields, childId), nestedFilter),
- });
- if (!exists) {
-     return this.makeError('notFound');
- }
-
- return this.processDelete(client, childType, childId);
+const deleteResult = await (client as any)[childType].deleteMany({
+    where: this.mergeFilters(this.makeIdFilter(typeInfo.idFields, childId), nestedFilter),
+});
+if (!deleteResult.count) {
+    return this.makeError('notFound');
+}
+return {
+    status: 200,
+    body: { meta: {} },
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/api/rest/index.ts` around lines 1434 - 1442, The current
nested DELETE checks membership with findFirst then calls
this.processDelete(client, childType, childId), which introduces a TOCTOU race;
instead delete using a parent-scoped where that combines the child's id filter
and the nestedFilter in a single operation. Update the call site to pass a
merged where (use this.mergeFilters(this.makeIdFilter(typeInfo.idFields,
childId), nestedFilter)) into the delete operation (or adjust processDelete to
accept and use a where object rather than just childId) so deletion is atomic
and scoped to the parent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Nested routes in the RestApiHandler

1 participant