Chapter 15 · Volume II — The ADL Domain Language

Resource Features


15.1 Overview

Resource features are toggleable behaviors declared in the features: block. Each feature activates a specific capability — additional endpoints, storage structures, or runtime behavior — without requiring the domain author to implement it.

resources:
  Post:
    features:
      softDelete: true
      audit: true
      versioning: true
      search: true
      export: true
      import: true
      caching: true
      cacheTtlSeconds: 300
      transactions: true
      validationMode: strict

All features default to false (or a sensible default for non-boolean features) unless explicitly enabled. A resource with no features: block has no optional features active — it is a plain CRUD resource.

Feature Summary

FeatureDefaultWhat It Adds
softDeletefalseLogical deletion, restore endpoint, scope filtering
auditfalseChange tracking table, audit endpoints
versioningfalseVersion history table, versions and revert endpoints
searchfalseFull-text search via ?search= parameter
exportfalseCSV export endpoint
importfalseJSON import endpoint
cachingfalseIn-memory read cache with TTL
cacheTtlSeconds300Cache lifetime (only when caching: true)
transactionstrueWrap writes in database transactions
validationMode"strict"strict rejects unknown fields; loose strips them

15.2 Soft Delete

When softDelete: true, the DELETE operation sets a deletedAt timestamp instead of removing the record from the database. Deleted records are excluded from queries by default but can be retrieved, listed, and restored.

Activation

features:
  softDelete: true

The resource MUST merge the SoftDelete object (or declare a deletedAt: { type: datetime } field) for soft delete to work:

object:
  $merge:
    - { $ref: '#/objects/Timestamp' }
    - { $ref: '#/objects/SoftDelete' }

Behavior

OperationWithout Soft DeleteWith Soft Delete
DELETE /:idRecord permanently removed. Returns 204.deletedAt set to current timestamp. Returns 204.
GET / (list)All records.Only records where deletedAt IS NULL (active scope).
GET /:id404 if not found.404 if soft-deleted (unless ?scope=all or ?scope=deleted).

Generated Endpoints

MethodPathActionDescription
POST/:id/restoreadl.{d}.{r}.restoreClear deletedAt, making the record active again

Scope Parameters

Soft delete automatically generates three scopes:

ScopeFilterDescription
active (default)deletedAt IS NULLOnly non-deleted records
deleteddeletedAt IS NOT NULLOnly soft-deleted records
all(no filter)All records regardless of deletion status
GET /api/v1/blog/posts                    → active only (default)
GET /api/v1/blog/posts?scope=deleted      → deleted only
GET /api/v1/blog/posts?scope=all          → everything

Restore

POST /api/v1/blog/posts/:id/restore

Clears the deletedAt field (sets to null), returning the record to the active scope. Returns 200 with the restored record. Returns 404 if the record does not exist or is not soft-deleted.

Hard Delete

When soft delete is enabled, there is no API endpoint for permanent deletion. Hard deletion requires direct database access or a Lua hook. A future version MAY add a purge endpoint for administrative hard deletion.


15.3 Audit Trail

When audit: true, the runtime records a JSON diff of changed fields after every create, update, and delete operation. Audit records are stored in a separate table and are queryable via dedicated endpoints.

Activation

features:
  audit: true

audit:
  track: [title, status, priority]
  exclude: [updatedAt]
  tableName: custom_audit_table
  retention:
    days: 90

Audit Configuration

KeyTypeDefaultDescription
trackstring[]all fieldsIf specified, only these fields are recorded in the diff.
excludestring[][]Fields to exclude from the diff. updatedAt SHOULD always be excluded.
tableNamestring{table}_auditOverride the default audit table name.
retention.daysintegerunlimitedAuto-cleanup of audit records older than N days.

If neither track nor exclude is specified, all field changes are recorded.

Audit Record Structure

Each audit record contains:

FieldTypeDescription
iduuidAudit record identifier
recordIduuidThe resource record that was changed
actionstring"create", "update", or "delete"
userIduuidThe user who performed the action
timestampdatetimeWhen the action occurred
changesjsonField-level diff

The changes field records what changed:

