Views
A view is a schema class created from an entity:
/** What an author may write. */
export class PostDraft extends Post.pick('slug', 'title', 'summary', 'body') {}
/** What the public index shows — no body. */
export class PostCard extends Post.pick('id', 'slug', 'title', 'summary', 'publishedAt') {}
/** Input of a custom read operation. */
export class BySlugInput extends Post.pick('slug') {}
A view is a full schema class: it has getFields(), validate(), and is usable as a
TypeScript type. Views chain: Post.pick(…).partial().
The five derivations
| Derivation | Produces |
|---|---|
Post.pick('a', 'b') | only those fields |
Post.omit('a') | all fields but those |
Post.rename({ a: 'b' }) | the same fields under other names — the constraints and what each adapter was handed follow |
Post.partial() | every field optional: an absent field is not updated |
Post.extend({ extra: text() }) | the same fields plus new ones |
The partial mode is retained when the view is used as an operation input.
A view describes another schema's rows, and that is why it gets no table. One that dropped
nothing describes exactly the same rows as its origin, so it is neither — and it is refused
at boot until it says which: .anchor() holds rows of its own, which is how User extends
AuthUser.
What a view remembers
A view keeps two things about where it came from: source, the declaration it was cut
from, and survived, what the cut left — keyed by the origin's field names. Both sit on
derivation, and anchor climbs from there to the schema whose rows the view describes.
class PostCard extends Post.pick('id', 'title') {}
PostCard.derivation.source // Post
PostCard.derivation.survived // { id: 'id', title: 'title', body: undefined, status: undefined }
PostCard.derivation.anchor // Post — whose rows this shape describes
A dropped field is present as undefined, so the trace says what is missing and not only
what remains. A rename carries the new name ({ note: 'comment' }), and both are
composed with the parent's, so Post.pick('a','b','c').omit('c') reports against Post
and never against the intermediate — exactly as source skips it.
partial() and extend() keep the trace unchanged: making a field optional does not
change where it came from, and an added field has no origin to report.
This is what lets the identity card publish a view as a Post without
body rather than as an anonymous shape — two views of one entity used to describe
identically.
Where views plug in
- Handler inputs — declare the view as the parameter type; the façade validates the wire body against it before your code runs (Handlers).
- Handler outputs —
Crud(Post, { list: PostCard })names one op's view;Crud(Post, PostPublic)scopes the whole handler (Handlers). - Forms —
useFormFor(PostDraft)derives its fields from the view (Forms).
The io projections
The boundary axis derives two field sets used by every surface:
Visibility.of(Post.getFields()).input // fields a client may WRITE — readOnly excluded
Visibility.of(Post.getFields()).output // fields a client may READ — writeOnly excluded
This is why a form built on Post does not show status or publishedAt inputs, without
additional per-form configuration.
Choosing between a field and a view
readOnly / writeOnly and pick / omit can all remove a field from a surface, but
their scope differs.
- boundary (on the field) defines a global rule. With
writeOnly(password), the password is excluded from every output. - pick / omit (on a view) selects fields for a specific use. For example,
Post.pick('id', 'title')defines the public index response.
The two mechanisms are complementary.
- Without
writeOnlyon the field, every output view would need to omitpassword. The global rule prevents that omission. - A public response (
id,title) and an admin response (+ authorEmail) instead need different views:authorEmailis included or excluded depending on the use.
Choose based on the rule's scope: global or specific to one use.
| The fact is… | It lives… | Example |
|---|---|---|
| true everywhere (invariant) | on the field — readOnly / writeOnly | a password never leaves |
| true for one use (local) | in a derivation — pick / omit | this endpoint returns only id + title |
A boundary rule is serialized in the card under
x-fougere.boundary, so a consumer in another language can apply it. A view derived in
TypeScript is not serialized.
Next: The identity card — the schema's portable form.