Models support declarative lifecycle events, field validation, and sanitization — all defined in JSON. These fire at the repository layer, ensuring consistency regardless of entry point (API, process, script).
Add events to model JSON:
{
"events": {
"before_create": [
{ "process": "set_defaults" },
{ "script": { "lang": "typescript", "file": "scripts/on_create.ts" } }
],
"after_create": [
{ "process": "notify", "sync": false, "on_error": "log" }
],
"before_update": [
{ "process": "check_transition", "condition": "old.status != status" }
],
"on_change": {
"customer_id": [{ "process": "fill_defaults" }]
}
}
}| Event | Modify data? | Abort? | Old data? |
|---|---|---|---|
before_validate |
Yes | Yes | update: Yes |
after_validate |
Yes | Yes | update: Yes |
before_create |
Yes | Yes | No |
after_create |
No (copy) | sync: Yes | No |
before_update |
Yes | Yes | Yes |
after_update |
No (copy) | sync: Yes | Yes |
before_delete |
No | Yes | Yes |
after_delete |
No (copy) | No | Yes |
before_save |
Yes | Yes | update: Yes |
after_save |
No (copy) | sync: Yes | update: Yes |
before_soft_delete |
No | Yes | Yes |
after_soft_delete |
No (copy) | No | Yes |
before_hard_delete |
No | Yes | Yes |
after_hard_delete |
No (copy) | No | Yes |
before_restore |
No | Yes | Yes |
after_restore |
No (copy) | No | Yes |
on_change |
Yes | Yes | Yes |
process— process name to callscript—{ "lang": "typescript", "file": "path" }condition— expression (supportsold.field,session.user_id)sync— true/false (default: true for before_, false for after_)on_error—"fail"/"log"/"ignore"retry—{ "max": 3, "delay": "5s", "backoff": "exponential" }timeout— duration string (default: 30s before, 60s after)priority— int (lower = first, default 50)bulk_mode—"each"/"batch"/"skip"
Before-event handlers modify data via return value merge:
export default function(ctx) {
return { phone: normalize(ctx.data.phone) };
}After-event handlers receive an immutable copy.
Every CRUD operation publishes to the event bus:
model.{name}.createdmodel.{name}.updatedmodel.{name}.deletedmodel.{name}.restored
Add validation to field definitions:
{
"fields": {
"email": {
"type": "email",
"sanitize": ["trim", "lowercase"],
"validation": {
"required": true,
"email": true,
"unique": { "scope": ["tenant_id"] },
"max_length": 255
}
},
"end_date": {
"type": "date",
"validation": {
"required": true,
"date_after": "start_date"
}
},
"cancel_reason": {
"type": "text",
"validation": {
"required_if": { "status": "cancelled" },
"min_length": 10
}
}
}
}Presence: required, email, url, phone, ip, ipv4, ipv6, uuid, json Format: alpha, alpha_num, alpha_dash, numeric, regex, starts_with, ends_with, contains, not_contains, lowercase, uppercase Size: min, max, min_length, max_length, between, length_between, size Comparison: in, not_in, confirmed, different, gt, gte, lt, lte Date: date_before, date_after, date_before_or_equal, date_after_or_equal Unique: unique (simple or scoped with case_insensitive) Relational: exists, exists_where, min_items, max_items File: file_size, file_type Immutability: immutable, immutable_after
required_if/required_unlessrequired_with/required_with_allrequired_without/required_without_allexclude_if/exclude_unlesswhen— apply validators conditionallyon—"create"/"update"/"always"
{
"validators": [
{
"name": "date_range",
"expression": "end_date >= start_date",
"message": "End date must be after start date"
}
]
}{
"error": "Validation failed",
"code": "VALIDATION_ERROR",
"errors": {
"email": [{ "rule": "required", "message": "Email is required" }],
"_model": [{ "rule": "date_range", "message": "End date must be after start date" }]
}
}{
"fields": {
"email": { "type": "email", "sanitize": ["trim", "lowercase"] },
"name": { "type": "string", "sanitize": ["trim", "title_case"] }
},
"sanitize": { "_all_strings": ["trim"] }
}Built-in: trim, trim_left, trim_right, lowercase, uppercase, title_case, strip_tags, strip_whitespace, slugify, normalize_email, normalize_phone, truncate:N, escape_html.
- All new fields (
events,validation,validators,sanitize) are optional - Existing
required,max,minauto-map to validators - Existing
mandatory_ifcontinues to work for UI
engine/internal/compiler/parser/model.go— typesengine/internal/runtime/validation/— validator + sanitizerengine/internal/runtime/hook/— event dispatcherengine/internal/infrastructure/persistence/repository.go— integrationengine/internal/presentation/api/crud_handler.go— 422 errors