Logic and expressions
Values in a plan are expressions: they only exist when the plan runs. So you use methods instead of operators, and $ helpers instead of if and for.
subtotal.plus(shipping).gte(100) // not subtotal + shipping >= 100
$.fmt`Hello ${user.name}` // not `Hello ${user.name}`Control flow
$.ensure(cond, error, details?) | Fail unless cond |
$.fail(error, details?) | Fail |
$.when(cond, () => { ... }) | Run when cond |
$.if(cond).then(() => ...).else(() => ...) | Branch |
$.forEach(list, (item) => { ... }) | Loop over a bounded list (may write) |
$.loop({ max, state, while }, (state) => next) | Loop that carries state from pass to pass (read-only) |
$.let(value) | Bind a value, so later steps share it |
$.select(cond, a, b) | Choose a value |
$.match(value, { a: ..., b: ..., _: ... }) | Choose by an enum or string (exhaustive, or with _) |
$.all(...), $.any(...) | Combine conditions |
$.return(value) | Respond now |
$.log(label, value) | Log for debugging |
Lists
Lists always have a known maximum length.
const lines = body.items.map((item) => ({ sku: item.sku, total: item.price.times(item.qty) }));
lines.sum((l) => l.total);
lines.count((l) => l.total.gt(100));
lines.some((l) => l.sku.eq("GIFT"));
lines.every((l) => l.total.gt(0));
lines.min((l) => l.total); lines.max((l) => l.total); // null when empty
lines.sortBy((l) => l.total, "desc").take(3);
tags.distinct(); tags.includes("urgent"); tags.concat(moreTags); list.slice(1, 4); list.first();map and filter bodies may read (a get inside map is a join) but never write. Use $.forEach to write.
Loops that carry state
When each pass depends on the one before (a running balance, walking a chain until it ends), use $.loop. It starts from state, checks while before each pass, and takes what the callback returns as the next state, for at most max passes (1-1000). It returns the final state.
a.route("POST /accounts/:accountId/charges")
.params({ accountId: t.id() })
.body({ amounts: t.array(t.int().min(1), 12) })
.errors({ missing: err.notFound("No such account") })
.handle(($, { params, body, errors }) => {
const account = Account.getOrFail({ accountId: params.accountId }, errors.missing);
// Each charge applies only if what the earlier ones left covers it.
const end = $.loop(
{ max: 12, state: { balance: account.balance, i: 0 }, while: (s) => s.i.lt(body.amounts.length) },
(s) => {
const charge = $.let(body.amounts.slice(s.i, s.i.plus(1)).first().ifNull(0));
return {
balance: $.select(s.balance.gte(charge), s.balance.minus(charge), s.balance),
i: s.i.plus(1),
};
},
);
return account.update({ balance: end.balance });
});- Usage: the checker counts the loop as
maxpasses. Here that's 12, so the route's worst case is known before it deploys. At run time it stops as soon aswhileis false. - State keeps its type. Every pass must return the same fields with the same types. A string or list that grows each pass has no bound and is rejected: bound it with
.truncate(n)or.take(n). Integers may change freely, so counters are fine. whileonly computes from the state. To stop on something you read, do the read in the body and carry the result in the state (for example, adonefield).- The body may read, but never write. Compute with
$.loop, then write the result.
$.let(value) binds a value to a variable. Use it when later code refers to a value more than once. Without it, an expression used twice is recorded twice, which matters when a loop builds on it pass after pass.
Strings
.lower(), .upper(), .trim(), .truncate(n), .slice(start, end), .replace(find, with), .padStart(n, "0"), .padEnd(n), .startsWith(), .endsWith(), .contains(), .matches(/^[a-z]+$/) (linear-time regex; its ops grow with the text's length), .length(), and .split(",", 10) (into a list of at most 10 parts).
Numbers
- Ints:
.plus(),.minus(),.times(),.div()(truncates),.divFloor(),.divCeil(),.mod(),.pow(),.abs(),.negate(),$.min(a, b),$.max(a, b). - Decimals (
t.decimal(2), or$.decimal("0.08")as a constant) mix with ints and stay exact:
const tax = price.times($.decimal("0.08")).round(2); // half-even by default
price.round(0, "up"); price.toInt("floor"); price.format({ decimals: 2, grouping: true }); // "1,234.50"Time
Timestamps are milliseconds. Calendar math takes an IANA time zone (UTC by default), and handles daylight-saving changes:
$.now().startOf("day", "Europe/Berlin");
order.placedAt.add(1, "week", order.tz);
order.placedAt.part("weekday", order.tz); // 1 = Monday
order.placedAt.formatTime("%Y-%m-%d %H:%M", order.tz);Null
Values that may be null propagate it through arithmetic and string functions. Use .isNull(), .isSet(), and .ifNull(fallback). See Types and null.