|
| 1 | +## Function transform |
| 2 | + |
| 3 | +Function transforms allow you to write custom [Elixir](https://elixir-lang.org/) code to transform your messages. This is useful for more complex transformations that are not possible with the path transform, such as: |
| 4 | + |
| 5 | +- Specifying a format that is necessary for your sink destination |
| 6 | +- Sanitizing sensitive data, such as PII or payment card data |
| 7 | +- Converting timestamp formats |
| 8 | +- Adding computed fields |
| 9 | +- And much more! |
| 10 | + |
| 11 | +### Function syntax |
| 12 | + |
| 13 | +Every function transform is implemented as an Elixir `transform/4` function: |
| 14 | + |
| 15 | +```Elixir |
| 16 | +def transform(action, record, changes, metadata) do |
| 17 | + # Your transform here |
| 18 | +end |
| 19 | +``` |
| 20 | + |
| 21 | +The `transform/4` function receives each key from the [message object](/reference/messages) as an argument: |
| 22 | + |
| 23 | +- `record`: The full record object |
| 24 | +- `changes`: The changes object |
| 25 | +- `action`: The action type (insert, update, delete) |
| 26 | +- `metadata`: The metadata object |
| 27 | + |
| 28 | +<Info> |
| 29 | + Your transform must define the `transform/4` function and may not define any other functions. |
| 30 | +</Info> |
| 31 | + |
| 32 | +### Elixir standard library |
| 33 | + |
| 34 | +The function transform allows you to use a subset of the Elixir standard library, including: |
| 35 | + |
| 36 | +- [String](https://hexdocs.pm/elixir/String.html) |
| 37 | +- [Map](https://hexdocs.pm/elixir/Map.html) |
| 38 | +- [Enum](https://hexdocs.pm/elixir/Enum.html) |
| 39 | +- [Date](https://hexdocs.pm/elixir/Date.html), [DateTime](https://hexdocs.pm/elixir/DateTime.html), and [NaiveDateTime](https://hexdocs.pm/elixir/NaiveDateTime.html) |
| 40 | +- [Kernel](https://hexdocs.pm/elixir/Kernel.html) |
| 41 | +- [Decimal](https://hexdocs.pm/decimal/readme.html) |
| 42 | +- [URI](https://hexdocs.pm/elixir/URI.html) |
| 43 | + |
| 44 | +Other parts of the Elixir standard library are not yet supported. If you need something specific, please [let us know](https://github.com/sequinstream/sequin/issues/new/choose) |
| 45 | +and we will be happy to help. |
| 46 | + |
| 47 | +### Examples |
| 48 | + |
| 49 | +Here are some examples of how to use the function transform: |
| 50 | + |
| 51 | +#### Extract the record ID |
| 52 | + |
| 53 | +```Elixir |
| 54 | +def transform(action, record, changes, metadata) do |
| 55 | + record["id"] |
| 56 | +end |
| 57 | +``` |
| 58 | + |
| 59 | +#### Format for webhook |
| 60 | + |
| 61 | +```Elixir |
| 62 | +# Format the record for ElastiCache with a specific key structure |
| 63 | +def transform(action, record, changes, metadata) do |
| 64 | + record_id = record["id"] |
| 65 | + |
| 66 | + %{ |
| 67 | + key: "#{metadata.table_schema}.#{metadata.table_name}.#{record_id}", |
| 68 | + value: record, |
| 69 | + ttl: 3600 # 1 hour TTL |
| 70 | + } |
| 71 | +end |
| 72 | +``` |
| 73 | + |
| 74 | +<Note> |
| 75 | + Both `record` and `changes` use string keys for access (ie. `record["id"]`). |
| 76 | + |
| 77 | + In contrast, the `metadata` object uses atom keys (ie. `metadata.table_schema`). |
| 78 | + |
| 79 | + This is because the `record` and `changes` objects are dynamically typed- they depend on the schema of the connected Postgres table. Metadata, on the other hand, is statically typed and will always have the same keys. |
| 80 | + |
| 81 | + See the [Elixir Map docs](https://hexdocs.pm/elixir/1.12/Map.html) for more information on the difference between string and atom keys. |
| 82 | +</Note> |
| 83 | + |
| 84 | +#### Sanitize sensitive data |
| 85 | + |
| 86 | +```Elixir |
| 87 | +# Remove or mask sensitive fields |
| 88 | +def transform(action, record, changes, metadata) do |
| 89 | + record |
| 90 | + |> Map.drop(["password", "credit_card", "ssn"]) |
| 91 | + |> Map.update!("email", fn email -> |
| 92 | + [name, domain] = String.split(email, "@") |
| 93 | + masked_name = String.slice(name, 0, 2) <> String.duplicate("*", String.length(name) - 2) |
| 94 | + masked_name <> "@" <> domain |
| 95 | + end) |
| 96 | +end |
| 97 | +``` |
| 98 | + |
| 99 | +#### Add computed property |
| 100 | + |
| 101 | +```Elixir |
| 102 | +# Add a computed full name field |
| 103 | +def transform(action, record, changes, metadata) do |
| 104 | + first_name = record["first_name"] |
| 105 | + last_name = record["last_name"] |
| 106 | + full_name = first_name <> " " <> last_name |
| 107 | + name_length = String.length(full_name) |
| 108 | + |
| 109 | + %{ |
| 110 | + full_name: full_name, |
| 111 | + name_length: name_length |
| 112 | + } |
| 113 | +end |
| 114 | +``` |
| 115 | + |
| 116 | +#### Convert timestamp formats |
| 117 | + |
| 118 | +```Elixir |
| 119 | +# Convert timestamp formats |
| 120 | +def transform(action, record, changes, metadata) do |
| 121 | + timestamp = record["timestamp"] |
| 122 | + |
| 123 | + %{ |
| 124 | + unix_timestamp: DateTime.to_unix(timestamp), |
| 125 | + truncated_timestamp: DateTime.truncate(timestamp, :second), |
| 126 | + iso8601_timestamp: DateTime.to_iso8601(timestamp) |
| 127 | + } |
| 128 | +end |
| 129 | +``` |
0 commit comments