Hooks (Calculated Field)
Hooks
is a feature in Tailor Platform that offers functionality similar to calculated fields in traditional relational databases. When a record is created or modified, Hooks automatically update specified field values based on CEL Script expressions.
These expressions serve several purposes:
- Performing calculations:
You can perform calculations using other fields within the same record. For example, a field like totalPrice
can be configured to automatically calculate the product of price
and quantity
fields.
- Adding user context:
Hooks allow the addition of current user context to the record. For instance, a field like createdById
can be populated with the user.id
.
Additionally, you can utilize user attributes, an array of UUIDs configured in the AttributesFields
in the Auth service. A typical use case for user.attributes
involves validation. Refer to this example.
Furthermore, the field updates itself whenever a new record is created or an existing one is updated. This ensures data consistency without manual recalculations, similar to calculated fields in a database, and helps you avoid writing complex logic in Pipeline.
Events
The create
and update
events are supported as a trigger to run the script.
CreateExpr
: Triggered when a new record is created.UpdateExpr
: Triggered when the target record is updated.
Examples
- Given an Order model, this is how you could for instance compute the total price of an item:
<span><span style="color: var(--shiki-color-text)">price: {</span></span>
<span><span style="color: var(--shiki-color-text)"> Type: tailordb.#TypeInt</span></span>
<span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">Unit price of a certain product</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)">}</span></span>
<span><span style="color: var(--shiki-color-text)">quantity: {</span></span>
<span><span style="color: var(--shiki-color-text)"> Type: tailordb.#TypeInt</span></span>
<span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">Order quantity of a certain product</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)">}</span></span>
<span><span style="color: var(--shiki-color-text)">totalPrice: {</span></span>
<span><span style="color: var(--shiki-color-text)"> Type: tailordb.#TypeInt</span></span>
<span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">Total price of a certain product</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)"> Hooks: {</span></span>
<span><span style="color: var(--shiki-color-text)"> CreateExpr: </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> _value.price * _value.quantity</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-color-text)"> UpdateExpr: </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> _value.price * _value.quantity</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-color-text)"> }</span></span>
<span><span style="color: var(--shiki-color-text)">}</span></span>
<span></span>
With such hooks in place, the totalPrice field will be computed and stored whenever the order is created or changed.
- Here's how to add user context to the Supplier model
<span><span style="color: var(--shiki-color-text)">createdById: {</span></span>
<span><span style="color: var(--shiki-color-text)"> Type: tailordb.#TypeUUID</span></span>
<span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">User ID of the logged in user</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)"> Hooks: {</span></span>
<span><span style="color: var(--shiki-color-text)"> CreateExpr: </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> user.id</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-color-text)"> }</span></span>
<span><span style="color: var(--shiki-color-text)">}</span></span>
<span></span>
- Example demonstrating how to set a default value to a field
<span><span style="color: var(--shiki-color-text)">quantity: {</span></span>
<span><span style="color: var(--shiki-color-text)"> Type: tailordb.#TypeInt</span></span>
<span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">Order quantity of a certain product</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)"> Hooks: {</span></span>
<span><span style="color: var(--shiki-color-text)"> CreateExpr: </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> _value != null ? _value : 2</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-color-text)"> }</span></span>
<span><span style="color: var(--shiki-color-text)">}</span></span>
<span></span>
Evaluation order
User input
Hooks run after input evaluation, having the consequence that any passed value may be overwritten by a hook's result, depending on the type of the hooks.
Example
<span><span style="color: var(--shiki-color-text)">price: {</span></span>
<span><span style="color: var(--shiki-color-text)"> Type: tailordb.#TypeInt</span></span>
<span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">Unit price of a certain product</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)">}</span></span>
<span><span style="color: var(--shiki-color-text)">quantity: {</span></span>
<span><span style="color: var(--shiki-color-text)"> Type: tailordb.#TypeInt</span></span>
<span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">Order quantity of a certain product</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)">}</span></span>
<span><span style="color: var(--shiki-color-text)">totalPrice: {</span></span>
<span><span style="color: var(--shiki-color-text)"> Type: tailordb.#TypeInt</span></span>
<span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">Total price of a certain product</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)"> Hooks: {</span></span>
<span><span style="color: var(--shiki-color-text)"> CreateExpr: </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> _value.price * _value.quantity</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-color-text)"> UpdateExpr: </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> _value.price * _value.quantity</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-color-text)"> }</span></span>
<span><span style="color: var(--shiki-color-text)">}</span></span>
<span></span>
When passing totalPrice = 100
, price = 5
, and quantity = 10
as inputs, 50
is stored on the totalPrice
field.
Validate vs Hooks
Validations are run after hooks.
Given an Order model with the following fields:
<span><span style="color: var(--shiki-color-text)">price: {</span></span>
<span><span style="color: var(--shiki-color-text)"> Type: tailordb.#TypeInt</span></span>
<span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">Unit price of a certain product</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)">}</span></span>
<span><span style="color: var(--shiki-color-text)">quantity: {</span></span>
<span><span style="color: var(--shiki-color-text)"> Type: tailordb.#TypeInt</span></span>
<span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">Order quantity of a certain product</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)">}</span></span>
<span><span style="color: var(--shiki-color-text)">totalPrice: {</span></span>
<span><span style="color: var(--shiki-color-text)"> Type: tailordb.#TypeInt</span></span>
<span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">Total price of a certain product</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)"> Hooks: {</span></span>
<span><span style="color: var(--shiki-color-text)"> CreateExpr: </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> _value.price * _value.quantity</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-color-text)"> UpdateExpr: </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> _value.price * _value.quantity</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-color-text)"> }</span></span>
<span><span style="color: var(--shiki-color-text)"> Validate: [</span></span>
<span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-comment)">// totalPrice value must be less than 100</span></span>
<span><span style="color: var(--shiki-color-text)"> {</span></span>
<span><span style="color: var(--shiki-color-text)"> Expr: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">_value < 100</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)"> Action: tailordb.#Permit.Deny</span></span>
<span><span style="color: var(--shiki-color-text)"> }</span></span>
<span><span style="color: var(--shiki-color-text)"> ]</span></span>
<span><span style="color: var(--shiki-color-text)">}</span></span>
<span></span>
Passing an initial price and quantity resulting in a total price lesser than "100" will successfully create the record and store the computed total price. However, if the total price exceeds "100", the validation will fail and the record won't be created.
Advanced use
createdAt, updatedAt field
The datetime each record was created and updated can be stored as follows
<span><span style="color: var(--shiki-color-text)">createdAt: {</span></span>
<span><span style="color: var(--shiki-color-text)"> Type: tailordb.#TypeDateTime</span></span>
<span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">The time when this record is created</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)"> Hooks: {</span></span>
<span><span style="color: var(--shiki-color-text)"> CreateExpr: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">now()</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)"> }</span></span>
<span><span style="color: var(--shiki-color-text)">}</span></span>
<span><span style="color: var(--shiki-color-text)">updatedAt: {</span></span>
<span><span style="color: var(--shiki-color-text)"> Type: tailordb.#TypeDateTime</span></span>
<span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">The time when this record is updated</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)"> Hooks: {</span></span>
<span><span style="color: var(--shiki-color-text)"> UpdateExpr: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">now()</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)"> }</span></span>
<span><span style="color: var(--shiki-color-text)">}</span></span>
<span></span>
In this example, the createdAt
field is evaluated only on create events, and the updatedAt
field is evaluated only on update events.
When the record is created, the current datetime value is stored in the createdAt
field, but not in the updatedAt
field.
When the record is updated, the value in the createdAt
field will remain unchanged, while the updatedAt
field will be updated with the current datetime.
Conditional default values
The default value of a field can be set in two ways: using the defaultValue property or using hooks.
If the default value should be set based on dynamic conditions, Hooks is the recommended method to define the default value.
In this example, the default value of the field price
is determined by the value of the type
field.
<span><span style="color: var(--shiki-color-text)">type: {</span></span>
<span><span style="color: var(--shiki-color-text)"> Type: tailordb.#TypeEnum</span></span>
<span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">Item category</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)"> AllowedValues: [</span></span>
<span><span style="color: var(--shiki-color-text)"> { Value: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">ITEMA</span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">Item A</span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-color-text)"> }</span><span style="color: var(--shiki-token-punctuation)">,</span></span>
<span><span style="color: var(--shiki-color-text)"> { Value: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">ITEMB</span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">Item B</span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-color-text)"> }</span></span>
<span><span style="color: var(--shiki-color-text)"> ]</span></span>
<span><span style="color: var(--shiki-color-text)">}</span></span>
<span><span style="color: var(--shiki-color-text)">price: {</span></span>
<span><span style="color: var(--shiki-color-text)"> Type: tailordb.#TypeInt</span></span>
<span><span style="color: var(--shiki-color-text)"> Description: </span><span style="color: var(--shiki-color-text)">"</span><span style="color: var(--shiki-token-string-expression)">Unit price of a certain product</span><span style="color: var(--shiki-color-text)">"</span></span>
<span><span style="color: var(--shiki-color-text)"> Hooks: {</span></span>
<span><span style="color: var(--shiki-color-text)"> CreateExpr: </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> _value.type == "ITEMA" ? 100 : null</span></span>
<span><span style="color: var(--shiki-token-string-expression)"> </span><span style="color: var(--shiki-color-text)">"""</span></span>
<span><span style="color: var(--shiki-color-text)"> }</span></span>
<span><span style="color: var(--shiki-color-text)">}</span></span>
<span></span>
In this example, the price
field is evaluated only on create events, and if the type is ITEMA
, then the value 100
is applied to the price
field.
However, if the type is ITEMB
, no value is applied to the price
field.
Additionally, on the update event, regardless of whether the type is ITEMA
or ITEMB
, no value is applied.