Entity / foldkit-entity

Reading an operation's input

Experimental: the smallest mapping that several operation shapes and foldkit-form needed.

An Entity does not decide what may be written; an operation does (a Remote mutation, an RPC, a form). Entity.input takes that operation's input schema and says what each key means in terms of the Entity, so a consumer can find the field's metadata for a label, or the relation's target for a picker.

ts
const CreatePostInput = Schema.Struct({
  title: Schema.String,
  authorId: Schema.String,
  notify: Schema.Boolean,
})

const CreatePost = Entity.input(Blog.Post, CreatePostInput, {
  authorId: Relation.input(Blog.Post.relations.author),
  notify: Entity.unmapped,
})

CreatePost.members.title // Blog.Post.fields.title: it names a field, so it maps itself
CreatePost.members.authorId.relation.target() // Blog.Author: what a picker chooses from
CreatePost.members.notify // Entity.unmapped: about the operation, not the Post
  • A key that names a field, with a value that fits it, maps itself. Only the present value has to fit: a key may be optional (a partial update), admit null (clearing it), or hold an Option (an id absent for something new), which is the input schema's rule to make. A declared write (Write.update, Write.insert) writes what an Option holds and nothing for one holding nothing or a key left out; its id may not be absent, since it names the row it writes.
  • Every other key needs an entry: a Field under another name, a relation's ids with Relation.input(relation), or Entity.unmapped. Nothing is inferred from a name like authorId.
  • An entry may name the member by its key: { headline: 'title', authorId: 'author' } is the title Field and Relation.input of author. It is the same reading, checked the same way; a name that is no field or relation is a type error.
  • Entity.fields(Post, 'id', 'title') is those fields' schemas, to spread into the input's struct, so the input keeps the field's rules without repeating Post.fields.title.schema per key.
  • Relation.input expects one id for a one, id | null for an optional one, and an array of ids for a many.
  • A derived member cannot be written, and a member of another Entity cannot be mapped. Both are type errors and throw.

An input that holds the target itself

A post created together with a new author carries the author, not an id. Relation.nested maps the key to the relation and to an input of its target:

ts
const NewAuthor = Entity.input(Blog.Author, Schema.Struct({ name: Schema.String }))

const CreatePost = Entity.input(
  Blog.Post,
  Schema.Struct({ title: Schema.String, author: NewAuthor.schema }),
  { author: Relation.nested(Blog.Post.relations.author, NewAuthor) },
)

A one holds the nested input's value, a many a list of them; the nested input must be of the relation's target, and both are checked by type and at runtime. Entity.selectFor then loads what the nested input writes of the target, and Entity.valuesFor turns the loaded target back into nested values. What a nested write does (insert, update, replace the list) is the operation's handler to decide.

Showing what is there

An edit screen has to load the current values and turn them into input values. Both follow from the reading, so neither is written by hand:

ts
const PostForEdit = Entity.selectFor(CreatePost)
// a Selection of `title` and `author`, the members the input writes; `author` as a ref

Entity.valuesFor(CreatePost, { title: 'Hello', author: { entity: 'Author', id: 'a1' } })
// { title: 'Hello', authorId: 'a1' }

selectFor selects every member the input writes, by the member's key, with each relation as refs. valuesFor reads such a value back under the input's keys: a field as it is, a relation as the id or ids of what it holds. A relation read without its ids (a nested Selection that left id out) fills nothing. An unmapped key appears in neither, since the Entity knows nothing about it.

The schema is an ordinary Schema.Struct, so declare it once and give the same value to the operation, for example Mutation.make('CreatePost', { Input: CreatePostInput, … }) in foldkit-remote.

Declaring what an input writes

An input says what each key is; a Write says what an operation does with them: Write.update one row, named by a key, setting every other key mapped to a field; Write.insert a row under an id the client chose; or Write.delete one, which sets nothing.

ts
const EditPost = Entity.input(Blog.Post, EditPostInput)
const Edit = Write.update(EditPost, { id: 'id' })

Write.writes(Edit) // the fields any invocation may change
Write.bind(Edit, { id: 'p1', title: 'Compilers' })
// { entity: 'Post', id: 'p1', values: { title: 'Compilers' } }, as the store holds it

It is declared data, like a query body: nothing is written by building one, and an Entity having a field grants no one the right to set it. bind encodes each value through its field's schema, so the patch is in the store's shape, and takes the keys to write when an author changed only some. expect names a key holding the revision the row was read at, for an interpreter to write only a row still at it. An unmapped key is not written. A key mapped to a one relation points the row elsewhere: bind gives it as a ref in links (none for an optional relation set to nothing), and each interpreter carries it its own way, a ref key in a store, a foreign key in a table. A many relation or a nested key is refused where the write is declared.