Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Apply field transforms

A field transform computes or rewrites values during a lens operation. Use one when a vertex map cannot express the change, such as deriving a field from a sibling or changing a field’s representation.

Prerequisites

The TypeScript SDK and a source BuiltSchema. The current TypeScript lens-document path retains value transforms through compilation and instantiation.

Transform an existing field

This document increments count on get and decrements it on put:

const document = {
  id: 'dev.example.increment-count',
  source: 'v1',
  target: 'v2',
  steps: [
    {
      apply_expr: {
        field: 'count',
        expr: 'add count 1',
        inverse: 'sub count 1',
        coercion: 'iso',
      },
    },
  ],
};

using chain = p.compileLensDocument(document, 'record:body');
using lens = chain.instantiate(sourceSchema);

const { view, complement } = lens.getJson(
  { count: 4 },
  'record:body',
);
const restored = lens.putJson(view, complement, 'record:body');

apply_expr evaluates its expression with the named field bound in the expression environment. An inverse is required only when edits must propagate backward through the transform. Declare coercion: 'iso' only when the forward and inverse expressions round-trip for every accepted value.

Compute a field from its parent record

compute_field evaluates an expression over the scalar fields in the parent fiber and writes the result under target:

const document = {
  id: 'dev.example.double-count',
  source: 'v1',
  target: 'v2',
  steps: [
    {
      compute_field: {
        target: 'double_count',
        expr: 'mul count 2',
        coercion: 'projection',
      },
    },
  ],
};

using chain = p.compileLensDocument(document, 'record:body');
using lens = chain.instantiate(sourceSchema);
const { view, complement } = lens.getJson(
  { count: 4 },
  'record:body',
);

The computed field is derived data. With no inverse, putJson uses the complement to restore the original source fields; edits made only to double_count do not determine a new count.

Verify that compilation retained the transform

const transforms = chain.fieldTransforms();
if ((transforms['record:body'] ?? []).length === 0) {
  throw new Error('field transform was not compiled');
}

const laws = lens.checkLaws(instanceBytes);
if (!laws.holds) throw new Error(laws.violation ?? 'lens law failed');

fieldTransforms() reports transforms by parent vertex. checkLaws accepts an encoded instance, while getJson and putJson provide the record-oriented path shown above.

Current serialization limitation

Value transforms are stored beside the structural ProtolensChain; they are not part of chain.toJson(). Reconstructing a handle with ProtolensChainHandle.fromJson(chain.toJson(), wasm) thus loses them.

The same distinction affects other surfaces. schema lens compile reports only the number of field-transform vertices and writes the structural chain. Python’s ProtolensChain.from_dsl_* constructors, the TypeScript compileLensDocument handle, and Rust’s panproto_lens_dsl::CompiledLens retain the value-level programs and their order relative to structural steps.

Migration mapping JSON has a separate expr_resolvers field, but panproto_mig::compile does not install those expressions as FieldTransform values. Do not place expression source in a migration mapping and expect schema lift to execute it.

Common failures

  • Attach a transform to the parent vertex whose fields it reads. A transform anchored at a scalar child cannot see its siblings.
  • Keep expressions within the evaluator’s step, depth, and list-length budgets. Evaluation errors fail the transform for that record.
  • Classify lossy computations as projection or opaque, not iso.
  • Test boundary values for partial operations such as division, parsing, head, and indexing.

See also