Skip to content

fix(orm): use raw alias for join keys when PK/FK has field access policy#2675

Open
lsmith77 wants to merge 1 commit into
zenstackhq:devfrom
lsmith77:fix-deny-allow-joins
Open

fix(orm): use raw alias for join keys when PK/FK has field access policy#2675
lsmith77 wants to merge 1 commit into
zenstackhq:devfrom
lsmith77:fix-deny-allow-joins

Conversation

@lsmith77
Copy link
Copy Markdown
Contributor

@lsmith77 lsmith77 commented May 19, 2026

fix #2674

When a PK or FK field carries a @deny or @allow field-level policy, the
policy handler wraps it in CASE WHEN … THEN NULL. This caused include to
silently return empty arrays because join conditions evaluated as fk = NULL.

Fix: Two-part:

  1. The policy handler emits a __zs_raw_<field> alias (the raw column value)
    alongside the potentially-nulled expression for any PK or FK field that has
    a field access policy.

  2. Join condition builders (buildJoinPairs, SQLite's buildRelationJoinFilter,
    M2M path in the lateral-join dialect) use the raw alias selectively:

    • HasMany (ownedByModel = false): raw alias on both sides — the child's
      FK may be denied to hide which parent it belongs to, but the parent must
      still be able to fetch its children.
    • BelongsTo (ownedByModel = true): raw alias only on the relation's PK
      side; the FK side stays as a plain ref. Denying the FK is designed to hide
      the relation entirely (preserves pre-existing behavior verified by
      field-level-policy.test.ts).

Regression tests added in tests/regression/test/issue-2674.test.ts.

Note: externalIdMapping in the issue title is a REST API option unrelated to
the fix; the root cause is purely at the ORM layer.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed cases where field-level authorization on primary/foreign keys could break nested includes; related records now load while denied key fields resolve to null.
  • New Features

    • Policy-aware join handling improved: key columns are exposed alongside guarded values so joins and lateral subqueries remain reliable when fields have conditional read policies.
  • Tests

    • Added regression tests validating nested include behavior and denied-key handling.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds detection and selection of raw column aliases for PK/FK fields with conditional read policies, updates join-pair construction and lateral/SQLite join filters to use those raw aliases, and includes regression tests validating three @deny + include scenarios.

Changes

Fix @deny policies on PK/FK fields breaking includes

Layer / File(s) Summary
Query utilities - raw-alias join key detection and construction
packages/orm/src/client/query-utils.ts
Introduces JOIN_KEY_RAW_PREFIX constant and exports fieldHasConditionalReadPolicy, joinKeyRef, and updates buildJoinPairs to detect and route join references through raw-alias columns when fields have @deny/@allow policy.
Policy handler - select raw column aliases
packages/plugins/policy/src/policy-handler.ts
Updates createSelectAllFieldsWithPolicies to append raw column selections for PK/FK fields guarded by field-level policies, exposing unmodified values alongside policy-wrapped results.
Lateral join dialect - use raw aliases for M2M joins
packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts
Refactors buildRelationJoinFilter M2M predicate to use joinKeyRef-computed raw-alias references instead of direct ${alias}.${field} string synthesis.
SQLite dialect - use raw aliases for all join types
packages/orm/src/client/crud/dialects/sqlite.ts
Refactors buildRelationJoinFilter to use joinKeyRef for both M2M and FK-pair join conditions, replacing potentially CASE-wrapped field references with stable raw aliases.
Regression test - three @deny + include scenarios
tests/regression/test/issue-2674.test.ts
Adds vitest suite with three test cases: @deny on Post PK with HasMany includes, @deny on Comment FK with BelongsTo includes, and @deny on both PK and FK, verifying nested include results populate correctly when denied keys are null.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hop on raw aliases bright and small,
When @deny casts shadows over the wall,
Joins still find their steady tune,
Includes return beneath the moon,
Keys stay hidden, but relations stand tall.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing ORM join key handling when PK/FK fields have field access policies.
Linked Issues check ✅ Passed The PR directly addresses all coding objectives from issue #2674: preventing NULLed PK/FK expressions from breaking join conditions, preserving @deny semantics, and adding regression tests.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing issue #2674: policy handler raw alias emission, join builders using raw aliases, and corresponding regression tests.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@lsmith77 lsmith77 force-pushed the fix-deny-allow-joins branch from 022e5dd to c109d4a Compare May 19, 2026 18:13
@lsmith77 lsmith77 changed the title fix(orm): use raw alias for join keys when PK/FK has @allow or @deny field policy fix(orm): use raw alias for join keys when PK/FK has field access policy May 19, 2026
@lsmith77 lsmith77 force-pushed the fix-deny-allow-joins branch from c109d4a to 531e8d1 Compare May 19, 2026 19:10
@lsmith77
Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
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.

