@zerotal/arch 1.7.3 → 1.7.5
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 +34 -2
- package/README.md +4 -2
- package/api-surface.md +4 -4
- package/docs/about.md +16 -16
- package/docs/admin/actions.md +9 -9
- package/docs/admin/auth.md +2 -2
- package/docs/admin/dashboard.md +5 -5
- package/docs/admin/extending-ui.md +12 -12
- package/docs/admin/extending.md +16 -7
- package/docs/admin/forms.md +4 -4
- package/docs/admin/index.md +2 -2
- package/docs/admin/operations.md +9 -9
- package/docs/admin/resources.md +3 -3
- package/docs/admin/structure.md +6 -6
- package/docs/admin/tables.md +8 -8
- package/docs/admin/testing.md +4 -4
- package/docs/ai.md +118 -11
- package/docs/application.md +12 -12
- package/docs/arch.md +25 -3
- package/docs/assets.md +11 -11
- package/docs/audit.md +13 -13
- package/docs/authentication.md +39 -39
- package/docs/authorization.md +13 -13
- package/docs/broadcasting/channels.md +2 -2
- package/docs/broadcasting/client.md +2 -2
- package/docs/broadcasting/events.md +9 -10
- package/docs/broadcasting/index.md +1 -1
- package/docs/broadcasting/testing.md +5 -5
- package/docs/cache.md +15 -13
- package/docs/carbon.md +35 -35
- package/docs/changelog.md +165 -1
- package/docs/client/index.md +277 -70
- package/docs/commands.md +4 -4
- package/docs/components.md +116 -115
- package/docs/config-system.md +4 -4
- package/docs/container.md +27 -27
- package/docs/context.md +27 -27
- package/docs/contributing.md +46 -0
- package/docs/controllers.md +10 -10
- package/docs/conventions.md +23 -15
- package/docs/cookies.md +3 -3
- package/docs/csrf.md +6 -6
- package/docs/database.md +22 -17
- package/docs/deployment.md +2 -2
- package/docs/devtools.md +16 -16
- package/docs/email-verification.md +8 -8
- package/docs/encryption.md +7 -7
- package/docs/errors.md +6 -6
- package/docs/events.md +12 -11
- package/docs/flow/components.md +32 -32
- package/docs/flow/decorators.md +18 -18
- package/docs/flow/events.md +16 -16
- package/docs/flow/forms.md +17 -17
- package/docs/flow/icons.md +199 -0
- package/docs/flow/index.md +20 -20
- package/docs/flow/layouts.md +35 -35
- package/docs/flow/lifecycle.md +16 -16
- package/docs/flow/models.md +9 -9
- package/docs/flow/pagination.md +8 -8
- package/docs/flow/performance.md +7 -7
- package/docs/flow/references.md +3 -3
- package/docs/flow/routing.md +26 -26
- package/docs/flow/testing.md +20 -20
- package/docs/getting-started.md +18 -13
- package/docs/health.md +17 -8
- package/docs/helpers.md +17 -17
- package/docs/i18n.md +10 -10
- package/docs/inertia/devtools.md +4 -4
- package/docs/inertia/index.md +12 -7
- package/docs/inertia/props.md +22 -22
- package/docs/inertia/rendering.md +86 -7
- package/docs/inertia/ssr.md +2 -2
- package/docs/lifecycle.md +2 -2
- package/docs/lock.md +11 -11
- package/docs/logger.md +12 -12
- package/docs/media.md +22 -22
- package/docs/middleware.md +17 -17
- package/docs/migrations.md +13 -13
- package/docs/monitor.md +6 -6
- package/docs/notifications.md +24 -24
- package/docs/orm/casts.md +14 -14
- package/docs/orm/factories.md +8 -8
- package/docs/orm/index.md +22 -20
- package/docs/orm/lifecycle.md +13 -13
- package/docs/orm/queries.md +28 -28
- package/docs/orm/relationships.md +16 -16
- package/docs/orm/serialization.md +11 -11
- package/docs/package-development.md +10 -10
- package/docs/pagination.md +14 -14
- package/docs/password-reset.md +6 -6
- package/docs/providers.md +11 -11
- package/docs/query-builder.md +18 -18
- package/docs/queue.md +14 -12
- package/docs/rate-limiting.md +6 -6
- package/docs/responses.md +14 -14
- package/docs/roles-and-2fa.md +9 -9
- package/docs/routing.md +44 -44
- package/docs/scaffolding.md +1 -1
- package/docs/scheduler.md +10 -10
- package/docs/seeding.md +6 -6
- package/docs/session.md +16 -14
- package/docs/social.md +14 -14
- package/docs/storage.md +25 -13
- package/docs/structure.md +2 -2
- package/docs/support-policy.md +36 -8
- package/docs/telemetry.md +12 -12
- package/docs/tenancy.md +34 -40
- package/docs/testing/browser.md +6 -6
- package/docs/testing/console.md +3 -3
- package/docs/testing/database.md +10 -10
- package/docs/testing/flow-browser.md +6 -6
- package/docs/testing/http.md +12 -12
- package/docs/testing/index.md +3 -3
- package/docs/testing/mocking.md +8 -8
- package/docs/upgrade.md +1 -1
- package/docs/validator.md +19 -19
- package/docs/view.md +14 -14
- package/package.json +4 -4
- package/src/index.ts +25 -1
- package/src/tools/searchDocs.ts +7 -0
- package/docs/client/auth.md +0 -113
- package/docs/client/errors.md +0 -139
- package/docs/client/files.md +0 -118
- package/docs/client/references.md +0 -58
- package/docs/client/requests.md +0 -131
- package/docs/client/resilience.md +0 -141
- package/docs/client/testing.md +0 -146
|
@@ -125,7 +125,7 @@ import {
|
|
|
125
125
|
Extend `BroadcastingEvent`, implement `broadcastOn()`, and dispatch — every subscribed client on
|
|
126
126
|
that channel receives the payload:
|
|
127
127
|
|
|
128
|
-
```ts
|
|
128
|
+
```ts fragment
|
|
129
129
|
// app/events/OrderShipmentStatusUpdated.ts
|
|
130
130
|
import { BroadcastingEvent, privateChannel } from "@zerotal/broadcasting";
|
|
131
131
|
import type { Order } from "../models/Order.ts";
|
|
@@ -11,7 +11,7 @@ live manager for an in-memory recorder, so the code under test broadcasts exactl
|
|
|
11
11
|
as it normally would and the test inspects what came out — no Redis, no Pusher
|
|
12
12
|
credentials, and no waiting on a socket.
|
|
13
13
|
|
|
14
|
-
```ts
|
|
14
|
+
```ts fragment
|
|
15
15
|
// in a test
|
|
16
16
|
import { Broadcast } from "@zerotal/broadcasting";
|
|
17
17
|
|
|
@@ -31,7 +31,7 @@ Broadcast.resetFake();
|
|
|
31
31
|
`afterEach` — otherwise the first test to fake broadcasting silently mutes every
|
|
32
32
|
test that follows it.
|
|
33
33
|
|
|
34
|
-
```ts
|
|
34
|
+
```ts fragment
|
|
35
35
|
afterEach(() => Broadcast.resetFake());
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -76,7 +76,7 @@ Proving a broadcast did _not_ happen is often the more valuable test, because a
|
|
|
76
76
|
stray broadcast reaches real users. Both negative assertions are worth reaching
|
|
77
77
|
for:
|
|
78
78
|
|
|
79
|
-
```ts
|
|
79
|
+
```ts fragment
|
|
80
80
|
it("does not broadcast when validation fails", async () => {
|
|
81
81
|
const fake = Broadcast.fake();
|
|
82
82
|
|
|
@@ -102,7 +102,7 @@ no special handling — save the model and assert. The event name is whatever
|
|
|
102
102
|
`broadcastAs()` returns and the payload whatever `broadcastWith()` builds, so
|
|
103
103
|
asserting on both is what pins that mapping down:
|
|
104
104
|
|
|
105
|
-
```ts
|
|
105
|
+
```ts fragment
|
|
106
106
|
const fake = Broadcast.fake();
|
|
107
107
|
|
|
108
108
|
await Post.create({ title: "Hello" });
|
|
@@ -117,7 +117,7 @@ its socket id along. The fake records the broadcast either way, so a test that o
|
|
|
117
117
|
asserts the event fired will pass whether or not `toOthers()` was used. To pin that
|
|
118
118
|
behaviour down, read the recorded entries directly:
|
|
119
119
|
|
|
120
|
-
```ts
|
|
120
|
+
```ts fragment
|
|
121
121
|
const [entry] = fake.recorded();
|
|
122
122
|
expect(entry.event).toBe("PostUpdated");
|
|
123
123
|
```
|
package/docs/cache.md
CHANGED
|
@@ -45,7 +45,9 @@ import { CacheConfig } from "@zerotal/cache";
|
|
|
45
45
|
import { env } from "zerotal";
|
|
46
46
|
|
|
47
47
|
export default CacheConfig({
|
|
48
|
-
|
|
48
|
+
// One of 'sqlite' | 'redis' | 'memory', written literally: the field is that
|
|
49
|
+
// union and `env()` returns a plain string.
|
|
50
|
+
driver: "sqlite",
|
|
49
51
|
prefix: env("CACHE_PREFIX", "zerotal:"), // prepended to every key
|
|
50
52
|
ttl: env("CACHE_TTL", 3600), // default TTL in seconds (1 hour)
|
|
51
53
|
|
|
@@ -66,7 +68,7 @@ export default CacheConfig({
|
|
|
66
68
|
|
|
67
69
|
## Basic operations
|
|
68
70
|
|
|
69
|
-
```ts
|
|
71
|
+
```ts fragment
|
|
70
72
|
// in a controller or service
|
|
71
73
|
import { Cache } from "@zerotal/cache";
|
|
72
74
|
|
|
@@ -93,13 +95,13 @@ await Cache.flush();
|
|
|
93
95
|
|
|
94
96
|
`remember()` checks the cache and, on a miss, calls the factory, stores the result, and returns it. Under high concurrency, multiple callers for the same key coalesce — the factory runs exactly once:
|
|
95
97
|
|
|
96
|
-
```ts
|
|
98
|
+
```ts fragment
|
|
97
99
|
function remember<T>(key: string, ttl: number, fn: () => Promise<T> | T): Promise<T>;
|
|
98
100
|
```
|
|
99
101
|
|
|
100
102
|
> **Tip** — Reach for `remember()` instead of a manual `get`/`set` pair. It avoids the cache stampede where many requests miss at once and all hit the database.
|
|
101
103
|
|
|
102
|
-
```ts
|
|
104
|
+
```ts fragment
|
|
103
105
|
// in a controller
|
|
104
106
|
import { Cache } from "@zerotal/cache";
|
|
105
107
|
import { Post } from "../app/models/Post.ts";
|
|
@@ -130,7 +132,7 @@ const stats = await Cache.remember(`user:${userId}:stats`, 60, async () => {
|
|
|
130
132
|
|
|
131
133
|
Group related keys under named tags so you can invalidate them together. `Cache.tags()` returns a `TaggedCache` whose keys all share a tag prefix — useful when several cache entries depend on the same underlying data:
|
|
132
134
|
|
|
133
|
-
```ts
|
|
135
|
+
```ts fragment
|
|
134
136
|
// in a controller
|
|
135
137
|
import { Cache } from "@zerotal/cache";
|
|
136
138
|
|
|
@@ -156,7 +158,7 @@ await Cache.tags([`user:${userId}`]).flush();
|
|
|
156
158
|
|
|
157
159
|
### Tag-based invalidation in model hooks
|
|
158
160
|
|
|
159
|
-
```ts
|
|
161
|
+
```ts fragment
|
|
160
162
|
// app/models/Post.ts — inside a lifecycle hook or observer
|
|
161
163
|
async afterCreate(post: Post) {
|
|
162
164
|
await Cache.tags(["posts"]).flush();
|
|
@@ -171,7 +173,7 @@ async afterUpdate(post: Post) {
|
|
|
171
173
|
|
|
172
174
|
There is no multi-get primitive — batch with `Promise.all` to cut round-trips to the backend:
|
|
173
175
|
|
|
174
|
-
```ts
|
|
176
|
+
```ts fragment
|
|
175
177
|
// in a controller
|
|
176
178
|
import { Cache } from "@zerotal/cache";
|
|
177
179
|
|
|
@@ -211,7 +213,7 @@ await Promise.all([
|
|
|
211
213
|
|
|
212
214
|
The `Cache` facade always uses the configured driver. To use a different driver for one use case, construct a `CacheManager` directly:
|
|
213
215
|
|
|
214
|
-
```ts
|
|
216
|
+
```ts fragment
|
|
215
217
|
// in a service
|
|
216
218
|
import { CacheManager, MemoryDriver } from "@zerotal/cache";
|
|
217
219
|
|
|
@@ -220,7 +222,7 @@ const local = new CacheManager(new MemoryDriver(), "req:", 30);
|
|
|
220
222
|
await local.set("computed-total", total);
|
|
221
223
|
```
|
|
222
224
|
|
|
223
|
-
```ts
|
|
225
|
+
```ts fragment
|
|
224
226
|
new CacheManager(driver: CacheDriver, prefix?: string, defaultTtl?: number)
|
|
225
227
|
```
|
|
226
228
|
|
|
@@ -230,7 +232,7 @@ new CacheManager(driver: CacheDriver, prefix?: string, defaultTtl?: number)
|
|
|
230
232
|
|
|
231
233
|
`with()` needs a `CacheManager` instance — resolve the framework's bound manager from the container with `app.container.makeSync("cache")`:
|
|
232
234
|
|
|
233
|
-
```ts
|
|
235
|
+
```ts fragment
|
|
234
236
|
// routes/api.ts
|
|
235
237
|
import { IdempotencyMiddleware, CacheManager } from "@zerotal/cache";
|
|
236
238
|
|
|
@@ -283,7 +285,7 @@ A replayed response includes `Idempotency-Replay: true` so clients can distingui
|
|
|
283
285
|
|
|
284
286
|
Pre-populate the cache at boot so the first real request is always fast. Call from a provider's `onStarted()`, which runs after the application has finished booting:
|
|
285
287
|
|
|
286
|
-
```ts
|
|
288
|
+
```ts fragment
|
|
287
289
|
// bootstrap/providers/AppServiceProvider.ts
|
|
288
290
|
import { ServiceProvider } from "zerotal";
|
|
289
291
|
import { Cache } from "@zerotal/cache";
|
|
@@ -305,7 +307,7 @@ Use the `memory` driver in tests for speed and isolation — it never persists b
|
|
|
305
307
|
|
|
306
308
|
To swap the cache for a single suite, rebind the `cache` singleton on the container before resolving the facade, then flush between tests to avoid bleed:
|
|
307
309
|
|
|
308
|
-
```ts
|
|
310
|
+
```ts fragment
|
|
309
311
|
// in test setup
|
|
310
312
|
import { Application } from "zerotal";
|
|
311
313
|
import { Cache, CacheManager, MemoryDriver } from "@zerotal/cache";
|
|
@@ -354,7 +356,7 @@ Cache errors extend `CacheError`, which extends the framework's `ZerotalError`.
|
|
|
354
356
|
| `CacheSerializationError` | `E_CACHE_SERIALIZATION` | A value cannot be serialised for storage. |
|
|
355
357
|
| `CacheDeserializationError` | `E_CACHE_DESERIALIZATION` | A stored value cannot be read back — usually a shape change. |
|
|
356
358
|
|
|
357
|
-
```typescript
|
|
359
|
+
```typescript fragment
|
|
358
360
|
// in a service
|
|
359
361
|
import { CacheDeserializationError } from "@zerotal/cache";
|
|
360
362
|
|
package/docs/carbon.md
CHANGED
|
@@ -28,7 +28,7 @@ import { Carbon, CarbonInterval } from "zerotal/carbon";
|
|
|
28
28
|
The constructor accepts a string, a millisecond timestamp, a native `Date`, a
|
|
29
29
|
Temporal type, or another Carbon. A second argument sets the timezone.
|
|
30
30
|
|
|
31
|
-
```typescript
|
|
31
|
+
```typescript fragment
|
|
32
32
|
// anywhere in your app
|
|
33
33
|
// Current date and time (system timezone)
|
|
34
34
|
const now = new Carbon();
|
|
@@ -60,7 +60,7 @@ const d8 = new Carbon(Temporal.Now.instant());
|
|
|
60
60
|
Named static factories cover the common boundary cases (all accept an optional
|
|
61
61
|
timezone string):
|
|
62
62
|
|
|
63
|
-
```typescript
|
|
63
|
+
```typescript fragment
|
|
64
64
|
// anywhere in your app
|
|
65
65
|
Carbon.today(); // today at 00:00:00
|
|
66
66
|
Carbon.tomorrow(); // tomorrow at 00:00:00
|
|
@@ -79,7 +79,7 @@ Carbon.create(input); // same as new Carbon(input)
|
|
|
79
79
|
Every modifier returns a **new** Carbon. Chain freely — the base instance never
|
|
80
80
|
changes.
|
|
81
81
|
|
|
82
|
-
```typescript
|
|
82
|
+
```typescript fragment
|
|
83
83
|
// anywhere in your app
|
|
84
84
|
const base = new Carbon("2026-01-15");
|
|
85
85
|
const nextWeek = base.addDays(7);
|
|
@@ -95,7 +95,7 @@ deadline.format("YYYY-MM-DD"); // '2026-02-15'
|
|
|
95
95
|
A Carbon carries its timezone; converting produces a new instance pointing at the
|
|
96
96
|
same instant.
|
|
97
97
|
|
|
98
|
-
```typescript
|
|
98
|
+
```typescript fragment
|
|
99
99
|
// anywhere in your app
|
|
100
100
|
// Read the timezone of an instance
|
|
101
101
|
const tz = Carbon.now("Asia/Tokyo").timezone; // 'Asia/Tokyo'
|
|
@@ -117,7 +117,7 @@ Reading a Carbon never changes it. Calendar fields are plain properties, so they
|
|
|
117
117
|
carry no parentheses; the three counts that depend on the surrounding calendar are
|
|
118
118
|
methods, because they compute an answer rather than expose a stored field.
|
|
119
119
|
|
|
120
|
-
```typescript
|
|
120
|
+
```typescript fragment
|
|
121
121
|
// anywhere in your app
|
|
122
122
|
const d = new Carbon("2026-06-15 09:30:45.123");
|
|
123
123
|
|
|
@@ -162,7 +162,7 @@ out and comparing them by hand.
|
|
|
162
162
|
|
|
163
163
|
### Predicates
|
|
164
164
|
|
|
165
|
-
```typescript
|
|
165
|
+
```typescript fragment
|
|
166
166
|
// anywhere in your app
|
|
167
167
|
d.isToday();
|
|
168
168
|
d.isTomorrow();
|
|
@@ -221,7 +221,7 @@ ranges, where inclusive bounds would place a boundary value in both buckets.
|
|
|
221
221
|
|
|
222
222
|
All arithmetic returns a new Carbon.
|
|
223
223
|
|
|
224
|
-
```typescript
|
|
224
|
+
```typescript fragment
|
|
225
225
|
// anywhere in your app
|
|
226
226
|
d.addNanoseconds(1) d.subtractNanoseconds(1)
|
|
227
227
|
d.addMicroseconds(1) d.subtractMicroseconds(1)
|
|
@@ -245,7 +245,7 @@ either direction from a computed value without branching.
|
|
|
245
245
|
|
|
246
246
|
To add a [CarbonInterval](#carboninterval), use `add()` / `subtract()`:
|
|
247
247
|
|
|
248
|
-
```typescript
|
|
248
|
+
```typescript fragment
|
|
249
249
|
// anywhere in your app
|
|
250
250
|
const interval = CarbonInterval.days(3).andHours(6);
|
|
251
251
|
|
|
@@ -259,7 +259,7 @@ Months and years are calendar units rather than fixed spans, so adding one lands
|
|
|
259
259
|
on the same day number in the target month — and clamps when that day does not
|
|
260
260
|
exist there:
|
|
261
261
|
|
|
262
|
-
```typescript
|
|
262
|
+
```typescript fragment
|
|
263
263
|
new Carbon("2026-01-31").addMonths(1); // → 2026-02-28
|
|
264
264
|
new Carbon("2024-02-29").addYears(1); // → 2025-02-28
|
|
265
265
|
```
|
|
@@ -267,7 +267,7 @@ new Carbon("2024-02-29").addYears(1); // → 2025-02-28
|
|
|
267
267
|
Clamping discards information, which makes month arithmetic asymmetric. Adding a
|
|
268
268
|
month and taking it back does not always return the original date:
|
|
269
269
|
|
|
270
|
-
```typescript
|
|
270
|
+
```typescript fragment
|
|
271
271
|
new Carbon("2026-01-31").addMonths(1).subtractMonths(1); // → 2026-01-28
|
|
272
272
|
```
|
|
273
273
|
|
|
@@ -285,7 +285,7 @@ and always reverse cleanly.
|
|
|
285
285
|
|
|
286
286
|
Snap to the start or end of a time period — all return a new Carbon.
|
|
287
287
|
|
|
288
|
-
```typescript
|
|
288
|
+
```typescript fragment
|
|
289
289
|
// anywhere in your app
|
|
290
290
|
d.startOfMinute(); // :00.000
|
|
291
291
|
d.endOfMinute(); // :59.999999999
|
|
@@ -325,7 +325,7 @@ day without also catching midnight the next morning.
|
|
|
325
325
|
|
|
326
326
|
The `with` prefix signals a new instance is returned.
|
|
327
327
|
|
|
328
|
-
```typescript
|
|
328
|
+
```typescript fragment
|
|
329
329
|
// anywhere in your app
|
|
330
330
|
d.withYear(2030);
|
|
331
331
|
d.withMonth(12); // 1-indexed
|
|
@@ -351,7 +351,7 @@ of setters.
|
|
|
351
351
|
All numeric diff methods return a **number** — positive when `this` is after
|
|
352
352
|
`other`.
|
|
353
353
|
|
|
354
|
-
```typescript
|
|
354
|
+
```typescript fragment
|
|
355
355
|
// anywhere in your app
|
|
356
356
|
const a = new Carbon("2026-01-01");
|
|
357
357
|
const b = new Carbon("2026-06-15");
|
|
@@ -373,7 +373,7 @@ b.diffInYears(a); // ~0.42
|
|
|
373
373
|
For a calendar-aware breakdown use `diffAsCarbonInterval`, which delegates to
|
|
374
374
|
Temporal's `until()`:
|
|
375
375
|
|
|
376
|
-
```typescript
|
|
376
|
+
```typescript fragment
|
|
377
377
|
// anywhere in your app
|
|
378
378
|
const age = birthday.diffAsCarbonInterval(Carbon.now(), "year");
|
|
379
379
|
// → CarbonInterval { years: 28, months: 3, days: 12, … }
|
|
@@ -394,7 +394,7 @@ The `largestUnit` defaults to `'day'` and accepts `'year'`, `'month'`, `'week'`,
|
|
|
394
394
|
Returns a human-readable relative string using `Intl.RelativeTimeFormat`. With no
|
|
395
395
|
argument it compares against now.
|
|
396
396
|
|
|
397
|
-
```typescript
|
|
397
|
+
```typescript fragment
|
|
398
398
|
// anywhere in your app
|
|
399
399
|
const posted = new Carbon("2026-06-08");
|
|
400
400
|
posted.diffForHumans(); // '1 week ago'
|
|
@@ -407,7 +407,7 @@ a.diffForHumans(b); // '5 months ago'
|
|
|
407
407
|
|
|
408
408
|
Pass an options object to tune the output:
|
|
409
409
|
|
|
410
|
-
```typescript
|
|
410
|
+
```typescript fragment
|
|
411
411
|
// anywhere in your app
|
|
412
412
|
a.diffForHumans(b, {
|
|
413
413
|
parts: 2, // include up to 2 units → '5 months, 1 week ago'
|
|
@@ -419,7 +419,7 @@ a.diffForHumans(b, {
|
|
|
419
419
|
});
|
|
420
420
|
```
|
|
421
421
|
|
|
422
|
-
```typescript
|
|
422
|
+
```typescript fragment
|
|
423
423
|
// anywhere in your app
|
|
424
424
|
const future = Carbon.now().addDays(3);
|
|
425
425
|
future.diffForHumans(); // '3 days from now'
|
|
@@ -434,7 +434,7 @@ future.diffForHumans({ absolute: true }); // '3 days'
|
|
|
434
434
|
|
|
435
435
|
### Token-based
|
|
436
436
|
|
|
437
|
-
```typescript
|
|
437
|
+
```typescript fragment
|
|
438
438
|
// anywhere in your app
|
|
439
439
|
d.format(); // '2026-06-15 09:30:00' (default)
|
|
440
440
|
d.format("YYYY-MM-DD"); // '2026-06-15'
|
|
@@ -472,7 +472,7 @@ d.format("YYYY-MM-DDTHH:mm:ssZ"); // '2026-06-15T09:30:00+02:00'
|
|
|
472
472
|
|
|
473
473
|
`intlFormat` delegates to `Intl.DateTimeFormat`:
|
|
474
474
|
|
|
475
|
-
```typescript
|
|
475
|
+
```typescript fragment
|
|
476
476
|
// anywhere in your app
|
|
477
477
|
d.intlFormat("en-US", { dateStyle: "full" });
|
|
478
478
|
// → 'Monday, June 15, 2026'
|
|
@@ -489,7 +489,7 @@ d.intlFormat("ja-JP", { year: "numeric", month: "long", day: "numeric" });
|
|
|
489
489
|
|
|
490
490
|
### Convenience formatters
|
|
491
491
|
|
|
492
|
-
```typescript
|
|
492
|
+
```typescript fragment
|
|
493
493
|
// anywhere in your app
|
|
494
494
|
d.toDateString(); // '2026-06-15'
|
|
495
495
|
d.toDateTimeString(); // '2026-06-15 09:30:00'
|
|
@@ -509,7 +509,7 @@ d.toLongDate(); // '15 June 2026'
|
|
|
509
509
|
|
|
510
510
|
## Serialisation
|
|
511
511
|
|
|
512
|
-
```typescript
|
|
512
|
+
```typescript fragment
|
|
513
513
|
// anywhere in your app
|
|
514
514
|
d.toDate(); // native Date
|
|
515
515
|
d.toISOString(); // '2026-06-15T07:30:00+00:00' (UTC)
|
|
@@ -529,7 +529,7 @@ d.toPlainDate(); // Temporal.PlainDate (loses time + timezone)
|
|
|
529
529
|
|
|
530
530
|
`valueOf()` enables direct comparison with `<`, `>`, `-`:
|
|
531
531
|
|
|
532
|
-
```typescript
|
|
532
|
+
```typescript fragment
|
|
533
533
|
// anywhere in your app
|
|
534
534
|
const a = new Carbon("2026-01-01");
|
|
535
535
|
const b = new Carbon("2026-06-15");
|
|
@@ -544,7 +544,7 @@ Math.min(+a, +b) === +a; // true
|
|
|
544
544
|
`CarbonInterval` is an immutable duration value object backed by
|
|
545
545
|
`Temporal.Duration`. Every method returns a new instance.
|
|
546
546
|
|
|
547
|
-
```typescript
|
|
547
|
+
```typescript fragment
|
|
548
548
|
// anywhere in your app
|
|
549
549
|
import { CarbonInterval } from "zerotal/carbon";
|
|
550
550
|
|
|
@@ -575,7 +575,7 @@ const built = new CarbonInterval({ years: 1, months: 6, days: 3 });
|
|
|
575
575
|
|
|
576
576
|
Chain `and*` methods to compose multi-unit intervals:
|
|
577
577
|
|
|
578
|
-
```typescript
|
|
578
|
+
```typescript fragment
|
|
579
579
|
// anywhere in your app
|
|
580
580
|
CarbonInterval.days(3).andHours(6).andMinutes(30);
|
|
581
581
|
// → 3 days 6 hours 30 minutes
|
|
@@ -592,7 +592,7 @@ Available: `andYears`, `andMonths`, `andWeeks`, `andDays`, `andHours`,
|
|
|
592
592
|
|
|
593
593
|
### Interval getters
|
|
594
594
|
|
|
595
|
-
```typescript
|
|
595
|
+
```typescript fragment
|
|
596
596
|
// anywhere in your app
|
|
597
597
|
const i = CarbonInterval.fromISO("P1Y2M3DT4H5M6S");
|
|
598
598
|
|
|
@@ -613,7 +613,7 @@ i.isZero; // false
|
|
|
613
613
|
|
|
614
614
|
### Interval arithmetic
|
|
615
615
|
|
|
616
|
-
```typescript
|
|
616
|
+
```typescript fragment
|
|
617
617
|
// anywhere in your app
|
|
618
618
|
const a = CarbonInterval.hours(2);
|
|
619
619
|
const b = CarbonInterval.minutes(30);
|
|
@@ -630,7 +630,7 @@ CarbonInterval.abs(a.negate()); // 2 hours (all fields positive)
|
|
|
630
630
|
`cascade()` rolls excess sub-units up into higher ones. It needs a reference date
|
|
631
631
|
for calendar-aware units (months, years) and defaults to now in UTC.
|
|
632
632
|
|
|
633
|
-
```typescript
|
|
633
|
+
```typescript fragment
|
|
634
634
|
// anywhere in your app
|
|
635
635
|
CarbonInterval.seconds(90).cascade();
|
|
636
636
|
// → { minutes: 1, seconds: 30 }
|
|
@@ -653,7 +653,7 @@ CarbonInterval.days(32).cascade(Carbon.today().inTimezone("UTC").toZonedDateTime
|
|
|
653
653
|
|
|
654
654
|
Calendar units (years, months) are approximated as average lengths.
|
|
655
655
|
|
|
656
|
-
```typescript
|
|
656
|
+
```typescript fragment
|
|
657
657
|
// anywhere in your app
|
|
658
658
|
CarbonInterval.hours(2).andMinutes(30).totalMinutes(); // 150
|
|
659
659
|
CarbonInterval.days(3).andHours(6).totalHours(); // 78
|
|
@@ -667,7 +667,7 @@ i.totalWeeks();
|
|
|
667
667
|
|
|
668
668
|
### Interval comparison
|
|
669
669
|
|
|
670
|
-
```typescript
|
|
670
|
+
```typescript fragment
|
|
671
671
|
// anywhere in your app
|
|
672
672
|
const a = CarbonInterval.hours(2);
|
|
673
673
|
const b = CarbonInterval.minutes(90);
|
|
@@ -681,7 +681,7 @@ CarbonInterval.compare(a, b); // 1 (a > b), -1 (a < b), 0 (equal)
|
|
|
681
681
|
|
|
682
682
|
### Human-readable output
|
|
683
683
|
|
|
684
|
-
```typescript
|
|
684
|
+
```typescript fragment
|
|
685
685
|
// anywhere in your app
|
|
686
686
|
CarbonInterval.days(1).andHours(2).andMinutes(30).forHumans();
|
|
687
687
|
// → '1 day 2 hours 30 minutes'
|
|
@@ -697,7 +697,7 @@ i.toString(); // alias for forHumans()
|
|
|
697
697
|
|
|
698
698
|
### Interval serialisation
|
|
699
699
|
|
|
700
|
-
```typescript
|
|
700
|
+
```typescript fragment
|
|
701
701
|
// anywhere in your app
|
|
702
702
|
CarbonInterval.days(1).andHours(2).toISO();
|
|
703
703
|
// → 'P1DT2H'
|
|
@@ -715,7 +715,7 @@ An interval is what [`add()` and `subtract()`](#add-and-subtract) accept, and wh
|
|
|
715
715
|
[`diffAsCarbonInterval()`](#differences) returns — the two directions between the
|
|
716
716
|
types:
|
|
717
717
|
|
|
718
|
-
```typescript
|
|
718
|
+
```typescript fragment
|
|
719
719
|
// anywhere in your app
|
|
720
720
|
const interval = CarbonInterval.days(3).andHours(6);
|
|
721
721
|
|
|
@@ -732,7 +732,7 @@ immutable and pure, so it needs no application — but a test that reaches for
|
|
|
732
732
|
**Pin the instant.** Pass a fixed input rather than using the current time, and
|
|
733
733
|
the assertion holds forever:
|
|
734
734
|
|
|
735
|
-
```typescript
|
|
735
|
+
```typescript fragment
|
|
736
736
|
// tests/dates/BillingPeriod.test.ts
|
|
737
737
|
import { test, expect } from "bun:test";
|
|
738
738
|
import { Carbon } from "zerotal/carbon";
|
|
@@ -751,7 +751,7 @@ test("a mid-month signup bills to the end of the month", () => {
|
|
|
751
751
|
calls `Carbon.now()` internally can only be tested by waiting or by mocking; one
|
|
752
752
|
that accepts a `now` parameter is tested by passing a date:
|
|
753
753
|
|
|
754
|
-
```typescript
|
|
754
|
+
```typescript fragment
|
|
755
755
|
// app/services/billing.ts
|
|
756
756
|
export function periodFor(signedUp: Carbon, now: Carbon = Carbon.now()): Period {
|
|
757
757
|
// …
|
|
@@ -766,7 +766,7 @@ month end.
|
|
|
766
766
|
transitions are where date code breaks, and none of them appear in a test written
|
|
767
767
|
around today:
|
|
768
768
|
|
|
769
|
-
```typescript
|
|
769
|
+
```typescript fragment
|
|
770
770
|
// tests/dates/BillingPeriod.test.ts
|
|
771
771
|
test("handles a leap day", () => {
|
|
772
772
|
expect(Carbon.create("2028-02-29T12:00:00Z").addYears(1).toDateString()).toBe("2029-02-28");
|
package/docs/changelog.md
CHANGED
|
@@ -27,9 +27,173 @@ the section for every version you cross and apply its migration notes, not only
|
|
|
27
27
|
majors. [Releases and versioning](/docs/support-policy#releases-and-versioning) explains
|
|
28
28
|
when that carve-out ends.
|
|
29
29
|
|
|
30
|
+
## 1.7.5 — 2026-08-23
|
|
31
|
+
|
|
32
|
+
Two bugs that shipped to every deployed app, a package promoted to `stable`, and
|
|
33
|
+
the gates that would have caught both.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **`@zerotal/arch` is `stable`.** Reviewed ahead of its 1.9.0 date. The API follows
|
|
38
|
+
SemVer strictly from here, and that promise covers the **MCP tool contract** — tool
|
|
39
|
+
names, their arguments, and the shape of what they return. That is what an agent
|
|
40
|
+
client is configured against, and nothing type-level can see it: `archTools` has the
|
|
41
|
+
same signature however the tools are named. `mcp-surface.md` records all nine and CI
|
|
42
|
+
diffs it on every change. The protocol revision the server speaks is not covered; it
|
|
43
|
+
follows the protocol.
|
|
44
|
+
|
|
45
|
+
- **INTERNAL — the writers behind `arch:install` are no longer public API.**
|
|
46
|
+
`detectAgents`, `applyMcpConfig`, `applyBlock`, `buildGuidelines` and the rest are
|
|
47
|
+
`@internal`: still exported, still working, no longer promised. Their only caller is
|
|
48
|
+
the install command, and freezing them would have committed the shape of `.mcp.json`
|
|
49
|
+
writing to the rest of the 1.x line on behalf of a caller who never arrived.
|
|
50
|
+
|
|
51
|
+
- **INTERNAL — `api-surface.md` honours `@internal` across every package.** The
|
|
52
|
+
contract has always read "anything importable without an `@internal` marker keeps its
|
|
53
|
+
shape", and the generator did not read the tag — so symbols already marked internal
|
|
54
|
+
were recorded as though promised. 374 entries across 13 packages are omitted now,
|
|
55
|
+
every one verified marked. Nothing changes at runtime or in the types; the file
|
|
56
|
+
listing the promises now lists the promises.
|
|
57
|
+
|
|
58
|
+
- **A modal locks the page behind it.** `<Modal>` and `<Drawer>` trapped focus
|
|
59
|
+
correctly while the page underneath kept scrolling, which on a phone reads as the
|
|
60
|
+
dialog having broken the page.
|
|
61
|
+
|
|
62
|
+
- **Flow marks the active nav link for everyone.** `<Link navigate>` set
|
|
63
|
+
`data-current`, which styles a link, and nothing that announces it. It sets
|
|
64
|
+
`aria-current="page"` alongside now, so a screen reader can tell which of thirty nav
|
|
65
|
+
items is the current page.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
|
|
69
|
+
- **Assets were cache-busted in development and not in production.** `asset()` appended
|
|
70
|
+
`?v=` only when a dev version was set, so every deployed Zerotal app served the
|
|
71
|
+
previous build's JavaScript and CSS to anyone with a warm cache — indefinitely, since
|
|
72
|
+
the URL never changed. The version is now derived from the built files themselves, so
|
|
73
|
+
it is stable across restarts and moves when the files do.
|
|
74
|
+
|
|
75
|
+
- **DevTools mounted on production pages.** The provider is gated on the environment, so
|
|
76
|
+
the endpoints are absent outside development — and the client took that to mean it
|
|
77
|
+
could start anyway, pinning a floating panel to the page whose tabs read
|
|
78
|
+
`Could not read the map — HTTP 404`. It now mounts only when the server half says it
|
|
79
|
+
is there, via a `<meta>` the middleware writes, and makes no request at all on a
|
|
80
|
+
public hostname.
|
|
81
|
+
|
|
82
|
+
- **Browser tests drove an unstyled site.** `Router.static("/", public)` is registered
|
|
83
|
+
only for the `web` environment, and a test app is not one — so every `FlowBrowser`
|
|
84
|
+
suite served pages without their stylesheet. Invisible to assertions that read text;
|
|
85
|
+
fatal for anything measuring layout.
|
|
86
|
+
|
|
87
|
+
### Documented
|
|
88
|
+
|
|
89
|
+
- **Every TypeScript example in the documentation is compiled against the real
|
|
90
|
+
packages**, on every pull request. 1,593 blocks. The gate found examples importing
|
|
91
|
+
symbols that do not exist (`currentUser`, `Layout` from the wrong package), calling
|
|
92
|
+
methods that were renamed (`Cache.put`), and configuring fields with `env()` where the
|
|
93
|
+
type is a literal union. Blocks deliberately written as fragments say so in their
|
|
94
|
+
fence and are recorded by key, so a new one is a deliberate act rather than a silent
|
|
95
|
+
exemption.
|
|
96
|
+
|
|
97
|
+
- **A break cannot ship without a release note.** `api:surface:check` demands a
|
|
98
|
+
regenerated snapshot when an export changes and then goes quiet, so the changelog was
|
|
99
|
+
defended by remembering — and 1.7.3 shipped the removal of Flow's `this.title(…)` with
|
|
100
|
+
no BREAKING entry. That entry is now in 1.7.3's notes, the support policy counts three
|
|
101
|
+
breaks rather than two, and `breaking:check` reads the snapshot diff so the next one
|
|
102
|
+
cannot pass silently.
|
|
103
|
+
|
|
104
|
+
- **A maturity label falls due.** The review release for a package below `stable` lives
|
|
105
|
+
in its `package.json` as `maturityReview`, and the package-conventions gate fails once
|
|
106
|
+
the version reaches it.
|
|
107
|
+
|
|
108
|
+
## 1.7.4 — 2026-08-21
|
|
109
|
+
|
|
110
|
+
A debug panel that was reaching production, a column type MySQL would not index, and
|
|
111
|
+
2,060 icons.
|
|
112
|
+
|
|
113
|
+
### Fixed
|
|
114
|
+
|
|
115
|
+
- **DevTools no longer appears on a production page.** The provider is gated on the
|
|
116
|
+
environment, so in production its routes are absent — and the browser client took that as
|
|
117
|
+
permission to start anyway and "connect to nothing". It did not: it mounted the panel first
|
|
118
|
+
and discovered the absence afterwards, so an app calling `DevTools.start()` unconditionally
|
|
119
|
+
served a floating DevTools bar to every visitor, its tabs reading
|
|
120
|
+
`Could not read the map — HTTP 404`.
|
|
121
|
+
|
|
122
|
+
`start()` now probes for the routes and builds nothing unless they answer — no shell, no
|
|
123
|
+
shadow root, no `EventSource`, no listeners. Any failure (404, offline, CSP) is read as
|
|
124
|
+
absent. **If your app calls `DevTools.start()`, take this release.**
|
|
125
|
+
|
|
126
|
+
- **A string column could not carry an index on MySQL.** `table.string()` compiled to `TEXT`
|
|
127
|
+
on every engine and discarded its `length`, and MySQL refuses to key a TEXT column without
|
|
128
|
+
a prefix length — so `table.string("email").unique()` failed at `CREATE TABLE`. MySQL now
|
|
129
|
+
gets `VARCHAR(length)`; SQLite and PostgreSQL keep `TEXT`. `char()` had the same bug and the
|
|
130
|
+
same fix. Found by the new MySQL suite on its first run against a real server.
|
|
131
|
+
|
|
132
|
+
### Added
|
|
133
|
+
|
|
134
|
+
- **`<Icon name="inbox" />` — 2,060 icons, bundled, typed by name.** The set ships inside
|
|
135
|
+
`@zerotal/flow-ui`, so there is nothing to install and no generator to run: a fresh app gets
|
|
136
|
+
autocomplete over every name and a compile error on a typo. Rendered on the server as inline
|
|
137
|
+
SVG, so there is no icon font, no sprite, no request per glyph, and nothing for a strict CSP
|
|
138
|
+
to block. Four icons are drawn for sign-in flows the set has no name for — `passkey`,
|
|
139
|
+
`two-factor`, `otp`, `magic-link` — and three brand marks ship for the social-login providers
|
|
140
|
+
`@zerotal/auth` supports. See [Icons](/docs/flow/icons).
|
|
141
|
+
|
|
142
|
+
- **The ORM suite runs against MySQL 8 in CI, and the job blocks merges.** The same smoke
|
|
143
|
+
suite that covers PostgreSQL — schema DDL and `ALTER`, identity columns, CRUD, type
|
|
144
|
+
round-trips, unique and NOT NULL enforcement, row locks, transaction rollback. MySQL moves
|
|
145
|
+
from _experimental_ to _supported, hardening_; see the
|
|
146
|
+
[Support Policy](/docs/support-policy).
|
|
147
|
+
|
|
148
|
+
### Changed
|
|
149
|
+
|
|
150
|
+
- **The starters link by route name.** Every hard-coded `href="/about"` in the React and Vue
|
|
151
|
+
templates now goes through `route()`, and the templates ship the generated route table so a
|
|
152
|
+
freshly scaffolded app type-checks before its first `zt dev`.
|
|
153
|
+
|
|
154
|
+
### Documented
|
|
155
|
+
|
|
156
|
+
- **The HTTP client guide is one page.** Eight pages became one, written from where the
|
|
157
|
+
package is used — your app calling somebody else's service — with straight URLs instead of
|
|
158
|
+
a route map threaded through every example. See [HTTP Client](/docs/client).
|
|
159
|
+
|
|
160
|
+
- **`route()` in Inertia**, for links and for form submissions, including the one thing Inertia
|
|
161
|
+
adds: a page renders in two processes, so `defineRoutes()` has to run in the SSR entry too.
|
|
162
|
+
See [Building URLs](/docs/inertia/rendering#building-urls-with-route).
|
|
163
|
+
|
|
164
|
+
- **Every package changelog has the release headings it was missing.** `[Unreleased]` had
|
|
165
|
+
accumulated four releases of shipped work — `@zerotal/flow-ui`'s newest heading read
|
|
166
|
+
`[1.5.0]` while 1.7.3 was on npm. Cutting a release now moves them.
|
|
167
|
+
|
|
30
168
|
## 1.7.3 — 2026-08-20
|
|
31
169
|
|
|
32
|
-
Two fields that accepted input and threw it away,
|
|
170
|
+
Two fields that accepted input and threw it away, a CI job that was testing nothing, and a
|
|
171
|
+
name given back to applications.
|
|
172
|
+
|
|
173
|
+
### Changed
|
|
174
|
+
|
|
175
|
+
- **BREAKING — `this.title(…)` is removed from Flow components.** Declare `static title`
|
|
176
|
+
instead, as a string or a function of the component:
|
|
177
|
+
|
|
178
|
+
```ts fragment
|
|
179
|
+
// Before
|
|
180
|
+
override async mount(): Promise<void> {
|
|
181
|
+
this.title(`Search: ${this.query}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// After
|
|
185
|
+
static title = (c: SearchPage) => (c.query ? `Search: ${c.query}` : "Search");
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The instance method held a name four separate components wanted for their own data — a
|
|
189
|
+
media row, a guide, a review, an issue — for a one-line accessor that belongs on the class.
|
|
190
|
+
The static form is also the better one: it is resolved on the server for every render and
|
|
191
|
+
every patch, so a title that depends on state follows it without an action remembering to
|
|
192
|
+
update it.
|
|
193
|
+
|
|
194
|
+
A call to `this.title(…)` on a component that declares its own `title` field now sets that
|
|
195
|
+
field instead of the document title, which is silent. Search your components for
|
|
196
|
+
`this.title(` before upgrading; every hit is either a migration or was already shadowed.
|
|
33
197
|
|
|
34
198
|
### Fixed
|
|
35
199
|
|