@antelopejs/interface-dms-builder 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -17
- package/dist/catalog.d.ts +7 -7
- package/dist/drafts.d.ts +10 -9
- package/dist/drafts.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mapping.d.ts +5 -0
- package/dist/mapping.js +2 -1
- package/dist/mapping.js.map +1 -1
- package/dist/operation-map.d.ts +66 -0
- package/dist/operation-map.js +57 -0
- package/dist/operation-map.js.map +1 -0
- package/dist/operations.d.ts +6 -5
- package/dist/operations.js.map +1 -1
- package/dist/project.d.ts +3 -2
- package/dist/project.js.map +1 -1
- package/dist/queries.d.ts +12 -11
- package/dist/resources.d.ts +3 -3
- package/dist/results.d.ts +4 -4
- package/dist/structure.d.ts +5 -4
- package/dist/values.d.ts +14 -10
- package/docs/1.introduction.md +28 -22
- package/docs/10.a-chart-without-code.md +47 -71
- package/docs/11.external-databases.md +94 -70
- package/docs/2.addressing-and-structure.md +48 -40
- package/docs/3.catalog.md +105 -49
- package/docs/4.page-operations.md +74 -60
- package/docs/5.resources.md +97 -71
- package/docs/6.queries.md +94 -95
- package/docs/7.values.md +38 -32
- package/docs/8.results-and-errors.md +62 -45
- package/docs/9.drafts-and-preview.md +84 -48
- package/docs/images/captures/editor-selected.webp +0 -0
- package/docs/tsconfig.json +4 -0
- package/package.json +2 -2
- package/docs/images/editor-selected.png +0 -0
package/docs/1.introduction.md
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
11
|
+
## Quick start
|
|
8
12
|
|
|
9
|
-
This example creates a **Revenue** page with a **Total revenue** card.
|
|
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.
|
|
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,
|
|
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
|
|
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
|
|
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`.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
- [
|
|
72
|
-
- [
|
|
73
|
-
- [
|
|
74
|
-
- [
|
|
75
|
-
- [
|
|
76
|
-
- [
|
|
77
|
-
- [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-

|
|
8
10
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
173
|
+
## What we built
|
|
195
174
|
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
8
|
-
2.
|
|
9
|
-
3.
|
|
10
|
-
4.
|
|
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.
|
|
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
|
|
18
|
+
## Wire the project
|
|
15
19
|
|
|
16
|
-
`ConfigureProject(steps, opts?)` applies its steps to `antelope.config.ts` as one edit.
|
|
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` |
|
|
21
|
-
| `ConfigureModule` |
|
|
22
|
-
| `CreateLocalModule` |
|
|
23
|
-
| `RemoveModule` |
|
|
24
|
-
| `RemoveConnection` |
|
|
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`
|
|
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`
|
|
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: "
|
|
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
|
|
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
|
|
89
|
+
### Remove a connection
|
|
82
90
|
|
|
83
|
-
|
|
91
|
+
In the **Database** workspace, the **Remove <connection>** 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
|
|
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
|
|
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
|
|
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
|
|
99
|
+
## Analyse a database
|
|
92
100
|
|
|
93
|
-
`AnalyseDatabase({ connection, module, schemas? })` starts a job and answers it at once
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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 |
|
|
127
|
-
| R2 |
|
|
128
|
-
| R3 |
|
|
129
|
-
| R4 |
|
|
130
|
-
| R5 |
|
|
131
|
-
| R6 |
|
|
132
|
-
| R7 |
|
|
133
|
-
| R8 |
|
|
134
|
-
| R9 |
|
|
135
|
-
| L1
|
|
136
|
-
|
|
|
137
|
-
|
|
|
138
|
-
|
|
|
139
|
-
|
|
|
140
|
-
|
|
|
141
|
-
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,17 @@ 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.
|
|
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.
|
|
186
|
-
- **Where a proposal comes from.** `MapResources` reads the profile of a new relation through the catalog connection at map time,
|
|
187
|
-
- **A TableView's `tabs
|
|
188
|
-
- **Mapping
|
|
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`.
|
|
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.
|
|
189
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:
|
|
190
213
|
|
|
191
214
|
```json
|
|
@@ -196,10 +219,11 @@ const result = await MapResources({
|
|
|
196
219
|
}
|
|
197
220
|
}
|
|
198
221
|
```
|
|
199
|
-
- **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`.
|
|
200
222
|
|
|
201
|
-
|
|
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
|
|
202
226
|
|
|
203
|
-
- [Resources](./5.resources.md)
|
|
204
|
-
- [Queries](./6.queries.md)
|
|
205
|
-
- [Results and
|
|
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.
|