🧹 Nitpick comments (1)
tests/regression/test/issue-2674.test.ts (1)

148-148: ⚡ Quick win

Assert FK redaction across all included rows.

Checking only comments[0] can miss partial leaks. Assert every included comment has postId === null in this scenario.

♻️ Proposed test tightening
-        expect(userResult?.comments[0]?.postId).toBeNull();
+        expect(userResult?.comments.every((comment) => comment.postId === null)).toBe(true);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/regression/test/issue-2674.test.ts` at line 148, The test currently
only asserts the first included comment's postId is null via
expect(userResult?.comments[0]?.postId).toBeNull(); and can miss partial
leaks—update the assertion to verify every included comment has postId === null
by iterating over userResult?.comments (or using an array matcher) and asserting
each comment.postId is null; reference the userResult variable and comments
property in the test (replace the single-index check with a forEach/map-based
assertion that fails if any comment has a non-null postId).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/regression/test/issue-2674.test.ts`:
- Line 148: The test currently only asserts the first included comment's postId
is null via expect(userResult?.comments[0]?.postId).toBeNull(); and can miss
partial leaks—update the assertion to verify every included comment has postId
=== null by iterating over userResult?.comments (or using an array matcher) and
asserting each comment.postId is null; reference the userResult variable and
comments property in the test (replace the single-index check with a
forEach/map-based assertion that fails if any comment has a non-null postId).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7ecac85c-bd93-4695-aaac-a2c298cfbec0

📥 Commits

Reviewing files that changed from the base of the PR and between ed01275 and 531e8d1.

📒 Files selected for processing (5)
  • packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts
  • packages/orm/src/client/crud/dialects/sqlite.ts
  • packages/orm/src/client/query-utils.ts
  • packages/plugins/policy/src/policy-handler.ts
  • tests/regression/test/issue-2674.test.ts

@lsmith77 lsmith77 force-pushed the fix-deny-allow-joins branch 3 times, most recently from 3f814a8 to b682b06 Compare May 20, 2026 20:33
Copy link
Copy Markdown
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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/regression/test/issue-2674.test.ts`:
- Around line 59-103: Add a positive-control assertion that the ADMIN principal
can resolve the relation before the USER checks: after creating post and comment
with db.$setAuth(admin) and before the USER query, call
db.$setAuth(admin).comment.findUnique({ where: { id: comment.id }, include: {
post: true } }) (or reuse an adminResult variable) and assert adminResult?.post
is not null (and optionally adminResult?.postId equals post.id); then proceed
with the existing userResult assertions so failures that affect all roles are
caught.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 817b55b5-eea1-47c4-af67-6414aacd2233

📥 Commits

Reviewing files that changed from the base of the PR and between 3f814a8 and b682b06.

📒 Files selected for processing (5)
  • packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts
  • packages/orm/src/client/crud/dialects/sqlite.ts
  • packages/orm/src/client/query-utils.ts
  • packages/plugins/policy/src/policy-handler.ts
  • tests/regression/test/issue-2674.test.ts

Comment thread packages/orm/src/client/query-utils.ts
Comment thread tests/regression/test/issue-2674.test.ts
@lsmith77 lsmith77 force-pushed the fix-deny-allow-joins branch from b682b06 to 2345079 Compare May 20, 2026 20:56
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.

@deny on PK/FK fields breaks include when external_id_mapping is configured — related records silently return empty

1 participant