@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,229 @@
|
|
|
1
|
+
# Queries
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
A query is derived data: a count, a sum, an average taken over a resource's rows and served from a page. `AddQuery` compiles a query template into a method on the resource's model and a route on the page's controller, so a block on that page can fetch a single number without you writing either half.
|
|
6
|
+
|
|
7
|
+
A query is addressed by a `QueryRef`, lives on a page, and reads from a resource. Templates decide what a query can compute; you pick one by id, fill in its parameters, and the builder emits the chain.
|
|
8
|
+
|
|
9
|
+
Queries read back structurally: `GetPageStructure` reports the queries it finds on a page in `PageStructure.queries`, whether the builder wrote them or a human did.
|
|
10
|
+
|
|
11
|
+
## Address a Query
|
|
12
|
+
|
|
13
|
+
`QueryRef` is `<pageRef>@<name>`, e.g. `/books@inStockCount`. The page part says where the route lives; the resource the query reads is recorded in the query itself, not in its ref.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import type { QueryRef } from "@antelopejs/interface-dms-builder";
|
|
17
|
+
|
|
18
|
+
const query: QueryRef = "/books@inStockCount";
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## List the Templates
|
|
22
|
+
|
|
23
|
+
`ListQueryTemplates(resourceType?)` returns the templates currently registered. It is a plain read, and the registry is live, so call it rather than hard-coding ids.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { ListQueryTemplates } from "@antelopejs/interface-dms-builder";
|
|
27
|
+
|
|
28
|
+
const templates = await ListQueryTemplates("database-table");
|
|
29
|
+
for (const template of templates) {
|
|
30
|
+
console.log(template.id, template.title, template.output);
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`QueryTemplateDescriptor`:
|
|
35
|
+
|
|
36
|
+
| Field | Type | Description |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| `id` | `string` | The template id, e.g. `count` or `aggregate`. This is `AddQueryInput.template`. |
|
|
39
|
+
| `resourceType` | `"database-table"` | The kind of resource the template reads. |
|
|
40
|
+
| `title` | `string` | A human-readable title. |
|
|
41
|
+
| `description` | `string` | What the template computes. |
|
|
42
|
+
| `output` | `QueryOutputKind` | The shape the query's route responds with. |
|
|
43
|
+
| `params` | `ConfigSchema` | The template's parameter schema, in the same vocabulary as the block catalog. |
|
|
44
|
+
|
|
45
|
+
`QueryOutputKind` is `"scalar"`: the route responds with a `{ value }` object.
|
|
46
|
+
|
|
47
|
+
Read `params` the way you read a block's `config` schema, described in [Catalog](./3.catalog.md) — it tells you which keys `AddQueryInput.params` accepts, and a key the template does not declare fails with `invalid_config`.
|
|
48
|
+
|
|
49
|
+
## Add a Query
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { AddQuery } from "@antelopejs/interface-dms-builder";
|
|
53
|
+
|
|
54
|
+
const result = await AddQuery("/books", {
|
|
55
|
+
name: "inStockCount",
|
|
56
|
+
resource: "book",
|
|
57
|
+
template: "count",
|
|
58
|
+
params: {
|
|
59
|
+
where: [{ field: "stock", op: "gt", value: 0 }],
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
if (result.ok) {
|
|
63
|
+
console.log(result.data.query); // "/books@inStockCount"
|
|
64
|
+
console.log(result.data.route); // "/books/stats/in-stock-count"
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`AddQueryInput`:
|
|
69
|
+
|
|
70
|
+
| Option | Type | Description |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `name` | `string` | The query name, a camelCase identifier. Required. It names the route method, and the model method too. |
|
|
73
|
+
| `resource` | `ResourceRef` | The resource the query reads. Required, and it must exist. |
|
|
74
|
+
| `template` | `string` | A `QueryTemplateDescriptor.id`. Required. |
|
|
75
|
+
| `params` | `Record<string, QueryParamValue>` | The template's parameters. |
|
|
76
|
+
| `endpoint` | `string` | The route path relative to the page's slug. Defaults to `/stats/<kebab-name>`. |
|
|
77
|
+
|
|
78
|
+
The result carries both addresses: `query` is the `QueryRef` to configure or remove it by, `route` is the page slug joined with the endpoint — the URL a block fetches from.
|
|
79
|
+
|
|
80
|
+
A few rules the operation enforces:
|
|
81
|
+
|
|
82
|
+
- `name` must be a camelCase identifier, and must not collide with a member the page or the model already has. A collision fails with `duplicate_name` in the `query` scope.
|
|
83
|
+
- `endpoint` must start with `/`, and must not collide with another route on the page. Any path is allowed otherwise, because read-back identifies a query by its shape rather than by its URL.
|
|
84
|
+
- When a model method of that name already exists compiling a *different* chain, the model method is emitted under a free suffixed name, so the queries already sharing the original keep their behavior.
|
|
85
|
+
|
|
86
|
+
## Filter the Rows
|
|
87
|
+
|
|
88
|
+
`QueryFilter` is one conjunct of a template's `where`. Filters combine with AND.
|
|
89
|
+
|
|
90
|
+
| Field | Type | Description |
|
|
91
|
+
| --- | --- | --- |
|
|
92
|
+
| `field` | `string` | A field of the resource. It must exist and must not be opaque. |
|
|
93
|
+
| `op` | `FilterOp` | The comparison. |
|
|
94
|
+
| `value` | `QueryParamValue` | A plain scalar baked into the chain, or a `QueryParamBinding`. |
|
|
95
|
+
|
|
96
|
+
`FilterOp` is `"eq" | "ne" | "gt" | "ge" | "lt" | "le"`. The ordering operators (`gt`, `ge`, `lt`, `le`) are rejected on `boolean` and `select` fields.
|
|
97
|
+
|
|
98
|
+
`QueryParamValue` is `unknown`, and a query's values are data: an `ExprValue` anywhere in a query's params is refused with `invalid_config`, since a raw expression would put uncheckable code inside a model body. The other sentinels are described in [Value Encodings](./7.values.md).
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { AddQuery } from "@antelopejs/interface-dms-builder";
|
|
102
|
+
|
|
103
|
+
await AddQuery("/books", {
|
|
104
|
+
name: "revenue",
|
|
105
|
+
resource: "book",
|
|
106
|
+
template: "aggregate",
|
|
107
|
+
params: {
|
|
108
|
+
op: "sum",
|
|
109
|
+
field: "price",
|
|
110
|
+
where: [{ field: "archived", op: "eq", value: false }],
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Bind a Value at Request Time
|
|
116
|
+
|
|
117
|
+
`QueryParamBinding` replaces a baked-in filter value with one supplied per request. It becomes a parameter of the model method and a declared parameter on the route.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { AddQuery } from "@antelopejs/interface-dms-builder";
|
|
121
|
+
|
|
122
|
+
await AddQuery("/books", {
|
|
123
|
+
name: "cheaperThan",
|
|
124
|
+
resource: "book",
|
|
125
|
+
template: "count",
|
|
126
|
+
params: {
|
|
127
|
+
where: [
|
|
128
|
+
{ field: "price", op: "lt", value: { $param: { name: "maxPrice" } } },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
// GET /books/stats/cheaper-than?maxPrice=20
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
| Field | Type | Description |
|
|
136
|
+
| --- | --- | --- |
|
|
137
|
+
| `$param.name` | `string` | A camelCase identifier. The route exposes the value under this name. |
|
|
138
|
+
| `$param.in` | `"query" \| "param"` | Where the route reads the value from. Defaults to `"query"`. |
|
|
139
|
+
|
|
140
|
+
`name` and `in` describe the route, not the model: the route reads the value from that source, coerces it, and passes it positionally to the model method, whose own parameter is named from the template.
|
|
141
|
+
|
|
142
|
+
Two rules to plan for:
|
|
143
|
+
|
|
144
|
+
- `in: "param"` reads a path segment, so the endpoint must declare a matching `:name`. The default `/stats/…` endpoint has none, so a path-bound value forces you to supply an endpoint that does. A mismatch fails with `invalid_config` rather than emitting a route whose value never binds.
|
|
145
|
+
- Two filters may bind the same parameter name, but only when they agree on its type and source.
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { AddQuery } from "@antelopejs/interface-dms-builder";
|
|
149
|
+
|
|
150
|
+
await AddQuery("/books", {
|
|
151
|
+
name: "countByAuthor",
|
|
152
|
+
resource: "book",
|
|
153
|
+
template: "count",
|
|
154
|
+
endpoint: "/stats/by-author/:authorId",
|
|
155
|
+
params: {
|
|
156
|
+
where: [
|
|
157
|
+
{
|
|
158
|
+
field: "authorId",
|
|
159
|
+
op: "eq",
|
|
160
|
+
value: { $param: { name: "authorId", in: "param" } },
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Configure a Query
|
|
168
|
+
|
|
169
|
+
`ConfigureQuery(query, patch, opts?)` recompiles the query from the merged patch and replaces both emitted members. Fields you leave out keep their current value, read back off disk.
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { ConfigureQuery } from "@antelopejs/interface-dms-builder";
|
|
173
|
+
|
|
174
|
+
await ConfigureQuery("/books@inStockCount", {
|
|
175
|
+
params: { where: [{ field: "stock", op: "ge", value: 5 }] },
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The patch is a `Partial<AddQueryInput>`, so `resource`, `template`, `params` and `endpoint` are all patchable. The query keeps the name in its `QueryRef`: a `name` in the patch is not applied.
|
|
180
|
+
|
|
181
|
+
A query that no longer reads back as an editable template fails with `opaque_target`. That is deliberate — an opaque chain means a human owns it now, and regenerating it from a template it no longer matches would revert their edit. Remove it and add a new one if you need to.
|
|
182
|
+
|
|
183
|
+
## Remove a Query
|
|
184
|
+
|
|
185
|
+
`RemoveQuery(query, opts?)` deletes the query's route from the page, and drops the model method it called once nothing else calls it — a method shared with another query stays. It works on an opaque query too.
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { RemoveQuery } from "@antelopejs/interface-dms-builder";
|
|
189
|
+
|
|
190
|
+
await RemoveQuery("/books@cheaperThan");
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Read a Query Back
|
|
194
|
+
|
|
195
|
+
`QueryStructure` is a query as found on disk. Read-back is purely structural: a route reads as a query when its shape and the chain it calls both parse, so a hand-written route matching the grammar is as editable as a generated one, and any edit that keeps the chain in-grammar is simply re-read.
|
|
196
|
+
|
|
197
|
+
| Field | Type | Description |
|
|
198
|
+
| --- | --- | --- |
|
|
199
|
+
| `name` | `string` | The query name. Always present. |
|
|
200
|
+
| `endpoint` | `string` | The route path relative to the page's slug, as `AddQueryInput.endpoint` takes it. Always present. |
|
|
201
|
+
| `resource` | `ResourceRef` | The resource the route's model parameter points at. |
|
|
202
|
+
| `modelMethod` | `string` | The model method the route calls. Several queries may share one method, but only while they want the exact same chain. |
|
|
203
|
+
| `output` | `QueryOutputKind` | The output shape, from the template descriptor. |
|
|
204
|
+
| `template` | `string` | The template the chain was compiled from. |
|
|
205
|
+
| `params` | `Record<string, unknown>` | The parameters that produced the chain. |
|
|
206
|
+
| `routeParams` | `{ name: string; in: "query" \| "param" \| "header" }[]` | The route's declared parameters — the source of truth for a bound value's `name` and `in`. `header` appears only on hand-written routes. |
|
|
207
|
+
| `opaque` | `boolean` | `true` when the query could not be reversed to an editable template. |
|
|
208
|
+
| `opaqueReason` | `"unparseable_route" \| "unparseable_chain"` | Why it is opaque. Set when `opaque === true`. |
|
|
209
|
+
|
|
210
|
+
The two opaque reasons say which half failed:
|
|
211
|
+
|
|
212
|
+
- **`unparseable_route`** — the GET wrapper is not the recognized `return { value: await model.m(...) }` shape.
|
|
213
|
+
- **`unparseable_chain`** — the model method it calls is not a chain any registered template accepts: hand-written, edited out of grammar, or emitted by a newer builder.
|
|
214
|
+
|
|
215
|
+
A query that no longer parses still reports its `endpoint`, `resource` and `modelMethod`; only `template` and `params` go missing. The builder can therefore always see and remove what it cannot edit.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { GetPageStructure } from "@antelopejs/interface-dms-builder";
|
|
219
|
+
|
|
220
|
+
const result = await GetPageStructure("/books");
|
|
221
|
+
if (result.ok) {
|
|
222
|
+
const editable = result.data.queries.filter((query) => !query.opaque);
|
|
223
|
+
const urls = result.data.queries.map((query) => `/books${query.endpoint}`);
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Next Steps
|
|
228
|
+
|
|
229
|
+
- [Value Encodings](./7.values.md) - Write configuration values that are not plain literals
|
package/docs/7.values.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Value Encodings
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Configuration values are plain JSON by default. `AddBlockInput.config`, a `ConfigureBlock` patch, a page's `permission` — all of it is written to source exactly as you pass it, as a literal.
|
|
6
|
+
|
|
7
|
+
Some values are not literals. A Form field's `type` is a `new DefaultDataTypes.StringType(...)` instance; a `TableView`'s controller is a class identifier with an import behind it. Three sentinel object shapes cover those cases: `DataTypeValue` for a DataType instance, `RefValue` for a builder-generated class, and `ExprValue` for a raw expression the builder cannot model. Each is recognized wherever a config value is read, at any depth — inside an array, inside a nested object.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import type {
|
|
11
|
+
DataTypeValue,
|
|
12
|
+
ExprValue,
|
|
13
|
+
RefValue,
|
|
14
|
+
} from "@antelopejs/interface-dms-builder";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Choose an Encoding
|
|
18
|
+
|
|
19
|
+
| You are writing | Encoding |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| A string, number, boolean, array or object | The value itself, as JSON. |
|
|
22
|
+
| A value the catalog marks `x-dataType` | `DataTypeValue` |
|
|
23
|
+
| A reference to a builder-generated resource class | `RefValue` |
|
|
24
|
+
| Anything else that must reach the source as code | `ExprValue` |
|
|
25
|
+
|
|
26
|
+
The catalog is what tells you which option needs which: an option marked `x-dataType` in a block's `ConfigSchema` takes a `DataTypeValue`, and nothing else. See [Catalog](./3.catalog.md).
|
|
27
|
+
|
|
28
|
+
## Write a DataType
|
|
29
|
+
|
|
30
|
+
`DataTypeValue` emits `new <DataType>(<config>)` and adds the import for you.
|
|
31
|
+
|
|
32
|
+
| Field | Type | Description |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `$dataType` | `string` | A `DataTypeDescriptor.id` from the catalog, e.g. `"string"` or `"select"`. |
|
|
35
|
+
| `config` | `Record<string, unknown>` | The DataType's options. Omit for a no-arg `new <DataType>()`. |
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { AddBlock } from "@antelopejs/interface-dms-builder";
|
|
39
|
+
|
|
40
|
+
await AddBlock({
|
|
41
|
+
page: "/customers",
|
|
42
|
+
name: "editor",
|
|
43
|
+
type: "Form",
|
|
44
|
+
config: {
|
|
45
|
+
fields: [
|
|
46
|
+
{
|
|
47
|
+
id: "name",
|
|
48
|
+
label: "Name",
|
|
49
|
+
type: { $dataType: "string", config: { placeholder: "Name" } },
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
// emits: type: new DefaultDataTypes.StringType({ placeholder: "Name" })
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The same encoding is what a resource field's `dataType` takes, described in [Resources](./5.resources.md).
|
|
58
|
+
|
|
59
|
+
A `$dataType` id the catalog does not list fails with `invalid_config`, and the message points you at `GetCatalog` for the valid ids.
|
|
60
|
+
|
|
61
|
+
## Reference a Generated Class
|
|
62
|
+
|
|
63
|
+
`RefValue` emits the bare class identifier of a builder-generated resource, with its import added. It is the structured, validated alternative to `ExprValue` for a class reference — a `RelationType`'s `dataApiController`, for instance.
|
|
64
|
+
|
|
65
|
+
| Field | Type | Description |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| `$ref.resource` | `string` | A `ResourceRef` — the resource's name, e.g. `"product"`. |
|
|
68
|
+
| `$ref.as` | `"dataApi" \| "table" \| "model"` | Which of the resource's classes to reference. Defaults to `"dataApi"`. |
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { AddField } from "@antelopejs/interface-dms-builder";
|
|
72
|
+
|
|
73
|
+
await AddField("order-line", {
|
|
74
|
+
name: "productId",
|
|
75
|
+
label: "Product",
|
|
76
|
+
dataType: {
|
|
77
|
+
$dataType: "relation",
|
|
78
|
+
config: { dataApiController: { $ref: { resource: "product" } } },
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
// emits: dataApiController: productDataAPI (imported from the resource's data-api.ts)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`as` selects among the three classes a resource generates: `"dataApi"` for the `DataController`, `"table"` for the `Table`, `"model"` for the `BasicDataModel`. An unknown resource fails with `invalid_config`, and the message points you at `ListResources`.
|
|
85
|
+
|
|
86
|
+
`AddBlockInput.controller` is the same reference in a dedicated field: a controller-leading block type takes a `ResourceRef` there rather than a `RefValue` in its config. See [Page Operations](./4.page-operations.md).
|
|
87
|
+
|
|
88
|
+
## Write a Raw Expression
|
|
89
|
+
|
|
90
|
+
`ExprValue` is emitted verbatim. Reach for it only when the builder cannot model the value structurally.
|
|
91
|
+
|
|
92
|
+
| Field | Type | Description |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| `$expr` | `string` | TypeScript source, spliced into the emitted file as written. |
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { ConfigureBlock } from "@antelopejs/interface-dms-builder";
|
|
98
|
+
|
|
99
|
+
await ConfigureBlock("/revenue#total", {
|
|
100
|
+
valueFormat: { $expr: "REVENUE_FORMAT" },
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Two constraints come with it:
|
|
105
|
+
|
|
106
|
+
- The referenced symbol must already be importable in the target file. Unlike the other two sentinels, `$expr` adds no import.
|
|
107
|
+
- `$expr` is refused inside a query's parameters — a query's values are data, and a raw expression would put uncheckable code inside a model body. See [Queries](./6.queries.md).
|
|
108
|
+
|
|
109
|
+
Consumers that run the builder in a restricted mode may refuse `$expr` outright, since arbitrary code defeats the structured guarantees the rest of the surface provides. `RefValue` is allowed in that mode precisely because it is validated; prefer it whenever the value you need is a generated class.
|
|
110
|
+
|
|
111
|
+
## Next Steps
|
|
112
|
+
|
|
113
|
+
- [Results and Errors](./8.results-and-errors.md) - Branch on an operation's result and read its diagnostics
|
|
114
|
+
|
|
115
|
+
## Nest a Block in a Config Value
|
|
116
|
+
|
|
117
|
+
A few options hold a component rather than data — a `ChartCard`'s `chart`, marked `x-component` in the catalog. `BlockValue` is how you write one: the builder emits the nested factory call and adds its import.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import type { BlockValue } from "@antelopejs/interface-dms-builder";
|
|
121
|
+
|
|
122
|
+
const chart: BlockValue = {
|
|
123
|
+
$block: { type: "ChartArea", config: { xaxisType: "datetime" } },
|
|
124
|
+
};
|
|
125
|
+
// → chart: ChartArea({ xaxisType: "datetime" })
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`type` is a catalog key, like `AddBlockInput.type`; an unknown one is rejected with `invalid_config`. Read-back is symmetrical: a nested factory call whose callee is a known block type comes back as a `$block` sentinel rather than as `$expr`, so the value stays editable.
|
|
129
|
+
|
|
130
|
+
Most nesting does *not* go through config — it goes through `.child`, which you express with `AddBlockInput.parent` or with `BlockDraft.children`. Reach for `$block` only where the catalog marks an option `x-component`.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Results and Errors
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Every operation that can fail returns an `OpResult`. It is a discriminated union: `ok: true` carries the operation's data and the file changes it staged, `ok: false` carries a single `BuilderError`. Nothing throws for an expected failure — an unknown ref, a duplicate name, a config the engine cannot validate all come back as values.
|
|
6
|
+
|
|
7
|
+
Mutations are transactional. The engine applies the edit in memory, typechecks the project, and only writes when the typecheck passes; a failure rolls the transaction back and returns `typecheck_failed`, so a rejected operation leaves no partial edit on disk.
|
|
8
|
+
|
|
9
|
+
## Branch on a Result
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { GetPageStructure } from "@antelopejs/interface-dms-builder";
|
|
13
|
+
|
|
14
|
+
const result = await GetPageStructure("/dashboard");
|
|
15
|
+
if (!result.ok) {
|
|
16
|
+
console.error(result.error.code);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { blocks, version } = result.data;
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`OpResult<T>` is:
|
|
24
|
+
|
|
25
|
+
| Variant | Fields |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| success | `ok: true`, `data: T`, `changes: FileChange[]`, `warnings?: OpWarning[]` |
|
|
28
|
+
| failure | `ok: false`, `error: BuilderError` |
|
|
29
|
+
|
|
30
|
+
`OpResult` defaults to `OpResult<void>`, which is what the operations that return nothing but a status use — `ConfigurePage`, `RemoveBlock`, `RemoveField` and the rest.
|
|
31
|
+
|
|
32
|
+
Reads that use the envelope — `GetPageStructure`, `GetResourceStructure` — return an empty `changes` array on success. The listing operations (`ListPages`, `ListCategories`, `ListResources`, `ListQueryTemplates`, `GetCatalog`) do not use the envelope at all: they return their value directly, and `RefreshSourceIndex` returns `void`.
|
|
33
|
+
|
|
34
|
+
## Read the Staged Changes
|
|
35
|
+
|
|
36
|
+
`changes` is what the operation wrote, one entry per file.
|
|
37
|
+
|
|
38
|
+
| Field | Type | Description |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| `path` | `string` | The file's absolute path. |
|
|
41
|
+
| `kind` | `"create" \| "modify" \| "delete"` | What happened to the file. |
|
|
42
|
+
| `diff` | `string` | A textual diff of the change. Empty for a deletion. |
|
|
43
|
+
|
|
44
|
+
A single operation usually touches more than one file — creating a page writes the page file and rewrites the barrel that exports it; creating a resource writes three files and rewrites a barrel.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { CreateResource } from "@antelopejs/interface-dms-builder";
|
|
48
|
+
|
|
49
|
+
const result = await CreateResource({
|
|
50
|
+
name: "product",
|
|
51
|
+
fields: [{ name: "title", dataType: { $dataType: "string" } }],
|
|
52
|
+
});
|
|
53
|
+
if (result.ok) {
|
|
54
|
+
for (const change of result.changes) {
|
|
55
|
+
console.log(change.kind, change.path);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Handle Warnings
|
|
61
|
+
|
|
62
|
+
`warnings` is present only when an otherwise successful operation has something non-fatal to report. It is optional: check for it rather than assuming an array.
|
|
63
|
+
|
|
64
|
+
| Code | Raised when |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `index_clamped` | An `AddBlock` or `MoveBlock` index fell outside the sibling range and was clamped into it. |
|
|
67
|
+
| `datatype_fallback` | A field's DataType is not one the builder maps, so it is handled as a string. |
|
|
68
|
+
| `data_not_dropped` | `DeleteResource` removed the source but could not clear the resource's table data. |
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { AddBlock } from "@antelopejs/interface-dms-builder";
|
|
72
|
+
|
|
73
|
+
const result = await AddBlock({
|
|
74
|
+
page: "/revenue",
|
|
75
|
+
name: "total",
|
|
76
|
+
type: "KpiCard",
|
|
77
|
+
config: { title: "Total" },
|
|
78
|
+
index: 99,
|
|
79
|
+
});
|
|
80
|
+
if (result.ok) {
|
|
81
|
+
for (const warning of result.warnings ?? []) {
|
|
82
|
+
console.warn(warning.code, warning.message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Handle Errors
|
|
88
|
+
|
|
89
|
+
`BuilderError` is a union discriminated by `code`. Each variant carries exactly what you need to act on it.
|
|
90
|
+
|
|
91
|
+
| Code | Payload | Meaning |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| `not_found` | `ref: string` | The page, category, block, resource, field or query does not exist. |
|
|
94
|
+
| `duplicate_name` | `name: string`, `scope: DuplicateScope` | The name is already taken within that scope. |
|
|
95
|
+
| `invalid_config` | `issues: ValidationIssue[]` | The request's configuration did not validate. |
|
|
96
|
+
| `opaque_target` | `path: BlockPath` | The target exists but the builder cannot rewrite it. |
|
|
97
|
+
| `unsupported` | `detail: string` | The operation is valid but out of scope for the engine, and `detail` says why. |
|
|
98
|
+
| `referential_integrity` | `blockedBy: string[]` | Something still depends on what you asked to delete. |
|
|
99
|
+
| `stale` | `ref: string`, `currentVersion: string` | The file changed since the version you passed as `expectedVersion`. |
|
|
100
|
+
| `typecheck_failed` | `diagnostics: TypecheckError[]` | The edit would not compile, and was rolled back. |
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { AddBlock } from "@antelopejs/interface-dms-builder";
|
|
104
|
+
|
|
105
|
+
const result = await AddBlock({
|
|
106
|
+
page: "/revenue",
|
|
107
|
+
name: "total",
|
|
108
|
+
type: "KpiCard",
|
|
109
|
+
config: { title: "Total" },
|
|
110
|
+
});
|
|
111
|
+
if (!result.ok) {
|
|
112
|
+
switch (result.error.code) {
|
|
113
|
+
case "duplicate_name":
|
|
114
|
+
console.error(`"${result.error.name}" is taken in ${result.error.scope}`);
|
|
115
|
+
break;
|
|
116
|
+
case "invalid_config":
|
|
117
|
+
for (const issue of result.error.issues) {
|
|
118
|
+
console.error(issue.pointer, issue.message);
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
case "typecheck_failed":
|
|
122
|
+
for (const diagnostic of result.error.diagnostics) {
|
|
123
|
+
console.error(`${diagnostic.file}:${diagnostic.line}`, diagnostic.message);
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
console.error(result.error.code);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Duplicate Scopes
|
|
133
|
+
|
|
134
|
+
`DuplicateScope` names the namespace the collision happened in, which tells you what to rename:
|
|
135
|
+
|
|
136
|
+
| Scope | The name collided with |
|
|
137
|
+
| --- | --- |
|
|
138
|
+
| `page` | A top-level block on the same page, or another page's route. |
|
|
139
|
+
| `siblings` | Another child of the same container block. |
|
|
140
|
+
| `category` | Another category. |
|
|
141
|
+
| `resource` | Another resource, or another field on the same resource. |
|
|
142
|
+
| `query` | A member of the page controller, a route path already served by the page, or a member of the resource's model. |
|
|
143
|
+
|
|
144
|
+
### Validation Issues
|
|
145
|
+
|
|
146
|
+
`ValidationIssue` addresses a failure by JSON pointer into the request:
|
|
147
|
+
|
|
148
|
+
| Field | Type | Description |
|
|
149
|
+
| --- | --- | --- |
|
|
150
|
+
| `pointer` | `string` | A JSON pointer into the offending input, e.g. `/where/0/op`. Empty when the failure is not tied to one key. |
|
|
151
|
+
| `message` | `string` | What is wrong, and often what to call to find the valid values. |
|
|
152
|
+
|
|
153
|
+
### Typecheck Errors
|
|
154
|
+
|
|
155
|
+
`TypecheckError` is one TypeScript diagnostic from the in-memory typecheck gate:
|
|
156
|
+
|
|
157
|
+
| Field | Type | Description |
|
|
158
|
+
| --- | --- | --- |
|
|
159
|
+
| `file` | `string` | The file the diagnostic points at. |
|
|
160
|
+
| `line` | `number` | The line number. |
|
|
161
|
+
| `message` | `string` | The diagnostic message. |
|
|
162
|
+
|
|
163
|
+
Diagnostics are collected across the project, not just the file the operation edited, so a change that breaks a consumer elsewhere is caught before anything is written.
|
|
164
|
+
|
|
165
|
+
## Make a Write Conditional
|
|
166
|
+
|
|
167
|
+
`MutationOpts` is the trailing options argument a mutating operation takes.
|
|
168
|
+
|
|
169
|
+
| Option | Type | Description |
|
|
170
|
+
| --- | --- | --- |
|
|
171
|
+
| `expectedVersion` | `string` | The version the consumer last read. A mismatch fails with `stale`. |
|
|
172
|
+
|
|
173
|
+
The operations that edit something that already exists take it: `ConfigurePage`, `DeletePage`, `AddBlock`, `ConfigureBlock`, `MoveBlock`, `RemoveBlock`, `DeleteResource`, `AddField`, `ConfigureField`, `RemoveField`, `AddQuery`, `ConfigureQuery` and `RemoveQuery`. The three creations — `CreatePage`, `CreateCategory` and `CreateResource` — take no options argument, and neither do `ConfigureCategory` and `DeleteCategory`.
|
|
174
|
+
|
|
175
|
+
The version comes from the structure you read: `PageStructure.version` for a page operation, `ResourceStructure.version` for a resource one. Both are content hashes — of the page file, and of a resource's two generated files.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import {
|
|
179
|
+
ConfigureBlock,
|
|
180
|
+
GetPageStructure,
|
|
181
|
+
} from "@antelopejs/interface-dms-builder";
|
|
182
|
+
|
|
183
|
+
const read = await GetPageStructure("/revenue");
|
|
184
|
+
if (!read.ok) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const write = await ConfigureBlock(
|
|
189
|
+
"/revenue#total",
|
|
190
|
+
{ title: "Revenue" },
|
|
191
|
+
{ expectedVersion: read.data.version },
|
|
192
|
+
);
|
|
193
|
+
if (!write.ok && write.error.code === "stale") {
|
|
194
|
+
// `write.error.currentVersion` is what the file hashes to now:
|
|
195
|
+
// re-read the structure, re-decide, and retry.
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Omitting `expectedVersion` writes unconditionally. Pass it whenever a human or another process may have touched the file between your read and your write.
|
|
200
|
+
|
|
201
|
+
Two operations extend `MutationOpts` with a field of their own: `ConfigureBlockOpts` adds `replace`, described in [Page Operations](./4.page-operations.md), and `DeleteResourceOpts` adds `keepData`, described in [Resources](./5.resources.md).
|