@antelopejs/interface-dms-builder 0.0.1
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/LICENSE +190 -0
- package/README.md +12 -0
- package/dist/addressing.d.ts +3 -0
- package/dist/addressing.js +3 -0
- package/dist/addressing.js.map +1 -0
- package/dist/catalog.d.ts +87 -0
- package/dist/catalog.js +3 -0
- package/dist/catalog.js.map +1 -0
- package/dist/drafts.d.ts +37 -0
- package/dist/drafts.js +7 -0
- package/dist/drafts.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/operations.d.ts +69 -0
- package/dist/operations.js +20 -0
- package/dist/operations.js.map +1 -0
- package/dist/queries.d.ts +55 -0
- package/dist/queries.js +9 -0
- package/dist/queries.js.map +1 -0
- package/dist/resources.d.ts +97 -0
- package/dist/resources.js +13 -0
- package/dist/resources.js.map +1 -0
- package/dist/results.d.ts +59 -0
- package/dist/results.js +3 -0
- package/dist/results.js.map +1 -0
- package/dist/structure.d.ts +62 -0
- package/dist/structure.js +3 -0
- package/dist/structure.js.map +1 -0
- package/dist/values.d.ts +19 -0
- package/dist/values.js +3 -0
- package/dist/values.js.map +1 -0
- package/docs/1.introduction.md +86 -0
- package/docs/2.addressing-and-structure.md +207 -0
- package/docs/3.catalog.md +176 -0
- package/docs/4.page-operations.md +341 -0
- package/docs/5.resources.md +276 -0
- package/docs/6.queries.md +229 -0
- package/docs/7.values.md +130 -0
- package/docs/8.results-and-errors.md +201 -0
- package/docs/9.drafts-and-preview.md +140 -0
- package/package.json +67 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# Page Operations
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Page operations cover everything that lives in a page file: the pages themselves, the categories they hang from, and the block tree inside each page. Reads (`ListPages`, `ListCategories`, `GetPageStructure`) tell you what is there; mutations (`CreatePage`, `AddBlock`, `MoveBlock`, …) rewrite the source and return the staged `FileChange` list.
|
|
6
|
+
|
|
7
|
+
Every mutation returns an `OpResult`, so branch on `ok` before touching `data`. The refs and shapes these operations trade in are described in [Addressing and Structure](./2.addressing-and-structure.md); the errors and warnings they return are described in [Results and Errors](./8.results-and-errors.md).
|
|
8
|
+
|
|
9
|
+
## List Pages and Categories
|
|
10
|
+
|
|
11
|
+
`ListPages` and `ListCategories` are plain reads: they return arrays, not an `OpResult`.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import {
|
|
15
|
+
ListCategories,
|
|
16
|
+
ListPages,
|
|
17
|
+
} from "@antelopejs/interface-dms-builder";
|
|
18
|
+
|
|
19
|
+
const pages = await ListPages();
|
|
20
|
+
const categories = await ListCategories();
|
|
21
|
+
|
|
22
|
+
for (const page of pages) {
|
|
23
|
+
console.log(page.ref, page.displayName, page.filepath);
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Read a Page's Structure
|
|
28
|
+
|
|
29
|
+
`GetPageStructure(ref)` returns the page's metadata, its block tree and its queries, wrapped in an `OpResult<PageStructure>`. Being a read, its `changes` array is empty.
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { GetPageStructure } from "@antelopejs/interface-dms-builder";
|
|
33
|
+
|
|
34
|
+
const result = await GetPageStructure("/dashboard");
|
|
35
|
+
if (!result.ok) {
|
|
36
|
+
throw new Error(`cannot read /dashboard: ${result.error.code}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { page, blocks, queries, version } = result.data;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Keep `version`: passing it as `expectedVersion` on the next mutation turns the write into a conditional one.
|
|
43
|
+
|
|
44
|
+
## Refresh the Source Index
|
|
45
|
+
|
|
46
|
+
The builder keeps an index of the project's source. Mutations invalidate it themselves, so you only need `RefreshSourceIndex` after something *else* changed the files — a hand edit, a checkout, a generator run.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { RefreshSourceIndex } from "@antelopejs/interface-dms-builder";
|
|
50
|
+
|
|
51
|
+
await RefreshSourceIndex(); // source index and catalog
|
|
52
|
+
await RefreshSourceIndex({ page: "/dashboard" }); // source index only
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`RefreshScope` has a single field:
|
|
56
|
+
|
|
57
|
+
| Option | Type | Description |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `page` | `PageRef` | Keep the cached catalog. Omit the scope entirely to drop the catalog as well. |
|
|
60
|
+
|
|
61
|
+
The scope does not narrow the source index: it is invalidated in full either way, and naming a page only spares the catalog cache. Pass a page when you know the change was confined to page source and left the component catalog alone — it saves rescanning the catalog, not the project.
|
|
62
|
+
|
|
63
|
+
Invalidating is not rescanning. The call only drops the cache; the cost lands on the next read, which rescans the project in full and memoizes the result. Two refreshes in a row therefore cost nothing extra, but refreshing before every read costs a full scan every time. The operation returns `void`, not an `OpResult`.
|
|
64
|
+
|
|
65
|
+
## Create a Page
|
|
66
|
+
|
|
67
|
+
`CreatePage(input)` writes a new page file, wires it into the nearest barrel, and returns the ref it assigned along with the file it created.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { CreatePage } from "@antelopejs/interface-dms-builder";
|
|
71
|
+
|
|
72
|
+
const result = await CreatePage({
|
|
73
|
+
name: "revenue",
|
|
74
|
+
displayName: "Revenue",
|
|
75
|
+
category: "pages",
|
|
76
|
+
icon: "i-ph-chart-line",
|
|
77
|
+
description: "Monthly revenue overview",
|
|
78
|
+
});
|
|
79
|
+
if (result.ok) {
|
|
80
|
+
console.log(result.data.ref, result.data.filepath);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`CreatePageInput`:
|
|
85
|
+
|
|
86
|
+
| Option | Type | Description |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| `name` | `string` | The page's route id/slug. Required. A duplicate fails with `duplicate_name`. |
|
|
89
|
+
| `displayName` | `string` | The label shown in the menu. Required. |
|
|
90
|
+
| `category` | `CategoryRef` | The category to attach to. Required, and it must already exist. |
|
|
91
|
+
| `icon` | `string` | Icon name. |
|
|
92
|
+
| `order` | `number` | Sort order within the category. |
|
|
93
|
+
| `description` | `string` | The page's description. |
|
|
94
|
+
| `permission` | `PagePermission` | The permission guarding the page. |
|
|
95
|
+
| `dir` | `string` | Explicit folder, relative to the src root, overriding the default category-relative placement. |
|
|
96
|
+
|
|
97
|
+
Without `dir`, the page lands at `<category-folder>/<name>/page.ts` when the category is declared in the project's source and has a barrel beside it, and at `src/<name>.ts` otherwise. With `dir`, it lands at `src/<dir>/<name>/page.ts`. Either way the nearest barrel above the file is wired, keeping a relative import chain up to `src/index.ts`. `dir` must stay inside the src tree; a `dir` that escapes it, or a project with no barrel to wire, fails with `unsupported`.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { CreatePage } from "@antelopejs/interface-dms-builder";
|
|
101
|
+
|
|
102
|
+
await CreatePage({
|
|
103
|
+
name: "revenue",
|
|
104
|
+
displayName: "Revenue",
|
|
105
|
+
category: "pages",
|
|
106
|
+
dir: "reporting", // → src/reporting/revenue/page.ts
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Configure a Page
|
|
111
|
+
|
|
112
|
+
`ConfigurePage(ref, patch, opts?)` patches the page's metadata in place. The patch is a `Partial<EditablePageMeta>`; keys you leave out are untouched, and a key set to `undefined` is removed from the page's options.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { ConfigurePage } from "@antelopejs/interface-dms-builder";
|
|
116
|
+
|
|
117
|
+
const result = await ConfigurePage("/revenue", {
|
|
118
|
+
displayName: "Monthly revenue",
|
|
119
|
+
order: 10,
|
|
120
|
+
hidden: false,
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Move a Page Between Categories
|
|
125
|
+
|
|
126
|
+
A page's category decides its route, so patching `category` moves the page rather than editing a field. `ConfigurePage` rewrites the `category` option and its import, moves the file to the directory beside the new category — where `CreatePage` would have put it — and moves the registration with it, so each directory's barrel still lists what it holds. Relative imports inside the page are re-anchored to its new location.
|
|
127
|
+
|
|
128
|
+
The result carries the page's new `ref`, and a `route_changed` warning naming the old one. **Nothing redirects the old route**: links and bookmarks to it break.
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { ConfigurePage } from "@antelopejs/interface-dms-builder";
|
|
132
|
+
|
|
133
|
+
const moved = await ConfigurePage("/form/revenue", { category: "pages.reports" });
|
|
134
|
+
if (moved.ok) {
|
|
135
|
+
console.log(moved.data.ref); // "/reports/revenue"
|
|
136
|
+
for (const warning of moved.warnings ?? []) {
|
|
137
|
+
console.log(warning.code, warning.message); // "route_changed", …
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The move is refused when the target category is unknown (`not_found`), when it is built-in or not declared in source so a page cannot reference it (`unsupported`), or when the destination already holds a page of that name (`duplicate_name`). A page whose options are not a plain object literal fails with `unsupported` too — the builder rewrites source, so it needs a literal to rewrite.
|
|
143
|
+
|
|
144
|
+
`opts` is a `MutationOpts`, so `expectedVersion` makes the write conditional.
|
|
145
|
+
|
|
146
|
+
## Delete a Page
|
|
147
|
+
|
|
148
|
+
`DeletePage(ref, opts?)` removes the page file and unwires every import of it.
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { DeletePage } from "@antelopejs/interface-dms-builder";
|
|
152
|
+
|
|
153
|
+
const result = await DeletePage("/revenue", { expectedVersion: version });
|
|
154
|
+
if (!result.ok && result.error.code === "stale") {
|
|
155
|
+
// Re-read the page and decide again.
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Create a Category
|
|
160
|
+
|
|
161
|
+
`CreateCategory(input)` declares a new category and registers it in the root barrel.
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import { CreateCategory } from "@antelopejs/interface-dms-builder";
|
|
165
|
+
|
|
166
|
+
const result = await CreateCategory({
|
|
167
|
+
name: "reports",
|
|
168
|
+
displayName: "Reports",
|
|
169
|
+
parent: "pages",
|
|
170
|
+
icon: "i-ph-folder",
|
|
171
|
+
order: 3,
|
|
172
|
+
});
|
|
173
|
+
// result.data.ref === "pages.reports"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`CreateCategoryInput`:
|
|
177
|
+
|
|
178
|
+
| Option | Type | Description |
|
|
179
|
+
| --- | --- | --- |
|
|
180
|
+
| `name` | `string` | The category's id segment. Required. A duplicate fails with `duplicate_name`. |
|
|
181
|
+
| `displayName` | `string` | The label shown in the menu. Required. |
|
|
182
|
+
| `parent` | `CategoryRef` | The parent category. Omit for a category under the root `pages`. |
|
|
183
|
+
| `icon` | `string` | Icon name. |
|
|
184
|
+
| `order` | `number` | Sort order among siblings. |
|
|
185
|
+
|
|
186
|
+
A `parent` that does not exist fails with `not_found`.
|
|
187
|
+
|
|
188
|
+
## Configure a Category
|
|
189
|
+
|
|
190
|
+
`ConfigureCategory(ref, patch)` patches an existing category. It takes no options argument.
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { ConfigureCategory } from "@antelopejs/interface-dms-builder";
|
|
194
|
+
|
|
195
|
+
await ConfigureCategory("pages.reports", {
|
|
196
|
+
displayName: "Reporting",
|
|
197
|
+
order: 1,
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Two refusals, both `unsupported`: re-parenting (a patch carrying `parent`), and a category that is built in or otherwise not declared in editable source.
|
|
202
|
+
|
|
203
|
+
## Delete a Category
|
|
204
|
+
|
|
205
|
+
`DeleteCategory(ref)` removes the category declaration, and deletes its file when nothing but imports is left in it.
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { DeleteCategory } from "@antelopejs/interface-dms-builder";
|
|
209
|
+
|
|
210
|
+
const result = await DeleteCategory("pages.reports");
|
|
211
|
+
if (!result.ok && result.error.code === "referential_integrity") {
|
|
212
|
+
console.log("still in use by", result.error.blockedBy);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
A category that still holds pages or child categories fails with `referential_integrity`, and `blockedBy` lists the refs standing in the way. Empty it first.
|
|
217
|
+
|
|
218
|
+
## Add a Block
|
|
219
|
+
|
|
220
|
+
`AddBlock(input, opts?)` emits a factory call into the page and returns the `BlockPath` of the new block.
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { AddBlock } from "@antelopejs/interface-dms-builder";
|
|
224
|
+
|
|
225
|
+
const result = await AddBlock({
|
|
226
|
+
page: "/revenue",
|
|
227
|
+
name: "total",
|
|
228
|
+
type: "KpiCard",
|
|
229
|
+
config: {
|
|
230
|
+
title: "Total revenue",
|
|
231
|
+
icon: "i-ph-currency-eur",
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
if (result.ok) {
|
|
235
|
+
console.log(result.data.path); // "/revenue#total"
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`AddBlockInput`:
|
|
240
|
+
|
|
241
|
+
| Option | Type | Description |
|
|
242
|
+
| --- | --- | --- |
|
|
243
|
+
| `page` | `PageRef` | The page to add to. Required. |
|
|
244
|
+
| `name` | `string` | The block's name, unique among its siblings. Required. A duplicate fails with `duplicate_name`. |
|
|
245
|
+
| `type` | `string` | A `BlockTypeDescriptor.type` from the catalog. Required. An unknown type fails with `unsupported`. |
|
|
246
|
+
| `config` | `Record<string, unknown>` | The factory's options object. Required. |
|
|
247
|
+
| `parent` | `BlockPath` | The container to nest under. Omit to add a top-level block. |
|
|
248
|
+
| `slot` | `string` | The slot within the parent container. |
|
|
249
|
+
| `index` | `number` | Position among siblings. Defaults to appending. |
|
|
250
|
+
| `controller` | `ResourceRef` | The resource whose DataAPI class is the leading argument, for controller-leading block types. |
|
|
251
|
+
|
|
252
|
+
`config` is plain JSON by default; values that are not literals — a DataType instance, a reference to a generated class — use the sentinel encodings in [Value Encodings](./7.values.md).
|
|
253
|
+
|
|
254
|
+
Nesting requires an editable container: a `parent` that resolves to an opaque block fails with `opaque_target`, and one that does not resolve at all fails with `not_found`. An out-of-range `index` is clamped, and the result carries an `index_clamped` warning.
|
|
255
|
+
|
|
256
|
+
Controller-leading types (catalog `controllerArg: true`, e.g. `TableView`) require `controller`; passing it for any other type is rejected. Both mismatches fail with `invalid_config`.
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import { AddBlock } from "@antelopejs/interface-dms-builder";
|
|
260
|
+
|
|
261
|
+
await AddBlock({
|
|
262
|
+
page: "/products",
|
|
263
|
+
name: "table",
|
|
264
|
+
type: "TableView",
|
|
265
|
+
controller: "product", // emitted as the imported productDataAPI class
|
|
266
|
+
config: { caption: "Products" },
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Configure a Block
|
|
271
|
+
|
|
272
|
+
`ConfigureBlock(path, patch, opts?)` rewrites a block's options object. By default the patch merges: listed keys are replaced, unlisted keys survive, and a key set to `undefined` is removed.
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { ConfigureBlock } from "@antelopejs/interface-dms-builder";
|
|
276
|
+
|
|
277
|
+
await ConfigureBlock("/revenue#total", {
|
|
278
|
+
title: "Revenue (net)",
|
|
279
|
+
description: undefined, // removes the option
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`ConfigureBlockOpts` extends `MutationOpts`:
|
|
284
|
+
|
|
285
|
+
| Option | Type | Description |
|
|
286
|
+
| --- | --- | --- |
|
|
287
|
+
| `replace` | `boolean` | Replace the whole options object instead of merging into it. |
|
|
288
|
+
| `expectedVersion` | `string` | The `PageStructure.version` you last read. A mismatch fails with `stale`. |
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
import { ConfigureBlock } from "@antelopejs/interface-dms-builder";
|
|
292
|
+
|
|
293
|
+
await ConfigureBlock(
|
|
294
|
+
"/revenue#total",
|
|
295
|
+
{ title: "Revenue" },
|
|
296
|
+
{ replace: true, expectedVersion: version },
|
|
297
|
+
);
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
An opaque block fails with `opaque_target`: the builder can read it but not rewrite it.
|
|
301
|
+
|
|
302
|
+
## Move a Block
|
|
303
|
+
|
|
304
|
+
`MoveBlock(path, dest, opts?)` moves a child block within its container or into another one.
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import { MoveBlock } from "@antelopejs/interface-dms-builder";
|
|
308
|
+
|
|
309
|
+
await MoveBlock("/revenue#grid/row/total", {
|
|
310
|
+
parent: "/revenue#grid/footer",
|
|
311
|
+
index: 0,
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`MoveDest`:
|
|
316
|
+
|
|
317
|
+
| Option | Type | Description |
|
|
318
|
+
| --- | --- | --- |
|
|
319
|
+
| `parent` | `BlockPath` | The destination container. |
|
|
320
|
+
| `slot` | `string` | The slot within the destination container. |
|
|
321
|
+
| `index` | `number` | Position among the destination's children. Required. |
|
|
322
|
+
|
|
323
|
+
The move is a child-into-container operation: the path must address a nested block, and `dest.parent` must name a container. Anything else — a top-level block, a destination without a `parent` — fails with `unsupported`. An out-of-range `index` is clamped with an `index_clamped` warning, and a destination that is opaque or missing fails with `opaque_target` or `not_found`.
|
|
324
|
+
|
|
325
|
+
## Remove a Block
|
|
326
|
+
|
|
327
|
+
`RemoveBlock(path, opts?)` deletes a block. A top-level block is removed as a static member; a nested one is dropped from its parent's `.child()` chain.
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import { RemoveBlock } from "@antelopejs/interface-dms-builder";
|
|
331
|
+
|
|
332
|
+
const result = await RemoveBlock("/revenue#grid/row/total");
|
|
333
|
+
if (result.ok) {
|
|
334
|
+
console.log(result.changes.map((change) => change.path));
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Next Steps
|
|
339
|
+
|
|
340
|
+
- [Resources](./5.resources.md) - Generate the tables and DataAPI controllers a page reads from
|
|
341
|
+
- [Drafts and Preview](./9.drafts-and-preview.md) - Write a page's whole block tree in one transaction
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Resources
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
A resource is the data half of a DMS feature: a database `Table`, a `DataController` exposing it over HTTP, and the model that binds them. `CreateResource` writes all of it into a `<name>/` folder — `database.ts`, `data-api.ts` and an `index.ts` barrel — and wires the nearest barrel above so the project keeps a relative import chain to `src/index.ts`.
|
|
6
|
+
|
|
7
|
+
You describe a resource in semantic terms: each field carries a DataType and a set of aspects such as *searchable* or *sortable*, and the engine maps those to the paired decorator stacks on the table and the controller. You never write a decorator yourself, and you never name one in a request.
|
|
8
|
+
|
|
9
|
+
A resource is also what a controller-leading block points at: `AddBlock` with `controller: "<ref>"` emits the resource's DataAPI class as the block's leading argument, as described in [Page Operations](./4.page-operations.md).
|
|
10
|
+
|
|
11
|
+
## Address a Resource and a Field
|
|
12
|
+
|
|
13
|
+
`ResourceRef` is the resource's name in kebab form — `"product"`, `"order-line"`. It is what `CreateResource` returns, what `ListResources` reports, and what every other resource operation takes.
|
|
14
|
+
|
|
15
|
+
`FieldPath` addresses one field within a resource: `<resourceRef>#<fieldName>`, e.g. `product#price`. `AddField` returns the path it created.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import type {
|
|
19
|
+
FieldPath,
|
|
20
|
+
ResourceRef,
|
|
21
|
+
} from "@antelopejs/interface-dms-builder";
|
|
22
|
+
|
|
23
|
+
const resource: ResourceRef = "product";
|
|
24
|
+
const field: FieldPath = "product#price";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Create a Resource
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { CreateResource } from "@antelopejs/interface-dms-builder";
|
|
31
|
+
|
|
32
|
+
const result = await CreateResource({
|
|
33
|
+
name: "product",
|
|
34
|
+
displayName: "Product",
|
|
35
|
+
fields: [
|
|
36
|
+
{
|
|
37
|
+
name: "title",
|
|
38
|
+
dataType: { $dataType: "string" },
|
|
39
|
+
label: "Title",
|
|
40
|
+
required: true,
|
|
41
|
+
searchable: true,
|
|
42
|
+
sortable: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "price",
|
|
46
|
+
dataType: { $dataType: "number" },
|
|
47
|
+
sortable: true,
|
|
48
|
+
filterable: true,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
if (result.ok) {
|
|
53
|
+
console.log(result.data.ref, result.data.files.dataApi);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`CreateResourceInput`:
|
|
58
|
+
|
|
59
|
+
| Option | Type | Description |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `name` | `string` | The resource name. Required, and it must contain at least one alphanumeric character. A duplicate fails with `duplicate_name`. |
|
|
62
|
+
| `fields` | `FieldSpec[]` | The resource's fields. Required. |
|
|
63
|
+
| `displayName` | `string` | A human-readable name for the resource. |
|
|
64
|
+
| `schema` | `string` | The database schema. Defaults to the DMS core schema, `CORE_SCHEMA_NAME` (`dms-core`). |
|
|
65
|
+
| `seeds` | `Record<string, unknown>[]` | Rows to seed as a fixture. Each row must supply its own `_id`. |
|
|
66
|
+
| `location` | `ResourceLocation` | Where the generated folder goes. Omit for `src/<name>/`. |
|
|
67
|
+
| `routes` | `ResourceRoute[]` | Which DataAPI routes to expose. Omit for the full set. Must be non-empty when provided. |
|
|
68
|
+
|
|
69
|
+
The names the engine derives from `name` are predictable: `product` yields a `Product` table class, a `ProductModel`, a `productDataAPI` controller on the route `/api/product`, and a `products` table.
|
|
70
|
+
|
|
71
|
+
## Describe a Field
|
|
72
|
+
|
|
73
|
+
`FieldSpec` is a `FieldAspects` plus the field's `name`. The name is immutable — renaming a field is `RemoveField` followed by `AddField`.
|
|
74
|
+
|
|
75
|
+
| Option | Type | Description |
|
|
76
|
+
| --- | --- | --- |
|
|
77
|
+
| `name` | `string` | The field name. Rejected with `invalid_config` when it is one of the catalog's `reservedFieldNames`. |
|
|
78
|
+
| `dataType` | `DataTypeValue` | The field's DataType, e.g. `{ $dataType: "string" }`. Required. |
|
|
79
|
+
| `label` | `string` | Column header label. Defaults to the field name. |
|
|
80
|
+
| `listable` | `boolean` | Selected and listed by the DataAPI. Default `true`. |
|
|
81
|
+
| `searchable` | `boolean` | Included in full-text search. |
|
|
82
|
+
| `sortable` | `boolean` | Sortable in the table view. |
|
|
83
|
+
| `filterable` | `boolean` | Column-filterable in the table view. |
|
|
84
|
+
| `access` | `"read" \| "readwrite"` | Field access mode. Default `"readwrite"`. |
|
|
85
|
+
| `required` | `boolean` | Whether the field is required. Default `false`, which makes the field optional. |
|
|
86
|
+
| `indexed` | `boolean` | Whether the table column is indexed. |
|
|
87
|
+
| `order` | `number` | Display order of the column. |
|
|
88
|
+
|
|
89
|
+
`dataType` takes a `DataTypeValue` sentinel, not a bare string; the ids come from the catalog's `dataTypes`. See [Value Encodings](./7.values.md) and [Catalog](./3.catalog.md).
|
|
90
|
+
|
|
91
|
+
## Choose the Exposed Routes
|
|
92
|
+
|
|
93
|
+
`ResourceRoute` names one group of DataAPI endpoints. Passing a subset lets a resource serve only what it needs — a singleton edit form wants `["get", "edit"]` and nothing else. An omitted route's HTTP endpoint is simply not registered.
|
|
94
|
+
|
|
95
|
+
| Route | What it exposes |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| `list` | The paginated listing (`list` + `count`). |
|
|
98
|
+
| `get` | Read a single row. |
|
|
99
|
+
| `create` | Create a row. |
|
|
100
|
+
| `edit` | Update a row. |
|
|
101
|
+
| `delete` | Delete a row. |
|
|
102
|
+
| `select` | Option lists for relation pickers that reference this resource. |
|
|
103
|
+
| `archive` | Soft delete and undo (`archive` + `restore`). |
|
|
104
|
+
| `export` | The export endpoints (`exportStart` / `exportStatus` / `exportDownload`). |
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { CreateResource } from "@antelopejs/interface-dms-builder";
|
|
108
|
+
|
|
109
|
+
await CreateResource({
|
|
110
|
+
name: "site-settings",
|
|
111
|
+
fields: [{ name: "title", dataType: { $dataType: "string" } }],
|
|
112
|
+
routes: ["get", "edit"],
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
An empty array, or a name that is not one of the eight, fails with `invalid_config`.
|
|
117
|
+
|
|
118
|
+
## Place the Generated Files
|
|
119
|
+
|
|
120
|
+
`ResourceLocation` decides which folder the `<name>/` folder lands in. The nearest ancestor barrel to the resolved folder is the one that gets wired, with the root barrel as the fallback.
|
|
121
|
+
|
|
122
|
+
| Form | Where the resource lands |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| omitted | `src/<name>/` |
|
|
125
|
+
| `"reporting/data"` | `src/reporting/data/<name>/` — an explicit folder relative to the src root, which must stay inside the src tree. |
|
|
126
|
+
| `{ category: "pages.reports" }` | The source folder of that category, mirroring page placement. A root category falls back to the src root. |
|
|
127
|
+
| `{ page: "/revenue" }` | Inside that page's feature folder, so the page imports `./<name>/data-api`. |
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { CreateResource } from "@antelopejs/interface-dms-builder";
|
|
131
|
+
|
|
132
|
+
await CreateResource({
|
|
133
|
+
name: "revenue-entry",
|
|
134
|
+
fields: [{ name: "amount", dataType: { $dataType: "number" } }],
|
|
135
|
+
location: { page: "/revenue" },
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
A `dir` string that escapes the src tree, an unknown category and an unknown page all fail with `invalid_config`.
|
|
140
|
+
|
|
141
|
+
## Add, Configure and Remove a Field
|
|
142
|
+
|
|
143
|
+
`AddField(ref, field, opts?)` appends a field to both generated files and returns its `FieldPath`.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { AddField } from "@antelopejs/interface-dms-builder";
|
|
147
|
+
|
|
148
|
+
const result = await AddField("product", {
|
|
149
|
+
name: "sku",
|
|
150
|
+
dataType: { $dataType: "string" },
|
|
151
|
+
label: "SKU",
|
|
152
|
+
indexed: true,
|
|
153
|
+
searchable: true,
|
|
154
|
+
});
|
|
155
|
+
if (result.ok) {
|
|
156
|
+
console.log(result.data.path); // "product#sku"
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
A name already present on the table or the controller fails with `duplicate_name`; a reserved name fails with `invalid_config`.
|
|
161
|
+
|
|
162
|
+
`ConfigureField(path, patch, opts?)` reads the field's current aspects, merges your `Partial<FieldAspects>` over them, and rewrites both declarations from the result. Aspects you leave out keep their current value.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import { ConfigureField } from "@antelopejs/interface-dms-builder";
|
|
166
|
+
|
|
167
|
+
await ConfigureField("product#price", {
|
|
168
|
+
filterable: true,
|
|
169
|
+
order: 2,
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
A field whose decorator stack the builder could not reverse — a hand-edited one, or one carrying a decorator the builder does not know — is opaque, and configuring it fails with `opaque_target`.
|
|
174
|
+
|
|
175
|
+
`RemoveField(path, opts?)` drops the field from both files.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { RemoveField } from "@antelopejs/interface-dms-builder";
|
|
179
|
+
|
|
180
|
+
await RemoveField("product#sku");
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
All three take a `MutationOpts`, whose `expectedVersion` is checked against the resource's version — a hash of both generated files.
|
|
184
|
+
|
|
185
|
+
## List Resources
|
|
186
|
+
|
|
187
|
+
`ListResources()` returns a `ResourceSummary[]` directly, without an `OpResult` wrapper.
|
|
188
|
+
|
|
189
|
+
| Field | Type | Description |
|
|
190
|
+
| --- | --- | --- |
|
|
191
|
+
| `ref` | `ResourceRef` | The resource's ref. |
|
|
192
|
+
| `className` | `string` | The generated table class name. |
|
|
193
|
+
| `tableName` | `string` | The database table name. |
|
|
194
|
+
| `route` | `string` | The DataAPI's base route. |
|
|
195
|
+
| `fieldCount` | `number` | How many fields the resource has. |
|
|
196
|
+
| `files` | `{ database: string; dataApi: string }` | The two source files. |
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import { ListResources } from "@antelopejs/interface-dms-builder";
|
|
200
|
+
|
|
201
|
+
const resources = await ListResources();
|
|
202
|
+
const refs = resources.map((resource) => resource.ref);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Read a Resource's Structure
|
|
206
|
+
|
|
207
|
+
`GetResourceStructure(ref)` returns an `OpResult<ResourceStructure>`. Like the other reads, its `changes` array is empty.
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { GetResourceStructure } from "@antelopejs/interface-dms-builder";
|
|
211
|
+
|
|
212
|
+
const result = await GetResourceStructure("product");
|
|
213
|
+
if (result.ok) {
|
|
214
|
+
const editable = result.data.fields.filter((field) => !field.opaque);
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`ResourceStructure`:
|
|
219
|
+
|
|
220
|
+
| Field | Type | Description |
|
|
221
|
+
| --- | --- | --- |
|
|
222
|
+
| `ref` | `ResourceRef` | The resource's ref. |
|
|
223
|
+
| `className` | `string` | The generated table class name. |
|
|
224
|
+
| `tableName` | `string` | The database table name. |
|
|
225
|
+
| `route` | `string` | The DataAPI's base route. |
|
|
226
|
+
| `schema` | `string` | The database schema the table is registered in. |
|
|
227
|
+
| `files` | `ResourceFiles` | The resource's three files. |
|
|
228
|
+
| `fields` | `ResourceFieldStructure[]` | The fields, in declaration order. |
|
|
229
|
+
| `routes` | `ResourceRoute[]` | The exposed routes when the resource serves a subset; omitted when it serves the full set. Best-effort: absent for a hand-edited route map the engine could not map back to semantic names. |
|
|
230
|
+
| `version` | `string` | A content hash of both generated files. |
|
|
231
|
+
|
|
232
|
+
`ResourceFiles` names the three paths a resource is composed of: `database`, `dataApi` and `index`.
|
|
233
|
+
|
|
234
|
+
`ResourceFieldStructure` is a `Partial<FieldAspects>` plus:
|
|
235
|
+
|
|
236
|
+
| Field | Type | Description |
|
|
237
|
+
| --- | --- | --- |
|
|
238
|
+
| `name` | `string` | The field name. Always present. |
|
|
239
|
+
| `opaque` | `boolean` | `true` when the field's decorator stack could not be reversed. |
|
|
240
|
+
| `opaqueReason` | `string` | Why it is opaque, e.g. an unsupported decorator or a missing `@Column` stack. Set when `opaque === true`. |
|
|
241
|
+
|
|
242
|
+
A cleanly-reversed field carries its aspects; an opaque one carries only `name` and `opaqueReason`. Read `opaque` before offering a field for editing.
|
|
243
|
+
|
|
244
|
+
## Delete a Resource
|
|
245
|
+
|
|
246
|
+
`DeleteResource(ref, opts?)` deletes the three generated files, unwires every import of them, removes the folder if it is now empty, and — by default — clears the resource's database table so a later resource reusing the name re-seeds cleanly instead of inheriting stale documents.
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import { DeleteResource } from "@antelopejs/interface-dms-builder";
|
|
250
|
+
|
|
251
|
+
const result = await DeleteResource("product");
|
|
252
|
+
if (result.ok) {
|
|
253
|
+
for (const warning of result.warnings ?? []) {
|
|
254
|
+
console.log(warning.code, warning.message);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`DeleteResourceOpts` extends `MutationOpts`:
|
|
260
|
+
|
|
261
|
+
| Option | Type | Description |
|
|
262
|
+
| --- | --- | --- |
|
|
263
|
+
| `keepData` | `boolean` | Leave the resource's table data in place instead of deleting every row. |
|
|
264
|
+
| `expectedVersion` | `string` | The `ResourceStructure.version` you last read. A mismatch fails with `stale`. |
|
|
265
|
+
|
|
266
|
+
When the rows cannot be dropped, the operation still succeeds and attaches a `data_not_dropped` warning naming the reason — the source is gone, the data may not be.
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import { DeleteResource } from "@antelopejs/interface-dms-builder";
|
|
270
|
+
|
|
271
|
+
await DeleteResource("product", { keepData: true });
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Next Steps
|
|
275
|
+
|
|
276
|
+
- [Queries](./6.queries.md) - Derive counts and aggregates from a resource and serve them from a page
|