External databases
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.
Overview
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:
ConfigureProjectwires the database intoantelope.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.AnalyseDatabasereads the catalog and proposes a mapping by fixed rules. It writes nothing.- A person in the builder's Database workspace, or an assistant through these operations, settles the proposal.
MapResourceswrites table classes, controllers and TableView pages into the module bound to the database.
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.
Wire the project
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.
| Step | Writes |
|---|---|
AddModule | A module entry. Refuses an id that exists. |
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. |
CreateLocalModule | Scaffolds a local module at path and adds its entry. With connection, the module's package.json record names that connection. |
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. |
RemoveConnection | Removes a named connection to an interface from every module's importOverrides. |
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.
The provider's entry depends on its engine:
| Engine | Package | env | disabledExports |
|---|---|---|---|
| PostgreSQL | @antelopejs/postgres | url | none |
| MongoDB | @antelopejs/mongodb | url and database | @antelopejs/interface-mongodb |
| RethinkDB | @antelopejs/rethinkdb | url, with the database as its path | @antelopejs/interface-rethinkdb |
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.
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.
import { ConfigureProject } from "@antelopejs/interface-dms-builder";
const catalog = "@antelopejs/interface-database-catalog";
const result = await ConfigureProject([
{
op: "AddModule",
id: "postgres-erp",
entry: {
source: { type: "package", package: "@antelopejs/postgres", version: "<version>" },
config: { mode: "external", statementTimeoutMs: 30000 },
env: { url: "ERP_DATABASE_URL" },
exportPriority: { "@antelopejs/interface-database": -1 },
},
},
{
op: "CreateLocalModule",
id: "erp-tables",
path: "modules/erp-tables",
connection: "erp",
entry: {
importOverrides: [{ interface: "@antelopejs/interface-database", source: "postgres-erp" }],
},
},
{
op: "ConfigureModule",
id: "dms",
entry: { importOverrides: [{ interface: "@antelopejs/interface-database", source: "mongodb" }] },
},
{
op: "ConfigureModule",
id: "dms-builder",
entry: { importOverrides: [{ interface: catalog, source: "postgres-erp", id: "erp" }] },
},
]);
if (!result.ok) console.error(result.error);
- The provider's
exportPriorityof-1keeps the project's own database the default for every module that does not pininterface-database. - The tables module pins
interface-databaseto 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 pinsinterface-databaseneeds nothing more. - When the project loads
dms-ai, the Database workspace gives it the same catalog connection asdms-builder.
The connection exists once the project restarts with the variable set.
Remove a connection
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.
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.
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.
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.
Analyse a database
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.
import { AnalyseDatabase, GetAnalysis } from "@antelopejs/interface-dms-builder";
const started = await AnalyseDatabase({ connection: "erp", module: "erp-tables" });
if (started.ok) {
const state = await GetAnalysis(started.data.id);
if (state.ok) {
console.log(state.data.status); // "running", "done" or "failed"
}
}
connectionmust be a catalog connection on thedms-builderentry, andmodulea local module other than the project's own. Otherwise the call fails withinvalid_config.- One job runs per connection. Starting another while one runs answers the running one.
- A finished job is kept until the next one on its connection, or until a restart.
- Relations that the module has mapped, or recorded as excluded, are not proposed again.
- When more than 200 relations are left to profile and the call passed no
schemas, the proposal holds a singleschemasquestion instead. Run the analysis again withschemas.
Read a proposal
A MappingProposal is a MapResources input in which every choice says where it came from:
interface Proposed<T> {
value: T;
rule: string; // "R6", "L1", "F5"…
kind: "fact" | "default";
}
A fact follows from the catalog with no judgement. A default is a judgement to check.
The relation rules apply in the order R1, R4, R3, R2, then R5 to R9. The first rule that applies decides.
| Rule | When | Then |
|---|---|---|
| R1 | The catalog says unmappable | Excluded, with its reason |
| R2 | No key | Stats only: table class and model, no page |
| R3 | No rows | Excluded |
| R4 | A framework's or a migration tool's table | Excluded |
| R5 | Keyed by its two references, with at most one other column | Stats only |
| R6 | Keyed, with rows and a label | TableView, and a label question when two candidates tie |
| R7 | Keyed, no label, referenced | TableView labelled by its key, and a label question |
| R8 | Keyed, no label, not referenced, with a time axis | Stats only |
| R9 | Anything else | A purpose question, nothing proposed |
| L1 | A label candidate | The label, by name, then by order |
| L2 | A relation or a column | Its title, from a short comment, else from its name |
| F1 | A column that no field type reads | Left out |
| F2 | A column named like a secret | Left out |
| F3 | A column | Its first declaration |
| F4 | A single column with a foreign key that the database declares on a TableView's single-column key | A relation column |
| F5 | A single column whose values are all found in a TableView's single-column key | A relation column |
| FL | A field | Whether it is listable, filterable, sortable and searchable |
| 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 |
| D2 | A number column whose name ends in total, amount, price, cost, revenue or subtotal | display, a price |
| 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 |
| 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 |
| W1 | Every proposed relation | Read-only (writable: false) |
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.
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.
Each resource also carries its rows, counted by the profile or estimated, when either is known.
The open list holds what the rules could not decide. Each item holds its options and what the proposal holds meanwhile:
- A
labelitem offers every column that a field reads, the key among them, except columns named like a secret. - A
referenceitem offers a plain column, the target its values were found in, and every TableView whose single-column key accepts the column. - A
purposeitem carries theresourceit would map if the answer is to map it. Its ownopenholds the questions that resource leaves, which apply only once the answer maps it. - A
field-typeitem asks which declaration reads a field of several native types.
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.
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.
Map relations
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.
import { MapResources } from "@antelopejs/interface-dms-builder";
const result = await MapResources({
module: "erp-tables",
connection: "erp",
resources: [
{
relation: "erp.customers",
label: "name",
title: "Customers",
tableView: true,
writable: false,
fields: [{ column: "name", searchable: true }, { column: "segment", filterable: true }],
},
],
excluded: [{ relation: "erp.schema_migrations", reason: "a framework's or a migration tool's table" }],
});
if (!result.ok) console.error(result.error);
- Names are the database's own.
_idis declared only where the relation has a key, and nothing creates or alters a table. A MongoDB collection's key is_iditself, which is declared once. A relation without a key gets no TableView, whatevertableViewsays. - Relation columns point at a relation of the batch, or at one the module mapped before, by its single-column key.
- A relation column's
labelis 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 alabeltherefore 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. - A column's
displayis{ kind: "badge", items: [{ value, label, tone }] }on astringcolumn, or{ kind: "price" }on anumberone. A relation column takes none. Whendisplayis left out, the column keeps the display it has if its relation was mapped before, read back from its controller asGetMappingreads it. A new relation takes the display that D1 or D2 proposes.nullwrites the column plain, over any display it had. A call that never sends adisplaytherefore gets the proposal on a first mapping and loses nothing on a remap. - Where a proposal comes from.
MapResourcesreads 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 lastAnalyseDatabase. When there is none either, D2 still applies by the column's name, no column becomes a badge, and the result warnsdisplay_not_proposed. A display that does not suit the column's declaration, such as a badge on a column declarednumber, or one on the primary key, is dropped and the column is written plain. When the display was sent, the result warnsdisplay_dropped. - A TableView's
tabs.tabs: { column }writes an "All" tab and one tab per value of that column's badge to the TableView, over thetabsit holds. The column must be drawn as a badge, or the tabs are left out with adisplay_droppedwarning. When the column is not filterable, the builder marks it filterable and warnsfield_flag_set. Whentabsis 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.nullwrites 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, asdisplay: nullremoves the badge. Tabs edited by hand are kept. - Mapping a relation again rewrites it from the new choices. Its page is created, kept or removed as
tableViewsays, and takes the newtitle. The module's relation columns that point at it take its newlabel. - Before a remap that removes a page,
PreviewMapResources(input)answerspagesToRemovefor the same input and writes nothing, so a caller can ask a person first. It refuses whatMapResourcesrefuses. - The module's
package.jsonrecords the connection and theexcludedtables with their reasons, so the next analysis does not propose those tables. Mapping one later removes it from the record:"dmsBuilder": { "database": { "connection": "erp", "excludedTables": { "erp.schema_migrations": "a framework's or a migration tool's table" } } } - In a mapped module,
CreateResource,AddFieldand aConfigureFieldthat changesdbFieldfail withinvalid_config, since the module's tables belong to the database.DeleteResourcekeeps the client's rows and warnsdata_kept.
Next steps
- Resources describes the resources a mapping writes. They are ordinary resources, read the same way.
- Queries shows how stats over a mapped relation name it by its ref.
- Results and errors shows how to branch on what every operation answers.