Skip to content

Add x-derived extension for computed fields on resource schemas #361

Description

@mryhmln

Background

Computed fields on resources — values derived at read time from other fields (e.g. isComplete, hasData) — are currently not expressible in the OpenAPI schema. The composition system added a derive: mechanism that declared and evaluated these per composition config, but that design was rejected: a computed property belongs on the resource itself, not on each composition that happens to assemble it.

The decision (see docs/architecture/cross-cutting/resource-composition.md Decision 7) is to move derived fields to resource schemas via an x-derived OpenAPI extension, making them available on the resource's own endpoints and naturally filterable via ?q= like any other field.

A working implementation of derived field computation (CEL expression evaluation, item-scope vs. collection-scope inference) exists in the composition assembler on commit 25bbcdeb of feat/declarative-resource-composition — the CEL evaluation logic is reusable; what changes is where the declarations live.

Design

x-derived is an extension on an OpenAPI schema property that declares a CEL expression to compute the field's value at read time:

components:
  schemas:
    ApplicationMember:
      properties:
        isComplete:
          type: boolean
          readOnly: true
          x-derived: "has(firstName) && has(lastName) && has(dateOfBirth)"

The expression is evaluated in the context of the resource record. Scope follows the same pattern as composition derives: expressions that reference items are collection-scope (evaluated against the full result set); all others are item-scope (evaluated per record).

Reusable expressions

Common expressions (e.g. collection-completeness checks that apply to many resources) are declared once in packages/contracts/derives.yaml and referenced via $ref:

# derives.yaml
isComplete:
  collection: "items.all(i, has(i.completedAt))"
  item: "has(completedAt)"
# a resource schema
properties:
  isComplete:
    type: boolean
    readOnly: true
    x-derived:
      $ref: './derives.yaml#/isComplete/collection'

This mirrors the derives: pattern used in the composition assembler, making expressions portable across resource schemas without duplication.

Known fields blocked on this feature

Task.slaDeadline

The workflow state machine references $object.slaDeadline in two places:

  1. Change detection: $this.data.changes.exists(c, c.field == "slaDeadline") && $object.slaDeadline != null — triggers SLA timer scheduling when the deadline changes
  2. Timer fireAt: fireAt: $object.slaDeadline (×2 — warning at -48h, breach at 0h)

Task.slaInfo is an array of SlaInfo entries (each with slaTypeCode, status, clockStartedAt, deadline). slaDeadline is the earliest active deadline across all entries — a scalar computed from the array.

Task.slaDeadline has been added to the schema as an engine-managed readOnly field (the engine computes and maintains it whenever slaInfo is updated). It is a candidate for x-derived when that feature lands — at that point the engine-managed declaration can be replaced with a CEL expression:

x-derived: "slaInfo.filter(e, e.status in ['active', 'warning']).map(e, e.deadline).min()"

Phases

  • Design the x-derived extension schema and document it in the x-extensions architecture doc; define the derives.yaml structure for reusable named expressions
  • Add x-derived support to the mock server resource handlers
  • Add x-derived validation to the resolve pipeline (validate CEL syntax, warn on unknown field references, resolve $ref expressions from derives.yaml)
  • Add x-derived fields to resource schemas where currently needed (starting with ApplicationMember)
  • Update the TypeScript client generator to mark x-derived fields as readonly computed properties

Validation

  • GET /applications/{id}/members returns computed fields declared with x-derived
  • ?q=isComplete:false filters by the derived field value
  • A $ref-based expression in derives.yaml resolves and evaluates correctly
  • npm run validate catches malformed CEL expressions in x-derived
  • Derived fields are marked readOnly in the generated TypeScript client

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions