Schema Model
The domain schema is the internal representation of a parsed .morph file. It captures entities, value objects, operations, invariants, and extensions — everything generators need to produce code.
Schema Structure
A compiled schema is organized by bounded contexts:
interface DomainSchema {
name: string;
extensions: ExtensionsConfig;
contexts: Record<string, ContextDef>;
}
Each context contains:
interface ContextDef {
entities: Record<string, EntityDef>;
valueObjects?: Record<string, ValueObjectDef>;
commands?: Record<string, CommandDef>;
queries?: Record<string, QueryDef>;
functions?: Record<string, FunctionDef>;
invariants?: InvariantDef[];
subscribers?: Record<string, SubscriberDef>;
ports?: Record<string, PortDef>;
contracts?: ContractDef[];
}
Schema definitions live in contexts/generation/domain-schema/src/schemas/.
Entities
Entities have identity, lifecycle, and aggregate boundaries:
interface EntityDef {
attributes: Record<string, AttributeDef>;
aggregate: AggregateDef; // { root: boolean, parent?, cascadeDelete? }
description: string;
relationships?: RelationshipDef[];
}
Every entity gets an implicit id field. Only aggregate roots (root: true) get repositories. Child entities (parent: "Order") are accessed through their parent’s repository.
Value Objects
Value objects are immutable composites with no identity:
interface ValueObjectDef {
attributes: Record<string, AttributeDef>;
description: string;
}
Unlike entities, value objects have no aggregate field and no relationships. They are embedded within entities as composite attribute types.
Operations
Three operation kinds with distinct capabilities:
| Command | Query | Function | |
|---|---|---|---|
| State change | Yes (writes) | No (reads) | No access |
| Events | Required (emits) | None | None |
| Pre-conditions | Yes | Yes | No |
| Post-conditions | Yes | No | No |
| Type parameters | No | No | Yes |
Type System
Attribute and parameter types use a discriminated union:
| Kind | Example | Schema |
|---|---|---|
primitive |
string, boolean, integer, float, date |
{ kind: "primitive", name: "string" } |
array |
string[] |
{ kind: "array", element: ... } |
optional |
string? |
{ kind: "optional", inner: ... } |
union |
"a" | "b" |
{ kind: "union", variants: [...] } |
entityId |
User.id |
{ kind: "entityId", entity: "User" } |
entity |
User |
{ kind: "entity", entity: "User" } |
valueObject |
Address |
{ kind: "valueObject", valueObject: "Address" } |
generic |
Result<T, E> |
{ kind: "generic", name: "Result", args: [...] } |
Invariant Conditions
Invariants use a condition expression AST supporting boolean algebra, comparisons, quantifiers, and field access:
type ConditionExpr =
| { kind: "equals" | "notEquals" | "greaterThan" | ... }
| { kind: "and" | "or"; conditions: ConditionExpr[] }
| { kind: "not"; condition: ConditionExpr }
| { kind: "forAll" | "exists"; variable: string; collection: string; condition: ConditionExpr }
| { kind: "contains"; collection: ValueExpr; element: ValueExpr }
| { kind: "implies"; premise: ConditionExpr; conclusion: ConditionExpr }
Value expressions reference fields, literals, variables, counts, and function calls.
Extensions
Infrastructure configuration declared alongside the domain:
interface ExtensionsConfig {
storage?: StorageConfig; // backends: StorageBackend[], default
auth?: AuthConfig; // providers: AuthProvider[], default
eventStore?: EventStoreConfig;
encoding?: EncodingConfig; // formats: EncodingFormat[], default
i18n?: I18nConfig; // languages, baseLanguage
sse?: SseConfig; // enabled flag
}
See Extensions for runtime configuration details.