@zerotal/arch 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/api-surface.md +283 -0
- package/docs/about.md +676 -0
- package/docs/admin/actions.md +183 -0
- package/docs/admin/auth.md +89 -0
- package/docs/admin/dashboard.md +155 -0
- package/docs/admin/extending-ui.md +229 -0
- package/docs/admin/extending.md +205 -0
- package/docs/admin/forms.md +155 -0
- package/docs/admin/index.md +144 -0
- package/docs/admin/operations.md +224 -0
- package/docs/admin/references.md +174 -0
- package/docs/admin/resources.md +132 -0
- package/docs/admin/structure.md +158 -0
- package/docs/admin/tables.md +185 -0
- package/docs/admin/testing.md +138 -0
- package/docs/ai.md +373 -0
- package/docs/application.md +435 -0
- package/docs/arch.md +263 -0
- package/docs/assets.md +755 -0
- package/docs/audit.md +367 -0
- package/docs/authentication.md +1000 -0
- package/docs/authorization.md +529 -0
- package/docs/broadcasting/channels.md +170 -0
- package/docs/broadcasting/client.md +108 -0
- package/docs/broadcasting/events.md +190 -0
- package/docs/broadcasting/index.md +163 -0
- package/docs/broadcasting/references.md +84 -0
- package/docs/broadcasting/testing.md +132 -0
- package/docs/cache.md +387 -0
- package/docs/carbon.md +830 -0
- package/docs/changelog.md +465 -0
- package/docs/client/auth.md +113 -0
- package/docs/client/errors.md +139 -0
- package/docs/client/files.md +118 -0
- package/docs/client/index.md +128 -0
- package/docs/client/references.md +58 -0
- package/docs/client/requests.md +131 -0
- package/docs/client/resilience.md +141 -0
- package/docs/client/testing.md +146 -0
- package/docs/commands.md +450 -0
- package/docs/components.md +2427 -0
- package/docs/config-system.md +258 -0
- package/docs/container.md +596 -0
- package/docs/context.md +579 -0
- package/docs/contributing.md +164 -0
- package/docs/controllers.md +354 -0
- package/docs/conventions.md +466 -0
- package/docs/cookies.md +101 -0
- package/docs/csrf.md +206 -0
- package/docs/database.md +476 -0
- package/docs/deployment.md +434 -0
- package/docs/devtools.md +1013 -0
- package/docs/email-verification.md +320 -0
- package/docs/encryption.md +380 -0
- package/docs/errors.md +317 -0
- package/docs/events.md +420 -0
- package/docs/flow/components.md +846 -0
- package/docs/flow/decorators.md +458 -0
- package/docs/flow/events.md +420 -0
- package/docs/flow/forms.md +551 -0
- package/docs/flow/index.md +568 -0
- package/docs/flow/layouts.md +793 -0
- package/docs/flow/lifecycle.md +336 -0
- package/docs/flow/pagination.md +402 -0
- package/docs/flow/performance.md +262 -0
- package/docs/flow/references.md +317 -0
- package/docs/flow/routing.md +550 -0
- package/docs/flow/testing.md +447 -0
- package/docs/getting-started.md +286 -0
- package/docs/health.md +269 -0
- package/docs/helpers.md +423 -0
- package/docs/i18n.md +414 -0
- package/docs/index.md +65 -0
- package/docs/inertia/build.md +95 -0
- package/docs/inertia/devtools.md +197 -0
- package/docs/inertia/index.md +267 -0
- package/docs/inertia/middleware.md +81 -0
- package/docs/inertia/props.md +507 -0
- package/docs/inertia/references.md +144 -0
- package/docs/inertia/rendering.md +189 -0
- package/docs/inertia/ssr.md +102 -0
- package/docs/inspirations.md +257 -0
- package/docs/lifecycle.md +331 -0
- package/docs/lock.md +454 -0
- package/docs/logger.md +517 -0
- package/docs/media.md +582 -0
- package/docs/middleware.md +509 -0
- package/docs/migrations.md +529 -0
- package/docs/monitor.md +377 -0
- package/docs/notifications.md +848 -0
- package/docs/orm/casts.md +365 -0
- package/docs/orm/factories.md +231 -0
- package/docs/orm/index.md +663 -0
- package/docs/orm/lifecycle.md +421 -0
- package/docs/orm/queries.md +692 -0
- package/docs/orm/relationships.md +440 -0
- package/docs/orm/serialization.md +251 -0
- package/docs/package-development.md +536 -0
- package/docs/pagination.md +340 -0
- package/docs/password-reset.md +335 -0
- package/docs/providers.md +447 -0
- package/docs/query-builder.md +465 -0
- package/docs/queue.md +549 -0
- package/docs/rate-limiting.md +253 -0
- package/docs/responses.md +336 -0
- package/docs/roles-and-2fa.md +418 -0
- package/docs/routing.md +1008 -0
- package/docs/scaffolding.md +335 -0
- package/docs/scheduler.md +533 -0
- package/docs/seeding.md +229 -0
- package/docs/session.md +451 -0
- package/docs/social.md +524 -0
- package/docs/storage.md +622 -0
- package/docs/structure.md +230 -0
- package/docs/support-policy.md +114 -0
- package/docs/telemetry.md +505 -0
- package/docs/tenancy.md +608 -0
- package/docs/testing/browser.md +153 -0
- package/docs/testing/console.md +130 -0
- package/docs/testing/database.md +238 -0
- package/docs/testing/flow-browser.md +216 -0
- package/docs/testing/http.md +441 -0
- package/docs/testing/index.md +240 -0
- package/docs/testing/mocking.md +410 -0
- package/docs/upgrade.md +192 -0
- package/docs/validator.md +549 -0
- package/docs/view.md +453 -0
- package/package.json +60 -0
- package/src/bin/mcp.ts +73 -0
- package/src/config.ts +63 -0
- package/src/errors.ts +27 -0
- package/src/index.ts +56 -0
- package/src/install/ArchInstallCommand.ts +189 -0
- package/src/install/detect.ts +98 -0
- package/src/install/guidelines.ts +290 -0
- package/src/install/markers.ts +77 -0
- package/src/install/mcpConfig.ts +81 -0
- package/src/mcp/index.ts +27 -0
- package/src/mcp/jsonrpc.ts +133 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/stdio.ts +111 -0
- package/src/mcp/types.ts +146 -0
- package/src/probe/ArchProbeCommand.ts +48 -0
- package/src/probe/sentinel.ts +13 -0
- package/src/probe/topics.ts +291 -0
- package/src/provider/ArchProvider.ts +101 -0
- package/src/tools/_probe.ts +164 -0
- package/src/tools/apiSurface.ts +241 -0
- package/src/tools/appInfo.ts +88 -0
- package/src/tools/baselines.ts +210 -0
- package/src/tools/context.ts +37 -0
- package/src/tools/doctor.ts +93 -0
- package/src/tools/index.ts +50 -0
- package/src/tools/logs.ts +265 -0
- package/src/tools/routes.ts +133 -0
- package/src/tools/schema.ts +119 -0
- package/src/tools/searchDocs.ts +345 -0
package/docs/helpers.md
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Helpers
|
|
3
|
+
description: Small, named, tree-shakeable functions for env, config, control flow, strings, and responses.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Helpers
|
|
7
|
+
|
|
8
|
+
`@zerotal/core` ships a set of small, focused helper functions for the things you
|
|
9
|
+
reach for constantly — reading environment variables, building responses,
|
|
10
|
+
massaging strings, and taming control flow. They are **named, tree-shakeable
|
|
11
|
+
exports**, never globals: import what you use.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// in a controller
|
|
15
|
+
import { env, config, tap, pipe, rescue, data_get, Str, collect } from "zerotal";
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Environment & paths
|
|
19
|
+
|
|
20
|
+
### env
|
|
21
|
+
|
|
22
|
+
Read an environment variable with an optional, type-coerced fallback. This is the
|
|
23
|
+
canonical way to read env values — every config file uses it. The return type
|
|
24
|
+
follows the fallback's type.
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// config/app.ts
|
|
28
|
+
env("APP_NAME", "Zerotal App"); // string
|
|
29
|
+
env("APP_DEBUG", false); // boolean — coerces 'true' / '1'
|
|
30
|
+
env("PORT", 3000); // number — coerces numeric strings
|
|
31
|
+
env("APP_KEY"); // string | undefined — no fallback
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### requireEnv
|
|
35
|
+
|
|
36
|
+
Read a variable that must exist. Throws a `ConfigError` at boot if it's missing —
|
|
37
|
+
use it for secrets your app cannot run without.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// config/app.ts
|
|
41
|
+
const key = requireEnv("APP_KEY"); // throws ConfigError if unset
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### basePath
|
|
45
|
+
|
|
46
|
+
Resolve a path relative to the project root (`process.cwd()`), regardless of which
|
|
47
|
+
file calls it. Use it when declaring route files so paths don't depend on the
|
|
48
|
+
caller's directory.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// bootstrap/app.ts
|
|
52
|
+
Application.create({ providers })
|
|
53
|
+
.routing({ web: basePath("routes/web.ts") })
|
|
54
|
+
.fileBasedRouting({ web: basePath("app/routes") });
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### setAppEnv
|
|
58
|
+
|
|
59
|
+
Map a CLI command name to `APP_ENV` before the app is created. Called once in the
|
|
60
|
+
managed `zt.ts`; you rarely call it yourself. `serve`/`start`/`s` → `web`,
|
|
61
|
+
`worker`/`queue:work` → `worker`, anything else → `console`. A no-op if `APP_ENV`
|
|
62
|
+
is already a valid runtime mode.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// zt.ts
|
|
66
|
+
setAppEnv(process.argv[2]);
|
|
67
|
+
const { default: app } = await import("./bootstrap/app.ts");
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Configuration access
|
|
71
|
+
|
|
72
|
+
### config
|
|
73
|
+
|
|
74
|
+
Read (and write) loaded configuration by dot-path, anywhere after boot.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// in a controller
|
|
78
|
+
config("app.name"); // string — typed from the registered config shape
|
|
79
|
+
config("app.port", 3000); // number — fallback must match the path's type
|
|
80
|
+
config.require("app.key"); // throws if absent
|
|
81
|
+
config.set("mail.driver", "log"); // override at runtime
|
|
82
|
+
config.all(); // the whole config map
|
|
83
|
+
config.safe("app.cors", {}); // returns the fallback if no app is booted
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Paths are type-aware: each resolves to the type declared by the owning package's
|
|
87
|
+
config shape, with autocomplete. Unknown paths fall back to `unknown`. See
|
|
88
|
+
[Configuration → Typed dot-paths](/docs/config-system#typed-dot-paths).
|
|
89
|
+
|
|
90
|
+
> **Tip** — `config.safe()` is the no-throw variant — handy in library code that
|
|
91
|
+
> may run before an application exists.
|
|
92
|
+
|
|
93
|
+
### request
|
|
94
|
+
|
|
95
|
+
Reach the current request's `HttpContext` from anywhere in the async call chain —
|
|
96
|
+
no thread-through required. With a key, it reads a single merged input value (route
|
|
97
|
+
params, then parsed body, then query string).
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// in a controller
|
|
101
|
+
request(); // the HttpContext
|
|
102
|
+
request("email"); // string | undefined — input named 'email'
|
|
103
|
+
request<number>("page", 1); // typed input with a fallback
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
> **Warning** — `request()` throws if called outside an active HTTP request.
|
|
107
|
+
> Body resolution is synchronous: it only sees body data already parsed and cached
|
|
108
|
+
> (e.g. via a `FormRequest` or `await ctx.body()`).
|
|
109
|
+
|
|
110
|
+
## Control flow
|
|
111
|
+
|
|
112
|
+
### tap / tapAsync
|
|
113
|
+
|
|
114
|
+
Run a side effect on a value and return the value unchanged — perfect for emitting
|
|
115
|
+
an event or logging in the middle of a chain without breaking it.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// in a controller
|
|
119
|
+
return tap(await User.create(data), (user) => Events.emit(new UserRegistered(user.id)));
|
|
120
|
+
|
|
121
|
+
return await tapAsync(await User.create(data), async (user) => {
|
|
122
|
+
await Notification.send(user, new WelcomeEmail());
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### pipe / pipeAsync
|
|
127
|
+
|
|
128
|
+
The sibling of `tap` — pass a value through a transformer and return the _result_.
|
|
129
|
+
Use `pipe` when the value should change, `tap` when it shouldn't.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// in a controller
|
|
133
|
+
const slug = pipe(post.title, (t) => t.toLowerCase().replace(/\s+/g, "-"));
|
|
134
|
+
const hashed = await pipeAsync(password, (p) => Hash.make(p));
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### rescue / rescueSync
|
|
138
|
+
|
|
139
|
+
Run a callback and fall back to a value (or a function of the caught error) instead
|
|
140
|
+
of throwing. `rescue` awaits; `rescueSync` is for hot paths that can't await
|
|
141
|
+
(JSON parsing, attribute decoding).
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// in a controller
|
|
145
|
+
const price = await rescue(() => stripe.getPrice(id), 0);
|
|
146
|
+
const user = await rescue(
|
|
147
|
+
() => User.findOrFail(id),
|
|
148
|
+
(e) => {
|
|
149
|
+
log(e);
|
|
150
|
+
return null;
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const payload = rescueSync(() => JSON.parse(raw), {});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### data_get
|
|
158
|
+
|
|
159
|
+
Safely read a deeply nested value by dot-notation, returning a default when any
|
|
160
|
+
segment is absent. Built for untyped JSON — webhooks, third-party API responses —
|
|
161
|
+
where optional chaining gets unwieldy. Numeric segments index into arrays.
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// in a webhook handler
|
|
165
|
+
data_get(payload, "user.address.city"); // 'Cape Town' or undefined
|
|
166
|
+
data_get(payload, "items.0.price", 0); // first item's price, or 0
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Strings — Str
|
|
170
|
+
|
|
171
|
+
`Str` is a namespace of pure string utilities. The common case-conversions are
|
|
172
|
+
also exported individually (`camelCase`, `snakeCase`).
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// in a controller
|
|
176
|
+
Str.camelCase("user_id"); // 'userId'
|
|
177
|
+
Str.snakeCase("userId"); // 'user_id'
|
|
178
|
+
Str.pascalCase("user-id"); // 'UserId'
|
|
179
|
+
Str.kebab("UserId"); // 'user-id'
|
|
180
|
+
Str.slugify("Hello, World!"); // 'hello-world'
|
|
181
|
+
Str.titleCase("hello world"); // 'Hello World'
|
|
182
|
+
Str.capitalize("hello"); // 'Hello'
|
|
183
|
+
Str.lcfirst("Hello"); // 'hello'
|
|
184
|
+
Str.truncate(text, 50); // 'long text...' (custom suffix optional)
|
|
185
|
+
Str.words(text, 10); // first 10 words + suffix
|
|
186
|
+
Str.squish(" a b "); // 'a b'
|
|
187
|
+
Str.start(path, "/"); // ensure leading '/'
|
|
188
|
+
Str.finish(path, "/"); // ensure trailing '/'
|
|
189
|
+
Str.after(s, ":"); // substring after first ':'
|
|
190
|
+
Str.before(s, ":"); // substring before first ':'
|
|
191
|
+
Str.afterLast(s, "/"); // substring after last '/'
|
|
192
|
+
Str.beforeLast(s, "/"); // substring before last '/'
|
|
193
|
+
Str.contains(s, "x"); // boolean
|
|
194
|
+
Str.replaceFirst(s, a, b);
|
|
195
|
+
Str.replaceLast(s, a, b);
|
|
196
|
+
Str.padLeft(s, 5, "0"); // '00042'
|
|
197
|
+
Str.reverse(s);
|
|
198
|
+
Str.random(32); // random alphanumeric string
|
|
199
|
+
Str.isAlphanumeric(s); // boolean
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Extend it with your own helper via `Str.macro(name, fn)`.
|
|
203
|
+
|
|
204
|
+
### Inflection & naming
|
|
205
|
+
|
|
206
|
+
These power the ORM's table-name convention and are exported directly:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// in a controller
|
|
210
|
+
import { pluralize, singularize, tableNameFor } from "zerotal";
|
|
211
|
+
|
|
212
|
+
pluralize("category"); // 'categories'
|
|
213
|
+
singularize("people"); // 'person'
|
|
214
|
+
tableNameFor("BlogPost"); // 'blog_posts'
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Objects — deepMerge
|
|
218
|
+
|
|
219
|
+
Recursively merge an override object onto a base, lodash-style: nested plain
|
|
220
|
+
objects merge key-by-key, while arrays, primitives, and class instances replace
|
|
221
|
+
wholesale. `undefined` values in the override are ignored. Neither argument is
|
|
222
|
+
mutated, and the result shares no mutable plain structure with either — every plain
|
|
223
|
+
object and array in the result is a fresh copy, so mutating a merged config can
|
|
224
|
+
never corrupt the module-level `defaults` it was built from. Prototype-polluting
|
|
225
|
+
keys (`__proto__`, `constructor`, `prototype`) are skipped, so it is safe to merge
|
|
226
|
+
untrusted input such as env files or parsed JSON. This is what makes the
|
|
227
|
+
`*Config({ ... })` factories preserve untouched defaults at every depth.
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
// in a config factory
|
|
231
|
+
import { deepMerge } from "zerotal";
|
|
232
|
+
|
|
233
|
+
deepMerge(
|
|
234
|
+
{ smtp: { host: "localhost", port: 1025, secure: false } },
|
|
235
|
+
{ smtp: { host: "mail.example.com" } },
|
|
236
|
+
);
|
|
237
|
+
// → { smtp: { host: 'mail.example.com', port: 1025, secure: false } }
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### `DeepPartial<T>` — the shape an override may take
|
|
241
|
+
|
|
242
|
+
`deepMerge` accepts a `DeepPartial<T>`: every key optional, all the way down. A plain
|
|
243
|
+
`Partial<T>` only makes the _top_ level optional, which would make the commonest override
|
|
244
|
+
anyone writes a type error:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
// in a config factory
|
|
248
|
+
import { deepMerge } from "zerotal";
|
|
249
|
+
import type { DeepPartial } from "zerotal";
|
|
250
|
+
|
|
251
|
+
interface MailConfigShape {
|
|
252
|
+
smtp: { host: string; port: number; secure: boolean };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Overriding one field of a nested block, without restating the others.
|
|
256
|
+
const override: DeepPartial<MailConfigShape> = { smtp: { host: "mail.example.com" } };
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Arrays, `Date`s, `Map`s, `Set`s and functions are left whole rather than made partial,
|
|
260
|
+
matching the merge itself — those replace wholesale, so asking for a partial of one would
|
|
261
|
+
describe something `deepMerge` never does. An explicit `undefined` is allowed too, because
|
|
262
|
+
the merge documents it as _skipped_ rather than blanking a default.
|
|
263
|
+
|
|
264
|
+
Write your own config factory's parameter as `Partial<XConfigShape>` when the shape is
|
|
265
|
+
flat, and `DeepPartial<XConfigShape>` when it nests — both satisfy the package linter's
|
|
266
|
+
`config-partial` rule.
|
|
267
|
+
|
|
268
|
+
### Arrays replace — they do not merge
|
|
269
|
+
|
|
270
|
+
An array in the override replaces the base array entirely. It is **never**
|
|
271
|
+
concatenated, de-duplicated, or merged element-by-element:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
// in a config factory
|
|
275
|
+
deepMerge({ hosts: ["a", "b"] }, { hosts: ["c"] });
|
|
276
|
+
// → { hosts: ['c'] } (not ['a','b','c'], not ['c','b'])
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
This is deliberate — there is no surprise-free universal rule for combining two
|
|
280
|
+
arrays. When you design a config or middleware option shape, pick the model that
|
|
281
|
+
matches how you want overrides to behave.
|
|
282
|
+
|
|
283
|
+
**Which model should I use?**
|
|
284
|
+
|
|
285
|
+
- **List the user should be able to extend** → expose a plain array and document
|
|
286
|
+
that setting it replaces the default. Have callers spread the default in
|
|
287
|
+
themselves: `SomeConfig({ hosts: [...DEFAULT_HOSTS, "extra"] })`.
|
|
288
|
+
- **Keyed, extensible sub-config** → model it as a nested **object** keyed by name
|
|
289
|
+
(like `cache.stores` or `storage.disks`) instead of an array. Objects merge, so a
|
|
290
|
+
user can add one entry without losing the built-ins.
|
|
291
|
+
|
|
292
|
+
> **Note** — The replacement array is deep-cloned, so mutating the merged result
|
|
293
|
+
> never reaches back into the value the caller passed in. Class instances (e.g. a
|
|
294
|
+
> configured `driver`) are replaced by reference — they keep their prototype and are
|
|
295
|
+
> never merged into.
|
|
296
|
+
|
|
297
|
+
## Fluent wrappers
|
|
298
|
+
|
|
299
|
+
### fluent
|
|
300
|
+
|
|
301
|
+
Wrap any value to chain `.pipe()` transforms and `.tap()` side effects, then unwrap
|
|
302
|
+
with `.get()`. Useful for readable builder-style code over a plain value.
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
// in a controller
|
|
306
|
+
const user = fluent(await User.find(id))
|
|
307
|
+
.tap((u) => log(`loaded ${u.email}`))
|
|
308
|
+
.get();
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### collect
|
|
312
|
+
|
|
313
|
+
Wrap an array in a `Collection` for chainable, immutable transformations — `map`,
|
|
314
|
+
`filter`, `reduce`, `groupBy`, `pluck`, `sum`, `first`, `unique`, and more — a
|
|
315
|
+
fluent collection pipeline.
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
// in a controller
|
|
319
|
+
const topNames = collect(orders)
|
|
320
|
+
.filter((o) => o.paid)
|
|
321
|
+
.pluck("customerId")
|
|
322
|
+
.unique()
|
|
323
|
+
.toArray();
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
> **Note** — `groupBy()` returns a plain `Record<string, T[]>`, not a `Collection`.
|
|
327
|
+
> Re-wrap a group with `collect(group)` if you need to keep chaining over it.
|
|
328
|
+
|
|
329
|
+
## Responses
|
|
330
|
+
|
|
331
|
+
These build and send the HTTP response for the current request. The terminal
|
|
332
|
+
helpers (`json`, `view`, `html`, `markdown`, `file`) set `ctx.response` directly;
|
|
333
|
+
`redirect()` and `redirectTo()` return a chainable `ResponseBuilder`.
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
// in a controller
|
|
337
|
+
import { json, view, html, markdown, redirect, redirectTo, abort } from "zerotal";
|
|
338
|
+
|
|
339
|
+
json({ user }); // 200 application/json
|
|
340
|
+
json({ error: "Nope" }, 422); // custom status
|
|
341
|
+
html("<h1>Hi</h1>"); // 200 text/html
|
|
342
|
+
view(Welcome, { title: "Hi" }); // render a view component + props
|
|
343
|
+
markdown("# Title\n\nBody"); // render markdown → HTML
|
|
344
|
+
|
|
345
|
+
redirect("/dashboard"); // 302 by default
|
|
346
|
+
redirect("/login", 301); // custom status
|
|
347
|
+
redirect().back(); // back to the referrer
|
|
348
|
+
redirect().intended("/home"); // to the originally-requested URL
|
|
349
|
+
redirectTo("posts.show", { id }); // redirect to a named route
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`redirect()` and `redirect().back()` return a `ResponseBuilder` that lets you flash
|
|
353
|
+
data and messages onto the redirect:
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
// in a controller
|
|
357
|
+
return redirect("/posts").withSuccess("Post created.").with("highlight", post.id);
|
|
358
|
+
|
|
359
|
+
return redirect().back().withErrors({ title: "Title is required." });
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
> **Tip** — Call `redirect()` with no arguments to pick the destination fluently:
|
|
363
|
+
> `redirect().to("posts.show", { id })`, `redirect().back()`, or
|
|
364
|
+
> `redirect().intended("/")`. The builder also exposes `withError`, `withWarning`,
|
|
365
|
+
> and `withInfo` alongside `withSuccess` and `withErrors`.
|
|
366
|
+
|
|
367
|
+
`abort()` throws a framework error that the exception handler renders:
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
// in a controller
|
|
371
|
+
abort("Something went wrong."); // → 500 with a message
|
|
372
|
+
abort(403, "You can't do that."); // status + message
|
|
373
|
+
abort(NotFoundError); // a ZerotalError subclass
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Dates — Carbon
|
|
377
|
+
|
|
378
|
+
Date and time get their own helper, `Carbon`, an immutable wrapper over the
|
|
379
|
+
`Temporal` API. It has its own page: [Carbon](/docs/carbon).
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
// in a controller
|
|
383
|
+
import { Carbon } from "zerotal/carbon";
|
|
384
|
+
|
|
385
|
+
Carbon.now().addDays(7).toDateString();
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## References
|
|
389
|
+
|
|
390
|
+
| Helper | Signature | Description |
|
|
391
|
+
| ----------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
392
|
+
| `env` | `env(key: string, fallback?: string \| boolean \| number)` | Read an env var, coerced to the fallback's type. |
|
|
393
|
+
| `requireEnv` | `requireEnv(key: string): string` | Read a required env var; throws `ConfigError` if unset. |
|
|
394
|
+
| `basePath` | `basePath(...segments: string[]): string` | Resolve a path from `process.cwd()`. |
|
|
395
|
+
| `setAppEnv` | `setAppEnv(command?: string): void` | Map a CLI command to `APP_ENV` before boot. |
|
|
396
|
+
| `config` | `config(path: string, fallback?): unknown` | Read config by dot-path (typed for known paths). |
|
|
397
|
+
| `config.set` | `config.set(path: string, value): void` | Override a config value at runtime. |
|
|
398
|
+
| `config.require` | `config.require(path: string): unknown` | Read config; throws when the path is absent. |
|
|
399
|
+
| `config.all` | `config.all(): Record<string, unknown>` | Return the whole config map. |
|
|
400
|
+
| `config.safe` | `config.safe(path: string, fallback): unknown` | Read config without throwing when no app is booted. |
|
|
401
|
+
| `request` | `request<T>(key?: string, fallback?: T): HttpContext \| T \| undefined` | Active `HttpContext`, or one merged input value. |
|
|
402
|
+
| `tap` / `tapAsync` | `tap<T>(value: T, cb: (v: T) => void): T` | Run a side effect, return the original value. |
|
|
403
|
+
| `pipe` / `pipeAsync` | `pipe<T, R>(value: T, fn: (v: T) => R): R` | Transform a value, return the result. |
|
|
404
|
+
| `rescue` / `rescueSync` | `rescue<T>(cb, fallback: T \| ((e) => T)): Promise<T>` | Run a callback, fall back instead of throwing. |
|
|
405
|
+
| `data_get` | `data_get(target, key: string, defaultValue?): unknown` | Read a nested value by dot-notation, with a default. |
|
|
406
|
+
| `deepMerge` | `deepMerge(base, override)` | Recursively merge objects; arrays/instances replace. |
|
|
407
|
+
| `fluent` | `fluent<T>(value: T): Fluent<T>` | Chainable `.pipe()` / `.tap()` / `.get()` wrapper. |
|
|
408
|
+
| `collect` | `collect<T>(items: T[]): Collection<T>` | Chainable, immutable array transformations. |
|
|
409
|
+
| `json` | `json(data: unknown, status = 200): void` | Send a JSON response. |
|
|
410
|
+
| `view` | `view(component, props?, status = 200): void` | Render a view component (or markup) as the response. |
|
|
411
|
+
| `html` | `html(markup, status = 200): void` | Send a raw HTML response. |
|
|
412
|
+
| `markdown` | `markdown(content, options?, status = 200): MarkdownBuilder` | Render markdown → HTML; chain `.withLayout()`. |
|
|
413
|
+
| `file` | `file(path, options?): Promise<void>` | Stream a file from disk; throws if missing. |
|
|
414
|
+
| `redirect` | `redirect(url?, status = 302): ResponseBuilder \| RedirectBuilder` | Redirect; no-arg form picks a destination fluently. |
|
|
415
|
+
| `redirectTo` | `redirectTo(name: string, params?, status = 302): ResponseBuilder` | Redirect to a named route. |
|
|
416
|
+
| `abort` | `abort(status \| message \| ErrorClass, message?): never` | Throw a framework HTTP error the handler renders. |
|
|
417
|
+
|
|
418
|
+
## Next steps
|
|
419
|
+
|
|
420
|
+
- [Configuration](/docs/config-system) — where `env()` and `config()` get their values.
|
|
421
|
+
- [Responses](/docs/responses) — the response layer in depth.
|
|
422
|
+
- [Carbon](/docs/carbon) — the date-time helper.
|
|
423
|
+
- [HTTP Context](/docs/context) — what `request()` reaches for in the async tree.
|