Helpers
Introduction
Bunyad ships small helpers you call from anywhere in the application. Most live in @bunyad/common. Import what you need, or rely on the globals that package installs (collect, dd, dump, blank, filled, env, and the other miscellaneous helpers listed below).
import { Arr, blank, collect, dataGet, tap } from "@bunyad/common";
const name = dataGet(user, "profile.name", "Guest");URL helpers such as route, url, and asset are documented under URL Generation. Path helpers, number formatters, and framework-wide facades (config, abort, view) live with their packages; this page covers the support helpers from @bunyad/common.
Available methods
Arrays and objects
Arr.accessible · Arr.add · Arr.collapse · Arr.dot · Arr.except · Arr.exists · Arr.first · Arr.flatten · Arr.forget · Arr.get · Arr.has · Arr.isAssoc · Arr.isList · Arr.join · Arr.last · Arr.map · Arr.only · Arr.pluck · Arr.prepend · Arr.pull · Arr.query · Arr.random · Arr.set · Arr.undot · Arr.where · Arr.whereNotNull · Arr.wrap · dataFill · dataForget · dataGet · dataSet
Miscellaneous
blank · collect · dd · defer · dump · env · filled · flushDeferred · flushOnce · now · once · optional · report · rescue · rescueAsync · retry · tap · throw_if · throw_unless · today · value · when · withValue
Also on this page: Fluent, Pipeline, and Crypt.
Arrays and objects
Import Arr from @bunyad/common. Methods are static on the object.
`Arr.accessible()`
Determine whether the value is an array or a plain object:
import { Arr } from "@bunyad/common";
Arr.accessible(["a"]); // true
Arr.accessible({ a: 1 }); // true
Arr.accessible("a"); // false`Arr.add()`
Set a value by key only when the key is missing (supports dotted paths via dataSet):
const user = { name: "Ada" };
Arr.add(user, "role", "admin");
Arr.add(user, "name", "Ignored"); // unchanged`Arr.collapse()`
Flatten an array of arrays one level:
Arr.collapse([
[1, 2],
[3, 4],
]); // [1, 2, 3, 4]`Arr.dot()`
Flatten a nested object into dotted keys:
Arr.dot({ user: { name: "Ada", meta: { active: true } } });
// { "user.name": "Ada", "user.meta.active": true }`Arr.except()`
Return a shallow copy without the given keys:
Arr.except({ id: 1, name: "Ada", password: "secret" }, ["password"]);
// { id: 1, name: "Ada" }`Arr.exists()`
Check whether an array index or object key exists (own property):
Arr.exists(["a", "b"], 1); // true
Arr.exists({ name: "Ada" }, "name"); // true`Arr.first()`
Arr.first([1, 2, 3]); // 1
Arr.first([1, 2, 3], (n) => n > 1); // 2
Arr.first([], undefined, 0); // 0`Arr.flatten()`
Arr.flatten([1, [2, [3]]]); // [1, 2, 3]
Arr.flatten([1, [2, [3]]], 1); // [1, 2, [3]]`Arr.forget()`
Remove one or more dotted keys in place:
const data = { user: { name: "Ada", role: "admin" } };
Arr.forget(data, "user.role");`Arr.get()`
Read a value with optional default. Uses dotted paths:
Arr.get({ products: [{ name: "Desk" }] }, "products.0.name"); // "Desk"
Arr.get({}, "missing", "default");`Arr.has()`
Return true when every given dotted path is present (not undefined):
Arr.has({ a: { b: 1 } }, "a.b"); // true
Arr.has({ a: 1 }, ["a", "b"]); // false`Arr.isAssoc()`
Arr.isAssoc({ a: 1 }); // true
Arr.isAssoc([1, 2, 3]); // false`Arr.isList()`
Arr.isList([1, 2, 3]); // true
Arr.isList({ a: 1 }); // false`Arr.join()`
Arr.join(["a", "b", "c"], ", "); // "a, b, c"
Arr.join(["a", "b", "c"], ", ", ", and "); // "a, b, and c"`Arr.last()`
Arr.last([1, 2, 3]); // 3
Arr.last([1, 2, 3], (n) => n < 3); // 2`Arr.map()`
Map an array or the values of an object:
Arr.map([1, 2], (n) => n * 2); // [2, 4]
Arr.map({ a: 1, b: 2 }, (v, k) => `${k}:${v}`); // ["a:1", "b:2"]`Arr.only()`
Arr.only({ id: 1, name: "Ada", role: "admin" }, ["id", "name"]);
// { id: 1, name: "Ada" }`Arr.pluck()`
const rows = [
{ name: "Desk", price: 200 },
{ name: "Chair", price: 100 },
];
Arr.pluck(rows, "name"); // ["Desk", "Chair"]
Arr.pluck(rows, "price", "name"); // { Desk: 200, Chair: 100 }`Arr.prepend()`
Arr.prepend([1, 2], 0); // [0, 1, 2]
Arr.prepend([1, 2], "Ada", "name"); // { name: "Ada", "0": 1, "1": 2 }`Arr.pull()`
Get a value and remove it:
const data = { name: "Ada", role: "admin" };
Arr.pull(data, "role"); // "admin"`Arr.query()`
Build a URL query string from an object:
Arr.query({ search: "desk", tags: ["wood", "oak"] });`Arr.random()`
Arr.random([1, 2, 3, 4]); // one item
Arr.random([1, 2, 3, 4], 2); // two items`Arr.set()`
Set a nested value by dotted path (mutates and returns the target):
const data = {};
Arr.set(data, "user.name", "Ada");`Arr.undot()`
Expand dotted keys into a nested object:
Arr.undot({ "user.name": "Ada", "user.role": "admin" });
// { user: { name: "Ada", role: "admin" } }`Arr.where()`
Arr.where([1, 2, 3, 4], (n) => n % 2 === 0); // [2, 4]`Arr.whereNotNull()`
Arr.whereNotNull([1, null, 2, undefined]); // [1, 2]`Arr.wrap()`
Arr.wrap("a"); // ["a"]
Arr.wrap(["a"]); // ["a"]
Arr.wrap(null); // []Nested data helpers
These functions work on objects and arrays with dotted paths. Wildcards (*) are supported on dataGet / dataSet where the segment is a list.
`dataGet()`
import { dataGet } from "@bunyad/common";
const user = {
profile: { name: "Ada" },
posts: [{ title: "One" }, { title: "Two" }],
};
dataGet(user, "profile.name"); // "Ada"
dataGet(user, "posts.*.title"); // ["One", "Two"]
dataGet(user, "missing", "default");If the default is a function, it is called when the path is missing.
`dataSet()`
import { dataSet } from "@bunyad/common";
const payload: Record<string, unknown> = {};
dataSet(payload, "user.profile.name", "Ada");Pass overwrite: false as the fourth argument to leave an existing value alone.
`dataFill()`
Like dataSet, but only writes when the path is missing:
import { dataFill } from "@bunyad/common";
const payload = { name: "Ada" };
dataFill(payload, "name", "Ignored");
dataFill(payload, "role", "admin");`dataForget()`
import { dataForget } from "@bunyad/common";
const payload = { user: { name: "Ada", role: "admin" } };
dataForget(payload, "user.role");Miscellaneous
Unless noted, these functions are available as named exports from @bunyad/common and as globals after that package loads.
`blank()` / `filled()`
blank is true for null, undefined, empty / whitespace strings, empty arrays, empty collections, and empty Map / Set. 0 and false are not blank. filled is the inverse:
blank(null); // true
blank(""); // true
blank(0); // false
filled("Ada"); // true`collect()`
Create a Collection:
collect([1, 2, 3]).sum();`dd()` / `dump()`
dump prints values with util.inspect and continues. dd dumps and then stops: it throws DdException inside an HTTP request (the kernel can render an HTML dump page), or calls process.exit(1) in a normal CLI process.
dump(user, orders);
dd(request.all());Use useDdThrow(true) or runWithDdThrow(() => …) in tests so dd throws instead of exiting. HTTP apps enable dump-page mode with enableHttpDd() / runWithHttpDd().
`env()`
Read an environment variable from Bun.env (or process.env). Empty string is treated as missing:
env("APP_NAME", "Bunyad");Prefer reading config for application settings; use env inside config files.
`now()` / `today()`
now(); // current Date
today(); // local midnight`once()` / `flushOnce()`
Memoize a callback. Prefer a string key on Bun (call-site stacks are not a reliable identity). A function-only form memoizes by function identity — hoist the closure if you need reuse:
const token = once("app-token", () => crypto.randomUUID());
const again = once("app-token", () => crypto.randomUUID()); // same value
flushOnce(); // clears keyed memoization`optional()`
Null-safe access. Without a callback, a missing value becomes a proxy that returns further proxies / nullish primitives. With a callback, the callback runs only when the value is present:
optional(user.address)?.street;
optional(user, (u) => u.name); // null when user is null`report()`
Log an error to stderr (and leave room for a custom handler later):
try {
// …
} catch (error) {
report(error);
}`rescue()` / `rescueAsync()`
Run a callback and return a fallback when it throws. Exceptions are reported unless you pass shouldReport = false:
const value = rescue(() => JSON.parse(raw), null);
const value = await rescueAsync(async () => fetchUser(id), null);`retry()`
Retry an async callback. Optional sleep (milliseconds or a function of the attempt) and a when predicate:
const result = await retry(
3,
async (attempt) => fetchThing(attempt),
100,
(error) => error instanceof TypeError,
);`tap()`
Run a side-effect callback and return the original value:
return tap(user, (u) => {
logger.info(u.id);
});`throw_if()` / `throw_unless()`
throw_if(!user, "User required.");
throw_unless(user.active, new Error("Inactive"));
throw_if(failed, DomainError, "code");`value()`
Invoke a function argument; otherwise return the value as-is:
value(5); // 5
value(() => 5); // 5`when()`
If the condition is truthy, return the value (invoking it when it is a function). Otherwise return the optional default:
when(user.isAdmin, "admin", "user");
when(flag, () => compute(), () => fallback());`withValue()`
Pass a value into a callback and return the callback’s result (or the value when no callback is given). Named withValue because with is reserved in JavaScript:
withValue(user, (u) => u.name.toUpperCase());`defer()` / `flushDeferred()`
Queue work to run after the current turn. The HTTP kernel calls flushDeferred() after the response. Mark a job with .always() so it still runs when the request failed:
defer(async () => {
await indexSearch(user);
}).always();
await flushDeferred();
await flushDeferred({ failed: true }); // skips non-always jobsTo run several independent tasks after the response, wrap them in Promise.all inside defer — see Concurrency.
Fluent
Fluent is a small attribute bag with typed readers and conditional helpers:
import { Fluent } from "@bunyad/common";
const input = Fluent.make({
name: "Ada",
age: "36",
tags: ["admin"],
});
input.string("name");
input.integer("age");
input.array("tags");
input.boolean("active", false);
input.only("name", "age");
input.whenHas("name", (f) => {
console.log(f.str("name"));
});Useful methods include get / set / fill, has / missing / filled, collect, enum / enums, scope, when / unless, and JSON helpers (toArray, toJson, toPrettyJson).
Pipeline
Pass a value through a series of pipes, then a destination. Pipes may be functions (passable, next) => …, objects with a handle method, or classes:
import { Pipeline } from "@bunyad/common";
const result = await Pipeline.send(user)
.through([trimName, ensureActive])
.then((u) => u);
await Pipeline.send(order)
.pipe(ValidateOrder)
.via("handle")
.finally((o) => console.log(o.id))
.thenReturn();when / unless on the pipeline instance conditionally register more pipes. thenReturn() runs the stack and returns the passable.
Crypt
Crypt encrypts and decrypts strings with AES-256-GCM using APP_KEY:
import { Crypt } from "@bunyad/common";
const payload = Crypt.encrypt("secret");
const plain = Crypt.decrypt(payload);Generate a key with Crypt.generateKey() and set APP_KEY before using encryption in production. Tests may call Crypt.setKey(...).