- Expressiveness
- Robustness
- UX
- DX
- JS application
- Interfacing directly with DB
start:
- action: pick-individual
choice:
- view-individual
- edit-individual
- enroll-individual
- action: make-individual
choice:
- when: individual[!enrolled]
then: enroll-individual
- otherwise: enroll-individual
entity:
individual:
entity: individual
fields:
- *
- name: enrolled
query: ...
enrolled_individual:
entity: individual
where: enrolled
actions:
pick-individual:
type: pick
entity: individual
make-individual:
type: make
entity: individual
enroll-individual:
type: edit
entity: individual[!enrolled]
New entities could be configured based on existing ones via YAML config. We call this process a refinement.
The basic syntax for refinements is
entity:
male:
entity: individual
...
This defined a new entity called male
which is based on pre-existing entity
called individual
(the ellipsis ...
means "other config keys", to be
described below).
There are two ways to refine an entity:
- Add new fields
- Add masks
The invariant is that a refinement entity is a subtype of a base entity — a
male
can be used anywhere individual
is expected.
-
TODO: Should we have a way to define new entities which has a subset of fields of a base entity? This is not a refinement with subtyping invariant.
-
TODO: Should we have a way to define new entities which has nominal tyope structure — e.g. subtyping doesn't hold and those entities are completely different than base ones.
Example:
entity:
enrolledIndividual:
entity: individual
fields:
hasEnrollments: exists(study_enrollment)
This defined an entity called enrolledIndividual
which has an additional
field called hasEnrollments
.
Example:
entity:
female:
entity: individual
where: sex = 'female'
This defines a new entity called female
which restricts individual
by only
allowing those for which the sex = 'female'
predicate evaluates is true.
It is possible to define a refinement which depends on current context.
For example we can say that individual is enrolled in some concrete study:
entity:
enrolledIndividual:
entity: individual
require:
- study: study
where: exists(study_enrollment.study = $study)
Another example would be to add a fields which depends on some value being in context:
entity:
enrolledIndividual:
entity: individual
require:
- study: study
where: exists(study_enrollment.study = $study)
fields:
enrollmentDate: top(study_enrollment.filter(study=$study).date)
Workflow is defined as a grammar which specifies productions for UI, the
start of the workflow is defined with start
non terminal symbol, the terminal
symbols are concrete actions.
Such grammar is restricted by the context type and requirements specified by actions.
workflow:
start:
- pick-or-make-individual:
- view-individual
- edit-individual
pick-or-make-individual:
- pick-individual
- make-individual
make-and-enroll-individual:
- make-individual:
- enroll-individual
pick-individual:
type: pick
entity: individual
make-individual:
type: make
entity: individual
enroll-individual:
type: process
require:
- individual: individual
- study: study
execute:
/insert(study_enrollment := {
individual := $individual,
study := $study,
})
- TODO: Describe execution semantics and type based dispatch.
- TODO: Describe execution semantics and type based dispatch.
Sometimes is is beneficial to do a custom dispatch without introducing more entity types. For that reason we have a match construct:
start:
- make-individual:
match:
- case: individual.age > 21
then:
- enroll-individual
- otherwise:
then:
- view-individual
- TODO: custom dispatch which introduces bindings into the context
start() = {
individual = pick-or-make-individual();
view-individual(individual) | edit-individual(individual)
}
pick-or-make-individual() = {
pick-individual() | make-individual()
}
make-and-enroll-individual() = {
make-individual();
enroll-individual();
}
pick-individual() = pick { entity: individual }
make-individual() = make { entity: individual }
view-individual(individual) = view(individual)
edit-individual(individual) = edit(individual)
enroll-individual(individual: individual, study: study) = process {
execute:
/insert(study_enrollment := {
individual := $individual,
study := $study,
})
}
- Reflect SQL structure into data model
- Relations
- o2m
- m2o
- m2m ???
- o2o ???
- Scalars
- Basic scalars
- Int
- Text
- Bool
- Date
- Time
- DateTime
- JSON
- ...
- Basic scalars
- Relations
- Map data model to GraphQL types
-
entity(id)
-
entity__list(limit, offset)
-
entity { m2o }
-
entity { o2m }
-
Working on syntax embedded in YAML.
workflow:
start:
action
workflow:
start:
- action
- another-action
workflow:
start:
action:
another-action
workflow:
start:
action:
another-action
UI as a Query
let pickIndividual = individual.pick
let viewIndividual = individual.view
pickIndividual {
navigate(id: "42") {
viewIndividual { ui, data }
}
}
Rabbit is extended with UI types and values. Thus queries represent recipes on how to get concrete datasets or how to render concrete UI for concrete datasets.
Examples of such queries:
-
Render a list of all individuals:
individual.pick
Now we can get a value of the selected item:
individual.pick.value
In this case the value is always
null
due to the fact that no selection has been made yet.To render a list of all individuals with a selected individual:
individual.pick(id: "id")
And not to get the selected value:
individual.pick(id: "id").value
-
Render a list of all a subset of individuals:
individual.filter(sex = 'male').pick
-
Render an individual after a list:
individual.pick.value.view
Note how you can navigate through
pick
query combinator.
Now that we have representation for screens we need a language to compose such
screens into workflows. I propose to define a Workflow
monad on top of
queries.
Examples of workflows:
-
Render a list of individuals with a view afterwards:
render(individual.pick) { render(view) }
-
Render a list of individuals with a view and a related site view afterwards:
render(individual.pick) { render(view) render(site.view) }
-
Render a list of individuals with a view based on the selected individual:
render(individual.pick) { render(switch( sex = 'male', view(title: "View Male"), sex = 'female', view(title: "View Female") )) }