CodamAIDocs
Topicdone

Field hooks

Reusable logic for a single value: store it encrypted, derive it, normalise it. What a field hook looks like, how you assign it to a field and when it runs.

Variants
before writing (beforeDatabaseChange)after reading (afterDatabaseRead)transforming hookderiving hook (runsOnEveryWrite)several hooks on one fieldPATCH, PUT with a field role, ROLLBACKnot searchable (keepsValueSearchable)

What this is about

A field hook is logic for a single value. It does not belong to a model but to a kind of value. That is why you can attach the same field hook to many fields in many models. Typical cases:

  • store a text encrypted and deliver it decrypted,
  • derive a value, such as total = price × quantity,
  • normalise a value, such as bringing a phone number into a fixed format.

The model hook sees the whole object and belongs to exactly one model. The field hook receives the value and returns the value to use instead.

Writing a field hook

A field hook implements HookFieldInterface<T>. T is the Java type of the field, so String for a text:

@Component
public class EncryptHook implements HookFieldInterface<String> {

  @Override
  public String beforeDatabaseChange(String value, FieldHookContext context) {
    return value == null ? null : cipher.encrypt(value);
  }

  @Override
  public String afterDatabaseRead(String value, FieldHookContext context) {
    return value == null ? null : cipher.decrypt(value);
  }
}

Both methods return a value, and CDMS puts it into the field. If you want to change nothing, return value unchanged.

The second parameter, the context (FieldHookContext), tells you where you are:

Methodreturns
item()the object with all its other fields
modelMeta()the description of the model
field(), fieldName()the description and the name of the field
method()the operation: CREATE, UPDATE, PATCH or READ

If the class is a Spring bean, for example with @Component, CDMS uses that bean. Otherwise CDMS creates the class itself through Spring. Either way it gets its dependencies through @Autowired or @Value, the key for example.

Assigning the field hook to a field

The assignment lives in the model, not in the code. In the hub you open the field and set the rule Field hook on the Rules tab. As its value you enter the fully qualified class name:

com.example.shop.hooks.EncryptHook

Separate several hooks with commas. They run in this order:

com.example.shop.hooks.CompressHook, com.example.shop.hooks.EncryptHook
  1. 1
    Modeller
    sets the rule Field hook (@hook) with the class name on the field
  2. 2
    Generator
    checks the name and writes the class into the description of the field
    A name without a package, such as EncryptHook, stops the generation. A class that does not exist in the project stops the compilation.
  3. 3
    CDMS
    creates every field hook at startup and checks that its type fits the field
    If it does not, the application does not start. The message names model, field and hook: field-hook-type-mismatch|Customer.phone -> …EncryptHook.
  4. 4
    Field hook
    runs from now on whenever this field is written and read

A field hook exists on simple fields only, that is text, number, date and enum fields. A relation gets no field hook; that is what the model hook is for.

The path of a value

sequenceDiagram
    participant C as Client
    participant D as CDMS
    participant H as Field hook
    participant DB as Database
    C->>D: POST code = "geheim"
    D->>D: model before hooks
    D->>H: beforeDatabaseChange("geheim")
    H-->>D: "x7Kq…"
    D->>DB: stores "x7Kq…"
    D->>DB: reads the object for the response
    D->>H: afterDatabaseRead("x7Kq…")
    H-->>D: "geheim"
    D->>D: model read hook
    D-->>C: 200, code = "geheim"

After writing, the field hook does not run. The response to a write is produced by reading, and that is where afterDatabaseRead runs.

When the hook runs before writing

Before writing, a field hook runs only for fields the operation writes. That way a stored value is never encrypted a second time.

OperationDoes beforeDatabaseChange run?
Create (POST)yes, for every field with a hook
Replace (PUT)yes, for every field with a hook
Change (PATCH)only for the fields the PATCH sends
PUT or PATCH that a field role refuses, keeping the stored valueno, the stored value stays as it is
a model before hook changes the fieldyes, this value passes the field hook too
Deleteno
Roll back to an old stateno, the old state is taken over as it was stored

The field hook runs after the model’s before hooks and before the rules are checked. A hook that derives a value can therefore also fill a mandatory field. If a field has several hooks, they run in the order from the model. When reading they run in reverse order: what was applied last is undone first.

A value that follows from others

A hook that derives a value must also run when only one of its sources changes. A PATCH with only price does not send total. That is why such a hook signs up for every write:

@Component
public class TotalHook implements HookFieldInterface<Integer> {

  @Override
  public Integer beforeDatabaseChange(Integer value, FieldHookContext context) {
    OrderEntity order = (OrderEntity) context.item();
    if (order.getPrice() == null || order.getQuantity() == null)
      return null;
    return order.getPrice() * order.getQuantity();
  }

  @Override
  public Integer afterDatabaseRead(Integer value, FieldHookContext context) {
    return value;
  }

  @Override
  public boolean runsOnEveryWrite() {
    return true;
  }
}

With runsOnEveryWrite() the hook runs on every create, replace and change of the object. It then also receives the stored value and ignores it. It does not run on delete or roll back.

Field roles and field hooks

A field role requires its role only for a change. If you send a record back as you read it, you need none. For that CDMS compares the sent value with the stored one as a reader sees it, that is after afterDatabaseRead. An encrypted field you send back unchanged therefore does not count as changed.

Decision table

Model hook or field hook?
What you want to doHook
transform a single value, such as encrypting itfield hook
derive a value from other fields of the same objectfield hook with runsOnEveryWrite()
the same logic on many fields in many modelsfield hook
check a rule that concerns several fields together, and refusemodel hook
maintain relations or write other recordsmodel hook
do something on a relationmodel hook, field hooks exist on simple fields only

Pitfalls

Where to go next

Sources in the code and the knowledge base
  • commons – global/commons/interfaces/HookFieldInterface, FieldHookContext; meta/MetaFieldInfo (hooks)
  • CDMS/cdms-system-layer – HookManagementSystem (callHooksBeforeDatabase, callHooksAfterDatabase, readValue, checkFits, createFieldHook, afterSingletonsInstantiated); session/HookRequestContext (addFieldWritten); AbstractLayer (markWritten, guardedValue)
  • CDMS/cdms-generator – CdmsYamlLoader (rule hook, rules.hooks), DtoMetaProcessor.fieldHooks
  • CDMS/frontend – shared/utils/cdmsFieldRules (HOOK_RULE)
  • CDMS/cdms-integrationtest – AbstractFieldHookTest, model secret, fieldhook/MarkHook, ZipHook, TotalHook
  • CDMS/cdms-system-layer – docs/adr/adr-017-feld-hooks-und-hook-verwaltung-ohne-profil.md
  • documentation/60-erweiterung/01-hooks.md
Search