@antelopejs/interface-dms-builder 0.3.1 → 0.4.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.
@@ -1,14 +1,18 @@
1
- # DMS Builder Documentation
1
+ ---
2
+ title: "Introduction"
3
+ description: "What the builder interface does, how an edit reaches your TypeScript files, and where each part of the contract is documented."
4
+ mode: explanation
5
+ ---
2
6
 
3
- Use `@antelopejs/interface-dms-builder` to create and edit DMS pages from a backend module or a tool. The operations write ordinary TypeScript into your project; there is no separate database of page definitions.
7
+ `@antelopejs/interface-dms-builder` creates and edits DMS pages from a backend module or a tool. The operations write ordinary TypeScript into your project. No separate database holds page definitions.
4
8
 
5
- If you want to edit a page with the mouse instead, start with the [visual builder guide](https://dms.antelopejs.com/docs/modules/builder/introduction).
9
+ To edit a page with the mouse instead, start with the [visual builder guide](https://dms.antelopejs.com/docs/modules/builder/introduction).
6
10
 
7
- ## Quick Start
11
+ ## Quick start
8
12
 
9
- This example creates a **Revenue** page with a **Total revenue** card. Run it once from a backend module after the builder is available, in a development project that loads both `@antelopejs/dms` and `@antelopejs/dms-builder`. Installing the interface package alone does not provide the implementation.
13
+ This example creates a **Revenue** page with a **Total revenue** card. It runs once from a backend module after the builder is available, in a development project that loads both `@antelopejs/dms` and `@antelopejs/dms-builder`. The interface package alone does not provide the implementation.
10
14
 
11
- The project must already have a `pages` category. Use `ListCategories()` to find yours, or [create a category](./4.page-operations.md) first. Choose an unused page name: running a creation twice returns `duplicate_name`.
15
+ The project must already have a `pages` category. `ListCategories()` lists yours, and [Create a category](./4.page-operations.md#create-a-category) adds one. The page name must be unused, because a second creation with the same name returns `duplicate_name`.
12
16
 
13
17
  ```typescript
14
18
  import {
@@ -45,33 +49,35 @@ for (const change of block.changes) {
45
49
  }
46
50
  ```
47
51
 
48
- After the backend reloads, open `/revenue` as a tenant owner. You should see the page and its card; the card has no revenue data yet. [Queries](./6.queries.md) connects a card to a resource when you are ready for real numbers.
52
+ After the backend reloads, a tenant owner who opens `/revenue` sees the page and its card. The card has no revenue data yet. [Queries](./6.queries.md) connects a card to a resource for real numbers.
49
53
 
50
- Each call saves independently. If `CreatePage` succeeds but `AddBlock` fails, the empty page remains; fix the block configuration and retry that call, not the page creation.
54
+ Each call saves on its own. If `CreatePage` succeeds and `AddBlock` fails, the empty page stays. Fix the block configuration and retry that call, not the page creation.
51
55
 
52
56
  ## How an edit reaches your files
53
57
 
54
- A *block* is one DMS component, such as a card or a table. Adding one writes a factory call into a `PageController` class, either as a static field or as a child of another block. The builder parses the source, stages the edit in memory and typechecks the project before writing it. A type error anywhere in the project can therefore block the edit.
58
+ A *block* is one DMS component, such as a card or a table. Adding one writes a factory call into a `PageController` class, either as a static field or as a child of another block. The builder parses the source, stages the edit in memory and typechecks the whole project before it writes. A type error anywhere in the project outside `node_modules` can therefore block the edit.
55
59
 
56
- Operations are `InterfaceFunction` exports from `@antelopejs/interface-core`. Expected failures return values you can inspect:
60
+ Operations are `InterfaceFunction` exports from `@antelopejs/interface-core`. An expected failure comes back as a value you can inspect:
57
61
 
58
62
  ```typescript
59
63
  // Success: { ok: true, data, changes, warnings? }
60
64
  // Failure: { ok: false, error }
61
65
  ```
62
66
 
63
- Listing operations such as `ListPages` return their value directly. See [Results and errors](./8.results-and-errors.md) for the full contract. When you read before writing, pass the structure's `version` as `expectedVersion` so another editor's work is not overwritten.
67
+ Listing operations such as `ListPages` return their value directly. [Results and errors](./8.results-and-errors.md) holds the full contract. When you read before you write, pass the structure's `version` as `expectedVersion`, so that the write fails instead of overwriting another editor's work.
64
68
 
65
- Hand-written declarations remain editable when the builder recognizes their shape. Otherwise, it marks them *opaque*: you can inspect them without asking the builder to rewrite code it does not understand. Use [Drafts and preview](./9.drafts-and-preview.md) when an editor needs to save a whole block tree in one transaction.
69
+ A caller that picks an operation by name, such as a route that forwards a request, reads `Operations`. It holds every operation under its exported name, with its own parameter and result types. `OperationName` is the union of those names.
66
70
 
67
- ## Documentation Sections
71
+ Hand-written declarations stay editable when the builder recognizes their shape. Otherwise the builder marks them *opaque*. You can inspect an opaque block, and the builder refuses to rewrite code it does not understand. [Drafts and preview](./9.drafts-and-preview.md) saves a whole block tree in one transaction.
68
72
 
69
- - [Addressing and Structure](./2.addressing-and-structure.md) - The refs that name a page, a block and a category, and the shapes the builder reads back
70
- - [Catalog](./3.catalog.md) - The block types, DataTypes and reserved names the builder can report
71
- - [Page Operations](./4.page-operations.md) - Create pages and categories, and edit a page's block tree
72
- - [Resources](./5.resources.md) - Generate tables, DataAPI controllers and their fields
73
- - [Queries](./6.queries.md) - Compile a measure, grouped or not, into a model method and a page route
74
- - [Value Encodings](./7.values.md) - Write configuration values that are not plain literals
75
- - [Results and Errors](./8.results-and-errors.md) - Branch on `OpResult`, read diagnostics, and make a write conditional
76
- - [Drafts and Preview](./9.drafts-and-preview.md) - Write a page's blocks and the data they read in one transaction, and preview either
77
- - [A Chart Without Code](./10.a-chart-without-code.md) - The whole journey, from picking a measure to reopening the page
73
+ ## Documentation sections
74
+
75
+ - [Addressing and structure](./2.addressing-and-structure.md) covers the refs that name a page, a block and a category, and the shapes the builder reads back.
76
+ - [Catalog](./3.catalog.md) lists the block types, DataTypes and reserved names the builder reports.
77
+ - [Page operations](./4.page-operations.md) creates pages and categories, and edits the block tree of a page.
78
+ - [Resources](./5.resources.md) generates tables, DataAPI controllers and their fields.
79
+ - [Queries](./6.queries.md) compiles a measure, grouped or not, into a model method and a page route.
80
+ - [Value encodings](./7.values.md) writes configuration values that are not plain literals.
81
+ - [Results and errors](./8.results-and-errors.md) branches on `OpResult`, reads diagnostics, and makes a write conditional.
82
+ - [Drafts and preview](./9.drafts-and-preview.md) writes the blocks of a page and the data they read in one transaction, and previews either.
83
+ - [A chart without code](./10.a-chart-without-code.md) follows one chart from the choice of a measure to the reopened page.
@@ -1,23 +1,20 @@
1
- # A Chart Without Code
1
+ ---
2
+ title: "A chart without code"
3
+ description: "Preview a revenue chart against real rows, save the chart card and its data route in one write, and reopen the page with the chart's settings filled in, through the calls behind the visual builder."
4
+ mode: tutorial
5
+ ---
2
6
 
3
- Build an editor that lets someone choose a table, a measure and a grouping, then
4
- save a chart without writing its data route by hand. This walkthrough follows the
5
- interface calls behind the visual builder; it is not a standalone script.
7
+ In this tutorial we save a revenue chart grouped by month without writing its data route by hand. We follow the interface calls that the visual builder makes when someone picks a table, a measure and a grouping. The calls run inside a project, so this page is not a standalone script.
6
8
 
7
- ![The visual builder with a chart selected and its settings open](./images/editor-selected.png)
9
+ ![The visual builder with a KPI card selected and its settings open](./images/captures/editor-selected.webp)
8
10
 
9
- You need an existing `/shop/board` page and an `order` resource with numeric
10
- `amount`, date `createdAt` and `status` fields. Run these calls inside a project
11
- that loads the builder. Use a disposable page for the save example: its draft
12
- replaces the page's existing blocks.
11
+ Before we start, we need an existing `/shop/board` page and an `order` resource with a numeric `amount` field, a date `createdAt` field and a `status` field. Run the calls inside a project that loads the builder. Use a disposable page for the save step, because its draft replaces the page's existing blocks.
13
12
 
14
- The result is a revenue chart grouped by month. Start with all paid orders;
15
- add period controls and comparison only after this first chart works.
13
+ We start with all paid orders. Period controls and a comparison come after the first chart works.
16
14
 
17
15
  ## Show the data before writing anything
18
16
 
19
- While the person is still choosing, `PreviewQuery` runs the calculation against
20
- the real table and answers what the route would:
17
+ While the person is still choosing, `PreviewQuery` runs the calculation against the real table and answers what the route would:
21
18
 
22
19
  ```typescript
23
20
  import { PreviewQuery } from "@antelopejs/interface-dms-builder";
@@ -46,16 +43,11 @@ if (preview.ok) {
46
43
  }
47
44
  ```
48
45
 
49
- Nothing has been written yet. On success, `preview.data.body` has the response
50
- shape the chart card reads, including its headline figure. The saved route uses
51
- the same calculation, but its values can change as rows change. Check `truncated`
52
- before treating a limited preview as a complete result. For a per-tenant resource,
53
- also pass the tenant id in `PreviewQuery`'s `tenant` option.
46
+ Nothing is written yet. On success, `preview.data.body` holds the response that the chart card reads, headline figure included. The saved route runs the same calculation, but its values change as the rows change. The preview stops at 500 points, so check `truncated` before you treat the preview as complete. For a per-tenant resource, also pass the tenant id in the `tenant` field of the `PreviewQuery` request.
54
47
 
55
48
  ## Save the block and its data together
56
49
 
57
- The query travels in the page draft, so one save writes the block, the model
58
- method and the route in a single transaction:
50
+ The query travels in the page draft, so one save writes the block, the model method and the route in a single transaction:
59
51
 
60
52
  ```typescript
61
53
  import { GetPageStructure, SetPageBlocks } from "@antelopejs/interface-dms-builder";
@@ -97,33 +89,28 @@ const saved = await SetPageBlocks("/shop/board", {
97
89
  if (!saved.ok) throw new Error(saved.error.code);
98
90
  ```
99
91
 
100
- After the backend reloads, open `/shop/board` as a tenant owner. The card reads
101
- `/dms/pages/shop/board/stats/revenue-chart` and shows the saved calculation.
102
- Either the blocks and query save together or neither changes.
92
+ After the backend reloads, open `/shop/board` as a tenant owner. The card reads `/dms/pages/shop/board/stats/revenue-chart` and shows the saved calculation. The blocks and the query save together, or neither changes.
103
93
 
104
- Two details the engine will hold you to. The block and the query cannot share a
105
- name: both become members of the page class, so a query named after the block it
106
- feeds comes back as `duplicate_name` in the `query` scope — name the block for
107
- what it is and the query for what it answers. And a `ChartCard` requires the
108
- chart it wraps, so a config without `chart` fails the save on `typecheck_failed`
109
- rather than on a validation message.
94
+ The engine holds us to two details:
110
95
 
111
- ## Reference: add a period and comparison
96
+ - Give the block and the query different names. Name the block for what it is and the query for what it answers, so the page class reads clearly.
97
+ - A `ChartCard` requires the chart it wraps. A config without `chart` fails the save with `typecheck_failed`, not with a validation issue.
112
98
 
113
- The first example intentionally has no period filter. To make the chart follow a
114
- page period, add `periodScope: "page"` to the card, bind `from` and `to` in the
115
- query's date filters, and supply the page's period selector. Set `compare: true`
116
- only when requests also supply `compareFrom` and `compareTo`. Pass the same filters
117
- and arguments to `PreviewQuery` to preview that period, rather than all paid orders.
118
- See [request-time bindings](./6.queries.md#bind-a-value-at-request-time).
99
+ ## Add a period and a comparison
119
100
 
120
- With those period filters added, the generated code has the following shape.
121
- These fragments illustrate the model method and route; they are not extra files
122
- you need to write for the walkthrough.
101
+ The first chart has no period filter. To make the chart follow a page period, we make three changes:
102
+
103
+ 1. Add `periodScope` to the card, set to the id of the page's `PeriodSelector`, such as `"page"`.
104
+ 2. Bind `from` and `to` in the query's date filters. See [request-time bindings](./6.queries.md#bind-a-value-at-request-time).
105
+ 3. Add the `PeriodSelector` with that id to the page.
106
+
107
+ To show a comparison, set `compare: true` on the query. The route then also reads `compareFrom` and `compareTo`, which the selector sends while a comparison is on. To preview that period instead of all paid orders, pass the same filters to `PreviewQuery`, with the period values in `args`.
108
+
109
+ With those period filters added, the builder writes code of the following shape. We do not write these fragments ourselves. They show what the save produces.
123
110
 
124
111
  A method on the resource's model:
125
112
 
126
- ```typescript
113
+ ```typescript fragment
127
114
  revenueChart(minCreatedAt: Date, maxCreatedAt: Date) {
128
115
  return this.table
129
116
  .filter((row) => row.key("status").eq("paid"))
@@ -138,9 +125,9 @@ revenueChart(minCreatedAt: Date, maxCreatedAt: Date) {
138
125
  }
139
126
  ```
140
127
 
141
- And a route on the page, guarded with the page's own permission:
128
+ A route on the page, guarded with the page's own permission:
142
129
 
143
- ```typescript
130
+ ```typescript fragment
144
131
  @Get("/stats/revenue-chart")
145
132
  async revenueChart(
146
133
  @AuthUserWithPermission(BoardPage) _user: User,
@@ -161,18 +148,13 @@ async revenueChart(
161
148
  }
162
149
  ```
163
150
 
164
- Ordinary code, in the application's own style, that a developer can read, edit
165
- and own.
151
+ Both fragments are ordinary code in the application's own files. A developer can read, edit and own them.
166
152
 
167
- `label` is what a legend and a tooltip call the series. The builder takes it from
168
- the field being measured — the label the resource gives it — and from the resource
169
- itself for a count, which has no field to name: left out, the helper falls back to
170
- the measure's own word, and a reader who never asked for one would be shown "sum".
153
+ `label` is what a legend and a tooltip call the series. For a measure over a field, the builder takes the field's label, or the field's name when the field has no label. For a count, which has no field, it takes the resource ref. Without a `label`, the helper falls back to the measure's own word, and the chart would show "sum".
171
154
 
172
- ## Reopen it
155
+ ## Reopen the chart
173
156
 
174
- `GetPageStructure` reads the route and its chain back into the parameters that
175
- produced them, so the editor reopens filled in rather than empty:
157
+ `GetPageStructure` reads the route and its chain back into the parameters that produced them, so the editor reopens with the chart's settings filled in:
176
158
 
177
159
  ```typescript
178
160
  import { GetPageStructure } from "@antelopejs/interface-dms-builder";
@@ -186,29 +168,24 @@ if (structure.ok) {
186
168
  }
187
169
  ```
188
170
 
189
- That read-back is what makes the journey repeatable rather than one-way. It is
190
- also what decides ownership: a chain edited past what the grammar accepts reads as
191
- `opaque`, and from then on the builder will list it and remove it but never
192
- rewrite it.
171
+ The read-back makes the journey repeatable in both directions. It also decides ownership. When a developer edits a chain past what the grammar accepts, the query reads back as `opaque`. From then on, the builder lists the query and can remove it, but never rewrites it.
193
172
 
194
- ## Where each guarantee lives
173
+ ## What we built
195
174
 
196
- | Guarantee | Where it comes from |
197
- | --- | --- |
198
- | Preview and saved queries use the same calculation | One `QueryPlan`, rendered by the emitter and run by the executor; rows and preview limits can differ |
199
- | The block and its data save together | `PageDraft.queries`, written in the page's transaction |
200
- | Reopening shows what was configured | The chain reads back into its parameters |
201
- | A developer's edit survives | An unreadable chain is opaque, and opaque is never rewritten |
202
- | The data is not public | The route carries the page's permission |
203
- | A tenant reads its own rows | The model provider is chosen from the resource's schema |
175
+ We now have a chart that someone configured without code. Each step rests on one part of the builder:
204
176
 
205
- ## Reference: what the block asks for
177
+ - The preview and the saved query use the same calculation, because one `QueryPlan` feeds both the emitter that writes the model method and the executor that runs the preview. The rows and the preview limit can still differ.
178
+ - The block and its data saved together, because `PageDraft.queries` is written in the page's transaction.
179
+ - The page reopened with its settings, because the chain reads back into its parameters.
180
+ - A developer's edit survives, because a chain the builder cannot read is opaque, and the builder never rewrites an opaque query.
181
+ - The data is not public, because the route carries the page's permission.
182
+ - A tenant reads its own rows, because the route's model provider follows the resource's schema.
206
183
 
207
- A block declares the response shape it can read beside its factory. The editor
208
- uses this catalog metadata to offer compatible calculations and find the option
209
- that controls the period:
184
+ ## See what the block asks for
210
185
 
211
- ```typescript
186
+ A block declares, beside its factory, the response shape it can read. The editor reads this catalog metadata to offer the calculations that fit and to find the option that controls the period. This is the `fetchUrl` option of `ChartCard`:
187
+
188
+ ```typescript fragment
212
189
  fetchUrl: ui(z.string().optional(), {
213
190
  label: "Data source",
214
191
  group: "data",
@@ -218,5 +195,4 @@ fetchUrl: ui(z.string().optional(), {
218
195
  });
219
196
  ```
220
197
 
221
- This is a schema fragment for a block author, not configuration to pass to
222
- `AddBlock`. An editor consumer reads it from [the catalog](./3.catalog.md).
198
+ This fragment is part of a block's schema, for a block author. It is not configuration to pass to `AddBlock`. An editor reads it from [the catalog](./3.catalog.md).
@@ -1,29 +1,33 @@
1
- # External Databases
1
+ ---
2
+ title: "External databases"
3
+ description: "The operations that wire a database the project does not own into antelope.config.ts, analyse its catalog into a mapping proposal, and map its relations to table classes, controllers and TableView pages."
4
+ mode: reference
5
+ ---
2
6
 
3
7
  ## Overview
4
8
 
5
- A DMS project can show pages over a database it does not own: a client's PostgreSQL, MongoDB or RethinkDB, read and written live through that engine's module (`@antelopejs/postgres`, `@antelopejs/mongodb`, `@antelopejs/rethinkdb`) in external mode. The builder takes such a database in four moves, each an operation:
9
+ A DMS project can show pages over a database it does not own, such as a client's PostgreSQL, MongoDB or RethinkDB database. The engine's module (`@antelopejs/postgres`, `@antelopejs/mongodb` or `@antelopejs/rethinkdb`) reads and writes it live, in external mode. The builder takes such a database in four operations:
6
10
 
7
- 1. **Wire it** into `antelope.config.ts` with `ConfigureProject`: the provider module, a local module for its tables, the builder's catalog connection, and the DMS module's pin to the project's own database.
8
- 2. **Analyse it** with `AnalyseDatabase`, which reads the catalog and proposes a mapping by fixed rules. It writes nothing.
9
- 3. **Decide** the proposal: a person in the builder's *Database* workspace, or an assistant through these operations.
10
- 4. **Map it** with `MapResources`, which writes table classes, controllers and TableView pages into the module bound to the database.
11
+ 1. `ConfigureProject` wires the database into `antelope.config.ts`. It adds the provider module, a local module for the tables, the builder's catalog connection, and the DMS module's pin to the project's own database.
12
+ 2. `AnalyseDatabase` reads the catalog and proposes a mapping by fixed rules. It writes nothing.
13
+ 3. A person in the builder's **Database** workspace, or an assistant through these operations, settles the proposal.
14
+ 4. `MapResources` writes table classes, controllers and TableView pages into the module bound to the database.
11
15
 
12
- The builder reaches the database through `@antelopejs/interface-database-catalog`, a connection declared on its own module entry. It never connects with credentials of its own.
16
+ The builder reaches the database through `@antelopejs/interface-database-catalog`, a connection declared on its own module entry. The builder never connects with credentials of its own.
13
17
 
14
- ## Wire the Project
18
+ ## Wire the project
15
19
 
16
- `ConfigureProject(steps, opts?)` applies its steps to `antelope.config.ts` as one edit. Every step is validated before anything is written, and a refused step writes nothing. With `{ dryRun: true }` it answers the changes it would make, as `OpResult.changes`, so a caller can show the diff before writing.
20
+ `ConfigureProject(steps, opts?)` applies its steps to `antelope.config.ts` in order, as one edit. It validates every step before it writes anything, and a refused step fails the call with `invalid_config` and writes nothing. With `{ dryRun: true }`, it answers the changes it would make as `OpResult.changes`, so a caller can show the diff before the write. The configuration must be a `defineConfig({ … })` or plain object literal with a literal `modules`.
17
21
 
18
22
  | Step | Writes |
19
23
  | --- | --- |
20
- | `AddModule` | a module entry, refusing an id that exists |
21
- | `ConfigureModule` | merges config, `importOverrides` (pins and connection ids), `exportPriority`, `disabledExports` and env variable names into an entry |
22
- | `CreateLocalModule` | scaffolds a local module and adds its entry; with `connection`, its record names that connection |
23
- | `RemoveModule` | removes an entry and the `envOverrides` entries supplying its config, refusing one another module still reads through; its files stay |
24
- | `RemoveConnection` | removes a named connection to an interface from every module's `importOverrides` |
24
+ | `AddModule` | A module entry. Refuses an id that exists. |
25
+ | `ConfigureModule` | Merges config, `importOverrides` (pins and connection ids), `exportPriority`, `disabledExports` and environment variable names into an existing entry. Refuses a pin that contradicts an existing one. |
26
+ | `CreateLocalModule` | Scaffolds a local module at `path` and adds its entry. With `connection`, the module's `package.json` record names that connection. |
27
+ | `RemoveModule` | Removes an entry and the `envOverrides` entries that supply its config. Refuses a module that another module's `importOverrides` names as a source. The module's files stay. |
28
+ | `RemoveConnection` | Removes a named connection to an interface from every module's `importOverrides`. |
25
29
 
26
- `env` takes variable names, never values: each is added to `envOverrides`, and to `.env.example` when the project has one.
30
+ `env` maps a config path to a variable name, never to a value. Each variable is added to `envOverrides`, and to `.env.example` when the project has one. A variable that already supplies another path is refused.
27
31
 
28
32
  The provider's entry depends on its engine:
29
33
 
@@ -31,9 +35,11 @@ The provider's entry depends on its engine:
31
35
  | --- | --- | --- | --- |
32
36
  | PostgreSQL | `@antelopejs/postgres` | `url` | none |
33
37
  | MongoDB | `@antelopejs/mongodb` | `url` and `database` | `@antelopejs/interface-mongodb` |
34
- | RethinkDB | `@antelopejs/rethinkdb` | `url`, the database as its path | `@antelopejs/interface-rethinkdb` |
38
+ | RethinkDB | `@antelopejs/rethinkdb` | `url`, with the database as its path | `@antelopejs/interface-rethinkdb` |
35
39
 
36
- `disabledExports` keeps core from ever offering the external instance's raw client, so a module that needs the DMS's own MongoDB or RethinkDB client never gets the client's database instead.
40
+ `disabledExports` stops core from offering the external instance's raw client. A module that needs the DMS's own MongoDB or RethinkDB client never gets the client's database instead.
41
+
42
+ Replace `<version>` in the example with the range of the provider package to install. The **Database** workspace fills in a default range under **Advanced settings**, in the **Package version** field.
37
43
 
38
44
  ```typescript
39
45
  import { ConfigureProject } from "@antelopejs/interface-dms-builder";
@@ -45,7 +51,7 @@ const result = await ConfigureProject([
45
51
  op: "AddModule",
46
52
  id: "postgres-erp",
47
53
  entry: {
48
- source: { type: "package", package: "@antelopejs/postgres", version: "^0.2.0" },
54
+ source: { type: "package", package: "@antelopejs/postgres", version: "<version>" },
49
55
  config: { mode: "external", statementTimeoutMs: 30000 },
50
56
  env: { url: "ERP_DATABASE_URL" },
51
57
  exportPriority: { "@antelopejs/interface-database": -1 },
@@ -71,26 +77,28 @@ const result = await ConfigureProject([
71
77
  entry: { importOverrides: [{ interface: catalog, source: "postgres-erp", id: "erp" }] },
72
78
  },
73
79
  ]);
80
+ if (!result.ok) console.error(result.error);
74
81
  ```
75
82
 
76
83
  - The provider's `exportPriority` of `-1` keeps the project's own database the default for every module that does not pin `interface-database`.
77
- - The tables module pins `interface-database` to the provider, and uses the DMS. Core passes that pin on to the DMS, which every other module reaches with the default, so the DMS pins the project's own database to settle it. A DMS that already pins `interface-database` needs nothing more.
84
+ - The tables module pins `interface-database` to the provider, and it uses the DMS. Core passes that pin on to the DMS, which every other module reaches with the default. The DMS therefore pins the project's own database to settle it. A DMS that already pins `interface-database` needs nothing more.
85
+ - When the project loads `dms-ai`, the **Database** workspace gives it the same catalog connection as `dms-builder`.
78
86
 
79
87
  The connection exists once the project restarts with the variable set.
80
88
 
81
- ### Remove a Connection
89
+ ### Remove a connection
82
90
 
83
- Removing a connection takes its catalog connections and its tables module out of the configuration, with `RemoveConnection` and `RemoveModule`. The tables module's folder is deleted with it. The database itself is untouched.
91
+ In the **Database** workspace, the **Remove &lt;connection&gt;** button takes a connection out of the configuration with `RemoveConnection` for its catalog connections and `RemoveModule` for its tables module. The tables module's folder is deleted with it. The database itself is untouched.
84
92
 
85
- The tables module is the one whose `dmsBuilder.database` record names the connection, which `CreateLocalModule` writes with `connection` (the single-step `CreateLocalModule` takes it in its options). Another module that reads through the provider is never removed or deleted.
93
+ The tables module is the one whose `dmsBuilder.database` record names the connection. `CreateLocalModule` writes that record from `connection`. The single-operation `CreateLocalModule` takes it in its options. A module that reads through the provider without such a record is never removed or deleted.
86
94
 
87
- The provider goes too, with the variables that supply its configuration in `envOverrides`, `.env` and `.env.example`, only when it was added for this connection alone: its `exportPriority` for `@antelopejs/interface-database` is below the project's own database, and no module or connection the removal leaves names it. Otherwise it stays, since it may be the project's own database.
95
+ The provider is removed too, with the variables that supply its configuration in `envOverrides`, `.env` and `.env.example`, only when it was added for this connection alone. That holds when its `exportPriority` for `@antelopejs/interface-database` is below the project's own database, and no module or connection that the removal leaves names it. Otherwise the provider stays, since it may be the project's own database.
88
96
 
89
- A removal is all or nothing: the folders are moved aside and `.env` is edited first, and both are put back when the configuration cannot be written.
97
+ A removal is all or nothing. The workspace moves the folders aside and edits `.env` first, and puts both back when the configuration cannot be written.
90
98
 
91
- ## Analyse a Database
99
+ ## Analyse a database
92
100
 
93
- `AnalyseDatabase({ connection, module, schemas? })` starts a job and answers it at once: profiling a database takes a while. `GetAnalysis(id)` answers the job's `progress` while it runs, then its `proposal`, or its `error`.
101
+ `AnalyseDatabase({ connection, module, schemas? })` starts a job and answers it at once, because profiling a database takes a while. `GetAnalysis(id)` answers the job's `progress` while it runs, then its `proposal` or its `error`.
94
102
 
95
103
  ```typescript
96
104
  import { AnalyseDatabase, GetAnalysis } from "@antelopejs/interface-dms-builder";
@@ -98,16 +106,19 @@ import { AnalyseDatabase, GetAnalysis } from "@antelopejs/interface-dms-builder"
98
106
  const started = await AnalyseDatabase({ connection: "erp", module: "erp-tables" });
99
107
  if (started.ok) {
100
108
  const state = await GetAnalysis(started.data.id);
101
- // state.data.status: "running" | "done" | "failed"
109
+ if (state.ok) {
110
+ console.log(state.data.status); // "running", "done" or "failed"
111
+ }
102
112
  }
103
113
  ```
104
114
 
115
+ - `connection` must be a catalog connection on the `dms-builder` entry, and `module` a local module other than the project's own. Otherwise the call fails with `invalid_config`.
105
116
  - One job runs per connection. Starting another while one runs answers the running one.
106
- - A finished job is kept until the next one on its connection, or a restart.
107
- - Relations the module has mapped, or recorded as excluded, are not proposed again.
108
- - Above 200 relations to profile, the proposal holds a single `schemas` question instead, and the analysis runs again with `schemas`.
117
+ - A finished job is kept until the next one on its connection, or until a restart.
118
+ - Relations that the module has mapped, or recorded as excluded, are not proposed again.
119
+ - When more than 200 relations are left to profile and the call passed no `schemas`, the proposal holds a single `schemas` question instead. Run the analysis again with `schemas`.
109
120
 
110
- ## Read a Proposal
121
+ ## Read a proposal
111
122
 
112
123
  A `MappingProposal` is a `MapResources` input in which every choice says where it came from:
113
124
 
@@ -119,45 +130,55 @@ interface Proposed<T> {
119
130
  }
120
131
  ```
121
132
 
122
- A **fact** follows from the catalog with no judgement. A **default** is a judgement to check.
133
+ A `fact` follows from the catalog with no judgement. A `default` is a judgement to check.
134
+
135
+ The relation rules apply in the order R1, R4, R3, R2, then R5 to R9. The first rule that applies decides.
123
136
 
124
137
  | Rule | When | Then |
125
138
  | --- | --- | --- |
126
- | R1 | the catalog says unmappable | excluded, with its reason |
127
- | R2 | no key | stats only: table class and model, no page |
128
- | R3 | no rows | excluded |
129
- | R4 | a framework's or a migration tool's table | excluded |
130
- | R5 | keyed by its two references | stats only |
131
- | R6 | keyed, with rows and a label | TableView |
132
- | R7 | keyed, no label, referenced | TableView labelled by its key, and a `label` question |
133
- | R8 | keyed, no label, not referenced, with a time axis | stats only |
134
- | R9 | anything else | a `purpose` question, nothing proposed |
135
- | L1, L2 | the label by name, then by order; titles from short comments, else from names | |
136
- | F1–F5 | columns no type reads and secrets left out; the first declaration; references the database declares, and references found whole, as relation columns | |
137
- | D1 | a `string` column the profile calls enum-like, of 2 to 10 categories that cover every non-null row | `display`: a badge, one toned item per value, by rows |
138
- | D2 | a `number` column whose name ends in `total`, `amount`, `price`, `cost`, `revenue` or `subtotal` | `display`: a price |
139
- | D3 | a relation column the database did not comment | `label`: the target's title in singular, "Customer" not "Customer id"; `MapResources` takes it too when a field leaves `label` out |
140
- | T1 | of the filterable badge columns of 2 to 6 values, the first named like `status`, `state`, `stage` or `type`, else the one with the fewest values | `tabs` on the resource |
141
- | W1 | every TableView | read-only |
142
-
143
- A badge's tones come from the value: `paid`, `active`, `done`, `completed`, `shipped`, `delivered` and `approved` are `success`; `pending`, `draft`, `processing`, `waiting` and `review` are `warning`; `failed`, `cancelled`, `canceled`, `refunded`, `rejected`, `error` and `blocked` are `error`; `new` and `open` are `info`. Other values take `primary`, `secondary`, `info` and `neutral` in turn. A price uses the DataType's default currency.
144
-
145
- Each field also carries the declarations its column `accepts`, and its profile's aggregates: traits, nulls, distinct values, range and categories. Never a row.
139
+ | R1 | The catalog says unmappable | Excluded, with its reason |
140
+ | R2 | No key | Stats only: table class and model, no page |
141
+ | R3 | No rows | Excluded |
142
+ | R4 | A framework's or a migration tool's table | Excluded |
143
+ | R5 | Keyed by its two references, with at most one other column | Stats only |
144
+ | R6 | Keyed, with rows and a label | TableView, and a `label` question when two candidates tie |
145
+ | R7 | Keyed, no label, referenced | TableView labelled by its key, and a `label` question |
146
+ | R8 | Keyed, no label, not referenced, with a time axis | Stats only |
147
+ | R9 | Anything else | A `purpose` question, nothing proposed |
148
+ | L1 | A label candidate | The label, by name, then by order |
149
+ | L2 | A relation or a column | Its title, from a short comment, else from its name |
150
+ | F1 | A column that no field type reads | Left out |
151
+ | F2 | A column named like a secret | Left out |
152
+ | F3 | A column | Its first declaration |
153
+ | F4 | A single column with a foreign key that the database declares on a TableView's single-column key | A relation column |
154
+ | F5 | A single column whose values are all found in a TableView's single-column key | A relation column |
155
+ | FL | A field | Whether it is listable, filterable, sortable and searchable |
156
+ | D1 | A `string` column that the profile calls enum-like, with 2 to 10 categories that cover every non-null row | `display`, a badge with one toned item per value, ordered by rows |
157
+ | D2 | A `number` column whose name ends in `total`, `amount`, `price`, `cost`, `revenue` or `subtotal` | `display`, a price |
158
+ | D3 | A relation column that the database did not comment | `label`, the target's title in the singular ("Customer", not "Customer id"). `MapResources` also applies it when a field leaves `label` out |
159
+ | T1 | Among the filterable badge columns with 2 to 6 values, the first named like `status`, `state`, `stage` or `type`, else the one with the fewest values | `tabs` on the resource |
160
+ | W1 | Every proposed relation | Read-only (`writable: false`) |
161
+
162
+ A badge's tone comes from its value. `paid`, `active`, `done`, `completed`, `shipped`, `delivered` and `approved` are `success`. `pending`, `draft`, `processing`, `waiting` and `review` are `warning`. `failed`, `cancelled`, `canceled`, `refunded`, `rejected`, `error` and `blocked` are `error`. `new` and `open` are `info`. Other values take `primary`, `secondary`, `info` and `neutral` in turn. A price takes the currency the database states, from an ISO 4217 code in the column's comment, else from a sibling currency column that holds one code in every row. When the database states none, it uses the DataType's default currency.
163
+
164
+ Each field also carries the declarations its column `accepts`, and its profile's aggregates: traits, nulls, distinct values, range and categories. A profile never holds a row.
146
165
 
147
166
  Each resource also carries its `rows`, counted by the profile or estimated, when either is known.
148
167
 
149
- What the rules could not decide is in `open`, each item with its options and what the proposal holds meanwhile:
168
+ The `open` list holds what the rules could not decide. Each item holds its options and what the proposal holds meanwhile:
150
169
 
151
- - A `label` item offers every column a field reads, the key among them.
170
+ - A `label` item offers every column that a field reads, the key among them, except columns named like a secret.
152
171
  - A `reference` item offers a plain column, the target its values were found in, and every TableView whose single-column key accepts the column.
153
- - A `purpose` item carries the `resource` it would map, should the answer be to map it, and in its own `open` the questions that resource leaves: they are asked only once the answer maps it.
172
+ - A `purpose` item carries the `resource` it would map if the answer is to map it. Its own `open` holds the questions that resource leaves, which apply only once the answer maps it.
154
173
  - A `field-type` item asks which declaration reads a field of several native types.
155
174
 
156
- A document database declares no field types, so its catalog reads them from a sample. When no type covers enough of a field's values (`int 60% | string 40%`), the field is left out and a `field-type` item asks which declaration reads it: its options are a declaration per type seen, and leaving it out. The `_id` of a collection without a key is never declared, so it is left out and asks nothing. `MapResources` accepts the declaration it answers. MongoDB and RethinkDB declare no references either, so F5 finds them all.
175
+ A document database declares no field types, so its catalog reads them from a sample. When no type covers enough of a field's values (`int 60% | string 40%`), the field is left out and a `field-type` item asks which declaration reads it. Its options are a declaration per type seen, and leaving the field out. The `_id` of a collection without a key is never declared, so it is left out and asks nothing. `MapResources` accepts the declaration the answer names. MongoDB and RethinkDB declare no references either, so F5 finds them all.
157
176
 
158
- ## Map Relations
177
+ `GetMapping(connection, module)` reads the relations a module maps back into the proposal's shape, with every choice citing the rule `mapped`, so a caller can change them and map again.
159
178
 
160
- `MapResources(input)` reads the descriptions of the relations through the builder's connection, checks every choice against them, and writes in one typechecked transaction. Then it creates the connection's category `pages.<connection>` and the TableView pages.
179
+ ## Map relations
180
+
181
+ `MapResources(input)` reads the descriptions of the relations through the builder's connection, checks every choice against them, and writes in one typechecked transaction. It then creates the connection's category, `pages.<connection>`, and the TableView pages.
161
182
 
162
183
  ```typescript
163
184
  import { MapResources } from "@antelopejs/interface-dms-builder";
@@ -177,15 +198,16 @@ const result = await MapResources({
177
198
  ],
178
199
  excluded: [{ relation: "erp.schema_migrations", reason: "a framework's or a migration tool's table" }],
179
200
  });
201
+ if (!result.ok) console.error(result.error);
180
202
  ```
181
203
 
182
- - **Names are the database's own.** `_id` is declared only where the relation has a key, and nothing creates or alters a table. A MongoDB collection's key is `_id` itself, which is declared once.
183
- - **Relation columns** point at a relation of the batch, or one the module mapped before, by its single-column key.
184
- - **A relation column's `label`** is written as sent. Left out, it keeps the label its controller holds if its relation was mapped before and held the column; otherwise it takes D3, the title of the relation it points at in singular ("Customer", not "Customer id"), as the target is titled by this call, by its page if it was mapped before, else by its name humanized. A column the database commented with a short first sentence takes that sentence instead (L2). A column that is not a relation column takes its name, humanized. A call that never sends a `label` therefore gets the analysis's labels on a first mapping and loses nothing on a remap.
185
- - **A column's `display`** is `{ kind: "badge", items: [{ value, label, tone }] }` on a `string` column, or `{ kind: "price" }` on a `number` one. A relation column takes none. Left out, the column keeps the display it has if its relation was mapped before, read back from its controller as `GetMapping` reads it; if the relation is new, it takes the display D1 or D2 proposes. `null` writes the column plain, over any display it had. A call that never sends a `display` therefore gets the proposal on a first mapping and loses nothing on a remap.
186
- - **Where a proposal comes from.** `MapResources` reads the profile of a new relation through the catalog connection at map time, since D1 needs it. When the profile cannot be read, it takes the proposal from the connection's last `AnalyseDatabase`; when there is none either, D2 still applies by the column's name, no column becomes a badge, and the result warns `display_not_proposed`. A display that does not suit the column's declaration, such as a badge on a column declared `number`, is dropped when it was kept or proposed, and refused only when sent.
187
- - **A TableView's `tabs`**: `tabs: { column }` writes "All" and a tab per value of that column's badge to the TableView, over the `tabs` it holds; the column must be filterable and drawn as a badge. Left out, a page already mapped keeps its tabs, and a new relation takes the tabs T1 proposes among its filterable badge columns: a column left unfilterable is not tabbed. `null` writes none: it adds no tabs to a new relation, and removes from a page already mapped the tabs a mapping wrote from a badge, as `display: null` removes the badge. Tabs edited by hand are kept.
188
- - **Mapping again** a relation already mapped rewrites it from the new choices. Its page is created, kept or removed as `tableView` says, and takes the new `title`. The module's relation columns pointing at it take its new `label`.
204
+ - **Names are the database's own.** `_id` is declared only where the relation has a key, and nothing creates or alters a table. A MongoDB collection's key is `_id` itself, which is declared once. A relation without a key gets no TableView, whatever `tableView` says.
205
+ - **Relation columns** point at a relation of the batch, or at one the module mapped before, by its single-column key.
206
+ - **A relation column's `label`** is written as sent. When it is left out, the column keeps the label its controller holds, if its relation was mapped before and held the column. Otherwise it takes D3, the title of the relation it points at in the singular ("Customer", not "Customer id"). That title comes from this call, else from the target's page if it was mapped before, else from its humanized name. A relation column that the database commented with a short first sentence takes that sentence instead (L2). A column that is not a relation column takes its humanized name on every mapping, even when the database commented it, so it does not take the analysis's title. A call that never sends a `label` therefore gets D3 or L2 titles for its relation columns on a first mapping and keeps them on a remap, and gets humanized names for its other columns.
207
+ - **A column's `display`** is `{ kind: "badge", items: [{ value, label, tone }] }` on a `string` column, or `{ kind: "price" }` on a `number` one. A relation column takes none. When `display` is left out, the column keeps the display it has if its relation was mapped before, read back from its controller as `GetMapping` reads it. A new relation takes the display that D1 or D2 proposes. `null` writes the column plain, over any display it had. A call that never sends a `display` therefore gets the proposal on a first mapping and loses nothing on a remap.
208
+ - **Where a proposal comes from.** `MapResources` reads the profile of a new relation through the catalog connection at map time, because D1 needs it. When it cannot read the profile, it takes the proposal from the connection's last `AnalyseDatabase`. When there is none either, D2 still applies by the column's name, no column becomes a badge, and the result warns `display_not_proposed`. A display that does not suit the column's declaration, such as a badge on a column declared `number`, or one on the primary key, is dropped and the column is written plain. When the display was sent, the result warns `display_dropped`.
209
+ - **A TableView's `tabs`.** `tabs: { column }` writes an "All" tab and one tab per value of that column's badge to the TableView, over the `tabs` it holds. The column must be drawn as a badge, or the tabs are left out with a `display_dropped` warning. When the column is not filterable, the builder marks it filterable and warns `field_flag_set`. When `tabs` is left out, a page already mapped keeps its tabs, and a new relation takes the tabs that T1 proposes among its filterable badge columns only. `null` writes none. It adds no tabs to a new relation, and it removes from a page already mapped the tabs that a mapping wrote from a badge, as `display: null` removes the badge. Tabs edited by hand are kept.
210
+ - **Mapping a relation again** rewrites it from the new choices. Its page is created, kept or removed as `tableView` says, and takes the new `title`. The module's relation columns that point at it take its new `label`.
189
211
  - **Before a remap that removes a page**, `PreviewMapResources(input)` answers `pagesToRemove` for the same input and writes nothing, so a caller can ask a person first. It refuses what `MapResources` refuses.
190
212
  - **The module's `package.json`** records the connection and the `excluded` tables with their reasons, so the next analysis does not propose those tables. Mapping one later removes it from the record:
191
213
 
@@ -197,10 +219,11 @@ const result = await MapResources({
197
219
  }
198
220
  }
199
221
  ```
200
- - **In a mapped module**, `CreateResource` and `AddField` are refused, since its tables are the database's. `DeleteResource` keeps the client's rows and warns `data_kept`.
201
222
 
202
- ## Next Steps
223
+ - **In a mapped module**, `CreateResource`, `AddField` and a `ConfigureField` that changes `dbField` fail with `invalid_config`, since the module's tables belong to the database. `DeleteResource` keeps the client's rows and warns `data_kept`.
224
+
225
+ ## Next steps
203
226
 
204
- - [Resources](./5.resources.md): the resources a mapping writes are ordinary ones, read the same way.
205
- - [Queries](./6.queries.md): stats over a mapped relation name it by its ref.
206
- - [Results and Errors](./8.results-and-errors.md): branch on what every operation answers.
227
+ - [Resources](./5.resources.md) describes the resources a mapping writes. They are ordinary resources, read the same way.
228
+ - [Queries](./6.queries.md) shows how stats over a mapped relation name it by its ref.
229
+ - [Results and errors](./8.results-and-errors.md) shows how to branch on what every operation answers.