@antelopejs/interface-dms-builder 0.2.2 → 0.2.3

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.
@@ -0,0 +1,124 @@
1
+ import type { ResourceRef } from "./resources";
2
+ import type { OpResult } from "./results";
3
+ export type ProposedKind = "fact" | "default";
4
+ export interface Proposed<T> {
5
+ value: T;
6
+ rule: string;
7
+ kind: ProposedKind;
8
+ }
9
+ export interface ProposedDeclaration extends Proposed<string> {
10
+ lossy?: string;
11
+ }
12
+ export interface ProposedRelationColumn extends Proposed<string> {
13
+ implied: boolean;
14
+ }
15
+ export interface ProposedRange {
16
+ min: number | string;
17
+ max: number | string;
18
+ }
19
+ export interface ProposedCategory {
20
+ value: string | number | boolean;
21
+ rows: number;
22
+ }
23
+ export interface ProposedColumnProfile {
24
+ traits: string[];
25
+ nulls?: number;
26
+ distinct?: number;
27
+ range?: ProposedRange;
28
+ categories?: ProposedCategory[];
29
+ }
30
+ export interface ProposedField {
31
+ column: string;
32
+ native: string;
33
+ accepts: string[];
34
+ declared: ProposedDeclaration;
35
+ label: Proposed<string>;
36
+ listable: Proposed<boolean>;
37
+ filterable: Proposed<boolean>;
38
+ sortable: Proposed<boolean>;
39
+ searchable: Proposed<boolean>;
40
+ relation?: ProposedRelationColumn;
41
+ profile?: ProposedColumnProfile;
42
+ }
43
+ export interface ProposedLeftOut {
44
+ column: string;
45
+ reason: string;
46
+ rule: string;
47
+ }
48
+ export interface ProposedWrites {
49
+ insert: boolean;
50
+ update: boolean;
51
+ delete: boolean;
52
+ }
53
+ export interface ProposedResource {
54
+ relation: string;
55
+ tableView: Proposed<boolean>;
56
+ writable: Proposed<boolean>;
57
+ label: Proposed<string | null>;
58
+ title: Proposed<string>;
59
+ fields: ProposedField[];
60
+ leftOut: ProposedLeftOut[];
61
+ writes: ProposedWrites;
62
+ }
63
+ export interface ProposedExclusion {
64
+ relation: string;
65
+ reason: string;
66
+ rule: string;
67
+ }
68
+ export type OpenItemKind = "schemas" | "purpose" | "label" | "reference";
69
+ export interface OpenItemOption {
70
+ value: unknown;
71
+ title: string;
72
+ }
73
+ export interface OpenItem {
74
+ id: string;
75
+ kind: OpenItemKind;
76
+ relation?: string;
77
+ column?: string;
78
+ question: string;
79
+ options: OpenItemOption[];
80
+ proposed: unknown;
81
+ resource?: ProposedResource;
82
+ }
83
+ export interface ProposalDatabase {
84
+ engine: string;
85
+ role: string;
86
+ writes: "none" | "some" | "all";
87
+ }
88
+ export interface MappingProposal {
89
+ connection: string;
90
+ module: string;
91
+ database: ProposalDatabase;
92
+ resources: ProposedResource[];
93
+ excluded: ProposedExclusion[];
94
+ open: OpenItem[];
95
+ }
96
+ export interface AnalyseDatabaseInput {
97
+ connection: string;
98
+ module: string;
99
+ schemas?: string[];
100
+ }
101
+ export interface AnalysisProgress {
102
+ relations: number;
103
+ described: number;
104
+ toProfile: number;
105
+ profiled: number;
106
+ timeouts: number;
107
+ }
108
+ export type AnalysisStatus = "running" | "done" | "failed";
109
+ export interface AnalysisState {
110
+ id: string;
111
+ connection: string;
112
+ status: AnalysisStatus;
113
+ progress: AnalysisProgress;
114
+ proposal?: MappingProposal;
115
+ error?: string;
116
+ }
117
+ export declare const AnalyseDatabase: (input: AnalyseDatabaseInput) => Promise<OpResult<AnalysisState>>;
118
+ export declare const GetAnalysis: (id: string) => Promise<OpResult<AnalysisState>>;
119
+ export interface MappedRelation {
120
+ ref: ResourceRef;
121
+ resource: ProposedResource;
122
+ included: string[];
123
+ }
124
+ export declare const GetMapping: (connection: string, module: string) => Promise<OpResult<MappedRelation[]>>;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GetMapping = exports.GetAnalysis = exports.AnalyseDatabase = void 0;
4
+ const interface_core_1 = require("@antelopejs/interface-core");
5
+ exports.AnalyseDatabase = (0, interface_core_1.InterfaceFunction)();
6
+ exports.GetAnalysis = (0, interface_core_1.InterfaceFunction)();
7
+ exports.GetMapping = (0, interface_core_1.InterfaceFunction)();
8
+ //# sourceMappingURL=analysis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analysis.js","sourceRoot":"","sources":["../src/analysis.ts"],"names":[],"mappings":";;;AAAA,+DAA+D;AAoLlD,QAAA,eAAe,GAC1B,IAAA,kCAAiB,GAEd,CAAC;AAMO,QAAA,WAAW,GACtB,IAAA,kCAAiB,GAAoD,CAAC;AAc3D,QAAA,UAAU,GACrB,IAAA,kCAAiB,GAEd,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  export * from "./addressing";
2
+ export * from "./analysis";
2
3
  export * from "./catalog";
3
4
  export * from "./drafts";
5
+ export * from "./mapping";
4
6
  export * from "./operations";
7
+ export * from "./project";
5
8
  export * from "./queries";
6
9
  export * from "./resources";
7
10
  export * from "./results";
package/dist/index.js CHANGED
@@ -15,9 +15,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./addressing"), exports);
18
+ __exportStar(require("./analysis"), exports);
18
19
  __exportStar(require("./catalog"), exports);
19
20
  __exportStar(require("./drafts"), exports);
21
+ __exportStar(require("./mapping"), exports);
20
22
  __exportStar(require("./operations"), exports);
23
+ __exportStar(require("./project"), exports);
21
24
  __exportStar(require("./queries"), exports);
22
25
  __exportStar(require("./resources"), exports);
23
26
  __exportStar(require("./results"), exports);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,+CAA6B;AAC7B,4CAA0B;AAC1B,2CAAyB;AACzB,+CAA6B;AAC7B,4CAA0B;AAC1B,8CAA4B;AAC5B,4CAA0B;AAC1B,8CAA4B;AAC5B,2CAAyB"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,+CAA6B;AAC7B,6CAA2B;AAC3B,4CAA0B;AAC1B,2CAAyB;AACzB,4CAA0B;AAC1B,+CAA6B;AAC7B,4CAA0B;AAC1B,4CAA0B;AAC1B,8CAA4B;AAC5B,4CAA0B;AAC1B,8CAA4B;AAC5B,2CAAyB"}