// On create:
{ "title": { "to": "New Post" }, "status": { "to": "draft" } }

// On update:
{ "title": { "from": "Old Title", "to": "New Title" }, "status": { "from": "draft", "to": "published" } }

// On delete:
{ "_action": "delete" }

Generated Endpoints

MethodPathActionDescription
GET/:id/auditadl.{d}.{r}.auditAudit trail for a specific record
GET/auditadl.{d}.{r}.auditAudit trail for all records of this resource

Both endpoints support pagination (?page=, ?limit=), date filtering (?after=, ?before=), and user filtering (?userId=).

Audit vs History [v0.3]

The audit trail (§15.3) and the history system (§21) serve different purposes:

AspectAudit TrailHistory of Change
AudienceDevelopers, DBAsEnd users, compliance officers
FormatJSON field diffsHuman-readable narrative labels
GranularityEvery field changeBusiness-significant events only
Configurationfeatures.audit + audit: blockhistory: block with event declarations
PurposeDebugging, forensicsActivity feeds, compliance logs

Both can be enabled simultaneously. The audit trail records mechanical field diffs; the history system records semantic business events.


15.4 Versioning

When versioning: true, the runtime copies the full record to a version history table before each update. Versions can be listed and reverted to.

Activation

features:
  versioning: true

Behavior

On every update (PATCH, PUT, or state machine transition), the runtime:

  1. Copies the current record (pre-update state) to {table}_versions with a version number.
  2. Applies the update to the main record.
  3. Returns the updated record.

Version numbers are sequential integers starting from 1. Each update increments the version.

Generated Endpoints

MethodPathActionDescription
GET/:id/versionsadl.{d}.{r}.versionsList all versions of a record (newest first)
POST/:id/revertadl.{d}.{r}.revertRestore the record to a previous version

Revert

The revert endpoint accepts a version number in the request body:

POST /api/v1/orders/orders/:id/revert
{ "version": 3 }

Reverting restores the record’s field values from the specified version. It creates a NEW version entry (the revert itself is a versioned change). The version history is append-only — reverting does not delete later versions.

Version Record Structure

FieldTypeDescription
iduuidVersion record identifier
recordIduuidThe resource record
versionintegerSequential version number
datajsonComplete record state at that version
userIduuidWho made the change
createdAtdatetimeWhen the version was created

When search: true, the runtime creates a full-text search index and enables the ?search= query parameter on list endpoints.

Activation

features:
  search: true

Behavior

The search index covers all text-type fields on the resource (string, text, markdown). When a ?search=term parameter is included in a list request, the query matches the term against all indexed fields simultaneously.

GET /api/v1/blog/posts?search=kubernetes

Returns all posts where any text field contains “kubernetes” (case-insensitive).

Store-Specific Implementation

StoreImplementationIndex Type
SQLiteLIKE '%term%' across text fieldsNo special index (full scan)
PostgreSQLtsvector column with GIN indexOptimized full-text search

The search behavior is transparent to the API consumer — the same ?search= parameter works on both stores. PostgreSQL provides significantly better performance on large datasets due to the GIN index.

Search + Filter Combination

Search can be combined with filters, sorting, and pagination:

GET /api/v1/blog/posts?search=kubernetes&filter[status]=published&sort=-createdAt&page=1&limit=10

The search term filters the result set, then filters, sorting, and pagination apply on top.


15.6 Export

When export: true, a CSV export endpoint is generated for the resource.

Activation

features:
  export: true

Generated Endpoint

MethodPathActionDescription
GET/exportadl.{d}.{r}.exportExport all records as CSV

Behavior

The export endpoint returns all records (subject to RBAC — the user must have list permission) as a CSV file with a header row:

Content-Type: text/csv
Content-Disposition: attachment; filename="posts.csv"

id,title,status,authorId,createdAt,updatedAt
550e8400-...,My Post,published,user-1,2026-01-01T00:00:00Z,2026-05-17T14:30:00Z

The export respects the same query parameters as the list endpoint — filters, sorting, and scopes can be applied:

GET /api/v1/blog/posts/export?filter[status]=published&sort=-createdAt

