Skip to content

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

@lsmith77

Description

@lsmith77

Description and expected behavior

When external_id_mapping is configured and a model's integer id (or a relation FK) has @deny, all include-based queries for relations using that field as a join key silently return empty arrays instead of the expected related records.

ZenStack implements field-level @deny by wrapping the source table in a SQL subquery that substitutes CASE WHEN <deny_condition> THEN NULL ELSE field END AS field. This NULL propagates into Prisma's include mechanism: Prisma reads the now-NULL PK from the parent row and constructs WHERE fk_field IN (NULL), which matches zero rows.

model Post {
    // Integer PK hidden from API consumers; compound natural key used as JSON:API id via externalIdMapping
    id           Int    @id @default(autoincrement()) @deny('all', auth().role != 'SUPERADMIN')
    external_ref String
    source       String
    comments     Comment[]

    @@unique([source, external_ref])
}

model Comment {
    id      Int  @id
    post_id Int? @deny('all', auth().role != 'SUPERADMIN')
    post    Post? @relation(fields: [post_id], references: [id])
}

With externalIdMapping: { Post: 'source_external_ref' } and a USER-role auth context:

// Expected: post with populated comments array
// Actual:   { id: null, comments: [] }
const post = await db.post.findUnique({
    where: { source_external_ref: { source: 'blog', external_ref: 'my-post' } },
    include: { comments: true },
});

The same failure cascades to all HasMany relations whose FK has @deny, and to nested includes where the intermediate model's id or FK is denied.

The expected behavior is that @deny only redacts the field value from the serialized response and does not affect the SQL join keys used internally to resolve include. This is especially important when external_id_mapping is in use: the integer id is already absent from the public identifier (replaced by a compound natural key), so @deny on it should only suppress it from attributes — not NULL it in SQL.

Screenshots

N/A

Environment:

  • ZenStack version: 3.6.1
  • Database type: PostgreSQL
  • Node.js version: 24.15.0
  • Package manager: npm

Additional context

The failure cascades through the entire include tree:

  • Post.id @deny → Prisma gets post.id = null → all HasMany WHERE post_id IN (null) return empty
  • Comment.post_id @deny → even if post.id is fixed, the Comment subquery returns post_id = nullWHERE post_id = {post.id} still matches nothing
  • Nested includes fail in the same way whenever an intermediate model's id or FK is denied

Only BelongsTo relations whose FK sits on the parent model (and is not itself denied) continue to work correctly, since Prisma fetches those using the parent's own attributes rather than the child's FK column.

Workaround: remove @deny from all PK/FK fields used as join keys and strip the values at the application layer if they must not be exposed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions