feat(models): add sensitiveDisplay to keep personal data out of error messages - #557
Merged
Merged
Conversation
… messages
`getTechnicalDisplay` identifies an entity in technical error messages by
quoting its display value:
User "Jane Doe (jane@example.com)" (0b7e…) is not deleted.
Those messages are returned to the client and written to server logs, so for
a model whose display field holds a person's name — or, composed, their email
address — an invalid delete or restore writes personal data into both.
Add an optional `sensitiveDisplay` flag on the entity model. When set, the
display value is left out and the id alone identifies the entity:
User 0b7e… is not deleted.
The id is enough to find the record, and every message built through
`getTechnicalDisplay` is covered at once — already-deleted, cannot-be-deleted-
because-it-has, depends-on, cannot-restore-directly and not-found — rather than
only the one that happened to be noticed.
The flag deliberately does not touch `fetchDisplay`, which fills the delete
dry-run payload: that display is shown to an authorized admin confirming what
they are about to delete, where the name is the point.
Like `displayField`, it is not inherited by child models.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
🎉 This PR is included in version 29.4.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
dwirz
pushed a commit
that referenced
this pull request
Sep 3, 2026
Follow-up to #557, which took the wrong half of the problem. It suppressed the display in the message itself, so the caller lost it too — but whoever triggered a failed delete or restore has to know which record is meant, and a bare id does not tell them. Only a log has no business keeping the name. So `sensitiveDisplay` keeps its meaning as the model author's declaration that a display is personal data, and is consumed differently: the error now carries a second, log-safe variant of its own message. message: User "Jane Doe (jane@example.com)" (0b7e…) is not deleted. logMessage: User 0b7e… is not deleted. `logMessage` lives on the `GraphQLError` base, so every subclass inherits it. It is deliberately a class field rather than an extension: extensions are serialized to the client, so carrying it there would change the response. The throw sites state their prose once and it is rendered twice, through either the full or the log-safe display renderer (`technicalMessage`, and `forbidden` for the common case). The two variants therefore cannot drift apart, and a message naming several entities has each of them redacted — the restrict and cascade messages name two. `getTechnicalDisplay` goes back to always quoting the display, which is what 29.3.3 did; the suppression now lives in `getLogSafeTechnicalDisplay`. Consumers on 29.4.0 that set the flag get their client-facing message back and should switch to logging `logMessage`. Co-authored-by: zwei-wealth-dev <38549277-zwei-wealth-dev@users.noreply.gitlab.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
getTechnicalDisplayidentifies an entity in technical error messages by quoting its display value:Those messages are returned to the GraphQL client and written to server logs. For a model whose
displayFieldholds a person's name — or, composed, their email address — an invalid delete or restore therefore writes personal data into both. In a centralised logging setup that means names and emails land in log storage and in CI job output.The id on its own is enough to find the record, so the display value buys nothing here that is worth that cost.
Change
A new optional
sensitiveDisplayflag on the entity model. When it is set,getTechnicalDisplayleaves the display value out and identifies the entity by id alone:Every message built through
getTechnicalDisplayis covered at once rather than only the one that happens to get noticed first —is already deleted,cannot be deleted because it has …,depends on … which cannot be deleted/restored,Can't restore … directly,is not deletedand the not-found message.Default behaviour is unchanged: without the flag the message is exactly what it is today.
Deliberately out of scope
The flag does not touch
fetchDisplay, which fills thetoDelete/toUnlink/restrictedmaps of the delete dry-run payload. That display is rendered to an authorised admin confirming what they are about to delete, which is precisely the case where the name is the point. Redacting it there would break the confirmation UX rather than protect anything, so it stays.Notes
displayField, the flag is not inherited by child models (INHERITED_FIELDScovers queriable/listQueriable/creatable/updatable/deletable), so it must be set on every model that declares a sensitivedisplayField. This is documented.EntityModelpicks the flag up through the existingObject.assign(this, omit(definition, …))in its constructor; no wiring needed.Verification
tests/unit/technical-display.spec.tscovers the flagged model, the ordinary model, and both fallbacks. It builds its own localModelsso no generated-schema snapshot churns.npm run lintclean,tsc --noEmitclean, full unit suite 157/157.