Computed fields (both virtual and stored) are included in the export. Relationship data is NOT included — only the FK values.


15.7 Import

When import: true, a JSON import endpoint is generated for the resource.

Activation

features:
  import: true

Generated Endpoint

MethodPathActionDescription
POST/importadl.{d}.{r}.importImport records from JSON

Behavior

The import endpoint accepts a JSON array of record objects:

POST /api/v1/orders/products/import
Content-Type: application/json

[
  { "sku": "PROD-001", "name": "Widget A", "unitPrice": "29.99", "isActive": true },
  { "sku": "PROD-002", "name": "Widget B", "unitPrice": "49.99", "isActive": true }
]

Each record is processed individually through the normal create pipeline — type validation, constraint checking, unique enforcement, and hook execution all apply. Records that fail validation are reported in the response; records that pass are created:

{
  "status": "ok",
  "code": 200,
  "data": {
    "imported": 2,
    "failed": 0,
    "errors": []
  }
}

If allowUserProvided: true is set on the id field and the submitted record includes an ID that already exists, the import performs an upsert (update the existing record).


15.8 Caching

When caching: true, the runtime maintains an in-memory read cache for the resource with a configurable TTL.

Activation

features:
  caching: true
  cacheTtlSeconds: 300
KeyTypeDefaultDescription
cachingbooleanfalseEnable in-memory read cache.
cacheTtlSecondsinteger300Cache entry lifetime in seconds.

Behavior

  • GET requests check the cache first. If a cache entry exists and has not expired, the cached response is returned without querying the database.
  • Write operations (create, update, delete, transition) invalidate the cache for the affected record. List cache entries are invalidated on any write to the resource.
  • Cache scope is per-server-instance. In a multi-instance deployment, each instance maintains its own cache. There is no cross-instance cache invalidation.

When to Use

Caching is appropriate for read-heavy reference data that changes infrequently: product catalogs, lookup tables, configuration records, geographic data. It is NOT appropriate for resources with high write frequency or where stale data is unacceptable.


15.9 Transactions

When transactions: true (the default), write operations are wrapped in database transactions.

features:
  transactions: true    # Default — can be omitted

A transaction wraps the entire write pipeline: validation, persistence, computed field recalculation, audit recording, and version creation. If any step fails, the entire operation is rolled back.

Setting transactions: false disables transactional wrapping. This is rarely needed and is provided for edge cases where the overhead of transaction management is unacceptable (e.g., high-throughput append-only logging resources).


15.10 Validation Mode

The validationMode feature controls how the runtime handles fields in the request body that are not declared in the resource’s object: block.

features:
  validationMode: strict    # Default
ModeBehavior
strictUnknown fields in the request body are rejected with HTTP 422. The error message identifies the unknown fields.
looseUnknown fields are silently stripped from the request body before processing. No error is returned.

strict is the default and is recommended for production. It catches typos and API consumer errors early. loose is useful for forward-compatible APIs where clients may send fields that the server has not yet defined.


15.11 Feature Interactions

Features compose independently. Enabling multiple features on the same resource produces additive behavior:

CombinationBehavior
softDelete + auditSoft-delete produces an audit entry with _action: "delete". Restore produces an audit entry with _action: "restore".
softDelete + versioningSoft-delete does NOT create a version entry (the record is not modified, only marked). Restore does NOT create a version entry.
audit + versioningBoth fire on update. The audit records the diff; the version records the full pre-update state.
softDelete + searchSoft-deleted records are excluded from search results in the active scope. ?scope=all&search=term searches across all records.
softDelete + exportExport respects the current scope. GET /export exports active records. GET /export?scope=all exports everything.
caching + any write featureAll write operations invalidate the cache before returning.

Hooks and Features

Features that produce events (audit, versioning, soft delete) fire AFTER the corresponding hooks. The execution order for an update with all features enabled:

1. beforeUpdate hooks
2. Validation
3. Persist update
4. Computed field recalculation
5. Version history recorded
6. Audit trail recorded
7. afterUpdate hooks
8. Cache invalidation
9. Bus event emitted

End of Chapter 15.

Next: Chapter 16 — Query Parameters