Language

Domain layer

L1 declares what exists, with enums, value types, entities, their fields, invariants and policies.

The domain layer (L1) describes what exists: the data of the application and the rules that hold for every stored record. It is the bottom layer, so its declarations reference only other domain declarations and cross cutting ones. Its keywords are enum, value and entity.

Enums

An enum is a closed set of named values.

enum OrderStatus {
  draft
  placed
  paid
  cancelled
}

Each value is an element with its own qualified name, such as sales.OrderStatus.draft. In expressions, you write a value without the enum name, as a bare enum value: status == draft.

Value types

A value type is a structured value without identity of its own, stored inside the entity that uses it.

value Line {
  product: String
  quantity: Int
  amount: Money
}

Value fields follow the same field syntax as entity fields. Their defaults and derived values can read the other fields of the same value type.

Entities

An entity is something with identity that the application stores. It holds fields, invariants and policies in any order.

module sales {
  enum OrderStatus {
    draft
    placed
    paid
    cancelled
  }

  value Line {
    product: String
    quantity: Int
    amount: Money
  }

  entity Customer {
    name: String unique
  }

  entity Order {
    reference: String unique
    customer: Customer
    lines: Line[] owned
    status: OrderStatus state default draft
    created_at: DateTime
    placed_at: DateTime?
    total: Money = sum(lines.map(l, l.amount))

    invariant "total never negative" { total >= 0 }

    policy read { actor.is(Clerk) }
    policy read fields total { status != draft || actor.is(Clerk) }
  }

  role Clerk
}

Fields

A field has a name, a type, optional modifiers, an optional default and an optional derived value, in this order:

name: Type[]? modifiers default <expression> = <expression>

Field types

TypeHolds
StringShort text
TextLong text
IntWhole number
DecimalDecimal number
MoneyAmount of money
Booltrue or false
DateCalendar date
DateTimePoint in time
DurationLength of time
FileUploaded file or blob
UuidUniversally unique identifier
An enumOne of its values
A value typeAn embedded structured value
An entityA relationship to a record of that entity

The type may be a declaration from another module by its path, such as crm.Account, when that module exports it.

Two suffixes change the type:

SuffixMeaningExample
[]A list of the typelines: Line[], tags: String[]
?Optional: the field may be empty (null)placed_at: DateTime?

Both can combine, list first: notes: Text[]?. A field without ? is required.

For typing, Int, Decimal and Money are the numeric types (an Int fits where a Decimal or Money is expected, not the other way round), String and Text compare with each other, and Date and DateTime are the temporal types. See expressions.

Modifiers

ModifierMeaning
uniqueNo two records share the value
stateThe field is the state of the entity. It must have an enum type, and only a machine changes it.
ownedThe referenced records belong to this entity and go away with it, for example the lines of an order

Defaults

default <expression> gives a field its initial value. The expression must fit the field type; a bare enum value works because it takes its type from the field.

status: OrderStatus state default draft
amount: Decimal default 0

null fits only an optional field: count: Int default null is an error, null is not assignable to Int.

Derived fields

= <expression> makes a field derived: its value is computed from other fields instead of stored. The expression must fit the field type.

total: Money = sum(lines.map(l, l.amount))

Inside an entity, defaults and derived values can read the entity’s fields, this, actor, now and tenant. See expressions.

Invariants

An invariant is a condition that must hold for every stored record of the entity. It has a label and a Bool condition.

invariant "total never negative" { total >= 0 }
invariant "paid after placed" { !has(paid_at) || paid_at > placed_at }

The label identifies the invariant: its qualified name is sales.Order.invariant["total never negative"].

Put a rule that must always be true here. A rule that must be true only for one action to run belongs to that action as a precondition.

Policies

Entity policies guard the records and fields of the entity. They can name the operations they cover (read, create, update, delete) and the fields they protect.

policy read { actor.is(crm.Clerk) || customer == actor.account }
policy create, update, delete { actor.is(crm.Clerk) }
policy read fields total { status != draft || actor.is(crm.Clerk) }

See access for the full rules.