@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
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,95 @@ All notable changes to this package are documented here. The format is
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
**Maturity:
|
|
8
|
-
|
|
7
|
+
**Maturity: `stable`** — matching this package's `maturity` field. The public API
|
|
8
|
+
follows SemVer strictly: anything importable without an `@internal` marker keeps its
|
|
9
|
+
shape for the rest of the 1.x line, and `api-surface.md` is diffed by CI on every
|
|
10
|
+
change. That promise covers the MCP tool contract too — the tool names, their inputs
|
|
11
|
+
and the shape of what they return — which is the part an agent client is configured
|
|
12
|
+
against. `mcp-surface.md` is diffed alongside it.
|
|
9
13
|
|
|
10
14
|
## [Unreleased]
|
|
11
15
|
|
|
16
|
+
## [1.8.0] — 2026-08-24
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **Agent skills — the depth the upfront block cannot afford.** The generated `AGENTS.md`
|
|
21
|
+
is short because every prompt it lands in pays for its whole length, so it points rather
|
|
22
|
+
than teaches. That constraint has a cost: an agent gets a map and no detail, and the
|
|
23
|
+
detail is where the expensive mistakes live. A skill is a file with a one-line
|
|
24
|
+
description that costs nothing until an agent decides it is relevant, so a procedure can
|
|
25
|
+
be written out in full. Two ship — `zerotal-schema-changes` (who owns the schema here,
|
|
26
|
+
the mixin columns nothing declares, and why an unguarded `ALTER TABLE` collides during a
|
|
27
|
+
release's `migrate`) and `zerotal-releases` (naming your own deploy steps, replacing the
|
|
28
|
+
asset directory rather than merging into it, `trustedProxies` behind a proxy, and the
|
|
29
|
+
pipe that hides a test suite's exit status). Both are shaped by the project like the
|
|
30
|
+
block is: the schema skill states this app's actual answer, and refuses to pick a side
|
|
31
|
+
when `synchronize` and migrations are both in play.
|
|
32
|
+
|
|
33
|
+
Written to `.agents/skills`, the cross-client path, plus `.claude/skills` when that agent
|
|
34
|
+
is detected. Frontmatter descriptions are JSON-quoted, because unquoted a colon makes the
|
|
35
|
+
rest a nested mapping and the file stops parsing — an inert skill looks exactly like an
|
|
36
|
+
installed one. Overriding a shipped skill is deleting its marker line: a `SKILL.md`
|
|
37
|
+
without the marker is yours and is never rewritten. Turn the whole feature off with
|
|
38
|
+
`ArchConfig({ skills: false })`.
|
|
39
|
+
|
|
40
|
+
- **`doctor` reports agent instructions that no longer describe this project.** The block
|
|
41
|
+
used to describe the framework, which moved when the framework did. It now also describes
|
|
42
|
+
the project, and every fact in it moves without anyone thinking about the file — add a
|
|
43
|
+
migrations directory, turn `synchronize` off, install a package. It still reads as current
|
|
44
|
+
while describing the app you used to have, and guidance that is confidently out of date
|
|
45
|
+
gets followed. Skills rot the same way and are easier to miss, because nothing reads one
|
|
46
|
+
until an agent decides it is relevant, by which point it is being acted on. The check
|
|
47
|
+
regenerates both in memory and compares, naming `arch:update` as the fix. A warning, never
|
|
48
|
+
a failure: a misleading instruction file does not stop an application working, and this
|
|
49
|
+
check earns the right to gate a deploy by failing only for what would. The server name is
|
|
50
|
+
read back from `.mcp.json` rather than assumed, so a project that renamed its server is
|
|
51
|
+
not reported as permanently stale.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- **The generated `AGENTS.md` block describes how this app is set up, not only what it
|
|
56
|
+
installed.** A package list answers "what is available here", which is not the question that
|
|
57
|
+
decides what to write: the framework's contracts are not uniform across projects, and the
|
|
58
|
+
places they differ are the places where guessing wrong compiles cleanly and fails at runtime.
|
|
59
|
+
`detectShape()` reads four facts off disk and the block states only the ones that change an
|
|
60
|
+
instruction — who owns the schema, whether route names are typed, whether
|
|
61
|
+
`exactOptionalPropertyTypes` or `noUncheckedIndexedAccess` are on, and whether there are
|
|
62
|
+
tests to run. Read from files rather than a booted app, because a project that will not boot
|
|
63
|
+
is often why the agent surface is being installed; `.env` is deliberately not among them,
|
|
64
|
+
since this output is committed and pasted into prompts. The `extends` chain is followed, so
|
|
65
|
+
an app inheriting a strict base is not read as unstrict. Additive — a caller that passes no
|
|
66
|
+
shape gets the block it got before.
|
|
67
|
+
|
|
68
|
+
## [1.7.5] — 2026-08-22
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- **`@zerotal/arch` is `stable`.** Reviewed ahead of its 1.9.0 date. The public API
|
|
73
|
+
follows SemVer strictly from here, and that promise covers the MCP tool contract —
|
|
74
|
+
tool names, their inputs, and the shape of what they return — because that is what
|
|
75
|
+
an agent client is configured against and nothing type-level can see it. The
|
|
76
|
+
protocol revision the server speaks is not covered; it follows the protocol.
|
|
77
|
+
|
|
78
|
+
- **INTERNAL — the writers behind `arch:install` are no longer public API.** `detectAgents`,
|
|
79
|
+
`applyMcpConfig`, `serverEntry`, `SERVER_ENTRY_PATH`, `applyBlock`, `fence`,
|
|
80
|
+
`BLOCK_START`, `BLOCK_END`, `agentsPreamble`, `buildGuidelines`, `claudeShim` and
|
|
81
|
+
their types. They are still exported and still work; they are no longer promised.
|
|
82
|
+
Their only caller is `ArchInstallCommand`, and freezing them would have committed
|
|
83
|
+
the shape of `.mcp.json` writing and marker fencing to the rest of the 1.x line on
|
|
84
|
+
behalf of a caller who never arrived. Marked before the label attached rather than
|
|
85
|
+
withdrawn after.
|
|
86
|
+
|
|
87
|
+
- **INTERNAL — `api-surface.md` now honours `@internal` across every package.** The
|
|
88
|
+
contract has always been stated as "anything importable without an `@internal`
|
|
89
|
+
marker keeps its shape", and the generator did not read the tag: symbols already
|
|
90
|
+
marked internal were recorded as though promised. They are omitted now — 374
|
|
91
|
+
entries across 13 packages, every one verified marked, either at its declaration
|
|
92
|
+
or by a module docblock covering a whole subpath as `@zerotal/core/dev` does.
|
|
93
|
+
Nothing changes at runtime or in the types. What changed is that the file listing
|
|
94
|
+
the promises lists the promises.
|
|
95
|
+
|
|
12
96
|
## [1.7.1] — 2026-08-16
|
|
13
97
|
|
|
14
98
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# @zerotal/arch
|
|
2
2
|
|
|
3
|
-
**Maturity:
|
|
4
|
-
|
|
3
|
+
**Maturity: stable.** The public API follows SemVer strictly: anything importable
|
|
4
|
+
without an `@internal` marker keeps its shape for the rest of the 1.x line. That
|
|
5
|
+
covers the MCP tool contract too — tool names, their inputs, and the shape of what
|
|
6
|
+
they return — since that is what an agent client is configured against. See
|
|
5
7
|
the [support policy](https://zerotal.dev/docs/support-policy#maturity-levels).
|
|
6
8
|
|
|
7
9
|
The Zerotal agent surface: an MCP server that hands a coding agent the framework's
|
package/api-surface.md
CHANGED
|
@@ -94,6 +94,7 @@ interface ArchConfigShape = {
|
|
|
94
94
|
mcpConfig: boolean
|
|
95
95
|
mcpConfigPath: string
|
|
96
96
|
serverName: string
|
|
97
|
+
skills: boolean
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
interface Detected = {
|
|
@@ -118,6 +119,7 @@ interface DoctorReport = {
|
|
|
118
119
|
interface GuidelineOptions = {
|
|
119
120
|
packages: string[]
|
|
120
121
|
serverName: string
|
|
122
|
+
shape?: ProjectShape
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
interface InstalledPackage = {
|
package/docs/about.md
CHANGED
|
@@ -131,7 +131,7 @@ my-app/
|
|
|
131
131
|
`zt.ts` is the entry point for **everything** — `bun zt serve`, `migrate`, `test`,
|
|
132
132
|
and every `make:*` generator run through it. It boots `bootstrap/app.ts`:
|
|
133
133
|
|
|
134
|
-
```ts
|
|
134
|
+
```ts fragment
|
|
135
135
|
// bootstrap/app.ts
|
|
136
136
|
import { Application } from "zerotal";
|
|
137
137
|
import { Handler } from "../app/exceptions/Handler.ts";
|
|
@@ -166,7 +166,7 @@ Read [Request Lifecycle](/docs/lifecycle) for the exact sequence.
|
|
|
166
166
|
Routes are registered by calling static `Router` methods at module load — map a path
|
|
167
167
|
to a controller + action, or to an inline closure:
|
|
168
168
|
|
|
169
|
-
```ts
|
|
169
|
+
```ts fragment
|
|
170
170
|
// routes/index.ts
|
|
171
171
|
import { Router, view, type HttpContext } from "zerotal";
|
|
172
172
|
import HomeController from "../app/controllers/HomeController.ts";
|
|
@@ -200,7 +200,7 @@ the app builder. Full details in [Routing](/docs/routing).
|
|
|
200
200
|
Controllers are plain classes; the action receives the request `HttpContext` and
|
|
201
201
|
dependencies resolve from the container:
|
|
202
202
|
|
|
203
|
-
```ts
|
|
203
|
+
```ts fragment
|
|
204
204
|
// app/controllers/PostController.ts
|
|
205
205
|
import type { HttpContext } from "zerotal";
|
|
206
206
|
import Post from "../models/Post.ts";
|
|
@@ -233,7 +233,7 @@ e.g. `ctx: HttpContext<{ post: Post }>`. More in [Controllers](/docs/controllers
|
|
|
233
233
|
Active Record–style models backed by `Bun.sql`. Columns and relationships are
|
|
234
234
|
decorators; the table name is derived by convention (so `@table` is optional):
|
|
235
235
|
|
|
236
|
-
```ts
|
|
236
|
+
```ts fragment
|
|
237
237
|
// app/models/Post.ts
|
|
238
238
|
import { Model, table, column, hasMany, belongsTo } from "@zerotal/orm";
|
|
239
239
|
|
|
@@ -255,7 +255,7 @@ export default class Post extends Model {
|
|
|
255
255
|
|
|
256
256
|
The fluent query builder and relationship loading:
|
|
257
257
|
|
|
258
|
-
```ts
|
|
258
|
+
```ts fragment
|
|
259
259
|
// in a controller or service
|
|
260
260
|
const posts = await Post.query()
|
|
261
261
|
.withScopes((s) => s.published())
|
|
@@ -321,7 +321,7 @@ export class StorePostRequest extends FormRequest {
|
|
|
321
321
|
}
|
|
322
322
|
```
|
|
323
323
|
|
|
324
|
-
```ts
|
|
324
|
+
```ts fragment
|
|
325
325
|
// in a controller — reads the current HttpContext, returns typed data,
|
|
326
326
|
// throws a 422 (JSON) or a redirect-back on failure.
|
|
327
327
|
const data = await StorePostRequest.validate();
|
|
@@ -333,7 +333,7 @@ More rules and patterns in [Validation](/docs/validator).
|
|
|
333
333
|
|
|
334
334
|
Session auth, bearer tokens, and policy-based authorization ship together:
|
|
335
335
|
|
|
336
|
-
```ts
|
|
336
|
+
```ts fragment
|
|
337
337
|
// in a controller / service
|
|
338
338
|
import { Auth, Gate, createToken } from "@zerotal/auth";
|
|
339
339
|
|
|
@@ -415,7 +415,7 @@ decorator and directive set.
|
|
|
415
415
|
Build a React or Vue SPA with no separate API layer. Controllers return page
|
|
416
416
|
responses; the Inertia client renders the matching component:
|
|
417
417
|
|
|
418
|
-
```ts
|
|
418
|
+
```ts fragment
|
|
419
419
|
// app/controllers/DashboardController.ts
|
|
420
420
|
import { inertia } from "@zerotal/inertia";
|
|
421
421
|
import { Post } from "../models/Post.ts";
|
|
@@ -428,7 +428,7 @@ export class DashboardController {
|
|
|
428
428
|
}
|
|
429
429
|
```
|
|
430
430
|
|
|
431
|
-
```tsx
|
|
431
|
+
```tsx fragment
|
|
432
432
|
// resources/js/pages/Dashboard.tsx (React)
|
|
433
433
|
import { Link } from "@inertiajs/react";
|
|
434
434
|
|
|
@@ -457,7 +457,7 @@ reloads, deferred props, SSR, and precognition. See [Inertia](/docs/inertia).
|
|
|
457
457
|
Push slow work off the request. Jobs serialize to a plain payload so they survive the
|
|
458
458
|
queue:
|
|
459
459
|
|
|
460
|
-
```ts
|
|
460
|
+
```ts fragment
|
|
461
461
|
// app/jobs/SendWelcomeEmail.ts
|
|
462
462
|
import { Job, JobRegistry } from "@zerotal/queue";
|
|
463
463
|
import { User } from "../models/User.ts";
|
|
@@ -487,7 +487,7 @@ export class SendWelcomeEmail extends Job {
|
|
|
487
487
|
JobRegistry.register(SendWelcomeEmail);
|
|
488
488
|
```
|
|
489
489
|
|
|
490
|
-
```ts
|
|
490
|
+
```ts fragment
|
|
491
491
|
// dispatch from anywhere
|
|
492
492
|
import { Queue, Bus } from "@zerotal/queue";
|
|
493
493
|
|
|
@@ -504,7 +504,7 @@ Run a worker with `bun zt queue:work`. There's also a
|
|
|
504
504
|
|
|
505
505
|
## Cache
|
|
506
506
|
|
|
507
|
-
```ts
|
|
507
|
+
```ts fragment
|
|
508
508
|
// in a controller or service
|
|
509
509
|
import { Cache } from "@zerotal/cache";
|
|
510
510
|
|
|
@@ -512,7 +512,7 @@ const posts = await Cache.remember("posts.recent", 60, () =>
|
|
|
512
512
|
Post.query().orderBy("created_at", "desc").limit(10).get(),
|
|
513
513
|
);
|
|
514
514
|
|
|
515
|
-
await Cache.
|
|
515
|
+
await Cache.set("key", value, 300);
|
|
516
516
|
await Cache.forget("key");
|
|
517
517
|
|
|
518
518
|
// Tag-based invalidation
|
|
@@ -527,7 +527,7 @@ In-memory and Redis drivers, plus idempotency helpers. See [Cache](/docs/cache).
|
|
|
527
527
|
One `Notification` class fans out across channels — mail, database, broadcast, Slack,
|
|
528
528
|
SMS:
|
|
529
529
|
|
|
530
|
-
```ts
|
|
530
|
+
```ts fragment
|
|
531
531
|
// app/notifications/InvoicePaid.ts
|
|
532
532
|
import { Notification, MailMessage } from "@zerotal/notifications";
|
|
533
533
|
|
|
@@ -548,7 +548,7 @@ export class InvoicePaid extends Notification {
|
|
|
548
548
|
}
|
|
549
549
|
```
|
|
550
550
|
|
|
551
|
-
```ts
|
|
551
|
+
```ts fragment
|
|
552
552
|
// The User model mixes in Notifiable, which provides .notify()
|
|
553
553
|
await user.notify(new InvoicePaid(invoice));
|
|
554
554
|
```
|
|
@@ -584,7 +584,7 @@ Generate or rotate the key with `bun zt key:generate`. Full system in
|
|
|
584
584
|
|
|
585
585
|
First-class HTTP, database, and fake helpers:
|
|
586
586
|
|
|
587
|
-
```ts
|
|
587
|
+
```ts fragment
|
|
588
588
|
// tests/posts.test.ts
|
|
589
589
|
import { createTestApp, Factory, assertDatabaseHas } from "@zerotal/testing";
|
|
590
590
|
import { NotificationFake } from "@zerotal/notifications";
|
package/docs/admin/actions.md
CHANGED
|
@@ -9,7 +9,7 @@ Actions power the row, header, and bulk buttons. The defaults are
|
|
|
9
9
|
`viewAction()/editAction()/deleteAction()` (rows), `createAction()` (header), and
|
|
10
10
|
`bulkDeleteAction()` (bulk) — override the corresponding methods to customize.
|
|
11
11
|
|
|
12
|
-
```ts
|
|
12
|
+
```ts fragment
|
|
13
13
|
import { action, editAction, deleteAction, textInput } from "@zerotal/admin";
|
|
14
14
|
|
|
15
15
|
static recordActions() {
|
|
@@ -28,7 +28,7 @@ static recordActions() {
|
|
|
28
28
|
A row with more than three visible actions collapses the surplus into an overflow
|
|
29
29
|
menu. Actions can also open a **modal form**:
|
|
30
30
|
|
|
31
|
-
```ts
|
|
31
|
+
```ts fragment
|
|
32
32
|
action("assign")
|
|
33
33
|
.label("Assign reviewer")
|
|
34
34
|
.form([ select("reviewer_id").options(reviewers).required() ])
|
|
@@ -43,7 +43,7 @@ action("assign")
|
|
|
43
43
|
A row with seven buttons is unreadable. `actionGroup` collapses several into one
|
|
44
44
|
labelled dropdown, so the row keeps the two people reach for and files the rest:
|
|
45
45
|
|
|
46
|
-
```ts
|
|
46
|
+
```ts fragment
|
|
47
47
|
import { actionGroup, replicateAction, deleteAction } from "@zerotal/admin";
|
|
48
48
|
|
|
49
49
|
static recordActions() {
|
|
@@ -67,7 +67,7 @@ nothing. Groups work in the header and the bulk toolbar too.
|
|
|
67
67
|
`replicateAction()` copies a record and opens the copy for editing. The primary key
|
|
68
68
|
and timestamps are always dropped; name anything else that must stay unique:
|
|
69
69
|
|
|
70
|
-
```ts
|
|
70
|
+
```ts fragment
|
|
71
71
|
replicateAction()
|
|
72
72
|
.excludeAttributes(["sku", "slug"])
|
|
73
73
|
.beforeReplicaSaved((data) => ({ ...data, name: `${data.name} (copy)`, status: "draft" })),
|
|
@@ -75,7 +75,7 @@ replicateAction()
|
|
|
75
75
|
|
|
76
76
|
## Import and export
|
|
77
77
|
|
|
78
|
-
```ts
|
|
78
|
+
```ts fragment
|
|
79
79
|
import { exportAction, importAction, bulkExportAction } from "@zerotal/admin";
|
|
80
80
|
|
|
81
81
|
static headerActions() {
|
|
@@ -94,7 +94,7 @@ exports the selection instead.
|
|
|
94
94
|
|
|
95
95
|
Pass `"xlsx"` for a spreadsheet instead:
|
|
96
96
|
|
|
97
|
-
```ts
|
|
97
|
+
```ts fragment
|
|
98
98
|
return [createAction(), exportAction(), exportAction("xlsx")];
|
|
99
99
|
```
|
|
100
100
|
|
|
@@ -107,7 +107,7 @@ header row is frozen with a filter over the used range.
|
|
|
107
107
|
Keep a column out of the file with `.exportable(false)`. Pay, internal notes and
|
|
108
108
|
anything else that should not travel in a spreadsheet belongs behind that flag:
|
|
109
109
|
|
|
110
|
-
```ts
|
|
110
|
+
```ts fragment
|
|
111
111
|
text("salary").exportable(false),
|
|
112
112
|
```
|
|
113
113
|
|
|
@@ -129,7 +129,7 @@ An import runs inline by default, capped at 2,000 rows — a synchronous import
|
|
|
129
129
|
holds a WebSocket round-trip open, and a bigger file looks like a hang. Hand it
|
|
130
130
|
to a queue instead and the cap lifts:
|
|
131
131
|
|
|
132
|
-
```ts
|
|
132
|
+
```ts fragment
|
|
133
133
|
static headerActions() {
|
|
134
134
|
return [createAction(), exportAction(), importAction({ queue: true })];
|
|
135
135
|
}
|
|
@@ -153,7 +153,7 @@ imported only when an import is actually queued.
|
|
|
153
153
|
|
|
154
154
|
Relation managers appear as tables on the View page.
|
|
155
155
|
|
|
156
|
-
```ts
|
|
156
|
+
```ts fragment
|
|
157
157
|
import { hasMany, belongsToMany } from "@zerotal/admin";
|
|
158
158
|
|
|
159
159
|
static relations() {
|
package/docs/admin/auth.md
CHANGED
|
@@ -9,7 +9,7 @@ Opt in with `Panel.auth({...})` (or `Panel.configure({ auth: {...} })`). The pag
|
|
|
9
9
|
live behind the `@zerotal/admin/auth` subpath, so the `@zerotal/auth` dependency
|
|
10
10
|
stays optional unless you enable them.
|
|
11
11
|
|
|
12
|
-
```ts
|
|
12
|
+
```ts fragment
|
|
13
13
|
import { Panel } from "@zerotal/admin";
|
|
14
14
|
|
|
15
15
|
Panel.auth({
|
|
@@ -52,7 +52,7 @@ left to your auth middleware — the rest of the flow ships here.
|
|
|
52
52
|
|
|
53
53
|
The panel is public until you set guard middleware. A typical setup:
|
|
54
54
|
|
|
55
|
-
```ts
|
|
55
|
+
```ts fragment
|
|
56
56
|
Panel.configure({ middleware: [AuthMiddleware, RequireRoleMiddleware.with("admin")] });
|
|
57
57
|
Panel.auth({ enabled: true }); // login lives outside that guard automatically
|
|
58
58
|
```
|
package/docs/admin/dashboard.md
CHANGED
|
@@ -7,7 +7,7 @@ description: Widgets, global search, the command palette, notifications, and the
|
|
|
7
7
|
|
|
8
8
|
Register widgets with `Panel.widgets(...)`; they render on the dashboard.
|
|
9
9
|
|
|
10
|
-
```ts
|
|
10
|
+
```ts fragment
|
|
11
11
|
import { Panel, statsWidget, stat, chartWidget, tableWidget } from "@zerotal/admin";
|
|
12
12
|
|
|
13
13
|
Panel.widgets(
|
|
@@ -41,7 +41,7 @@ The dashboard answers "how is the business doing". A resource's own widgets
|
|
|
41
41
|
answer "what is going on in _this_ list" — a pending count above the orders
|
|
42
42
|
table, stock value above products:
|
|
43
43
|
|
|
44
|
-
```ts
|
|
44
|
+
```ts fragment
|
|
45
45
|
export class OrderResource extends Resource {
|
|
46
46
|
static override widgets() {
|
|
47
47
|
return [
|
|
@@ -62,7 +62,7 @@ someone watching a queue actually wants.
|
|
|
62
62
|
A dashboard on a second screen is stale the moment it renders. `.poll()` gives a
|
|
63
63
|
widget an interval:
|
|
64
64
|
|
|
65
|
-
```ts
|
|
65
|
+
```ts fragment
|
|
66
66
|
statsWidget(async () => [
|
|
67
67
|
stat("Awaiting payment", await Order.query().where("status", "pending").count()),
|
|
68
68
|
]).poll("30s"),
|
|
@@ -85,7 +85,7 @@ page.
|
|
|
85
85
|
The admin owns the bell + notifications page UI; your app supplies the data through a
|
|
86
86
|
provider (the same split as relations — admin UI, app data):
|
|
87
87
|
|
|
88
|
-
```ts
|
|
88
|
+
```ts fragment
|
|
89
89
|
import { Panel } from "@zerotal/admin";
|
|
90
90
|
|
|
91
91
|
Panel.notifications({
|
|
@@ -124,7 +124,7 @@ notifications" depends on your auth and your schema. When both are the ordinary
|
|
|
124
124
|
ones — `@zerotal/auth` for the user, `@zerotal/notifications`' `DatabaseChannel`
|
|
125
125
|
for storage — there is a ready-made one:
|
|
126
126
|
|
|
127
|
-
```ts
|
|
127
|
+
```ts fragment
|
|
128
128
|
import { databaseNotifications } from "@zerotal/admin";
|
|
129
129
|
|
|
130
130
|
Panel.notifications(databaseNotifications());
|
|
@@ -23,7 +23,7 @@ everything else alone.
|
|
|
23
23
|
**Table cells.** The column still owns its label, sorting, search and export;
|
|
24
24
|
only the cell is yours:
|
|
25
25
|
|
|
26
|
-
```ts
|
|
26
|
+
```ts fragment
|
|
27
27
|
text("health")
|
|
28
28
|
.label("Health")
|
|
29
29
|
.sortable()
|
|
@@ -32,7 +32,7 @@ text("health")
|
|
|
32
32
|
|
|
33
33
|
**Infolist entries.** The section's grid still places it:
|
|
34
34
|
|
|
35
|
-
```ts
|
|
35
|
+
```ts fragment
|
|
36
36
|
textEntry("route").render((value) => <RouteMap path={String(value)} />),
|
|
37
37
|
```
|
|
38
38
|
|
|
@@ -40,7 +40,7 @@ textEntry("route").render((value) => <RouteMap path={String(value)} />),
|
|
|
40
40
|
saves like any other — you supply only the control. Bind your markup to
|
|
41
41
|
`form.<key>` for the value to round-trip:
|
|
42
42
|
|
|
43
|
-
```ts
|
|
43
|
+
```ts fragment
|
|
44
44
|
customField("coordinates")
|
|
45
45
|
.label("Location")
|
|
46
46
|
.required()
|
|
@@ -55,7 +55,7 @@ what lets a control react to a sibling field.
|
|
|
55
55
|
A contributed page can only add a page. A hook adds markup at a named position
|
|
56
56
|
in the chrome — a trial banner, a compliance notice, an environment badge:
|
|
57
57
|
|
|
58
|
-
```ts
|
|
58
|
+
```ts fragment
|
|
59
59
|
Panel.renderHook("page.header.end", () => <TrialBanner />);
|
|
60
60
|
|
|
61
61
|
// Conditional placement: register once, decide per render.
|
|
@@ -92,7 +92,7 @@ A resource does not have to be backed by a model. Return rows from `data()` and
|
|
|
92
92
|
the panel filters, sorts and paginates them in memory, so search, tabs,
|
|
93
93
|
summaries and the query builder all keep working:
|
|
94
94
|
|
|
95
|
-
```ts
|
|
95
|
+
```ts fragment
|
|
96
96
|
export class RegionResource extends Resource {
|
|
97
97
|
static override async data() {
|
|
98
98
|
return await fetch("https://api.example.com/regions").then((r) => r.json());
|
|
@@ -115,7 +115,7 @@ rows and not millions. Past that, back it with a model or a view.
|
|
|
115
115
|
|
|
116
116
|
Four statics change how a list renders:
|
|
117
117
|
|
|
118
|
-
```ts
|
|
118
|
+
```ts fragment
|
|
119
119
|
export class ProductResource extends Resource {
|
|
120
120
|
static override tableLayout = "grid"; // "table" (default) | "grid" | "kanban" | "calendar"
|
|
121
121
|
static override striped = true;
|
|
@@ -131,7 +131,7 @@ title, and the next few render as label/value pairs. No second description.
|
|
|
131
131
|
|
|
132
132
|
**Kanban** turns a status column into lanes, for records that read as a pipeline:
|
|
133
133
|
|
|
134
|
-
```ts
|
|
134
|
+
```ts fragment
|
|
135
135
|
static override tableLayout = "kanban";
|
|
136
136
|
static override kanbanColumn = "status";
|
|
137
137
|
static override kanbanLanes = { pending: "Pending", paid: "Paid", shipped: "Shipped" };
|
|
@@ -144,7 +144,7 @@ does, not a second source of truth. A value present in the data but missing from
|
|
|
144
144
|
|
|
145
145
|
**Calendar** lays the page out as a month grid keyed on a date column:
|
|
146
146
|
|
|
147
|
-
```ts
|
|
147
|
+
```ts fragment
|
|
148
148
|
static override tableLayout = "calendar";
|
|
149
149
|
static override calendarColumn = "startsOn";
|
|
150
150
|
```
|
|
@@ -159,7 +159,7 @@ on wide or long tables.
|
|
|
159
159
|
|
|
160
160
|
A resource whose records nest under each other renders as a tree:
|
|
161
161
|
|
|
162
|
-
```ts
|
|
162
|
+
```ts fragment
|
|
163
163
|
export class CategoryResource extends Resource {
|
|
164
164
|
static override treeParentColumn = "parentId";
|
|
165
165
|
}
|
|
@@ -176,7 +176,7 @@ than disappearing, so a filtered tree never hides a record.
|
|
|
176
176
|
|
|
177
177
|
A resource whose text exists in several languages edits one at a time:
|
|
178
178
|
|
|
179
|
-
```ts
|
|
179
|
+
```ts fragment
|
|
180
180
|
export class PostResource extends Resource {
|
|
181
181
|
static override translatable = ["title", "excerpt"];
|
|
182
182
|
static override locales = ["en", "fr"];
|
|
@@ -193,7 +193,7 @@ turning this on for an existing column does not blank it.
|
|
|
193
193
|
|
|
194
194
|
A column can carry its own filter box in the table header:
|
|
195
195
|
|
|
196
|
-
```ts
|
|
196
|
+
```ts fragment
|
|
197
197
|
text("sku").filterable();
|
|
198
198
|
selectColumn("status", STATUS).filterable();
|
|
199
199
|
```
|
|
@@ -209,7 +209,7 @@ the active-filter chips, and land in a saved view.
|
|
|
209
209
|
Filters sit above the table by default. Once there are more than a few, collapse
|
|
210
210
|
them:
|
|
211
211
|
|
|
212
|
-
```ts
|
|
212
|
+
```ts fragment
|
|
213
213
|
static override filterLayout = "panel"; // "inline" (default) | "panel" | "drawer"
|
|
214
214
|
```
|
|
215
215
|
|
package/docs/admin/extending.md
CHANGED
|
@@ -55,11 +55,20 @@ That last row is the important one. A panel with no authorization wired stays
|
|
|
55
55
|
closed in production, the same posture the panel guard already takes, which is
|
|
56
56
|
what makes it safe for a package to add pages without the app asking.
|
|
57
57
|
|
|
58
|
-
```ts
|
|
58
|
+
```ts fragment
|
|
59
59
|
// config/admin.ts
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
import { AdminConfig } from "@zerotal/admin";
|
|
61
|
+
import { Auth } from "@zerotal/auth";
|
|
62
|
+
|
|
63
|
+
// Through the factory, not a bare object: it is what types `ability`, and every
|
|
64
|
+
// other config in the framework is written this way.
|
|
65
|
+
export default AdminConfig({
|
|
66
|
+
// `userOrNull()`, not `user()` — this runs for guests too, and `user()` throws
|
|
67
|
+
// rather than returning one. `can()` comes from the permissions mixin on your
|
|
68
|
+
// own User model, which is why this block is a fragment: the type depends on
|
|
69
|
+
// your app, not on the framework.
|
|
70
|
+
authorize: (ability) => Auth.userOrNull()?.can(ability) ?? false,
|
|
71
|
+
});
|
|
63
72
|
```
|
|
64
73
|
|
|
65
74
|
Resources are the exception: they authorize through their own
|
|
@@ -75,7 +84,7 @@ and without depending on `@zerotal/admin`.
|
|
|
75
84
|
|
|
76
85
|
A contributing provider resolves the binding in its `onBooting` and pushes:
|
|
77
86
|
|
|
78
|
-
```ts
|
|
87
|
+
```ts fragment
|
|
79
88
|
// packages/queue/src/admin.ts
|
|
80
89
|
interface AdminHost {
|
|
81
90
|
enabled(id: string): boolean;
|
|
@@ -117,7 +126,7 @@ JSX, no `@zerotal/flow` dependency and no build configuration. The panel owns
|
|
|
117
126
|
the markup, which also means every console looks like the rest of the admin
|
|
118
127
|
without trying to.
|
|
119
128
|
|
|
120
|
-
```ts
|
|
129
|
+
```ts fragment
|
|
121
130
|
panel.console({
|
|
122
131
|
slug: "jobs",
|
|
123
132
|
title: "Jobs",
|
|
@@ -180,7 +189,7 @@ is read early enough.
|
|
|
180
189
|
Application code can name the panel directly, so it doesn't need the container
|
|
181
190
|
dance. Group a set of related contributions behind an `AdminPlugin`:
|
|
182
191
|
|
|
183
|
-
```ts
|
|
192
|
+
```ts fragment
|
|
184
193
|
await Panel.plugin({
|
|
185
194
|
id: "billing",
|
|
186
195
|
install: (panel) => {
|
package/docs/admin/forms.md
CHANGED
|
@@ -8,7 +8,7 @@ description: Build create and edit forms, and lay out the read-only view screen.
|
|
|
8
8
|
`form()` returns fields (and layout components). An empty `form()` disables
|
|
9
9
|
Create/Edit for the resource.
|
|
10
10
|
|
|
11
|
-
```ts
|
|
11
|
+
```ts fragment
|
|
12
12
|
import {
|
|
13
13
|
textInput, textarea, select, toggle, datePicker, fileUpload, richEditor,
|
|
14
14
|
formSection, formTabs, formTab, wizard, wizardStep,
|
|
@@ -80,7 +80,7 @@ patch)` runs server-side and merges a patch into the form (e.g. derive a slug).
|
|
|
80
80
|
`repeater` edits an array of objects sharing one sub-schema; `builder` edits an array
|
|
81
81
|
of typed blocks. Rows can be added, removed, and reordered.
|
|
82
82
|
|
|
83
|
-
```ts
|
|
83
|
+
```ts fragment
|
|
84
84
|
import { repeater, builder, builderBlock, textInput, textarea, fileUpload } from "@zerotal/admin";
|
|
85
85
|
|
|
86
86
|
repeater("contacts").minItems(1).addActionLabel("Add contact").schema([
|
|
@@ -101,7 +101,7 @@ Repeaters serialize to `[{ ...fields }]`; builders to `[{ type, data }]`.
|
|
|
101
101
|
`infolist()` describes the read-only detail page. Omit it to fall back to a section
|
|
102
102
|
derived from `columns()`.
|
|
103
103
|
|
|
104
|
-
```ts
|
|
104
|
+
```ts fragment
|
|
105
105
|
import { section, textEntry, iconEntry } from "@zerotal/admin";
|
|
106
106
|
|
|
107
107
|
static infolist() {
|
|
@@ -136,7 +136,7 @@ usefully read as text:
|
|
|
136
136
|
`repeatableEntry` is the read side of `repeater`: whatever the form wrote into a
|
|
137
137
|
JSON column, this reads back in the same shape.
|
|
138
138
|
|
|
139
|
-
```ts
|
|
139
|
+
```ts fragment
|
|
140
140
|
section("Line items").schema([
|
|
141
141
|
repeatableEntry("lines")
|
|
142
142
|
.placeholder("This order has no line items.")
|
package/docs/admin/index.md
CHANGED
|
@@ -47,7 +47,7 @@ List / View / Create / Edit page per resource under the configured `path`.
|
|
|
47
47
|
|
|
48
48
|
## Quick start
|
|
49
49
|
|
|
50
|
-
```ts
|
|
50
|
+
```ts fragment
|
|
51
51
|
// app/admin.ts
|
|
52
52
|
import { Panel, Resource, text, textInput } from "@zerotal/admin";
|
|
53
53
|
import { User } from "./models/User.ts";
|
|
@@ -88,7 +88,7 @@ Edit / View / Delete — and the form above on the Create and Edit pages.
|
|
|
88
88
|
Configure the panel with `Panel.configure(...)` in `app/admin.ts`, or by exporting
|
|
89
89
|
an `admin` config object the provider merges on boot.
|
|
90
90
|
|
|
91
|
-
```ts
|
|
91
|
+
```ts fragment
|
|
92
92
|
Panel.configure({
|
|
93
93
|
path: "/admin",
|
|
94
94
|
brand: "Acme",
|