Helpers
Helpers
Section titled “Helpers”The @framework/support package provides small, typed helpers inspired by Laravel’s helpers. Unlike Laravel they are imported functions, not globals — and the context-aware ones resolve the current request or job through the framework’s runtime, so there is nothing to pass by hand:
import { config, env, abort, logger, correlationId } from '@framework/support';Generated applications already depend on
@framework/support. Inside requests and jobs the context helpers see the real actor and correlation id; anywhere else they fall back to a standalone default.
Laravel parity
Section titled “Laravel parity”| Laravel | This framework | Notes |
|---|---|---|
config('app.name') |
config('app.name') |
Reads the validated, frozen config; throws before boot |
env('REDIS_URL') |
env('REDIS_URL', 'redis://…') |
Process env always wins over files |
abort(404) / abort_if() |
abort(404) / abort_if(cond, 403) |
Throws the catalogued HttpError envelope |
logger() |
logger('orders').info(…) |
JSON lines with correlation id + actor |
retry() |
retry(work, { attempts, backoffMs }) |
Exponential backoff (queues use defineJob) |
now() / Str::uuid() |
nowIso() / uuid() |
ISO-8601 and RFC 4122 v4 |
tap() / value() |
tap() / value() |
Same semantics |
| Auth user | currentActor() / execution() |
Scope-aware; anonymous outside requests/jobs |
Configuration
Section titled “Configuration”config(path, fallback?)
Section titled “config(path, fallback?)”Dot-path access into the booted application’s validated config — the same frozen object the container exposes as CONFIG:
config('http.port'); // 3000config('database.url'); // postgres://…config('app.debug', false); // with fallbackconfig('no.such.path', 'fallback'); // 'fallback'Throws RUNTIME_NOT_BOOTED when no Application was constructed — scripts should load config explicitly with loadAppConfig() instead.
env(name, fallback?)
Section titled “env(name, fallback?)”Process-environment lookup. Process env always wins over .env files (the framework’s documented precedence); an empty string counts as missing:
env('REDIS_URL'); // string | undefinedenv('QUEUE_NAME', 'default'); // stringContext
Section titled “Context”The HTTP kernel and the queue worker bind the current execution to the async chain, so these work anywhere below a request or a job:
| Helper | Returns |
|---|---|
execution() |
Full context: kind (request/job/command), correlationId, actor |
correlationId() |
The current request/job correlation id ('standalone' outside one) |
currentActor() |
{ id, type } of who is acting (anonymous outside a scope) |
await bus.publish(createDomainEvent('orders', 'created', payload, execution()));logger().info('reserving stock', { orderId }); // log lines carry correlationIdErrors
Section titled “Errors”abort(status | code, message?)
Section titled “abort(status | code, message?)”Throws the framework’s HttpError, which the error handler renders as the standard JSON envelope. Accepts an HTTP status or a catalog code:
abort(404); // NOT_FOUNDabort('VALIDATION_FAILED', 'bad input'); // 422 with a messageabort_if(condition, …) / abort_unless(condition, …)
Section titled “abort_if(condition, …) / abort_unless(condition, …)”Guard-clause variants:
abort_unless(actor.id === order.ownerId, 403);abort_if(order.status === 'shipped', 409, 'Shipped orders cannot change');Logging
Section titled “Logging”logger(scope?)
Section titled “logger(scope?)”One JSON object per line on stderr, carrying time, level, scope, message, the current correlationId and actorId — logs from one request (and the jobs it dispatched) grep together:
logger('orders').info('order created', { orderId: 'o-1' });// {"time":"…","level":"info","scope":"orders","message":"order created",// "correlationId":"0aca8…","actorId":"user-1","data":{"orderId":"o-1"}}Values must be JSON-safe. Redact secrets before logging them — pass objects through
redactForLogs(); CLI diagnostics redact automatically.
Typed config paths (autocomplete)
Section titled “Typed config paths (autocomplete)”The global config() takes any string. For editor autocomplete on every
path and exact return types, create a reader once per app with your
AppConfig type (generated apps ship this as app/config/helpers.ts):
import { createConfigReader } from '@framework/support';import type { AppConfig } from '../../config';
export const { get: config, getWithDefault: configOrDefault } = createConfigReader<AppConfig>();config('http.port'); // type: number — 'http.' suggests every known keyconfig('app.name'); // type: stringconfigOrDefault('owner', 'system'); // fallback keeps the NonNullable typeA wrong path or mismatched fallback is a compile-time error, and the runtime still reads the same validated config — only the types were added.
Utilities
Section titled “Utilities”| Helper | Purpose |
|---|---|
retry(work, { attempts, backoffMs }) |
Exponential-backoff retry for transient failures (queue semantics belong in defineJob) |
sleep(ms) |
Promise-based delay |
tap(value, fn) |
Run fn for its side effect, return value |
value(input) |
Resolve input or input() (lazy values) |
uuid() |
RFC 4122 v4 UUID |
nowIso() |
Current time as ISO-8601 (the framework’s JSON timestamp format) |
redactForLogs(obj) |
Mask secret-looking keys (apiKey, password, …) |
isSecret(key) |
Whether a key name looks like a secret |
const reachable = await retry(() => pingService(), { attempts: 3, backoffMs: 200 });const order = tap(await orders.find(id), (o) => logger().info('loaded', { id: o.id }));