On this page

On this page

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 jobs

To 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(...).