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

DerivationProduces
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 outputsCrud(Post, { list: PostCard }) names one op's view; Crud(Post, PostPublic) scopes the whole handler (Handlers).
  • FormsuseFormFor(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 writeOnly on the field, every output view would need to omit password. The global rule prevents that omission.
  • A public response (id, title) and an admin response (+ authorEmail) instead need different views: authorEmail is 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 / writeOnlya password never leaves
true for one use (local)in a derivation — pick / omitthis 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.

Built with Fougere — this site runs on the framework it documents.