@@ -0,0 +1,49 @@
1
+ import type { CategoryRef, PageRef } from "./addressing";
2
+ import type { ResourceRef } from "./resources";
3
+ import type { OpResult } from "./results";
4
+ export interface MappedField {
5
+ column: string;
6
+ declared?: string;
7
+ label?: string;
8
+ listable?: boolean;
9
+ filterable?: boolean;
10
+ sortable?: boolean;
11
+ searchable?: boolean;
12
+ relation?: string;
13
+ }
14
+ export interface MappedResource {
15
+ relation: string;
16
+ className?: string;
17
+ label?: string;
18
+ title?: string;
19
+ tableView: boolean;
20
+ writable: boolean;
21
+ fields: MappedField[];
22
+ }
23
+ export interface ExcludedRelation {
24
+ relation: string;
25
+ reason: string;
26
+ }
27
+ export interface MapResourcesInput {
28
+ module: string;
29
+ connection: string;
30
+ resources: MappedResource[];
31
+ excluded?: ExcludedRelation[];
32
+ }
33
+ export interface LeftOutColumn {
34
+ relation: string;
35
+ column: string;
36
+ reason: string;
37
+ }
38
+ export interface MappedResult {
39
+ relation: string;
40
+ ref: ResourceRef;
41
+ page?: PageRef;
42
+ files: string[];
43
+ }
44
+ export interface MapResourcesResult {
45
+ resources: MappedResult[];
46
+ leftOut: LeftOutColumn[];
47
+ category?: CategoryRef;
48
+ }
49
+ export declare const MapResources: (input: MapResourcesInput) => Promise<OpResult<MapResourcesResult>>;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MapResources = void 0;
4
+ const interface_core_1 = require("@antelopejs/interface-core");
5
+ exports.MapResources = (0, interface_core_1.InterfaceFunction)();
6
+ //# sourceMappingURL=mapping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapping.js","sourceRoot":"","sources":["../src/mapping.ts"],"names":[],"mappings":";;;AAAA,+DAA+D;AAmGlD,QAAA,YAAY,GACvB,IAAA,kCAAiB,GAEd,CAAC"}
@@ -15,6 +15,7 @@ export interface CreatePageInput {
15
15
  description?: string;
16
16
  permission?: PagePermission;
17
17
  dir?: string;
18
+ module?: string;
18
19
  }
