Skip to content

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 your expressions.

These expressions serve several purposes:

  1. 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.

  1. 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.

Hooks Properties

The following table shows all available hook properties and their scripting language support:

PropertyScripting LanguageTrigger EventDescription
createJavaScriptRecord creationTriggered when a new record is created. You can use _value to refer to the current field value and _data to refer to the entire record
updateJavaScriptRecord updateTriggered when the target record is updated. You can use _value to refer to the current field value and _data to refer to the entire record
create_exprCELRecord creationTriggered when a new record is created. You can use _value to refer to the current field value and _value.abc to refer to the specific field
update_exprCELRecord updateTriggered when the target record is updated. You can use _value to refer to the current field value and _value.abc to refer to the specific field

Examples

JavaScript

js
hooks = {  // if shopifyID is null, set status to 'awaiting_payment', else set status to the current value
  create = "_data.shopifyID == null ? 'awaiting_payment' : _value"
}

CEL

js
hooks = {  // if shopifyID is null, set status to 'awaiting_payment', else set status to the current value
  create_expr = "_value.shopifyID == null ? 'awaiting_payment' : '_value'"
}

Compute the total price of an item

typescript
import { db } from "@tailor-platform/sdk";

export const order = db.type("Order", {
  price: db.int().description("Unit price of a certain product"),
  quantity: db.int().description("Order quantity of a certain product"),
  totalPrice: db
    .int()
    .description("Total price of a certain product")
    .hooks({
      create: ({ data }) => data.price * data.quantity,
      update: ({ data }) => data.price * data.quantity,
    }),
});
sh
  price = {
    type        = "integer"
    description = "Unit price of a certain product"
  }
  quantity = {
    type        = "integer"
    description = "Order quantity of a certain product"
  }
  totalPrice = {
    type        = "integer"
    description = "Total price of a certain product"
    hooks = {
      create = "_data.price * _data.quantity"
      update = "_data.price * _data.quantity"
    }
  }

With such hooks in place, the totalPrice field will be computed and stored whenever the order is created or changed.

Add user context to the Supplier model

typescript
import { db } from "@tailor-platform/sdk";

export const supplier = db.type("Supplier", {
  createdById: db
    .uuid()
    .description("User ID of the logged in user")
    .hooks({
      create: ({ user }) => user.id,
    }),
});
sh
  createdById = {
    type        = "uuid"
    description = "User ID of the logged in user"
    hooks = {
      create = "user.id"
    }
  }

Set a default value to a field

typescript
import { db } from "@tailor-platform/sdk";

export const order = db.type("Order", {
  quantity: db
    .int()
    .description("Order quantity of a certain product")
    .hooks({
      create: ({ value }) => value ?? 2,
    }),
});
sh
  quantity: {
    type        = "integer"
    description = "Order quantity of a certain product"
    hooks = {
      create = "_value != null ? _value : 2"
    }
  }

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

typescript
import { db } from "@tailor-platform/sdk";

export const order = db.type("Order", {
  price: db.int().description("Unit price of a certain product"),
  quantity: db.int().description("Order quantity of a certain product"),
  totalPrice: db
    .int()
    .description("Total price of a certain product")
    .hooks({
      create: ({ data }) => data.price * data.quantity,
      update: ({ data }) => data.price * data.quantity,
    }),
});
sh
  price = {
    type        = "integer"
    description = "Unit price of a certain product"
  }
  quantity = {
    type        = "integer"
    description = "Order quantity of a certain product"
  }
  totalPrice = {
    type        = "integer"
    description = "Total price of a certain product"
    hooks = {
      create = "_data.price * _data.quantity"
      update = "_data.price * _data.quantity"
    }
  }

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.

For an Order model, if price * quantity is less than 100, the record is created and the computed total price is stored. If the total exceeds 100, validation fails and the record is not created.

typescript
import { db } from "@tailor-platform/sdk";

export const order = db.type("Order", {
  price: db.int().description("Unit price of a certain product"),
  quantity: db.int().description("Order quantity of a certain product"),
  totalPrice: db
    .int()
    .description("Total price of a certain product")
    .hooks({
      create: ({ data }) => data.price * data.quantity,
      update: ({ data }) => data.price * data.quantity,
    })
    .validate([({ value }) => value >= 100, "totalPrice value must be less than 100"]),
});
sh
  price = {
    type        = "integer"
    description = "Unit price of a certain product"
  }
  quantity = {
    type        = "integer"
    description = "Order quantity of a certain product"
  }
  totalPrice = {
    type        = "integer"
    description = "Total price of a certain product"
    hooks = {
      create = "_data.price * _data.quantity"
      update = "_data.price * _data.quantity"
    }
    validate = [
      {
        expr   = "_value < 100"
        action = "deny"
      }
    ]
  }

Advanced usage

createdAt, updatedAt field

The datetime each record was created and updated can be stored as follows

typescript
import { db } from "@tailor-platform/sdk";

export const order = db.type("Order", {
  createdAt: db
    .datetime()
    .description("The time when this record is created")
    .hooks({
      create: () => new Date(),
    }),
  updatedAt: db
    .datetime()
    .description("The time when this record is updated")
    .hooks({
      update: () => new Date(),
    }),
});

Or use the built-in timestamps helper:

typescript
import { db } from "@tailor-platform/sdk";

export const order = db.type("Order", {
  ...db.fields.timestamps(),
});
sh
  createdAt = {
    type        = "datetime"
    description = "The time when this record is created"
    hooks = {
      create = "(new Date()).toISOString()"
    }
  }
  updatedAt = {
    type        = "datetime"
    description = "The time when this record is updated"
    hooks = {
      update = "(new Date()).toISOString()"
    }
  }

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

Hooks can be used to set the default value of a field, especially when it depends on dynamic conditions.

In this example, the default value of the field price is determined by the value of the type field.

typescript
import { db } from "@tailor-platform/sdk";

export const order = db.type("Order", {
  type: db
    .enum([
      { value: "ITEMA", description: "Item A" },
      { value: "ITEMB", description: "Item B" },
    ])
    .description("Item category"),
  price: db
    .int()
    .description("Unit price of a certain product")
    .hooks({
      create: ({ data }) => (data.type === "ITEMA" ? 100 : null),
    }),
});
sh
  type = {
    type        = "enum"
    description = "Item category"
    allowed_values : [
      {
        value       = "ITEMA"
        description = "Item A"
      },
      {
        value       = "ITEMB"
        description = "Item B"
      }
    ]
  }
  price = {
    type        = "integer"
    description = "Unit price of a certain product"
    hooks = {
      create = "_data.type == 'ITEMA' ? 100 : null"
    }
  }

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.