@aotter/mantle 0.2.0 → 0.2.1-alpha.2
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/README.md +6 -2
- package/dist/build/compiler.d.ts +6 -6
- package/dist/build/compiler.js +6 -3
- package/dist/build/spec/domain/model/ManifestGrammar.d.ts +39 -1
- package/dist/build/spec/domain/model/RuntimePlan.d.ts +16 -3
- package/dist/build/spec/domain/model/SqlIr.d.ts +30 -0
- package/dist/build/spec/kernel/diagnostic.d.ts +1 -1
- package/dist/domain/describe/chunk-CW7X5Q5T.js +1 -0
- package/dist/domain/describe/index.d.ts +1 -0
- package/dist/domain/describe/index.js +1 -1
- package/dist/domain/describe/match.d.ts +19 -0
- package/dist/domain/describe/public.d.ts +3 -0
- package/dist/domain/describe/public.js +1 -0
- package/dist/infrastructure/adminEntries.js +19 -3
- package/dist/infrastructure/bundle.js +43 -8
- package/dist/infrastructure/cli.js +1 -1
- package/dist/infrastructure/generate.js +17 -3
- package/dist/kernel/binds.d.ts +18 -3
- package/dist/kernel/binds.js +1 -1
- package/dist/kernel/contracts.d.ts +7 -0
- package/dist/kernel/contracts.js +9 -0
- package/dist/kernel/driver/bun-sqlite.js +3 -3
- package/dist/kernel/driver/d1.d.ts +1 -1
- package/dist/kernel/driver/d1.js +6 -3
- package/dist/kernel/driver/postgres.d.ts +2 -1
- package/dist/kernel/driver/postgres.js +11 -6
- package/dist/kernel/mcp-apps.d.ts +3 -2
- package/dist/kernel/mcp-apps.js +7 -2
- package/dist/kernel/mcp-handler.js +22 -9
- package/dist/kernel/mcp.d.ts +18 -0
- package/dist/kernel/mcp.js +1 -0
- package/dist/kernel/rest-router.js +3 -5
- package/dist/kernel/rest.d.ts +2 -1
- package/dist/kernel/rest.js +1 -0
- package/dist/kernel/route-pattern.d.ts +26 -0
- package/dist/kernel/route-pattern.js +34 -0
- package/dist/kernel/store.d.ts +15 -1
- package/dist/kernel/store.js +51 -18
- package/dist/usecase/adminBindings.js +2 -2
- package/dist/usecase/adminClient.js +4 -1
- package/dist/usecase/generate.js +7 -5
- package/dist/usecase/handlers.js +1 -1
- package/dist/usecase/publishing.js +20 -7
- package/dist/usecase/rest.js +5 -5
- package/dist/usecase/stores.js +8 -3
- package/docs/examples/README.md +1 -1
- package/docs/examples/app-shell/README.md +58 -10
- package/docs/examples/app-shell/package.json +6 -6
- package/docs/examples/club-events/manifests/club-events.yaml +2 -0
- package/docs/examples/generated-web/seed.sql +5 -4
- package/docs/examples/purchasing/manifests/purchasing.yaml +3 -0
- package/docs/handbook/cloudflare/media-r2.md +6 -3
- package/docs/handbook/cloudflare/service-entry.md +2 -0
- package/docs/handbook/concepts/lifecycle-and-locales.md +21 -1
- package/docs/handbook/examples/hub.md +1 -1
- package/docs/handbook/guides/admin-ui.md +1 -1
- package/docs/handbook/guides/agent-setup.md +13 -8
- package/docs/handbook/guides/agent-workflow-acceptance.md +129 -19
- package/docs/handbook/guides/local-testing.md +50 -5
- package/docs/handbook/guides/mcp-apps.md +40 -0
- package/docs/handbook/guides/native-admin.md +218 -11
- package/docs/handbook/guides/native-execution.md +5 -4
- package/docs/handbook/guides/revisions.md +584 -0
- package/docs/handbook/guides/size-and-startup.md +140 -0
- package/docs/handbook/guides/sql-patterns.md +47 -1
- package/docs/handbook/guides/typed-queries.md +8 -3
- package/docs/handbook/navigation.json +4 -0
- package/docs/handbook/reference/authorization.md +2 -0
- package/docs/handbook/reference/describe.md +56 -4
- package/docs/handbook/reference/diagnostics.md +12 -3
- package/docs/handbook/reference/procedure.md +4 -4
- package/docs/handbook/reference/schema.md +71 -7
- package/docs/handbook/reference/sql.md +2 -1
- package/docs/handbook/reference/surface.md +13 -2
- package/docs/handbook/reference/trigger.md +12 -0
- package/docs/handbook/releases/index.md +75 -1
- package/docs/handbook/start/agents.md +6 -2
- package/docs/handbook/start/project-and-cli.md +4 -0
- package/docs/handbook/start/quickstart-worker.md +1 -1
- package/docs/skills/develop/SKILL.md +15 -3
- package/docs/skills/integrate/SKILL.md +3 -1
- package/docs/skills/performance/SKILL.md +169 -0
- package/docs/skills/update/SKILL.md +9 -5
- package/docs/upgrade-0.1-to-0.2.md +151 -39
- package/package.json +10 -11
- package/skills/README.md +6 -2
- package/skills/mantle/SKILL.md +19 -5
- package/dist/domain/describe/chunk-SGMRVDGE.js +0 -1
- package/dist/domain/describe/target-hash.js +0 -1
|
@@ -92,7 +92,7 @@ wrote through the app's own Views with HTTP Triggers (`mantle help trigger`).
|
|
|
92
92
|
| --- | --- |
|
|
93
93
|
| `POST /entries` | Body `{ collection, data }`; creates an entry. |
|
|
94
94
|
| `GET /entries/{id}?collection=<schema>` | Reads an entry and its current `entry.version`. |
|
|
95
|
-
| `PATCH /entries/{id}?collection=<schema>` | Body `{ data, expectedVersion }`; sends the observed version, not the next one. Publishing entries must be drafts. |
|
|
95
|
+
| `PATCH /entries/{id}?collection=<schema>` | Body `{ data, expectedVersion }`; sends the observed version, not the next one. A key left out of `data` keeps its value and `null` clears it ([leave out, replace or clear](#leave-out-replace-or-clear)). Publishing entries must be drafts unless the Schema sets `publishing: { liveEdits: true }`; a [lock](../reference/schema.md#locks) that holds refuses it with 409 `kind: "lock"`. |
|
|
96
96
|
| `POST /entries/{id}/publish?collection=<schema>` | Body `{ expectedVersion }`; validates required data before publishing. |
|
|
97
97
|
| `POST /entries/{id}/unpublish?collection=<schema>` | Body `{ expectedVersion }`; returns the entry to draft. |
|
|
98
98
|
| `POST /operations/{name}` | Body is the Procedure's input; runs an Admin operation (a Procedure with `requires: ctx.admin` or a `target`). Returns `{ ok: true, output }`, where `output` is what REST and MCP return (`{ results }` for a SQL Procedure). |
|
|
@@ -107,7 +107,15 @@ wrote through the app's own Views with HTTP Triggers (`mantle help trigger`).
|
|
|
107
107
|
|
|
108
108
|
Entry create/read/patch/transitions return
|
|
109
109
|
`{ collection, entry, parentEntryId, parentEntryTitle, related }`; the entry has
|
|
110
|
-
`{ id, collection, locale, status, version, data, created_at, updated_at }`.
|
|
110
|
+
`{ id, collection, locale, status, version, data, created_at, updated_at }`. On a Schema with
|
|
111
|
+
[locks](../reference/schema.md#locks), each entry of the editor payload and of `GET /entries` also has
|
|
112
|
+
`locks: { update, delete }`: per verb, `{ lock, message }` of the first lock that holds for your generic write of that row,
|
|
113
|
+
or `false`. Admin disables Save, rename, Delete and transitions on such a row and shows the
|
|
114
|
+
message; the operational "Unlock editing" toggle does not override it. The write itself stays the authority: a refusal is
|
|
115
|
+
409 `CONFLICT` with `kind: "lock"`. Every publishing entry has `locks` too: the draft-only rule (without `liveEdits`) and
|
|
116
|
+
"unpublish before deleting" are the implicit lock `lock: "publishing"`, named only when no declared lock holds for that verb.
|
|
117
|
+
It binds data writes only, so Publish, Unpublish and Archive stay available; with `liveEdits` a published entry's form is
|
|
118
|
+
enabled with a "changes go live on save" notice, and Save waits until its required fields are filled.
|
|
111
119
|
Transitions accept `{}` to use the current version; tests should send the
|
|
112
120
|
observed `expectedVersion` to catch stale writes. Generic writes reject native
|
|
113
121
|
columns inside `data`. See [publishing](../concepts/lifecycle-and-locales.md).
|
|
@@ -145,7 +153,7 @@ refused View or collection from a missing one. Showing or hiding a component gra
|
|
|
145
153
|
| Hook | Route | Surface |
|
|
146
154
|
| --- | --- | --- |
|
|
147
155
|
| `useView(name, input, { limit? })` -> `{ rows, status, hasMore, loadMore, loadingMore }` | `GET /views/{name}` | browser and MCP App |
|
|
148
|
-
| `useOperation(name)` -> `{ run, pending }`; success refreshes
|
|
156
|
+
| `useOperation(name, { refresh? })` -> `{ run, pending }`; success refreshes every mounted View (each input, each loaded page), entries, entry editors, totals and badges once, `refresh` narrowing the Views ([below](#scoped-refresh)) | `POST /operations/{name}` | browser and MCP App |
|
|
149
157
|
| `useEntries(collection, { filter?, search?, limit? })` -> `{ items, status, hasMore, loadMore, loadingMore }` | `GET /entries` (one `filter_field`/`filter_value`) | browser only |
|
|
150
158
|
| `useEntry(collection, id)` | `GET /entries/{id}` | browser only |
|
|
151
159
|
| `useCollectionTotal(collection)` | `GET /collections/{name}/statistics`, its `total` | browser only |
|
|
@@ -158,6 +166,18 @@ options) starts again from the first page. A failed `loadMore()` leaves `status`
|
|
|
158
166
|
calling `loadMore()` again retries. Admin's own lists work the same way, under a "Load more" button, and so does the MCP App, which
|
|
159
167
|
shows the button only when the host has `serverTools` (it calls the View's tool again with the same input and the `cursor`).
|
|
160
168
|
|
|
169
|
+
<a id="scoped-refresh"></a>By default an operation's success reads every shown View again, since Admin cannot know which a
|
|
170
|
+
Procedure changed: every mounted `useView`, for each input it is mounted with, and for each page it has loaded, one request
|
|
171
|
+
per page (a View with five loaded pages makes five requests). To keep that cheap, unmount what is collapsed (a closed
|
|
172
|
+
branch of a tree reads nothing, and reads again only when it is next shown), keep `limit` small so that fewer pages
|
|
173
|
+
accumulate, and prefer one wider View over many narrow ones. A page with many Views (say one per expanded branch of a
|
|
174
|
+
tree) can also name the ones to read again:
|
|
175
|
+
`useOperation("move", { refresh: ["children", { view: "children", input: { parent: id } }] })`. A name covers every input of
|
|
176
|
+
that View; `{ view, input }` covers the one `useView(view, input)` (any `limit`); `refresh: []` reads no View again. The names and inputs are typed like `useView`'s.
|
|
177
|
+
Views not named are marked stale rather than read again, so each reads again when it is next shown. Entries, open entries,
|
|
178
|
+
totals and badges still refresh: the Procedure may write any collection, and skipping those would show stale records with no
|
|
179
|
+
way to name them. Leave `refresh` out when unsure; a View you forget to name keeps showing its old rows until it is shown again.
|
|
180
|
+
|
|
161
181
|
In an MCP host, `useView` and `useOperation` call the View's or operation's admin MCP tool (its name lowercased, kebab to snake) through the host (`tools/call`),
|
|
162
182
|
under the caller's MCP authorization; the browser-only hooks read `unavailable` there, so declare the data an App needs as an
|
|
163
183
|
admin View. Without the host's `serverTools`, `run` is `undefined`. `useHost()` is `null` in the browser and the host's API
|
|
@@ -213,14 +233,164 @@ collection's name, `<field>` one of its fields and `<column>` a column of an adm
|
|
|
213
233
|
| `list.actions:<schema>` | browser | `collection`, `selection` (empty in the toolbar, the selected ids in the bulk bar) |
|
|
214
234
|
| `cell:<schema>.<field>` | browser | `value`, `row` (the list's columns only, `null` when empty), `property`, `surface` |
|
|
215
235
|
| `cell:view:<view>.<column>` | both | `value`, `row`, `property`, `surface` |
|
|
216
|
-
| `input:<schema>.<field>` | browser | `value`, `onChange`, `readOnly`, `property`, `surface` |
|
|
236
|
+
| `input:<schema>.<field>` | browser | `value`, `onChange`, `readOnly`, `property`, `surface`, optional `onStatusChange`, `error`, `errorId` |
|
|
217
237
|
|
|
218
238
|
`record.aside:` and `record.actions:` render beside Admin's own side cards and actions, which they do not replace. A
|
|
219
|
-
record form's top-level fields go through `input:`; a View's parameters and a Procedure's inputs do not. A
|
|
220
|
-
|
|
221
|
-
|
|
239
|
+
record form's top-level fields go through `input:`; a View's parameters and a Procedure's inputs do not. A field the Schema
|
|
240
|
+
does not require is `T | null` in the generated types, and so is every field of a publishing collection, whose draft may lack a
|
|
241
|
+
required one: it reads `null` when unset, and `onChange(null)` clears it (Save sends `null`, which the entry PATCH stores as
|
|
242
|
+
NULL; a field left out keeps its value). The built-in controls clear the same way, and an emptied required field is `null` too:
|
|
243
|
+
an operational record refuses it with a validation error, and a draft keeps it empty until Publish, which refuses it. A read-only field
|
|
244
|
+
(a bound or `readOnly` property) never reaches the slot: it has no `input:` component and shows its value read-only.
|
|
245
|
+
|
|
246
|
+
`readOnly` is one flag for the whole form, not for the field: it is `true` whenever this caller cannot edit this record now,
|
|
247
|
+
which is when any of these holds:
|
|
248
|
+
|
|
249
|
+
- the caller is not an admin, which includes the moment while `/me` is still loading, so expect it to start `true`;
|
|
250
|
+
- the collection is read-only (`readOnly: true` at the root of its Schema), or the entry's version is outside the safe integer range;
|
|
251
|
+
- an operational record has not been unlocked with "Unlock editing";
|
|
252
|
+
- a publishing entry is not a draft: published without `publishing: { liveEdits: true }`, or archived, which stays read-only
|
|
253
|
+
even with `liveEdits`. With `liveEdits` a published entry's form is editable and `readOnly` is `false`;
|
|
254
|
+
- a declared [Schema lock](../reference/schema.md#locks) holds the row for `update` (`entry.locks.update`). No toggle overrides
|
|
255
|
+
it. The form shows the lock's message, unless the collection is read-only, whose read-only hint shows instead.
|
|
256
|
+
|
|
257
|
+
The built-in form wraps the fields in a `<fieldset disabled>` that also blocks the pointer. That disables native form controls
|
|
258
|
+
inside it, but not a custom element that is focusable on its own (a `contenteditable`, a `tabIndex` element, a third-party
|
|
259
|
+
widget's buttons): it can still take keyboard focus and call `onChange`. Honour `readOnly` in your component. The Save button
|
|
260
|
+
stays disabled either way, so such a change is never sent.
|
|
261
|
+
|
|
262
|
+
A custom input can report validation and uncommitted local text with the optional
|
|
263
|
+
`onStatusChange({ error, dirty })` callback. Each call replaces that mounted root field's report:
|
|
264
|
+
`error` is a message (or `null`), and `dirty` says the control holds unsaved text outside `onChange`.
|
|
265
|
+
An error disables Save even if another field changes; a dirty local draft uses the record's existing
|
|
266
|
+
navigation and `beforeunload` guard. These are independent: a valid but uncommitted draft can be dirty,
|
|
267
|
+
and a validity error need not claim a change. Pass `{}` or call with no argument to clear both.
|
|
268
|
+
|
|
269
|
+
Admin renders the error as an alert beside the input and passes back `error` and `errorId`.
|
|
270
|
+
Connect your control with `aria-invalid` and `aria-describedby`, and respect `readOnly`:
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
import * as React from "react";
|
|
274
|
+
import type { InputProps } from "@aotter/mantle-admin/app";
|
|
275
|
+
|
|
276
|
+
function JsonInput({ value, onChange, onStatusChange, error, errorId, readOnly }: InputProps<unknown>) {
|
|
277
|
+
const [text, setText] = React.useState(() => JSON.stringify(value));
|
|
278
|
+
return <textarea value={text} readOnly={readOnly} aria-invalid={Boolean(error)} aria-describedby={errorId}
|
|
279
|
+
onChange={event => {
|
|
280
|
+
const next = event.target.value;
|
|
281
|
+
setText(next);
|
|
282
|
+
try {
|
|
283
|
+
const parsed = JSON.parse(next);
|
|
284
|
+
onChange(parsed);
|
|
285
|
+
onStatusChange?.({}); // the form now owns the valid draft and protects it until Save
|
|
286
|
+
} catch {
|
|
287
|
+
onStatusChange?.({ error: "Enter valid JSON", dirty: true });
|
|
288
|
+
}
|
|
289
|
+
}} />;
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Do not emit invalid text through `onChange` merely to mark the form dirty. If valid text stays local
|
|
294
|
+
until an explicit Apply action, keep reporting `{ dirty: true }` until Apply calls `onChange`.
|
|
295
|
+
The server's generated checks remain authoritative: a parsed value the component does not validate
|
|
296
|
+
can still be refused on Save, with the error above the form.
|
|
297
|
+
|
|
298
|
+
Reports belong to mounted **root** `input:<schema>.<field>` slots, including an object or array as
|
|
299
|
+
one structured value; there are no per-array-index input slots. A report clears when that field
|
|
300
|
+
unmounts. Another record, a clean newer server version, an explicit **Load latest version**,
|
|
301
|
+
or a completed Save resets the inputs and their local state. A row becoming locked/read-only
|
|
302
|
+
preserves its local draft and dirty navigation protection; the reported error is hidden until it
|
|
303
|
+
becomes editable again. Reports never override `readOnly` or enable Save on a locked row. A related
|
|
304
|
+
re-read preserves a dirty local draft. A draft typed while Save is in flight is kept. A custom
|
|
305
|
+
component that conditionally hides its own editor while its field stays mounted should clear its
|
|
306
|
+
report when discarding that editor's draft. Procedure input forms do not use these record slots.
|
|
307
|
+
|
|
308
|
+
At runtime the component also receives `field` and `build`, which the built-in control uses to render itself. They are internal,
|
|
309
|
+
not part of `InputProps`, and may change in any release: do not read them. Pass the props you were given to `renderDefault`, as
|
|
310
|
+
with every slot.
|
|
311
|
+
|
|
312
|
+
In the browser Admin, a View column that reads a Schema field also goes through that field's `cell:` component, with the column's own
|
|
222
313
|
`cell:view:` component outside it; the MCP App renders only `cell:view:`. The developer workspace (`/admin/dev`) is Admin's own tool and does not go through any slot. A record with folded child collections opens on a workbench at `/admin/c/<schema>/<id>`, which is not `RecordView`; the record form (and so `RecordView`, `record.aside:`, `record.actions:` and `input:`) is its `/edit` page. A `Shell` that does not call `renderDefault` has no status bar: the record form's Save and Publish bar then stays in the page.
|
|
223
314
|
|
|
315
|
+
Creating a parent opens its field editor first. Save the parent fields, then use the named workspace link or the child-collection links above the form to continue in its workbench. Missing publish requirements name their fields; a read-only requirement must be set by an available operation or related record rather than typed into the form. Workspace links use the same unsaved-changes guard as other Admin navigation.
|
|
316
|
+
|
|
317
|
+
While the record form has unsaved changes, leaving it asks first: Admin's own links, its in-app navigation and Back/Forward
|
|
318
|
+
show a confirm dialog, and a reload, closing the tab or another site show the browser's own prompt (`beforeunload`). Save,
|
|
319
|
+
"Load latest version" or confirming the leave clears it, and a `#hash` change on the same page never asks; a cancelled Back or
|
|
320
|
+
Forward leaves the browser history as it was; there is nothing to configure or opt out of. Custom pages and
|
|
321
|
+
operation forms have no such guard. An `input:` component's own state counts when it calls `onChange`
|
|
322
|
+
or reports `onStatusChange({ dirty: true })`; unreported local state is outside the form's guard.
|
|
323
|
+
|
|
324
|
+
### Media fields
|
|
325
|
+
|
|
326
|
+
A string field with `x-mantle-ref: media_assets` holds the id of an asset in the media library (`media_assets`), never a
|
|
327
|
+
URL. The record form renders a built-in control for it: a thumbnail of the asset, the id in a text box, **Pick**, which opens
|
|
328
|
+
the library in a dialog, and **Upload**, which uploads an image and fills the id. It works at the top level and in the
|
|
329
|
+
properties of nested objects, including objects inside an array:
|
|
330
|
+
|
|
331
|
+
```yaml
|
|
332
|
+
properties:
|
|
333
|
+
cover: { type: string, x-mantle-ref: media_assets }
|
|
334
|
+
gallery:
|
|
335
|
+
type: array
|
|
336
|
+
items:
|
|
337
|
+
type: object
|
|
338
|
+
properties:
|
|
339
|
+
image: { type: string, x-mantle-ref: media_assets }
|
|
340
|
+
caption: { type: string }
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
An array of bare ids (`items: { type: string, x-mantle-ref: media_assets }`) gets a plain text box per item; wrap the id
|
|
344
|
+
in an object, as above, to get the picker. The control needs the media library mounted (`site` and `media` in `admin()`,
|
|
345
|
+
see [media on R2](../cloudflare/media-r2.md)). Upload chooses a media purpose from the site's purposes by the collection
|
|
346
|
+
and field names, and is disabled when the site declares none. Procedure and View input forms render the same control with
|
|
347
|
+
Upload disabled, so an operation's media input is picked from the library.
|
|
348
|
+
|
|
349
|
+
Only a top-level field goes through `input:`. To add to the built-in control, wrap it with `renderDefault`:
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
"input:products.cover": (props) => (
|
|
353
|
+
<div>
|
|
354
|
+
{props.renderDefault(props)}
|
|
355
|
+
<p className="text-xs text-muted-foreground">Use a 1200 x 630 image; it is also the social card.</p>
|
|
356
|
+
</div>
|
|
357
|
+
),
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
For a custom structured input or custom page, import the browser-only picker from the optional
|
|
361
|
+
`@aotter/mantle-admin/media` entry. It runs inside `<MantleAdmin>` and uses the same caller-authorized media library and
|
|
362
|
+
upload routes. Keep the picker import in the browser entry; when also building an MCP App, use a sibling
|
|
363
|
+
`admin.mcp.tsx` without it ([separate entries](mcp-apps.md#customising-admin-for-mcp-hosts)). The host build rejects browser
|
|
364
|
+
media routes. There is no public raw media HTTP client or upload function.
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
import * as React from "react";
|
|
368
|
+
import { MediaPicker } from "@aotter/mantle-admin/media";
|
|
369
|
+
|
|
370
|
+
type Card = { image?: string; caption?: string };
|
|
371
|
+
|
|
372
|
+
function CardImage({ value, onChange, readOnly }: { value: Card; onChange: (value: Card) => void; readOnly: boolean }) {
|
|
373
|
+
const [open, setOpen] = React.useState(false);
|
|
374
|
+
return <>
|
|
375
|
+
<button type="button" disabled={readOnly} onClick={() => setOpen(true)}>Choose image</button>
|
|
376
|
+
<MediaPicker open={open} onOpenChange={setOpen} readOnly={readOnly} uploadPurpose="post-cover"
|
|
377
|
+
onSelect={asset => onChange({ ...value, image: asset.id })} />
|
|
378
|
+
</>;
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
`onSelect` receives only `{ id, primaryUrl: string | null }` after an explicit selection and closes the dialog via
|
|
383
|
+
`onOpenChange(false)`. Cancel never calls `onSelect` or clears a value. The application owns the structured value and
|
|
384
|
+
Save; selection only updates its local draft. Pass the input's `readOnly`, and key a reused custom input by its record
|
|
385
|
+
identity so an open picker does not follow navigation to another record.
|
|
386
|
+
|
|
387
|
+
`uploadPurpose` is an exact site media purpose, not an asset filter: existing assets remain selectable across purposes.
|
|
388
|
+
Omit it for selection only; an unknown purpose disables upload, and the upload helper refuses unknown explicit purposes
|
|
389
|
+
rather than choosing another policy. Upload uses the existing raster image variants and server permission checks.
|
|
390
|
+
Uploads commit to the library independently of selection: closing, cancelling, becoming read-only or unmounting never
|
|
391
|
+
selects a late upload and never deletes an already committed asset. Search stays in the dialog and preserves the page's
|
|
392
|
+
draft. Applications that do not import `/media` do not load this optional component.
|
|
393
|
+
|
|
224
394
|
`plugins` are fragments of `components`, applied in order: each one's `renderDefault` is the plugin before it, the first
|
|
225
395
|
plugin's is the built-in component, and the application's own `components` are outermost. A plugin is
|
|
226
396
|
`{ components: { ... } }`, so one can be written once and shared.
|
|
@@ -303,7 +473,9 @@ returns its one row on one page and has no cursor.
|
|
|
303
473
|
Generated full/partial input checks validate creates, edits, operations and
|
|
304
474
|
filters. Plain titled string `oneOf` choices use the same enum checks; general
|
|
305
475
|
or constrained alternatives remain refused. Draft creates can be incomplete;
|
|
306
|
-
publish checks required data. Publishing edits apply only to drafts
|
|
476
|
+
publish checks required data. Publishing edits apply only to drafts unless the Schema sets
|
|
477
|
+
`publishing: { liveEdits: true }`, which opens published (never archived) entries; a live edit of a published entry must still pass those publish checks
|
|
478
|
+
(`INPUT_VALIDATION_FAILED`), keeps `published_at` and goes live on save. Read-only
|
|
307
479
|
collections refuse generic creates, edits, deletes and transitions with 403 `AUTH_DENIED`
|
|
308
480
|
("This record is read-only; use its operations.") before any read or write, while
|
|
309
481
|
their declared Procedures can still run.
|
|
@@ -312,7 +484,7 @@ Writes use compiler-printed native Procedure frames, bind recipes and only the
|
|
|
312
484
|
supplied field fragments. They do not update omitted columns or run a schema
|
|
313
485
|
interpreter. PATCH retains its visible-row read and exact OCC check; the mutation
|
|
314
486
|
returns its row. Missing/excluded rows are 404; stale locks or failed state guards
|
|
315
|
-
are 409. Safe versions are numbers; exact native int64 strings can be bound
|
|
487
|
+
are 409 (a [lock](../reference/schema.md#locks) that holds is `kind: "lock"`). Safe versions are numbers; exact native int64 strings can be bound
|
|
316
488
|
without rounding, while unsafe numbers are rejected. The console keeps versions
|
|
317
489
|
outside its supported safe range read-only rather than casting them to numbers.
|
|
318
490
|
Full cross-target int64 output parity remains incomplete; bare driver values
|
|
@@ -331,6 +503,24 @@ from `@aotter/mantle/media`; other custom ContractErrors must carry an explicit
|
|
|
331
503
|
status, otherwise the boundary returns a redacted 500. Do not leak native
|
|
332
504
|
error messages, tokens or credentials to clients.
|
|
333
505
|
|
|
506
|
+
### Leave out, replace or clear
|
|
507
|
+
|
|
508
|
+
Each write path reads a field it is given differently. Pick the one whose rule fits the edit:
|
|
509
|
+
|
|
510
|
+
| Write path | Field left out | A value | `null` |
|
|
511
|
+
| --- | --- | --- | --- |
|
|
512
|
+
| Admin create (`POST /entries`) | unset, or the Schema's default | sets it | dropped, the same as leaving it out, so the default applies |
|
|
513
|
+
| Admin patch (`PATCH /entries/{id}`, the record form's Save) | keeps the stored value | replaces it | clears it to NULL; a required operational field refuses it, and so does a live edit (`liveEdits`) of a field `publish` requires (`INPUT_VALIDATION_FAILED`) |
|
|
514
|
+
| Store `update` (a ref handler's `ctx.store`) | keeps the stored value | replaces it | clears it to NULL; on a published row a field `publish` requires is refused (`INPUT_VALIDATION_FAILED`) |
|
|
515
|
+
| SQL Procedure (`input.x`) | binds NULL, so `SET note = input.note` clears it | replaces it | binds NULL too, where the input's schema accepts `null` |
|
|
516
|
+
| `ref` handler input | the key is absent | present | present, as `null`, where the input's schema accepts it |
|
|
517
|
+
|
|
518
|
+
A SQL Procedure cannot tell a field left out from one cleared, since both bind NULL. For a partial update, keep the stored
|
|
519
|
+
value with `COALESCE(input.note, note)`, and give clearing its own input, such as `clearNote: true` with
|
|
520
|
+
`CASE WHEN input.clearNote THEN NULL ELSE COALESCE(input.note, note) END`
|
|
521
|
+
([SQL patterns](sql-patterns.md#change-only-the-fields-given-coalesce)). A ref handler sees the difference: test with
|
|
522
|
+
`Object.hasOwn(input, "note")`, and pass only the keys present to `store.collection(...).update`.
|
|
523
|
+
|
|
334
524
|
## Statistics
|
|
335
525
|
|
|
336
526
|
`GET /admin/api/collections/{name}/statistics?range=7d` supports primary or
|
|
@@ -386,7 +576,12 @@ In the entry editor and every operation form (including a `collectionAction` cre
|
|
|
386
576
|
`x-mantle-ref` to another Admin collection is a searchable picker: it lists the target through the same `/entries` route
|
|
387
577
|
(20 records per search, read as the caller), shows each record's title and stores the referenced value (`id`, or the
|
|
388
578
|
ref's unique field). A target the caller cannot list (403/404) or that is not an Admin collection keeps the plain text
|
|
389
|
-
input.
|
|
579
|
+
input. In an operation bound to a row (`target`), a picker whose Schema folds under that row's collection
|
|
580
|
+
(`nav.parentField`) lists only that row's children, filtered by `<parentField> = <the bound row's value>` as the folded
|
|
581
|
+
child list is: `switch-revision`'s `revision` on a post lists that post's revisions. There is no way to pick another
|
|
582
|
+
parent's child there: an operation that needs one binds to that other row instead, or is not bound (`collectionAction`
|
|
583
|
+
or the Operations page), where the picker lists the whole target. The picker is a convenience; the
|
|
584
|
+
server still validates the value. CSV exports use `/entries/export` and
|
|
390
585
|
`/views/{name}/export`; native browser downloads stream paged server output.
|
|
391
586
|
A failure after headers aborts the stream, so a download can fail after partial
|
|
392
587
|
bytes have arrived. Existing databases need an app-owned migration for the
|
|
@@ -433,7 +628,10 @@ tabs. The parent is the reference named by `Schema.spec.uiSchema.nav.parentField
|
|
|
433
628
|
the first required unique-field reference. A Schema with two or more required `id` references to
|
|
434
629
|
existing Schemas (translations excluded) and no `parentField`, with or without `standalone`, is refused by `mantle generate` with `SCHEMA_UI_INVALID`; the diagnostic lists the
|
|
435
630
|
candidates and the `nav: { parentField: <field> }` line that keeps the first one that is not a parent cycle. This applies to every build, with or without Admin, so a join table with two required `id` references needs it too. `parentField` is
|
|
436
|
-
refused on a translation child and when its parent chain leads back to the Schema.
|
|
631
|
+
refused on a translation child and when its parent chain leads back to the Schema. An explicit
|
|
632
|
+
`parentField` decides the fold direction: a required reference from that parent back to the child
|
|
633
|
+
(a post's required `current_revision`) is not an inferred fold and does not count as a cycle,
|
|
634
|
+
while two `parentField`s pointing at each other are still refused. Choosing the
|
|
437
635
|
parent changes Admin navigation only: storage and authorization are unchanged.
|
|
438
636
|
|
|
439
637
|
Set `nav.standalone: true` to put a child with a required `id` reference at the top level instead;
|
|
@@ -542,6 +740,15 @@ Names such as `approve` never determine placement. An operation without a row
|
|
|
542
740
|
target or collection action appears on the Operations page, whose sidebar link
|
|
543
741
|
appears whenever such operations exist.
|
|
544
742
|
|
|
743
|
+
A collection action of a folded child (one with a parent; see above) also shows in that child's panel of the parent's
|
|
744
|
+
workbench. Run from there, the input named exactly like the child's `nav.parentField` (the child's field holding the
|
|
745
|
+
parent's value) is filled with this parent's value and locked: the form hides it and the dialog lists it under the
|
|
746
|
+
bound values, like a row operation's `id`. The name is the rule because it is unambiguous (one input at most, the same
|
|
747
|
+
name as the column the action writes). The value takes the input's type (a numeric parent key binds a number); an
|
|
748
|
+
input with that name whose `x-mantle-ref` names another Schema or key, or whose type cannot hold the parent's value, is
|
|
749
|
+
left to the person, and so is every input outside a workbench. With `nav.parentField: post`, `new-revision { post }` placed with
|
|
750
|
+
`collectionAction: post_revisions` runs on the open post.
|
|
751
|
+
|
|
545
752
|
A record's page shows its row operations as labeled buttons in the header (more than three: two buttons and a
|
|
546
753
|
"More actions" menu); list rows keep them in an "Actions for …" menu.
|
|
547
754
|
Generated row metadata includes simple AND-ed equality/IN literal conditions
|
|
@@ -46,7 +46,7 @@ transport conversion or runtime schema interpreter.
|
|
|
46
46
|
Declared authorization, ownership/TTL SQL policy, explicit OCC and atomic writes
|
|
47
47
|
remain required. Generate them as the native application would implement them.
|
|
48
48
|
Transaction rollback and uncertain COMMIT handling must still preserve data-loss
|
|
49
|
-
protection and client disposal responsibility; native errors are rethrown (
|
|
49
|
+
protection and client disposal responsibility; native errors are rethrown (with the contract exceptions below), and
|
|
50
50
|
redaction before they reach HTTP users belongs to the application boundary through `wire` / `surfaceFailure`. Removing generic error
|
|
51
51
|
normalization does not authorize dropping transaction outcome evidence.
|
|
52
52
|
|
|
@@ -57,12 +57,12 @@ unique `id =`), so a failed count check after autocommit has changed nothing.
|
|
|
57
57
|
On PG it runs at the server's default isolation, not SERIALIZABLE: a concurrent
|
|
58
58
|
OCC loser re-evaluates its `version =` predicate, matches zero rows and returns
|
|
59
59
|
`CONFLICT` instead of SQLSTATE 40001. A server ErrorResponse (SQLSTATE and
|
|
60
|
-
severity) is a known rollback and is rethrown natively, except a unique violation (23505, SQLite's `UNIQUE constraint failed`), which, in a generated Procedure or a Store write, is `ContractError("CONFLICT")` with the native error as `cause` on every target, and except connection,
|
|
60
|
+
severity) is a known rollback and is rethrown natively, except a unique violation (23505, SQLite's `UNIQUE constraint failed`), which, in a generated Procedure or a Store write, is `ContractError("CONFLICT")` with the native error as `cause` on every target, and serialization/deadlock refusals (`40001` / `40P01`) with a definitive server response, which are `CONFLICT` with `kind: "serialization"` and the native `cause`, and except connection,
|
|
61
61
|
resource and shutdown classes (08, 28, 53, 57, 58, XX), which, like any failure
|
|
62
62
|
without a server response (lost socket), are `OUTCOME_UNKNOWN`. SQLSTATE is
|
|
63
63
|
`errno` on Bun.SQL and `code` on postgres.js. Multi-statement Procedures run in
|
|
64
64
|
`begin("isolation level serializable", ...)`: a failure before the callback
|
|
65
|
-
returns was rolled back by `begin`; only a failed COMMIT is classified as above.
|
|
65
|
+
returns was rolled back by `begin`; only a failed COMMIT is classified as above. The same serialization/deadlock mapping applies to Store batches and interactive transactions, including a definitive refusal at COMMIT. No write is retried automatically; re-read before deciding whether it still applies.
|
|
66
66
|
|
|
67
67
|
**Date-time wire form:** a View output that is a declared `date-time` column
|
|
68
68
|
(including `created_at`/`updated_at`) returns the handbook's wire form, an ISO
|
|
@@ -85,7 +85,8 @@ storage representations. Bound optional fields become explicit required values,
|
|
|
85
85
|
with null representing absence; D1 cannot bind undefined. Native output keys are
|
|
86
86
|
SQL names rather than a post-query camel-case projection. PG uses the driver's
|
|
87
87
|
default type parsers (including Date and int8 strings); custom application parser
|
|
88
|
-
configuration changes application-owned behavior.
|
|
88
|
+
configuration changes application-owned behavior. The Store alone converts a row's
|
|
89
|
+
`version` to a number ([typed queries](typed-queries.md)). Procedures expose raw
|
|
89
90
|
`{ results }`, not a claim of validated authored output.
|
|
90
91
|
|
|
91
92
|
The application resolves a verified Caller once at its entry, using its selected
|