19
20
  export interface CreateCategoryInput {
20
21
  name: string;
@@ -22,6 +23,7 @@ export interface CreateCategoryInput {
22
23
  parent?: CategoryRef;
23
24
  icon?: string;
24
25
  order?: number;
26
+ module?: string;
25
27
  }
26
28
  export interface AddBlockInput {
27
29
  page: PageRef;
@@ -1 +1 @@
1
- {"version":3,"file":"operations.js","sourceRoot":"","sources":["../src/operations.ts"],"names":[],"mappings":";;;AAAA,+DAA+D;AA4FlD,QAAA,SAAS,GAAG,IAAA,kCAAiB,GAAgC,CAAC;AAC9D,QAAA,cAAc,GACzB,IAAA,kCAAiB,GAAoC,CAAC;AAC3C,QAAA,UAAU,GAAG,IAAA,kCAAiB,GAA+B,CAAC;AAS9D,QAAA,eAAe,GAC1B,IAAA,kCAAiB,GAEd,CAAC;AACO,QAAA,gBAAgB,GAC3B,IAAA,kCAAiB,GAAsD,CAAC;AAM7D,QAAA,kBAAkB,GAC7B,IAAA,kCAAiB,GAA2C,CAAC;AAElD,QAAA,UAAU,GACrB,IAAA,kCAAiB,GAId,CAAC;AAOO,QAAA,aAAa,GACxB,IAAA,kCAAiB,GAMd,CAAC;AACO,QAAA,UAAU,GACrB,IAAA,kCAAiB,GAA4D,CAAC;AAEnE,QAAA,cAAc,GACzB,IAAA,kCAAiB,GAEd,CAAC;AACO,QAAA,iBAAiB,GAC5B,IAAA,kCAAiB,GAKd,CAAC;AACO,QAAA,cAAc,GACzB,IAAA,kCAAiB,GAA2C,CAAC;AAElD,QAAA,QAAQ,GACnB,IAAA,kCAAiB,GAKd,CAAC;AACO,QAAA,cAAc,GACzB,IAAA,kCAAiB,GAMd,CAAC;AACO,QAAA,SAAS,GACpB,IAAA,kCAAiB,GAEd,CAAC;AACO,QAAA,WAAW,GACtB,IAAA,kCAAiB,GAEd,CAAC"}
1
+ {"version":3,"file":"operations.js","sourceRoot":"","sources":["../src/operations.ts"],"names":[],"mappings":";;;AAAA,+DAA+D;AAoGlD,QAAA,SAAS,GAAG,IAAA,kCAAiB,GAAgC,CAAC;AAC9D,QAAA,cAAc,GACzB,IAAA,kCAAiB,GAAoC,CAAC;AAC3C,QAAA,UAAU,GAAG,IAAA,kCAAiB,GAA+B,CAAC;AAS9D,QAAA,eAAe,GAC1B,IAAA,kCAAiB,GAEd,CAAC;AACO,QAAA,gBAAgB,GAC3B,IAAA,kCAAiB,GAAsD,CAAC;AAM7D,QAAA,kBAAkB,GAC7B,IAAA,kCAAiB,GAA2C,CAAC;AAElD,QAAA,UAAU,GACrB,IAAA,kCAAiB,GAId,CAAC;AAOO,QAAA,aAAa,GACxB,IAAA,kCAAiB,GAMd,CAAC;AACO,QAAA,UAAU,GACrB,IAAA,kCAAiB,GAA4D,CAAC;AAEnE,QAAA,cAAc,GACzB,IAAA,kCAAiB,GAEd,CAAC;AACO,QAAA,iBAAiB,GAC5B,IAAA,kCAAiB,GAKd,CAAC;AACO,QAAA,cAAc,GACzB,IAAA,kCAAiB,GAA2C,CAAC;AAElD,QAAA,QAAQ,GACnB,IAAA,kCAAiB,GAKd,CAAC;AACO,QAAA,cAAc,GACzB,IAAA,kCAAiB,GAMd,CAAC;AACO,QAAA,SAAS,GACpB,IAAA,kCAAiB,GAEd,CAAC;AACO,QAAA,WAAW,GACtB,IAAA,kCAAiB,GAEd,CAAC"}
@@ -0,0 +1,40 @@
1
+ import type { OpResult } from "./results";
2
+ export interface ImportOverrideInput {
3
+ interface: string;
4
+ source?: string;
5
+ id?: string;
6
+ }
7
+ export interface ModuleEntryInput {
8
+ source?: Record<string, unknown>;
9
+ config?: Record<string, unknown>;
10
+ env?: Record<string, string>;
11
+ importOverrides?: ImportOverrideInput[];
12
+ exportPriority?: Record<string, number>;
13
+ }
14
+ export interface AddModuleStep {
15
+ op: "AddModule";
16
+ id: string;
17
+ entry: ModuleEntryInput;
18
+ }
19
+ export interface ConfigureModuleStep {
20
+ op: "ConfigureModule";
21
+ id: string;
22
+ entry: ModuleEntryInput;
23
+ }
24
+ export interface CreateLocalModuleStep {
25
+ op: "CreateLocalModule";
26
+ id: string;
27
+ path: string;
28
+ entry?: ModuleEntryInput;
29
+ }
30
+ export type ProjectConfigStep = AddModuleStep | ConfigureModuleStep | CreateLocalModuleStep;
31
+ export interface ProjectConfigOpts {
32
+ dryRun?: boolean;
33
+ }
34
+ export interface ProjectConfigResult {
35
+ files: string[];
36
+ }
37
+ export declare const ConfigureProject: (steps: ProjectConfigStep[], opts?: ProjectConfigOpts | undefined) => Promise<OpResult<ProjectConfigResult>>;
38
+ export declare const AddModule: (id: string, entry: ModuleEntryInput, opts?: ProjectConfigOpts | undefined) => Promise<OpResult<ProjectConfigResult>>;
39
+ export declare const ConfigureModule: (id: string, entry: ModuleEntryInput, opts?: ProjectConfigOpts | undefined) => Promise<OpResult<ProjectConfigResult>>;
40
+ export declare const CreateLocalModule: (id: string, path: string, entry?: ModuleEntryInput | undefined, opts?: ProjectConfigOpts | undefined) => Promise<OpResult<ProjectConfigResult>>;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CreateLocalModule = exports.ConfigureModule = exports.AddModule = exports.ConfigureProject = void 0;
4
+ const interface_core_1 = require("@antelopejs/interface-core");
5
+ exports.ConfigureProject = (0, interface_core_1.InterfaceFunction)();
6
+ exports.AddModule = (0, interface_core_1.InterfaceFunction)();
7
+ exports.ConfigureModule = (0, interface_core_1.InterfaceFunction)();
8
+ exports.CreateLocalModule = (0, interface_core_1.InterfaceFunction)();
9
+ //# sourceMappingURL=project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":";;;AAAA,+DAA+D;AA8ElD,QAAA,gBAAgB,GAC3B,IAAA,kCAAiB,GAKd,CAAC;AAGO,QAAA,SAAS,GACpB,IAAA,kCAAiB,GAMd,CAAC;AAGO,QAAA,eAAe,GAC1B,IAAA,kCAAiB,GAMd,CAAC;AAGO,QAAA,iBAAiB,GAC5B,IAAA,kCAAiB,GAOd,CAAC"}
@@ -48,6 +48,7 @@ export interface CreateResourceInput {
48
48
  seeds?: Record<string, unknown>[];
49
49
  location?: ResourceLocation;
50
50
  routes?: ResourceRoute[];
51
+ module?: string;
51
52
  }
52
53
  export interface ResourceFieldStructure extends Partial<FieldAspects> {
53
54
  name: string;
@@ -75,6 +76,7 @@ export interface ResourceSummary {
75
76
  database: string;
76
77
  dataApi: string;
77
78
  };
79
+ sourceModule?: string;
78
80
  }
79
81
  export interface DeleteResourceOpts extends MutationOpts {
80
82
  keepData?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"resources.js","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":";;;AAAA,+DAA+D;AAgNlD,QAAA,cAAc,GACzB,IAAA,kCAAiB,GAId,CAAC;AAcO,QAAA,iBAAiB,GAC5B,IAAA,kCAAiB,GAMd,CAAC;AAOO,QAAA,cAAc,GACzB,IAAA,kCAAiB,GAEd,CAAC;AACO,QAAA,QAAQ,GACnB,IAAA,kCAAiB,GAMd,CAAC;AACO,QAAA,cAAc,GACzB,IAAA,kCAAiB,GAMd,CAAC;AACO,QAAA,WAAW,GACtB,IAAA,kCAAiB,GAEd,CAAC;AACO,QAAA,aAAa,GACxB,IAAA,kCAAiB,GAAoC,CAAC;AAC3C,QAAA,oBAAoB,GAC/B,IAAA,kCAAiB,GAEd,CAAC"}
1
+ {"version":3,"file":"resources.js","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":";;;AAAA,+DAA+D;AAwNlD,QAAA,cAAc,GACzB,IAAA,kCAAiB,GAId,CAAC;AAcO,QAAA,iBAAiB,GAC5B,IAAA,kCAAiB,GAMd,CAAC;AAOO,QAAA,cAAc,GACzB,IAAA,kCAAiB,GAEd,CAAC;AACO,QAAA,QAAQ,GACnB,IAAA,kCAAiB,GAMd,CAAC;AACO,QAAA,cAAc,GACzB,IAAA,kCAAiB,GAMd,CAAC;AACO,QAAA,WAAW,GACtB,IAAA,kCAAiB,GAEd,CAAC;AACO,QAAA,aAAa,GACxB,IAAA,kCAAiB,GAAoC,CAAC;AAC3C,QAAA,oBAAoB,GAC/B,IAAA,kCAAiB,GAEd,CAAC"}
package/dist/results.d.ts CHANGED
@@ -14,10 +14,10 @@ export interface TypecheckError {
14
14
  message: string;
15
15
  }
16
16
  export interface OpWarning {
17
- code: "query_opaque" | "query_kept" | "index_clamped" | "datatype_fallback" | "data_not_dropped" | "route_changed";
17
+ code: "query_opaque" | "query_kept" | "index_clamped" | "datatype_fallback" | "data_not_dropped" | "route_changed" | "data_kept" | "page_not_written" | "relation_missing";
18
18
  message: string;
19
19
  }
20
- export type DuplicateScope = "page" | "siblings" | "category" | "resource" | "query";
20
+ export type DuplicateScope = "page" | "siblings" | "category" | "resource" | "query" | "module";
21
21
  export type BuilderError = {
22
22
  code: "not_found";
23
23
  ref: string;
@@ -55,6 +55,7 @@ export interface PageSummary {
55
55
  icon?: string;
56
56
  order?: number;
57
57
  hidden?: boolean;
58
+ sourceModule?: string;
58
59
  }
59
60
  export interface CategorySummary {
60
61
  ref: CategoryRef;
@@ -1,44 +1,14 @@
1
1
  # DMS Builder Documentation
2
2
 
3
- ## Overview
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.
4
4
 
5
- The DMS Builder interface edits a DMS project's source tree programmatically. Where the DMS itself is authored by writing TypeScript — a `PageController` here, a `DataController` there — the builder exposes those same authoring moves as operations: create a page, add a block to it, generate a resource with its table and API, declare a query that counts rows.
6
-
7
- Every operation reads or rewrites real source files. There is no separate database of pages, and no runtime registry to keep in sync: the builder parses the project, applies the edit with an AST rewrite, typechecks the result in memory, and writes only if it compiles. What you get back is the list of files it changed.
8
-
9
- A *block* is the builder's name for one DMS component. Every type the catalog reports is a `dms-base` factory returning a `ComponentBuilder`, and `AddBlock` writes that factory call either as a static field of the page's `PageController` or as a `.child()` of another block — the same declaration you would otherwise have typed by hand.
10
-
11
- Operations are `InterfaceFunction` exports, so you import one and call it like any async function; the call is routed to the module implementing the interface. Reads such as `ListPages` return their value directly. Everything that can fail — every mutation, and the two structure reads — returns an `OpResult`, a discriminated union you branch on rather than a value you wrap in `try`.
12
-
13
- ```typescript
14
- import type { OpResult } from "@antelopejs/interface-dms-builder";
15
-
16
- // { ok: true; data: T; changes: FileChange[]; warnings?: OpWarning[] }
17
- // { ok: false; error: BuilderError }
18
- ```
19
-
20
- ## Key Features
21
-
22
- - **Source as the Single Source of Truth** — Operations parse and rewrite the project's own TypeScript, so hand-written code and generated code are the same code.
23
- - **Typechecked Writes** — Each mutation is staged in memory, typechecked across the project, and rolled back on any diagnostic, so a rejected operation leaves nothing behind.
24
- - **A Discoverable Catalog** — `GetCatalog` reports every block factory and DataType the builder can emit, each with a schema for its options, so a caller validates a configuration before writing it.
25
- - **Stable String Addresses** — A page, a block, a category, a resource, a field and a query are each named by a plain string ref you can store and pass back into the next call.
26
- - **Structural Read-Back** — Blocks, resource fields and queries are read back off disk by shape, so a hand-written declaration matching the grammar is as editable as a generated one, and one that drifts out of grammar is reported as opaque rather than silently rewritten.
27
- - **Resource Generation** — A single call writes a `Table`, a `DataController` and a model into a wired folder, described in semantic field aspects rather than in decorators.
28
- - **Derived Queries** — A query template compiles into a model method and a page route, giving a card on a page a number to fetch without hand-writing either half.
29
- - **Optimistic Concurrency** — Structures carry a content-hash `version` you can pass back as `expectedVersion` to make the next write conditional.
30
-
31
- ## Dependencies
32
-
33
- This interface relies on the following AntelopeJS packages:
34
-
35
- - [@antelopejs/interface-core](https://github.com/AntelopeJS/interface-core) - The `InterfaceFunction` call surface every operation is declared with.
36
-
37
- The implementation lives in the dms-builder module, so the interface only works in a project that also loads `@antelopejs/dms`: the block factories and DataTypes the catalog reports are read from its [`@antelopejs/interface-dms/base`](https://github.com/AntelopeJS/dms) interface, and the pages and resources the builder writes import from it. Clearing a deleted resource's rows goes through [@antelopejs/interface-database](https://github.com/AntelopeJS/interface-database).
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).
38
6
 
39
7
  ## Quick Start
40
8
 
41
- The example creates a page under the root `pages` category and puts a KPI card on it.
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.
10
+
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`.
42
12
 
43
13
  ```typescript
44
14
  import {
@@ -75,6 +45,25 @@ for (const change of block.changes) {
75
45
  }
76
46
  ```
77
47
 
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.
49
+
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.
51
+
52
+ ## How an edit reaches your files
53
+
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.
55
+
56
+ Operations are `InterfaceFunction` exports from `@antelopejs/interface-core`. Expected failures return values you can inspect:
57
+
58
+ ```typescript
59
+ // Success: { ok: true, data, changes, warnings? }
60
+ // Failure: { ok: false, error }
61
+ ```
62
+
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.
64
+
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.
66
+
78
67
  ## Documentation Sections
79
68
 
80
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
@@ -1,30 +1,18 @@
1
1
  # A Chart Without Code
2
2
 
3
- The journey the no-code data sources were built for, end to end: someone picks a
4
- resource, a measure and a grouping, sees their own data, saves, and reopens the
5
- page to find it as they left it. No route written, no endpoint typed.
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.
6
6
 
7
- This page follows what the engine does underneath, so a consumer building that
8
- experience knows which call belongs where.
7
+ ![The visual builder with a chart selected and its settings open](./images/editor-selected.png)
9
8
 
10
- ## What the block asks for
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
13
 
12
- A block declares the shape of answer it can read, next to its factory:
13
-
14
- ```typescript
15
- fetchUrl: ui(z.string().optional(), {
16
- label: "Data source",
17
- group: "data",
18
- widget: "dataSource",
19
- responseShape: "card",
20
- periodOption: "periodScope",
21
- });
22
- ```
23
-
24
- An editor reads that from the catalog and offers only calculations whose answer
25
- the block can display — a series cannot be wired into a slot that renders one
26
- number — and knows which of the block's own options carries the period, so
27
- binding a source writes both in one patch.
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.
28
16
 
29
17
  ## Show the data before writing anything
30
18
 
@@ -50,13 +38,19 @@ const preview = await PreviewQuery({
50
38
  response: "card",
51
39
  },
52
40
  });
41
+ if (preview.ok) {
42
+ console.log(preview.data.body); // Chart-card data from the current rows
43
+ console.log(preview.data.truncated);
44
+ } else {
45
+ console.error(preview.error);
46
+ }
53
47
  ```
54
48
 
55
- Nothing has been written at this point: no route, no model method, no file. The
56
- numbers on screen are the ones the saved page will show, because the calculation
57
- previewed and the calculation written are the same plan rendered two ways — and
58
- `preview.data.body` is the envelope the route will serve, arranged by the same
59
- helper, so the card's headline figure is previewed rather than guessed at.
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.
60
54
 
61
55
  ## Save the block and its data together
62
56
 
@@ -64,9 +58,12 @@ The query travels in the page draft, so one save writes the block, the model
64
58
  method and the route in a single transaction:
65
59
 
66
60
  ```typescript
67
- import { SetPageBlocks } from "@antelopejs/interface-dms-builder";
61
+ import { GetPageStructure, SetPageBlocks } from "@antelopejs/interface-dms-builder";
68
62
 
69
- await SetPageBlocks("/shop/board", {
63
+ const current = await GetPageStructure("/shop/board");
64
+ if (!current.ok) throw new Error(current.error.code);
65
+
66
+ const saved = await SetPageBlocks("/shop/board", {
70
67
  blocks: [
71
68
  {
72
69
  name: "revenueCard",
@@ -74,7 +71,6 @@ await SetPageBlocks("/shop/board", {
74
71
  config: {
75
72
  title: "Revenue",
76
73
  fetchUrl: "/dms/pages/shop/board/stats/revenue-chart",
77
- periodScope: "page",
78
74
  chart: {
79
75
  $block: { type: "ChartArea", config: { xaxisType: "category" } },
80
76
  },
@@ -92,21 +88,18 @@ await SetPageBlocks("/shop/board", {
92
88
  groupBy: "createdAt",
93
89
  bucket: "month",
94
90
  timezone: "Europe/Brussels",
95
- where: [
96
- { field: "status", op: "eq", value: "paid" },
97
- { field: "createdAt", op: "ge", value: { $param: { name: "from" } } },
98
- { field: "createdAt", op: "le", value: { $param: { name: "to" } } },
99
- ],
91
+ where: [{ field: "status", op: "eq", value: "paid" }],
100
92
  },
101
93
  response: "card",
102
- compare: true,
103
94
  },
104
95
  ],
105
- });
96
+ }, { expectedVersion: current.data.version });
97
+ if (!saved.ok) throw new Error(saved.error.code);
106
98
  ```
107
99
 
108
- Either all of it lands or none of it does. A draft that fails on its query leaves
109
- the blocks as they were.
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.
110
103
 
111
104
  Two details the engine will hold you to. The block and the query cannot share a
112
105
  name: both become members of the page class, so a query named after the block it
@@ -115,7 +108,18 @@ what it is and the query for what it answers. And a `ChartCard` requires the
115
108
  chart it wraps, so a config without `chart` fails the save on `typecheck_failed`
116
109
  rather than on a validation message.
117
110
 
118
- ## What ends up in the project
111
+ ## Reference: add a period and comparison
112
+
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).
119
+
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.
119
123
 
120
124
  A method on the resource's model:
121
125
 
@@ -171,11 +175,15 @@ the measure's own word, and a reader who never asked for one would be shown "sum
171
175
  produced them, so the editor reopens filled in rather than empty:
172
176
 
173
177
  ```typescript
178
+ import { GetPageStructure } from "@antelopejs/interface-dms-builder";
179
+
174
180
  const structure = await GetPageStructure("/shop/board");
175
- const source = structure.data.queries.find((q) => q.name === "revenueChart");
176
- // source.template === "series"
177
- // source.response === "card"
178
- // source.params === the parameters above, with the ordering made explicit
181
+ if (structure.ok) {
182
+ const source = structure.data.queries.find((q) => q.name === "revenueChart");
183
+ console.log(source?.template); // "series"
184
+ console.log(source?.response); // "card"
185
+ console.log(source?.params); // Saved parameters, including explicit ordering
186
+ }
179
187
  ```
180
188
 
181
189
  That read-back is what makes the journey repeatable rather than one-way. It is
@@ -187,9 +195,28 @@ rewrite it.
187
195
 
188
196
  | Guarantee | Where it comes from |
189
197
  | --- | --- |
190
- | The preview matches the saved page | One `QueryPlan`, rendered by the emitter and run by the executor |
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 |
191
199
  | The block and its data save together | `PageDraft.queries`, written in the page's transaction |
192
200
  | Reopening shows what was configured | The chain reads back into its parameters |
193
201
  | A developer's edit survives | An unreadable chain is opaque, and opaque is never rewritten |
194
202
  | The data is not public | The route carries the page's permission |
195
203
  | A tenant reads its own rows | The model provider is chosen from the resource's schema |
204
+
205
+ ## Reference: what the block asks for
206
+
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:
210
+
211
+ ```typescript
212
+ fetchUrl: ui(z.string().optional(), {
213
+ label: "Data source",
214
+ group: "data",
215
+ widget: "dataSource",
216
+ responseShape: "card",
217
+ periodOption: "periodScope",
218
+ });
219
+ ```
220
+
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).
@@ -0,0 +1,150 @@
1
+ # External Databases
2
+
3
+ ## Overview
4
+
5
+ A DMS project can show pages over a database it does not own: a client's PostgreSQL, read and written live through a postgres module in external mode. The builder takes such a database in four moves, each an operation:
6
+
7
+ 1. **Wire it** into `antelope.config.ts` with `ConfigureProject`: the provider module, a local module for its tables, and the builder's catalog connection.
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
+
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.
13
+
14
+ ## Wire the Project
15
+
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.
17
+
18
+ | Step | Writes |
19
+ | --- | --- |
20
+ | `AddModule` | a module entry, refusing an id that exists |
21
+ | `ConfigureModule` | merges config, `importOverrides` (pins and connection ids), `exportPriority` and env variable names into an entry |
22
+ | `CreateLocalModule` | scaffolds a local module and adds its entry |
23
+
24
+ `env` takes variable names, never values: each is added to `envOverrides`, and to `.env.example` when the project has one.
25
+
26
+ ```typescript
27
+ import { ConfigureProject } from "@antelopejs/interface-dms-builder";
28
+
29
+ const catalog = "@antelopejs/interface-database-catalog";
30
+
31
+ const result = await ConfigureProject([
32
+ {
33
+ op: "AddModule",
34
+ id: "postgres-erp",
35
+ entry: {
36
+ source: { type: "package", package: "@antelopejs/postgres", version: "^0.2.0" },
37
+ config: { mode: "external", statementTimeoutMs: 30000 },
38
+ env: { url: "ERP_DATABASE_URL" },
39
+ },
40
+ },
41
+ {
42
+ op: "CreateLocalModule",
43
+ id: "erp-tables",
44
+ path: "modules/erp-tables",
45
+ entry: {
46
+ importOverrides: [{ interface: "@antelopejs/interface-database", source: "postgres-erp" }],
47
+ },
48
+ },
49
+ {
50
+ op: "ConfigureModule",
51
+ id: "mongodb",
52
+ entry: { exportPriority: { "@antelopejs/interface-database": 1 } },
53
+ },
54
+ {
55
+ op: "ConfigureModule",
56
+ id: "dms-builder",
57
+ entry: { importOverrides: [{ interface: catalog, source: "postgres-erp", id: "erp" }] },
58
+ },
59
+ ]);
60
+ ```
61
+
62
+ The connection exists once the project restarts with the variable set.
63
+
64
+ ## Analyse a Database
65
+
66
+ `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`.
67
+
68
+ ```typescript
69
+ import { AnalyseDatabase, GetAnalysis } from "@antelopejs/interface-dms-builder";
70
+
71
+ const started = await AnalyseDatabase({ connection: "erp", module: "erp-tables" });
72
+ if (started.ok) {
73
+ const state = await GetAnalysis(started.data.id);
74
+ // state.data.status: "running" | "done" | "failed"
75
+ }
76
+ ```
77
+
78
+ - One job runs per connection. Starting another while one runs answers the running one.
79
+ - A finished job is kept until the next one on its connection, or a restart.
80
+ - Relations the module has mapped, or recorded as excluded, are not proposed again.
81
+ - Above 200 relations to profile, the proposal holds a single `schemas` question instead, and the analysis runs again with `schemas`.
82
+
83
+ ## Read a Proposal
84
+
85
+ A `MappingProposal` is a `MapResources` input in which every choice says where it came from:
86
+
87
+ ```typescript
88
+ interface Proposed<T> {
89
+ value: T;
90
+ rule: string; // "R6", "L1", "F5"…
91
+ kind: "fact" | "default";
92
+ }
93
+ ```
94
+
95
+ A **fact** follows from the catalog with no judgement. A **default** is a judgement to check.
96
+
97
+ | Rule | When | Then |
98
+ | --- | --- | --- |
99
+ | R1 | the catalog says unmappable | excluded, with its reason |
100
+ | R2 | no key | stats only: table class and model, no page |
101
+ | R3 | no rows | excluded |
102
+ | R4 | a framework's or a migration tool's table | excluded |
103
+ | R5 | keyed by its two foreign keys | stats only |
104
+ | R6 | keyed, with rows and a label | TableView |
105
+ | R7 | keyed, no label, referenced | TableView labelled by its key, and a `label` question |
106
+ | R8 | keyed, no label, not referenced, with a time axis | stats only |
107
+ | R9 | anything else | a `purpose` question, nothing proposed |
108
+ | L1, L2 | the label by name, then by order; titles from short comments, else from names | |
109
+ | F1–F5 | columns no type reads and secrets left out; the first declaration; declared foreign keys and references found whole as relation columns | |
110
+ | W1 | every TableView | read-only |
111
+
112
+ Each field also carries the declarations its column `accepts`, and its profile's aggregates: traits, nulls, distinct values, range and categories. Never a row.
113
+
114
+ What the rules could not decide is in `open`, each item with its options and what the proposal holds meanwhile. A `purpose` item carries the `resource` it would map, should the answer be to map it.
115
+
116
+ ## Map Relations
117
+
118
+ `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.
119
+
120
+ ```typescript
121
+ import { MapResources } from "@antelopejs/interface-dms-builder";
122
+
123
+ const result = await MapResources({
124
+ module: "erp-tables",
125
+ connection: "erp",
126
+ resources: [
127
+ {
128
+ relation: "erp.customers",
129
+ label: "name",
130
+ title: "Customers",
131
+ tableView: true,
132
+ writable: false,
133
+ fields: [{ column: "name", searchable: true }, { column: "segment", filterable: true }],
134
+ },
135
+ ],
136
+ excluded: [{ relation: "erp.audit_log", reason: "a log" }],
137
+ });
138
+ ```
139
+
140
+ - **Names are the database's own.** `_id` is declared only where the relation has a key, and nothing creates or alters a table.
141
+ - **Relation columns** point at a relation of the batch, or one the module mapped before, by its single-column key.
142
+ - **Mapping again** a relation already mapped rewrites it from the new choices. Its page is created, kept or removed as `tableView` says.
143
+ - **`excluded`** is recorded in the module's `package.json`, so the next analysis does not propose those relations. Mapping one later removes it from the record.
144
+ - **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`.
145
+
146
+ ## Next Steps
147
+
148
+ - [Resources](./5.resources.md): the resources a mapping writes are ordinary ones, read the same way.
149
+ - [Queries](./6.queries.md): stats over a mapped relation name it by its ref.
150
+ - [Results and Errors](./8.results-and-errors.md): branch on what every operation answers.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- Every builder operation names the thing it edits with a string ref. There are three of them — a `PageRef` for a page, a `BlockPath` for a block inside a page, and a `CategoryRef` for a category — and each is a plain string you can store, log and pass back into the next call.
5
+ Every builder operation identifies its target with a string ref. This page covers `PageRef` for a page, `BlockPath` for a block inside it, and `CategoryRef` for a menu category. [Resources](./5.resources.md) and [Queries](./6.queries.md) define their own refs.
6
6
 
7
7
  Alongside the refs, this page describes the read shapes the builder hands back: the page metadata it can read and patch, the block tree it resolves from source, and the lightweight summaries the listing operations return. The operations that produce and consume these shapes live in [Page Operations](./4.page-operations.md).
8
8
 
@@ -102,7 +102,7 @@ The DMS also accepts an `Action` there. That is a live object rather than data,
102
102
 
103
103
  ## Patch a Page's Metadata
104
104
 
105
- `EditablePageMeta` is the subset of `PageMeta` that `ConfigurePage` accepts as a patch: `displayName`, `icon`, `category`, `order`, `description`, `hidden` and `permission`. `ref`, `id` and `filepath` are not patchable — they follow from where the page lives.
105
+ `EditablePageMeta` is the subset of `PageMeta` that `ConfigurePage` accepts as a patch: `displayName`, `icon`, `category`, `order`, `description`, `hidden`, `permission` and `live`. `ref`, `id` and `filepath` are not patchable — they follow from where the page lives.
106
106
 
107
107
  Patching `category` moves the page, which changes its `ref`: the result carries the new one, and a `route_changed` warning says what the old route was. See [Page Operations](./4.page-operations.md).
108
108
 
@@ -127,6 +127,7 @@ const patch: Partial<EditablePageMeta> = {
127
127
  | `editable` | `boolean` | `false` marks an opaque, non-canonical block. |
128
128
  | `opaqueReason` | `string` | Why the block is opaque. Set when `editable === false`. |
129
129
  | `slot` | `string` | The slot the block sits in, when its parent declares slots. |
130
+ | `meta` | `Record<string, unknown>` | Child metadata beyond `slot`, such as `{ colSpan: 2 }`. Keep it when constructing a draft from the tree. |
130
131
  | `config` | `Record<string, unknown>` | The block's options object. Present only when the block is editable. |
131
132
  | `controller` | `ResourceRef` | For a controller-leading block such as `TableView`, the resource its leading argument resolves to, when it is a known generated resource. |
132
133
  | `children` | `BlockNode[]` | Container blocks only, in `.child()` order. |
package/docs/6.queries.md CHANGED
@@ -1,8 +1,29 @@
1
1
  # Queries
2
2
 
3
- ## Overview
3
+ ## Count rows without writing a route
4
4
 
5
- A query is derived data: a count, a sum, an average taken over a resource's rows and served from a page. `AddQuery` compiles a query template into a method on the resource's model and a route on the page's controller, so a block on that page can fetch its data without you writing either half.
5
+ A query gives a page data to display: a count, a total or a series of points. With an existing `/books` page and a `book` resource, this example creates a route that counts the books:
6
+
7
+ ```typescript
8
+ import { AddQuery } from "@antelopejs/interface-dms-builder";
9
+
10
+ const result = await AddQuery("/books", {
11
+ name: "bookCount",
12
+ resource: "book",
13
+ template: "count",
14
+ });
15
+ if (result.ok) {
16
+ console.log(result.data.route); // "/dms/pages/books/stats/book-count"
17
+ } else {
18
+ console.error(result.error);
19
+ }
20
+ ```
21
+
22
+ After the backend reloads, a signed-in user with access to `/books` can request that URL through the frontend. It returns `{ "value": N }`, where `N` is the number of rows. The operation writes a model method and a page route; it does not add a visible block. Use the returned `route` as the block's `fetchUrl`.
23
+
24
+ Choose an unused query name. For an editor that must preview before saving, use [PreviewQuery and page drafts](./9.drafts-and-preview.md) instead of calling `AddQuery` on each change.
25
+
26
+ ## Query reference
6
27
 
7
28
  The route is guarded with the page's own permission, as a parameter decorator on that route alone: whoever may open the page may read what it derives, and no other route on the controller is affected.
8
29
 
@@ -198,7 +219,7 @@ await AddQuery("/dashboard", {
198
219
  where: [{ field: "status", op: "eq", value: "paid" }],
199
220
  },
200
221
  });
201
- // GET /dashboard/stats/monthly-revenue
222
+ // GET /dms/pages/dashboard/stats/monthly-revenue
202
223
  // { series: [{ x: 202601, y: 1250 }, { x: 202602, y: 980 }] }
203
224
  ```
204
225
 
@@ -217,7 +238,7 @@ await AddQuery("/dashboard", {
217
238
  Three rules worth planning for:
218
239
 
219
240
  - **A series is always ordered.** Databases return groups in no particular order — MongoDB scrambles them — so an ordering is emitted whether or not you ask for one. Omitting `orderBy` gives a timeline: the group, ascending.
220
- - **Omitting `timezone` buckets in UTC**, which moves every row near a period boundary into its neighbour. Pass the zone the reader thinks in.
241
+ - **Omitting `timezone` buckets in UTC.** A row near a period boundary may belong to a different bucket in the reader's local time. Pass the zone the reader uses for reporting.
221
242
  - **A top N is an ordering plus a limit**, not a separate template: `orderBy: "measure"`, `direction: "desc"`, `limit: 5`.
222
243
 
223
244
  There is no week bucket. Neither database adapter exposes an ISO week, and deriving one from the day of the year puts the first days of January in the previous year's week — a wrong week being worse than a missing one.
@@ -264,7 +285,7 @@ await AddQuery("/dashboard", {
264
285
  response: "card",
265
286
  compare: true,
266
287
  });
267
- // GET /dashboard/stats/revenue?from=…&to=…&compareFrom=…&compareTo=…
288
+ // GET /dms/pages/dashboard/stats/revenue?from=…&to=…&compareFrom=…&compareTo=…
268
289
  // { value, series, previousValue, delta, comparisonSeries }
269
290
  ```
270
291
 
@@ -320,11 +341,12 @@ A query that no longer parses still reports its `endpoint`, `resource` and `mode
320
341
 
321
342
  ```typescript
322
343
  import { GetPageStructure } from "@antelopejs/interface-dms-builder";
344
+ import { pageRoutePath } from "@antelopejs/interface-dms/page";
323
345
 
324
346
  const result = await GetPageStructure("/books");
325
347
  if (result.ok) {
326
348
  const editable = result.data.queries.filter((query) => !query.opaque);
327
- const urls = result.data.queries.map((query) => `/books${query.endpoint}`);
349
+ const urls = result.data.queries.map((query) => pageRoutePath("/books", query.endpoint));
328
350
  }
329
351
  ```
330
352
 
package/docs/7.values.md CHANGED
@@ -4,10 +4,11 @@
4
4
 
5
5
  Configuration values are plain JSON by default. `AddBlockInput.config`, a `ConfigureBlock` patch, a page's `permission` — all of it is written to source exactly as you pass it, as a literal.
6
6
 
7
- Some values are not literals. A Form field's `type` is a `new DefaultDataTypes.StringType(...)` instance; a `TableView`'s controller is a class identifier with an import behind it. Three sentinel object shapes cover those cases: `DataTypeValue` for a DataType instance, `RefValue` for a builder-generated class, and `ExprValue` for a raw expression the builder cannot model. Each is recognized wherever a config value is read, at any depth — inside an array, inside a nested object.
7
+ Some options need TypeScript values rather than JSON literals. A form field needs a DataType instance, a relation needs a controller class, and a chart card needs a nested chart. The structured encodings below let the builder write those values and their imports. Use a raw `$expr` only as an escape hatch; drafts and query parameters reject it.
8
8
 
9
9
  ```typescript
10
10
  import type {
11
+ BlockValue,
11
12
  DataTypeValue,
12
13
  ExprValue,
13
14
  RefValue,
@@ -21,6 +22,7 @@ import type {
21
22
  | A string, number, boolean, array or object | The value itself, as JSON. |
22
23
  | A value the catalog marks `x-dataType` | `DataTypeValue` |
23
24
  | A reference to a builder-generated resource class | `RefValue` |
25
+ | A nested component in an option marked `x-component` | `BlockValue` |
24
26
  | Anything else that must reach the source as code | `ExprValue` |
25
27
 
26
28
  The catalog is what tells you which option needs which: an option marked `x-dataType` in a block's `ConfigSchema` takes a `DataTypeValue`, and nothing else. See [Catalog](./3.catalog.md).
@@ -103,15 +105,11 @@ await ConfigureBlock("/revenue#total", {
103
105
 
104
106
  Two constraints come with it:
105
107
 
106
- - The referenced symbol must already be importable in the target file. Unlike the other two sentinels, `$expr` adds no import.
108
+ - The referenced symbol must already be in scope in the target file. `$expr` adds no import.
107
109
  - `$expr` is refused inside a query's parameters — a query's values are data, and a raw expression would put uncheckable code inside a model body. See [Queries](./6.queries.md).
108
110
 
109
111
  Consumers that run the builder in a restricted mode may refuse `$expr` outright, since arbitrary code defeats the structured guarantees the rest of the surface provides. `RefValue` is allowed in that mode precisely because it is validated; prefer it whenever the value you need is a generated class.
110
112
 
111
- ## Next Steps
112
-
113
- - [Results and Errors](./8.results-and-errors.md) - Branch on an operation's result and read its diagnostics
114
-
115
113
  ## Nest a Block in a Config Value
116
114
 
117
115
  A few options hold a component rather than data — a `ChartCard`'s `chart`, marked `x-component` in the catalog. `BlockValue` is how you write one: the builder emits the nested factory call and adds its import.
@@ -128,3 +126,7 @@ const chart: BlockValue = {
128
126
  `type` is a catalog key, like `AddBlockInput.type`; an unknown one is rejected with `invalid_config`. Read-back is symmetrical: a nested factory call whose callee is a known block type comes back as a `$block` sentinel rather than as `$expr`, so the value stays editable.
129
127
 
130
128
  Most nesting does *not* go through config — it goes through `.child`, which you express with `AddBlockInput.parent` or with `BlockDraft.children`. Reach for `$block` only where the catalog marks an option `x-component`.
129
+
130
+ ## Next Steps
131
+
132
+ - [Results and Errors](./8.results-and-errors.md) - Branch on an operation's result and read its diagnostics
@@ -1,18 +1,29 @@
1
1
  # Drafts and Preview
2
2
 
3
- ## Overview
3
+ Use `PreviewLayout` to inspect a page draft without writing files, then `SetPageBlocks` to save its complete block tree in one transaction. A successful preview does not mean the draft compiles: the write performs that check.
4
4
 
5
- The per-block operations — `AddBlock`, `ConfigureBlock`, `MoveBlock`, `RemoveBlock` — each rewrite the page file, typecheck the project and write to disk. That is the right shape for an agent applying one edit at a time, and the wrong shape for an editor: a visual builder holds a whole page in memory, and replaying twenty edits at save time would mean twenty typechecks and twenty chances to stop half way.
5
+ > **Warning:** `SetPageBlocks` replaces the whole block tree. Blocks you omit are removed. For one small change to an existing page, use `ConfigureBlock` instead.
6
6
 
7
- `SetPageBlocks` is the declarative counterpart. You hand it the page's whole block tree and it writes that tree: one transaction, one typecheck, one `changes` list. `PreviewLayout` is its read-only twin — it serializes a draft through the real `dms-base` factories so you can render it without writing anything.
7
+ ## Preview one card
8
+
9
+ With an existing `/dashboard` page, this read-only example returns a serialized KPI card. An editor can render `preview.data.components.total`; no source file changes.
8
10
 
9
11
  ```typescript
10
- import {
11
- PreviewLayout,
12
- SetPageBlocks,
13
- } from "@antelopejs/interface-dms-builder";
12
+ import { PreviewLayout } from "@antelopejs/interface-dms-builder";
13
+
14
+ const preview = await PreviewLayout("/dashboard", {
15
+ blocks: [{ name: "total", type: "KpiCard", config: { title: "Total revenue" } }],
16
+ });
17
+ if (preview.ok) {
18
+ console.log(preview.data.components.total);
19
+ console.log(preview.data.degraded);
20
+ } else {
21
+ console.error(preview.error);
22
+ }
14
23
  ```
15
24
 
25
+ The per-block operations each save immediately. A visual editor usually holds edits locally and calls `SetPageBlocks` once, so the block tree and its queries either pass one typecheck together or remain unchanged.
26
+
16
27
  ## Describe a Draft
17
28
 
18
29
  `PageDraft` is a page as you want it written.
@@ -40,7 +51,11 @@ await SetPageBlocks("/shop/board", {
40
51
  {
41
52
  name: "revenueChart",
42
53
  type: "ChartCard",
43
- config: { title: "Revenue", fetchUrl: "/dms/pages/shop/board/stats/revenue" },
54
+ config: {
55
+ title: "Revenue",
56
+ fetchUrl: "/dms/pages/shop/board/stats/revenue",
57
+ chart: { $block: { type: "ChartArea", config: { xaxisType: "category" } } },
58
+ },
44
59
  },
45
60
  ],
46
61
  queries: [
@@ -72,7 +87,7 @@ await SetPageBlocks("/shop/board", {
72
87
 
73
88
  A whole-tree write has to answer for blocks the builder can read but not rewrite: a `CustomComponent`, a block behind a computed expression, anything `GetPageStructure` reported with `editable: false`. `preserve` is that answer — the block is matched by name among its siblings and its source text is written back untouched.
74
89
 
75
- Round-tripping a page therefore means keeping every block you did not edit, marking the opaque ones:
90
+ To change only the page title, preserve every top-level block, including its children. This avoids reconstructing editable blocks from only part of their configuration:
76
91
 
77
92
  ```typescript
78
93
  import {
@@ -82,22 +97,32 @@ import {
82
97
 
83
98
  const structure = await GetPageStructure("/dashboard");
84
99
  if (structure.ok) {
85
- const blocks = structure.data.blocks.map((block) =>
86
- block.editable
87
- ? { name: block.name, type: block.type as string, config: block.config }
88
- : { name: block.name, preserve: true },
89
- );
90
- await SetPageBlocks("/dashboard", { blocks }, {
100
+ const blocks = structure.data.blocks.map((block) => ({
101
+ name: block.name,
102
+ preserve: true,
103
+ }));
104
+ const result = await SetPageBlocks("/dashboard", {
105
+ page: { displayName: "Revenue" },
106
+ blocks,
107
+ }, {
91
108
  expectedVersion: structure.data.version,
92
109
  });
110
+ if (!result.ok) console.error(result.error);
93
111
  }
94
112
  ```
95
113
 
114
+ When you do edit a block, supply its full draft, including its `children`, `slot`, `meta` and `controller` where applicable. Do not reduce a block tree to names and configuration objects: omitted children are deleted.
115
+
96
116
  A `preserve` block whose name matches nothing on disk is rejected with `invalid_config`.
97
117
 
98
118
  ## Write a Whole Page
99
119
 
100
120
  ```typescript
121
+ import { GetPageStructure, SetPageBlocks } from "@antelopejs/interface-dms-builder";
122
+
123
+ const structure = await GetPageStructure("/dashboard");
124
+ if (!structure.ok) throw new Error(structure.error.code);
125
+
101
126
  const result = await SetPageBlocks(
102
127
  "/dashboard",
103
128
  {
@@ -124,10 +149,13 @@ const result = await SetPageBlocks(
124
149
  },
125
150
  ],
126
151
  },
127
- { expectedVersion: "8826e812c4f58d51" },
152
+ { expectedVersion: structure.data.version },
128
153
  );
154
+ if (!result.ok) throw new Error(result.error.code);
129
155
  ```
130
156
 
157
+ Read the version immediately before preparing the draft. If the write returns `stale`, read the page again and review the other editor's changes before retrying.
158
+
131
159
  On success the result carries the staged `changes` and the page's new `version`, ready to be passed as the next `expectedVersion`.
132
160
 
133
161
  Blocks absent from the draft are removed. Static members that are not blocks are left where they are, and named imports nothing references any more are dropped, so a page shrinks cleanly rather than accumulating dead imports.
@@ -179,7 +207,13 @@ const result = await PreviewQuery({
179
207
  name: "revenue",
180
208
  resource: "order",
181
209
  template: "series",
182
- params: { op: "sum", field: "amount", groupBy: "createdAt", bucket: "month" },
210
+ params: {
211
+ op: "sum", field: "amount", groupBy: "createdAt", bucket: "month",
212
+ where: [
213
+ { field: "createdAt", op: "ge", value: { $param: { name: "from" } } },
214
+ { field: "createdAt", op: "lt", value: { $param: { name: "to" } } },
215
+ ],
216
+ },
183
217
  },
184
218
  args: { from: "2026-01-01T00:00:00.000Z", to: "2026-09-01T00:00:00.000Z" },
185
219
  });
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/interface-dms-builder",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "AntelopeJS interface for the DMS builder: create and edit pages, blocks, resources and queries in a DMS project from another module.",
5
5
  "keywords": [
6
6
  "antelopejs",