@zerotal/arch 1.7.4 → 1.8.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 +86 -2
- package/README.md +4 -2
- package/api-surface.md +2 -0
- 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 +268 -1
- package/docs/client/index.md +13 -13
- package/docs/commands.md +36 -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 +28 -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 +99 -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 +5 -5
- package/docs/flow/index.md +21 -21
- package/docs/flow/layouts.md +99 -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 +6 -6
- package/docs/inertia/props.md +21 -21
- package/docs/inertia/rendering.md +9 -9
- 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 +23 -10
- 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 +29 -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/config.ts +8 -0
- package/src/index.ts +25 -1
- package/src/install/ArchInstallCommand.ts +46 -3
- package/src/install/guidelines.ts +84 -0
- package/src/install/shape.ts +177 -0
- package/src/install/skills.ts +271 -0
- package/src/provider/ArchProvider.ts +148 -0
- package/src/tools/searchDocs.ts +7 -0
|
@@ -59,7 +59,7 @@ rules and signs the result with the app's `APP_KEY`, and the client echoes the s
|
|
|
59
59
|
or other headers via `auth.headers`, or set `authEndpoint: false` to skip the fetch and rely on
|
|
60
60
|
connection-level authorization instead:
|
|
61
61
|
|
|
62
|
-
```ts
|
|
62
|
+
```ts fragment
|
|
63
63
|
// in your client code
|
|
64
64
|
const socket = new Socket({
|
|
65
65
|
auth: { headers: { "X-CSRF-TOKEN": csrf } }, // sent on POST /broadcasting/auth
|
|
@@ -76,7 +76,7 @@ client.
|
|
|
76
76
|
The server is also Pusher-protocol compatible, so the reference
|
|
77
77
|
[pusher-js](https://github.com/pusher/pusher-js) client works unchanged:
|
|
78
78
|
|
|
79
|
-
```ts
|
|
79
|
+
```ts fragment
|
|
80
80
|
// in your client code
|
|
81
81
|
import Pusher from "pusher-js";
|
|
82
82
|
|
|
@@ -7,7 +7,7 @@ description: Write a broadcast event, dispatch it, and broadcast model changes a
|
|
|
7
7
|
|
|
8
8
|
You only have to implement `broadcastOn()`; the rest default sensibly.
|
|
9
9
|
|
|
10
|
-
```ts
|
|
10
|
+
```ts fragment
|
|
11
11
|
// app/events/OrderShipmentStatusUpdated.ts
|
|
12
12
|
import { BroadcastingEvent, privateChannel } from "@zerotal/broadcasting";
|
|
13
13
|
import type { Order } from "../models/Order.ts";
|
|
@@ -48,7 +48,7 @@ export class OrderShipmentStatusUpdated extends BroadcastingEvent {
|
|
|
48
48
|
|
|
49
49
|
Three ways to broadcast, in order of ergonomics:
|
|
50
50
|
|
|
51
|
-
```ts
|
|
51
|
+
```ts fragment
|
|
52
52
|
// in a controller
|
|
53
53
|
import { broadcast, Broadcast } from "@zerotal/broadcasting";
|
|
54
54
|
|
|
@@ -75,7 +75,7 @@ several statements; otherwise treat `broadcast(...)` as the whole call.
|
|
|
75
75
|
|
|
76
76
|
A broadcastable event emitted on the [event bus](/docs/events) is broadcast automatically:
|
|
77
77
|
|
|
78
|
-
```ts
|
|
78
|
+
```ts fragment
|
|
79
79
|
// in a controller
|
|
80
80
|
import { Events } from "zerotal";
|
|
81
81
|
|
|
@@ -84,7 +84,7 @@ Events.emit(new OrderShipmentStatusUpdated(order)); // runs listeners AND broadc
|
|
|
84
84
|
|
|
85
85
|
Broadcast to multiple channels by returning an array from `broadcastOn()`:
|
|
86
86
|
|
|
87
|
-
```ts
|
|
87
|
+
```ts fragment
|
|
88
88
|
// in an event's broadcastOn()
|
|
89
89
|
broadcastOn() {
|
|
90
90
|
return [privateChannel(`orders.${this.order.id}`), privateChannel(`users.${this.order.userId}`)];
|
|
@@ -95,7 +95,7 @@ broadcastOn() {
|
|
|
95
95
|
|
|
96
96
|
When a full event class is overkill, broadcast inline:
|
|
97
97
|
|
|
98
|
-
```ts
|
|
98
|
+
```ts fragment
|
|
99
99
|
// in a controller
|
|
100
100
|
Broadcast.on(`orders.${order.id}`).as("OrderPlaced").with(order).toOthers().send();
|
|
101
101
|
Broadcast.private(`orders.${order.id}`).as("OrderPlaced").with({ id: order.id }).send();
|
|
@@ -110,7 +110,7 @@ Broadcast.to("posts", "PostViewed", { id: post.id, viewedAt: Date.now() });
|
|
|
110
110
|
Get the members currently subscribed to a presence channel (real driver only —
|
|
111
111
|
returns `[]` under the fake/null driver):
|
|
112
112
|
|
|
113
|
-
```ts
|
|
113
|
+
```ts fragment
|
|
114
114
|
// in a controller
|
|
115
115
|
const members = Broadcast.getMembers("presence-chat.room1");
|
|
116
116
|
// → [{ id, info }, …]
|
|
@@ -122,7 +122,7 @@ Broadcast a model's lifecycle changes by mapping them to a `BroadcastingEvent` t
|
|
|
122
122
|
`dispatchesEvents`. When the model fires the event on the [event bus](/docs/events), it is
|
|
123
123
|
broadcast automatically (see [dispatching](#dispatching-events)) — no manual broadcast call.
|
|
124
124
|
|
|
125
|
-
```ts
|
|
125
|
+
```ts fragment
|
|
126
126
|
// app/events/PostCreated.ts
|
|
127
127
|
import { BroadcastingEvent, privateChannel } from "@zerotal/broadcasting";
|
|
128
128
|
import type { Post } from "../models/Post.ts";
|
|
@@ -140,7 +140,7 @@ export class PostCreated extends BroadcastingEvent {
|
|
|
140
140
|
}
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
```ts
|
|
143
|
+
```ts fragment
|
|
144
144
|
// app/models/Post.ts
|
|
145
145
|
@table("posts")
|
|
146
146
|
export class Post extends Model {
|
|
@@ -164,7 +164,6 @@ import { broadcastsModelEvents, privateChannel } from "@zerotal/broadcasting";
|
|
|
164
164
|
|
|
165
165
|
@table("orders")
|
|
166
166
|
export class Order extends Model {
|
|
167
|
-
@column() id!: number;
|
|
168
167
|
@column() status!: string;
|
|
169
168
|
}
|
|
170
169
|
|
|
@@ -179,7 +178,7 @@ broadcastsModelEvents(Order, {
|
|
|
179
178
|
The wire event name defaults to `${ModelName}${Event}` (e.g. `OrderUpdated`) and the payload to
|
|
180
179
|
`{ order }` (the model under its camel-cased name). On the client:
|
|
181
180
|
|
|
182
|
-
```ts
|
|
181
|
+
```ts fragment
|
|
183
182
|
// in your client code
|
|
184
183
|
Socket.private(`orders.${id}`).listen("OrderUpdated", (e) => render(e.order));
|
|
185
184
|
```
|
|
@@ -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");
|