Skip to content

Commit e7ad416

Browse files
feat(schema)!: enforce mandatory filters + nonNull-filterable relations on WhereUnique (#471)
`WhereUnique` historically carried only `unique` fields, so a singular `entity(where: { id })` lookup could bypass the cascade the plural `entities(where: { … })` query enforced through `filterable: { nonNull: true }`. Two practical consequences this commit closes: 1. A model with `filterable: { nonNull: true }` on a simple/enum field (e.g. `Relation.status`) makes `RelationWhere.status` non-nullable — every plural query must opt in — but `RelationWhereUnique` never exposed `status`, so the singular `relation(where: { id })` slipped past the same enforcement. 2. A child model with a relation FK marked `filterable: { nonNull: true }` couldn't nest the related model's Where on a singular lookup — `report(where: { id, relation: { status: [ACTIVE] } })` was a schema error because `ReportWhereUnique` only had `id`. This commit extends `${model.name}WhereUnique` to include: - non-relation fields marked `filterable: { nonNull: true }`, typed as `[T]!` with the model's `filterable.default` honored — same shape and nullability as the plural Where, so the cascade is enforced - relation FK fields marked `filterable: { nonNull: true }`, typed as `RelatedWhere!` — every consumer of the singular query must nest the related model's Where; the related model's own mandatory filters are in turn enforced at the inner Where level Both additions are non-nullable inputs, so the typegen surfaces every existing `entity(where: { id })` call site that needs to opt into the cascade — the same "surface everything via typegen" pattern that drove the original `filterable: { nonNull: true }` design for plural queries. Just `filterable: true` relations are NOT exposed here on purpose: the intent is enforcement, and the existing plural Where is where optional filters belong. To enable the cascade for a new relation FK, mark it `filterable: { nonNull: true }` in the model spec — typegen will then fail every site that needs updating. BREAKING CHANGE: `${model.name}WhereUnique` now carries non-nullable fields when the model has `filterable: { nonNull: true }` simple/enum fields or relation FKs. Any consumer schema with such fields will see its `entity(where: { … })` queries fail typegen until they opt into the cascade by providing those fields. Upgrade requires updating every singular-by-id query to nest the mandatory filters (the typegen errors list exactly the sites that need touching). Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent fda01f5 commit e7ad416

1 file changed

Lines changed: 30 additions & 4 deletions

File tree

src/schema/generate.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,36 @@ export const generateDefinitions = ({
131131
},
132132
]),
133133
),
134-
input(
135-
`${model.name}WhereUnique`,
136-
model.fields.filter(({ unique }) => unique).map((field) => ({ name: field.name, type: field.type })),
137-
),
134+
input(`${model.name}WhereUnique`, [
135+
...model.fields.filter(({ unique }) => unique).map((field) => ({ name: field.name, type: field.type })),
136+
// Mandatory filters (filterable: { nonNull: true }) on simple/enum fields — required
137+
// on WhereUnique queries too, so callers of `entity(where: { id })` can't bypass the
138+
// cascade the plural query enforces.
139+
...model.fields
140+
.filter(
141+
({ kind, filterable }) => typeof filterable === 'object' && filterable.nonNull === true && kind !== 'relation',
142+
)
143+
.map((field) => ({
144+
name: field.name,
145+
type: field.type,
146+
list: true,
147+
default: typeof field.filterable === 'object' ? field.filterable.default : undefined,
148+
nonNull: true,
149+
})),
150+
// Same intent for relation FKs marked mandatory-filterable: every singular lookup
151+
// must nest the related model's Where, e.g.
152+
// `report(where: { id, relation: { status: [ACTIVE] } })`
153+
// Marking a child model's relation FK as `filterable: { nonNull: true }` is what opts
154+
// it into this enforcement — the related model's own nested mandatory filters then
155+
// apply at the inner Where level.
156+
...model.relations
157+
.filter(({ field: { filterable } }) => typeof filterable === 'object' && filterable.nonNull === true)
158+
.map(({ name, targetModel }) => ({
159+
name,
160+
type: `${targetModel.name}Where`,
161+
nonNull: true,
162+
})),
163+
]),
138164
...(model.fields.some(({ orderable }) => orderable)
139165
? [
140166
input(

0 commit comments

Comments
 (0)