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 = null → WHERE 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.
Description and expected behavior
When
external_id_mappingis configured and a model's integerid(or a relation FK) has@deny, allinclude-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
@denyby wrapping the source table in a SQL subquery that substitutesCASE 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 constructsWHERE fk_field IN (NULL), which matches zero rows.With
externalIdMapping: { Post: 'source_external_ref' }and aUSER-role auth context:The same failure cascades to all HasMany relations whose FK has
@deny, and to nested includes where the intermediate model'sidor FK is denied.The expected behavior is that
@denyonly redacts the field value from the serialized response and does not affect the SQL join keys used internally to resolveinclude. This is especially important whenexternal_id_mappingis in use: the integeridis already absent from the public identifier (replaced by a compound natural key), so@denyon it should only suppress it fromattributes— not NULL it in SQL.Screenshots
N/A
Environment:
Additional context
The failure cascades through the entire include tree:
Post.id @deny→ Prisma getspost.id = null→ all HasManyWHERE post_id IN (null)return emptyComment.post_id @deny→ even ifpost.idis fixed, the Comment subquery returnspost_id = null→WHERE post_id = {post.id}still matches nothingidor FK is deniedOnly 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
@denyfrom all PK/FK fields used as join keys and strip the values at the application layer if they must not be exposed.