@vantage-ui/client 0.0.0-stage → 0.4.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.
- package/CHANGELOG.md +46 -0
- package/bin/vantage-client.js +5 -0
- package/dist/client-connection.d.ts +41 -0
- package/dist/client-connection.js +260 -0
- package/dist/client-core.d.ts +70 -0
- package/dist/client-core.js +140 -0
- package/dist/client-loading.d.ts +13 -0
- package/dist/client-loading.js +229 -0
- package/dist/client-runs.d.ts +18 -0
- package/dist/client-runs.js +88 -0
- package/dist/client-writes.d.ts +14 -0
- package/dist/client-writes.js +114 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.js +61 -0
- package/dist/codegen/cli.d.ts +8 -0
- package/dist/codegen/cli.js +54 -0
- package/dist/codegen/emit.d.ts +3 -0
- package/dist/codegen/emit.js +190 -0
- package/dist/codegen/fetch.d.ts +4 -0
- package/dist/codegen/fetch.js +41 -0
- package/dist/codegen/meta.d.ts +13 -0
- package/dist/codegen/meta.js +59 -0
- package/dist/codegen/runs.d.ts +20 -0
- package/dist/codegen/runs.js +58 -0
- package/dist/codegen/schema.d.ts +97 -0
- package/dist/codegen/schema.js +23 -0
- package/dist/collection-cell.d.ts +69 -0
- package/dist/collection-cell.js +153 -0
- package/dist/collection.d.ts +32 -0
- package/dist/collection.js +122 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +14 -0
- package/dist/mock-query.d.ts +9 -0
- package/dist/mock-query.js +75 -0
- package/dist/mock-runs.d.ts +61 -0
- package/dist/mock-runs.js +165 -0
- package/dist/mock-store.d.ts +78 -0
- package/dist/mock-store.js +158 -0
- package/dist/mock-transport.d.ts +16 -0
- package/dist/mock-transport.js +38 -0
- package/dist/model.d.ts +5 -0
- package/dist/model.js +169 -0
- package/dist/persist.d.ts +47 -0
- package/dist/persist.js +108 -0
- package/dist/protocol.d.ts +236 -0
- package/dist/protocol.js +5 -0
- package/dist/react.d.ts +59 -0
- package/dist/react.js +169 -0
- package/dist/reserved.d.ts +6 -0
- package/dist/reserved.js +13 -0
- package/dist/run/frames.d.ts +6 -0
- package/dist/run/frames.js +24 -0
- package/dist/run/hooks.d.ts +18 -0
- package/dist/run/hooks.js +87 -0
- package/dist/run/messages.d.ts +20 -0
- package/dist/run/messages.js +42 -0
- package/dist/run/outcome.d.ts +12 -0
- package/dist/run/outcome.js +27 -0
- package/dist/run/run.d.ts +82 -0
- package/dist/run/run.js +246 -0
- package/dist/run/storage.d.ts +3 -0
- package/dist/run/storage.js +20 -0
- package/dist/run/types.d.ts +92 -0
- package/dist/run/types.js +3 -0
- package/dist/run/variables.d.ts +26 -0
- package/dist/run/variables.js +70 -0
- package/dist/schema-meta.d.ts +91 -0
- package/dist/schema-meta.js +3 -0
- package/dist/signal.d.ts +45 -0
- package/dist/signal.js +163 -0
- package/dist/store.d.ts +45 -0
- package/dist/store.js +144 -0
- package/dist/table.d.ts +62 -0
- package/dist/table.js +82 -0
- package/dist/testing.d.ts +61 -0
- package/dist/testing.js +195 -0
- package/dist/transport.d.ts +28 -0
- package/dist/transport.js +32 -0
- package/dist/types.d.ts +159 -0
- package/dist/types.js +2 -0
- package/dist/util.d.ts +18 -0
- package/dist/util.js +46 -0
- package/package.json +54 -4
- package/skills/vantage-client/SKILL.md +390 -0
- package/skills/vantage-client/references/protocol.md +67 -0
- package/skills/vantage-client/references/react.md +242 -0
- package/skills/vantage-client/references/testing.md +173 -0
- package/README.md +0 -3
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// `generate`: TypeScript for one API, from its `/v1/schema`.
|
|
2
|
+
//
|
|
3
|
+
// Per table it writes:
|
|
4
|
+
// - `XFields`: the record as data (what a draft edits);
|
|
5
|
+
// - `XInput`: what `create()` takes (mandatory columns required);
|
|
6
|
+
// - `X`: the model the app reads. Reference columns become
|
|
7
|
+
// `LazyReference`s, `has_many` references `LazyCollection`s, and
|
|
8
|
+
// row actions become methods and `$run` factories;
|
|
9
|
+
// - `X<Action>Run`: a row action's run types (args, state, inputs, messages).
|
|
10
|
+
//
|
|
11
|
+
// plus `<Action>Run` per project action, `Schema` (types) and `schema`
|
|
12
|
+
// (runtime metadata the client builds models from). Everything comes out in the schema's own order (the API's
|
|
13
|
+
// and the YAML's), so the same schema always gives the same file.
|
|
14
|
+
import { pascal } from "../util.js";
|
|
15
|
+
import { fieldMeta, metaTable, required, runMeta } from "./meta.js";
|
|
16
|
+
import { RunNames, runInterface } from "./runs.js";
|
|
17
|
+
import { refuseReservedNames } from "./schema.js";
|
|
18
|
+
const stringify = (value) => JSON.stringify(value ?? null);
|
|
19
|
+
/** Schema text inside a generated comment: it can neither close the comment nor break the line. */
|
|
20
|
+
const doc = (text) => String(text ?? "").replace(/\*\//g, "*\\/").replace(/[^\S ]+/g, " ");
|
|
21
|
+
/** Throws a `GenerateError` when an entity member takes a name the model reserves. */
|
|
22
|
+
export function emit(schema, source) {
|
|
23
|
+
refuseReservedNames(schema);
|
|
24
|
+
const hash = typeof schema.hash === "string" ? schema.hash : "";
|
|
25
|
+
let out = "";
|
|
26
|
+
const line = (text = "") => (out += text + "\n");
|
|
27
|
+
line(`// Generated by @vantage-ui/client generate from ${doc(source)} (schema ${doc(hash)}). Never edit by hand.`);
|
|
28
|
+
line("// When the API changes, the Vantage web dev container regenerates it from schema.json;");
|
|
29
|
+
line("// elsewhere, run `npx vantage-client generate --from <api>/v1/schema` again.\n");
|
|
30
|
+
// Only the types the file uses, so it passes `noUnusedLocals`.
|
|
31
|
+
const tables = schema.tables ?? [];
|
|
32
|
+
const rowActionsAnywhere = tables.some((t) => (t.actions ?? []).some((a) => a.per_row === true));
|
|
33
|
+
const used = [
|
|
34
|
+
["ActionResult", rowActionsAnywhere],
|
|
35
|
+
["LazyCollection", tables.some((t) => (t.has_many ?? []).length > 0)],
|
|
36
|
+
["LazyReference", tables.some((t) => (t.references ?? []).length > 0)],
|
|
37
|
+
["Model", tables.length > 0],
|
|
38
|
+
["RunFactory", rowActionsAnywhere],
|
|
39
|
+
];
|
|
40
|
+
const types = used.filter(([, uses]) => uses).map(([name]) => name);
|
|
41
|
+
if (types.length > 0)
|
|
42
|
+
line(`import type { ${types.join(", ")} } from "@vantage-ui/client";\n`);
|
|
43
|
+
const models = [];
|
|
44
|
+
const fields = [];
|
|
45
|
+
const inputs = [];
|
|
46
|
+
const tableActions = [];
|
|
47
|
+
const metaTables = {};
|
|
48
|
+
const runNames = new RunNames([
|
|
49
|
+
...tables.flatMap((t) => ["", "Fields", "Input", "Action"].map((suffix) => `${pascal(t.name ?? "")}${suffix}`)),
|
|
50
|
+
...["Models", "Fields", "Inputs", "TableActions", "Schema", "ProjectActions"],
|
|
51
|
+
]);
|
|
52
|
+
for (const raw of tables) {
|
|
53
|
+
const table = { ...raw, columns: raw.columns ?? [], references: raw.references ?? [], has_many: raw.has_many ?? [], actions: raw.actions ?? [] };
|
|
54
|
+
const name = table.name ?? "";
|
|
55
|
+
const ty = pascal(name);
|
|
56
|
+
const readOnly = table.read_only === true;
|
|
57
|
+
const rowActions = table.actions.filter((a) => a.per_row === true);
|
|
58
|
+
const toolbar = table.actions.filter((a) => a.per_row !== true);
|
|
59
|
+
// --- fields (data)
|
|
60
|
+
line(`/** \`${doc(typeof table.key === "string" ? table.key : name)}\` as data${readOnly ? " (read-only)" : ""} */`);
|
|
61
|
+
line(`export interface ${ty}Fields {`);
|
|
62
|
+
line(" readonly id: string;");
|
|
63
|
+
for (const c of table.columns.filter((c) => c.name !== "id" && c.hidden !== true)) {
|
|
64
|
+
const ro = readOnly || c.readonly === true;
|
|
65
|
+
if (typeof c.label === "string")
|
|
66
|
+
line(` /** ${doc(c.label)} */`);
|
|
67
|
+
line(` ${ro ? "readonly " : ""}${key(c.name ?? "")}: ${columnType(c)} | null;`);
|
|
68
|
+
}
|
|
69
|
+
line("}\n");
|
|
70
|
+
// --- create input
|
|
71
|
+
if (!readOnly) {
|
|
72
|
+
line(`export interface ${ty}Input {`);
|
|
73
|
+
line(` /** Optional client-made id (\`${doc(name)}:<uuid>\` or bare); the server makes one otherwise. */`);
|
|
74
|
+
line(" id?: string;");
|
|
75
|
+
for (const c of table.columns.filter((c) => c.name !== "id" && c.readonly !== true)) {
|
|
76
|
+
const req = c.mandatory === true;
|
|
77
|
+
line(` ${key(c.name ?? "")}${req ? "" : "?"}: ${columnType(c)}${req ? "" : " | null"};`);
|
|
78
|
+
}
|
|
79
|
+
line("}\n");
|
|
80
|
+
inputs.push(` ${key(name)}: ${ty}Input;`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
inputs.push(` ${key(name)}: never;`);
|
|
84
|
+
}
|
|
85
|
+
// --- model
|
|
86
|
+
const relColumns = table.references.map((r) => r.column).filter((c) => typeof c === "string" && table.references.some((r) => r.name === c));
|
|
87
|
+
const omit = relColumns.length === 0 ? `${ty}Fields` : `Omit<${ty}Fields, ${relColumns.map(stringify).join(" | ")}>`;
|
|
88
|
+
line(`export interface ${ty} extends Model<${stringify(name)}, ${ty}Fields>, Readonly<${omit}> {`);
|
|
89
|
+
for (const r of table.references) {
|
|
90
|
+
line(` /** \`${doc(r.column)}\` → ${doc(r.table)} */`);
|
|
91
|
+
line(` readonly ${key(r.name ?? "")}: LazyReference<${pascal(r.table ?? "")}>;`);
|
|
92
|
+
}
|
|
93
|
+
for (const h of table.has_many)
|
|
94
|
+
line(` readonly ${key(h.name ?? "")}: LazyCollection<${pascal(h.table ?? "")}>;`);
|
|
95
|
+
line(" /** Row actions whose `when:` holds right now (decided by the server). */");
|
|
96
|
+
line(` readonly $actions: readonly ${rowActions.length === 0 ? "never" : `${ty}Action`}[];`);
|
|
97
|
+
for (const a of rowActions) {
|
|
98
|
+
const input = fieldsType(a.fields);
|
|
99
|
+
const arg = input === "{}" ? "" : inputOptional(a.fields) ? `input?: ${input}` : `input: ${input}`;
|
|
100
|
+
line(` /** ${doc(a.label)}: a run, awaited to its end */`);
|
|
101
|
+
line(` ${key(a.name ?? "")}(${arg}): Promise<ActionResult<${ty}>>;`);
|
|
102
|
+
}
|
|
103
|
+
const rowRuns = rowActions.map((a) => runNames.take(`${ty}${pascal(a.name ?? "")}`));
|
|
104
|
+
if (rowActions.length > 0) {
|
|
105
|
+
line(" /** The row actions as runs: `start`, `attach`, live `state` and messages. */");
|
|
106
|
+
line(` readonly $run: { ${rowActions.map((a, i) => `${key(a.name ?? "")}: RunFactory<${rowRuns[i]}>`).join("; ")} };`);
|
|
107
|
+
}
|
|
108
|
+
line("}\n");
|
|
109
|
+
if (rowActions.length > 0)
|
|
110
|
+
line(`export type ${ty}Action = ${rowActions.map((a) => stringify(a.name ?? "")).join(" | ")};\n`);
|
|
111
|
+
rowActions.forEach((a, i) => line(runInterface(rowRuns[i], fieldsType(a.fields), a, key)));
|
|
112
|
+
const entries = toolbar.map((a) => ` /** ${doc(a.label)} */\n ${key(a.name ?? "")}: ${fieldsType(a.fields)};`);
|
|
113
|
+
tableActions.push(` ${key(name)}: {\n${entries.join("\n")}\n };`);
|
|
114
|
+
models.push(` ${key(name)}: ${ty};`);
|
|
115
|
+
fields.push(` ${key(name)}: ${ty}Fields;`);
|
|
116
|
+
metaTables[name] = metaTable(table);
|
|
117
|
+
}
|
|
118
|
+
const projectActions = [];
|
|
119
|
+
const metaActions = {};
|
|
120
|
+
for (const action of schema.actions ?? []) {
|
|
121
|
+
const name = action.name ?? "";
|
|
122
|
+
const title = typeof action.title === "string" ? action.title : typeof action.key === "string" ? action.key : "";
|
|
123
|
+
const kind = typeof action.kind === "string" ? action.kind : "";
|
|
124
|
+
let args;
|
|
125
|
+
if (kind === "http_request") {
|
|
126
|
+
const params = (action.params ?? []).filter((p) => typeof p === "string").map((p) => `${key(p)}: string`);
|
|
127
|
+
args = params.length === 0 ? "{}" : `{ ${params.join("; ")} }`;
|
|
128
|
+
}
|
|
129
|
+
else
|
|
130
|
+
args = fieldsType(action.fields);
|
|
131
|
+
const run = runNames.take(pascal(name));
|
|
132
|
+
line(`/** ${doc(title)}${kind ? ` (${doc(kind)})` : ""} */`);
|
|
133
|
+
line(runInterface(run, args, action, key));
|
|
134
|
+
projectActions.push(` ${key(name)}: ${run};`);
|
|
135
|
+
metaActions[name] = { key: action.key ?? null, kind: action.kind ?? null, fields: fieldMeta(action.fields), ...runMeta(action) };
|
|
136
|
+
}
|
|
137
|
+
line("/** Project actions (`action/*.yaml`) a client can run: `db.run.<name>.start(args)`. */");
|
|
138
|
+
line(projectActions.length ? `export interface ProjectActions {\n${projectActions.join("\n")}\n}\n` : "export interface ProjectActions {}\n");
|
|
139
|
+
line(`export interface Models {\n${models.join("\n")}\n}\n`);
|
|
140
|
+
line(`export interface Fields {\n${fields.join("\n")}\n}\n`);
|
|
141
|
+
line(`export interface Inputs {\n${inputs.join("\n")}\n}\n`);
|
|
142
|
+
line(`export interface TableActions {\n${tableActions.join("\n")}\n}\n`);
|
|
143
|
+
line("export interface Schema {\n models: Models;\n fields: Fields;\n inputs: Inputs;\n tableActions: TableActions;\n projectActions: ProjectActions;\n}\n");
|
|
144
|
+
line("/** Runtime metadata `createVantage({ schema })` builds models from. */");
|
|
145
|
+
line(`export const schema = ${JSON.stringify({ hash, tables: metaTables, actions: metaActions }, null, 2)} as const;`);
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function columnType(column) {
|
|
149
|
+
if (Array.isArray(column.enum) && column.enum.length > 0)
|
|
150
|
+
return column.enum.map(stringify).join(" | ");
|
|
151
|
+
switch (typeof column.type === "string" ? column.type : "string") {
|
|
152
|
+
case "int":
|
|
153
|
+
case "float":
|
|
154
|
+
return "number";
|
|
155
|
+
// Exact: a string on the wire, so pence never become 0.30000000000000004.
|
|
156
|
+
case "decimal":
|
|
157
|
+
return "number | string";
|
|
158
|
+
case "bool":
|
|
159
|
+
return "boolean";
|
|
160
|
+
// date, time, timestamp, datetime, duration: ISO strings on the wire.
|
|
161
|
+
default:
|
|
162
|
+
return "string";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function inputOptional(fields) {
|
|
166
|
+
return (fields ?? []).every((f) => !required(f));
|
|
167
|
+
}
|
|
168
|
+
function fieldsType(list) {
|
|
169
|
+
const fields = list ?? [];
|
|
170
|
+
if (fields.length === 0)
|
|
171
|
+
return "{}";
|
|
172
|
+
const parts = fields.map((f) => {
|
|
173
|
+
let ty;
|
|
174
|
+
if (Array.isArray(f.choices) && f.widget !== "checklist")
|
|
175
|
+
ty = f.choices.map(stringify).join(" | ");
|
|
176
|
+
else if (f.type === "int" || f.type === "float" || f.type === "decimal")
|
|
177
|
+
ty = "number";
|
|
178
|
+
else if (f.type === "bool")
|
|
179
|
+
ty = "boolean";
|
|
180
|
+
else
|
|
181
|
+
ty = "string";
|
|
182
|
+
return `${key(f.name ?? "")}${required(f) ? "" : "?"}: ${ty}`;
|
|
183
|
+
});
|
|
184
|
+
return `{ ${parts.join("; ")} }`;
|
|
185
|
+
}
|
|
186
|
+
/** A property name: bare when it is an identifier, quoted otherwise. */
|
|
187
|
+
function key(name) {
|
|
188
|
+
const plain = name !== "" && /^[A-Za-z0-9_$]+$/.test(name) && !/^[0-9]/.test(name);
|
|
189
|
+
return plain ? name : stringify(name);
|
|
190
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Where `generate --from` reads the schema: an `http(s)://` URL (usually a
|
|
2
|
+
// server's `/v1/schema`), or anything else as a path to a JSON file.
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { GenerateError, isSchema } from "./schema.js";
|
|
5
|
+
export async function loadSchema(from) {
|
|
6
|
+
const text = /^https?:\/\//i.test(from) ? await fetchText(from) : await readText(from);
|
|
7
|
+
let body;
|
|
8
|
+
try {
|
|
9
|
+
body = JSON.parse(text);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new GenerateError(`not a schema: ${from} did not return JSON`);
|
|
13
|
+
}
|
|
14
|
+
if (!isSchema(body))
|
|
15
|
+
throw new GenerateError(`not a schema: ${from} has no \`tables\` list`);
|
|
16
|
+
return { ...body, hash: typeof body.hash === "string" ? body.hash : "" };
|
|
17
|
+
}
|
|
18
|
+
async function fetchText(url) {
|
|
19
|
+
let response;
|
|
20
|
+
try {
|
|
21
|
+
response = await fetch(url);
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
throw new GenerateError(`cannot reach ${url}: ${reason(e)}`);
|
|
25
|
+
}
|
|
26
|
+
if (!response.ok)
|
|
27
|
+
throw new GenerateError(`cannot reach ${url}: HTTP ${response.status}`);
|
|
28
|
+
return response.text();
|
|
29
|
+
}
|
|
30
|
+
async function readText(path) {
|
|
31
|
+
try {
|
|
32
|
+
return await readFile(path, "utf8");
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
throw new GenerateError(`cannot read ${path}: ${reason(e)}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function reason(e) {
|
|
39
|
+
const cause = e.cause;
|
|
40
|
+
return cause?.code ?? cause?.message ?? (e instanceof Error ? e.message : String(e));
|
|
41
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ServerField, ServerMessage, ServerStateVar, ServerTable } from "./schema.js";
|
|
2
|
+
type Meta = Record<string, unknown>;
|
|
3
|
+
export declare function metaTable(table: ServerTable): Meta;
|
|
4
|
+
/** A run's variables (type, starting value, whether the client writes it) and message names. */
|
|
5
|
+
export declare function runMeta(action: {
|
|
6
|
+
state?: ServerStateVar[] | null;
|
|
7
|
+
messages?: ServerMessage[] | null;
|
|
8
|
+
}): Meta;
|
|
9
|
+
/** What a client form needs to render a field. */
|
|
10
|
+
export declare function fieldMeta(fields: ServerField[] | undefined | null): Meta[];
|
|
11
|
+
/** Required unless it says `required: false` or has a default. */
|
|
12
|
+
export declare function required(f: ServerField): boolean;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The runtime metadata (`export const schema = …`) a generated file carries:
|
|
2
|
+
// per table its load policy, the API's sort and filter whitelists, columns
|
|
3
|
+
// (with enum values), references, has-many and actions, and the project
|
|
4
|
+
// actions, in the shape `createVantage({ schema })` reads.
|
|
5
|
+
const or = (value) => value ?? null;
|
|
6
|
+
export function metaTable(table) {
|
|
7
|
+
const columns = Object.fromEntries((table.columns ?? []).map((c) => [
|
|
8
|
+
String(c.name ?? ""),
|
|
9
|
+
{
|
|
10
|
+
type: or(c.type),
|
|
11
|
+
...(c.readonly === true && { readonly: true }),
|
|
12
|
+
...(c.hidden === true && { hidden: true }),
|
|
13
|
+
...(Array.isArray(c.enum) && c.enum.length > 0 && { enum: c.enum }),
|
|
14
|
+
},
|
|
15
|
+
]));
|
|
16
|
+
const references = Object.fromEntries((table.references ?? []).map((r) => [String(r.name ?? ""), { column: or(r.column), table: or(r.table) }]));
|
|
17
|
+
const hasMany = Object.fromEntries((table.has_many ?? []).map((h) => [String(h.name ?? ""), { table: or(h.table), foreignKey: or(h.foreign_key) }]));
|
|
18
|
+
const actions = Object.fromEntries((table.actions ?? []).map((a) => [String(a.name ?? ""), { perRow: a.per_row === true, label: or(a.label), fields: fieldMeta(a.fields), ...runMeta(a) }]));
|
|
19
|
+
return {
|
|
20
|
+
key: or(table.key),
|
|
21
|
+
load: or(table.load),
|
|
22
|
+
persist: or(table.persist),
|
|
23
|
+
live: or(table.live),
|
|
24
|
+
readOnly: or(table.read_only),
|
|
25
|
+
idColumn: or(table.id_column),
|
|
26
|
+
title: or(table.title),
|
|
27
|
+
collectionLimit: or(table.collection_limit),
|
|
28
|
+
// The API's whitelists: what a collection may sort and filter by, and its default sort.
|
|
29
|
+
sortable: or(table.sortable),
|
|
30
|
+
filter: or(table.filter),
|
|
31
|
+
defaultSort: or(table.default_sort),
|
|
32
|
+
columns,
|
|
33
|
+
references,
|
|
34
|
+
hasMany,
|
|
35
|
+
actions,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** A run's variables (type, starting value, whether the client writes it) and message names. */
|
|
39
|
+
export function runMeta(action) {
|
|
40
|
+
const state = Object.fromEntries((action.state ?? []).map((v) => [String(v.name ?? ""), { type: typeof v.type === "string" ? v.type : "any", default: or(v.default), input: v.input === true }]));
|
|
41
|
+
return { state, messages: (action.messages ?? []).map((m) => String(m.name ?? "")) };
|
|
42
|
+
}
|
|
43
|
+
/** What a client form needs to render a field. */
|
|
44
|
+
export function fieldMeta(fields) {
|
|
45
|
+
return (fields ?? []).map((f) => ({
|
|
46
|
+
name: or(f.name),
|
|
47
|
+
type: typeof f.type === "string" ? f.type : "string",
|
|
48
|
+
label: or(f.label),
|
|
49
|
+
required: required(f),
|
|
50
|
+
choices: or(f.choices),
|
|
51
|
+
default: or(f.default),
|
|
52
|
+
widget: or(f.widget),
|
|
53
|
+
options: f.options != null,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
/** Required unless it says `required: false` or has a default. */
|
|
57
|
+
export function required(f) {
|
|
58
|
+
return (typeof f.required === "boolean" ? f.required : true) && f.default == null;
|
|
59
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ServerMessage, ServerStateVar } from "./schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Hands out run type names (`<Base>Run`) that are valid identifiers and
|
|
4
|
+
* clash with nothing else in the file: a base starting with a digit gets
|
|
5
|
+
* `_`, an empty one (a name with no ASCII letters) becomes `Action`, and a
|
|
6
|
+
* taken name gets `2`, `3`, … in the order names are asked for.
|
|
7
|
+
*/
|
|
8
|
+
export declare class RunNames {
|
|
9
|
+
private used;
|
|
10
|
+
constructor(taken: Iterable<string>);
|
|
11
|
+
take(base: string): string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* `export interface <name> { args; state; inputs; messages }` for an action
|
|
15
|
+
* that takes `args` (a type) and declares `state` and `messages`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function runInterface(name: string, args: string, action: {
|
|
18
|
+
state?: ServerStateVar[] | null;
|
|
19
|
+
messages?: ServerMessage[] | null;
|
|
20
|
+
}, key: (name: string) => string): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The run types of one action: what `start` takes (`args`), the shared
|
|
2
|
+
// variables (`state`), the ones a client may write (`inputs`) and the
|
|
3
|
+
// messages (`messages`), as one interface the client's `Run<T>` reads.
|
|
4
|
+
/** A run variable's or message field's type (`int`, `list`, …) in TypeScript. */
|
|
5
|
+
function typeOf(type) {
|
|
6
|
+
switch (type) {
|
|
7
|
+
case "string":
|
|
8
|
+
return "string";
|
|
9
|
+
case "int":
|
|
10
|
+
case "float":
|
|
11
|
+
return "number";
|
|
12
|
+
case "bool":
|
|
13
|
+
return "boolean";
|
|
14
|
+
case "list":
|
|
15
|
+
return "unknown[]";
|
|
16
|
+
case "map":
|
|
17
|
+
return "Record<string, unknown>";
|
|
18
|
+
default:
|
|
19
|
+
return "unknown";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Hands out run type names (`<Base>Run`) that are valid identifiers and
|
|
24
|
+
* clash with nothing else in the file: a base starting with a digit gets
|
|
25
|
+
* `_`, an empty one (a name with no ASCII letters) becomes `Action`, and a
|
|
26
|
+
* taken name gets `2`, `3`, … in the order names are asked for.
|
|
27
|
+
*/
|
|
28
|
+
export class RunNames {
|
|
29
|
+
used;
|
|
30
|
+
constructor(taken) {
|
|
31
|
+
this.used = new Set(taken);
|
|
32
|
+
}
|
|
33
|
+
take(base) {
|
|
34
|
+
const root = `${base === "" ? "Action" : /^[0-9]/.test(base) ? `_${base}` : base}Run`;
|
|
35
|
+
let name = root;
|
|
36
|
+
for (let n = 2; this.used.has(name); n++)
|
|
37
|
+
name = `${root}${n}`;
|
|
38
|
+
this.used.add(name);
|
|
39
|
+
return name;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `export interface <name> { args; state; inputs; messages }` for an action
|
|
44
|
+
* that takes `args` (a type) and declares `state` and `messages`.
|
|
45
|
+
*/
|
|
46
|
+
export function runInterface(name, args, action, key) {
|
|
47
|
+
const vars = action.state ?? [];
|
|
48
|
+
const object = (parts) => (parts.length === 0 ? "{}" : `{ ${parts.join("; ")} }`);
|
|
49
|
+
const variable = (v) => `${key(v.name ?? "")}: ${typeOf(v.type)}`;
|
|
50
|
+
const state = object(vars.map(variable));
|
|
51
|
+
const inputs = object(vars.filter((v) => v.input === true).map(variable));
|
|
52
|
+
const messages = object((action.messages ?? []).map((m) => {
|
|
53
|
+
const fields = Object.entries(m.fields ?? {}).map(([field, type]) => `${key(field)}: ${typeOf(type)}`);
|
|
54
|
+
// A message without fields may be sent with no payload at all (`emit("done")`).
|
|
55
|
+
return `${key(m.name ?? "")}: ${fields.length === 0 ? "Record<string, never> | null" : object(fields)}`;
|
|
56
|
+
}));
|
|
57
|
+
return `export interface ${name} {\n args: ${args};\n state: ${state};\n inputs: ${inputs};\n messages: ${messages};\n}\n`;
|
|
58
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Json } from "../protocol.js";
|
|
2
|
+
/** A one-line reason `generate` stops; the CLI prints it and exits 1. */
|
|
3
|
+
export declare class GenerateError extends Error {
|
|
4
|
+
}
|
|
5
|
+
export interface ServerColumn {
|
|
6
|
+
name?: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
enum?: Json[] | null;
|
|
9
|
+
label?: string | null;
|
|
10
|
+
hidden?: boolean;
|
|
11
|
+
readonly?: boolean;
|
|
12
|
+
mandatory?: boolean;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface ServerField {
|
|
16
|
+
name?: string;
|
|
17
|
+
type?: string;
|
|
18
|
+
label?: string | null;
|
|
19
|
+
required?: boolean;
|
|
20
|
+
choices?: Json[] | null;
|
|
21
|
+
default?: Json;
|
|
22
|
+
widget?: string | null;
|
|
23
|
+
options?: Json;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
/** A run's shared variable. */
|
|
27
|
+
export interface ServerStateVar {
|
|
28
|
+
name?: string;
|
|
29
|
+
type?: string;
|
|
30
|
+
default?: Json;
|
|
31
|
+
input?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** A message a run may send, with its payload fields' types. */
|
|
34
|
+
export interface ServerMessage {
|
|
35
|
+
name?: string;
|
|
36
|
+
fields?: Record<string, string> | null;
|
|
37
|
+
}
|
|
38
|
+
export interface ServerTableAction {
|
|
39
|
+
name?: string;
|
|
40
|
+
label?: string | null;
|
|
41
|
+
per_row?: boolean;
|
|
42
|
+
fields?: ServerField[] | null;
|
|
43
|
+
state?: ServerStateVar[] | null;
|
|
44
|
+
messages?: ServerMessage[] | null;
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
}
|
|
47
|
+
export interface ServerTable {
|
|
48
|
+
name?: string;
|
|
49
|
+
key?: string;
|
|
50
|
+
load?: string;
|
|
51
|
+
persist?: boolean;
|
|
52
|
+
live?: Json;
|
|
53
|
+
read_only?: boolean;
|
|
54
|
+
id_column?: string;
|
|
55
|
+
title?: string | null;
|
|
56
|
+
collection_limit?: number;
|
|
57
|
+
sortable?: string[] | null;
|
|
58
|
+
filter?: string[] | null;
|
|
59
|
+
default_sort?: {
|
|
60
|
+
column: string;
|
|
61
|
+
direction: "asc" | "desc";
|
|
62
|
+
} | null;
|
|
63
|
+
columns?: ServerColumn[] | null;
|
|
64
|
+
references?: {
|
|
65
|
+
name?: string;
|
|
66
|
+
column?: string;
|
|
67
|
+
table?: string;
|
|
68
|
+
}[] | null;
|
|
69
|
+
has_many?: {
|
|
70
|
+
name?: string;
|
|
71
|
+
table?: string;
|
|
72
|
+
foreign_key?: string;
|
|
73
|
+
}[] | null;
|
|
74
|
+
actions?: ServerTableAction[] | null;
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
}
|
|
77
|
+
export interface ServerAction {
|
|
78
|
+
name?: string;
|
|
79
|
+
key?: string;
|
|
80
|
+
kind?: string;
|
|
81
|
+
title?: string | null;
|
|
82
|
+
fields?: ServerField[] | null;
|
|
83
|
+
params?: Json[] | null;
|
|
84
|
+
state?: ServerStateVar[] | null;
|
|
85
|
+
messages?: ServerMessage[] | null;
|
|
86
|
+
[key: string]: unknown;
|
|
87
|
+
}
|
|
88
|
+
export interface ServerSchema {
|
|
89
|
+
hash?: string;
|
|
90
|
+
tables?: ServerTable[] | null;
|
|
91
|
+
actions?: ServerAction[] | null;
|
|
92
|
+
[key: string]: unknown;
|
|
93
|
+
}
|
|
94
|
+
/** Refuse the first entity member that takes a name the model reserves. */
|
|
95
|
+
export declare function refuseReservedNames(schema: ServerSchema): void;
|
|
96
|
+
/** Whether `value` looks like a schema at all: an object with a `tables` array. */
|
|
97
|
+
export declare function isSchema(value: unknown): value is ServerSchema;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The shape of `/v1/schema` as far as code generation reads it. Every
|
|
2
|
+
// field is optional: the emitter treats a missing one like the server's
|
|
3
|
+
// default.
|
|
4
|
+
import { reservedClash } from "../reserved.js";
|
|
5
|
+
/** A one-line reason `generate` stops; the CLI prints it and exits 1. */
|
|
6
|
+
export class GenerateError extends Error {
|
|
7
|
+
}
|
|
8
|
+
/** Refuse the first entity member that takes a name the model reserves. */
|
|
9
|
+
export function refuseReservedNames(schema) {
|
|
10
|
+
for (const table of schema.tables ?? []) {
|
|
11
|
+
const clash = reservedClash(table.name ?? "", [
|
|
12
|
+
...(table.columns ?? []).filter((c) => c.name !== "id" && c.hidden !== true).map((c) => ({ kind: "column", name: c.name ?? "" })),
|
|
13
|
+
...[...(table.references ?? []), ...(table.has_many ?? [])].map((r) => ({ kind: "reference", name: r.name ?? "" })),
|
|
14
|
+
...(table.actions ?? []).filter((a) => a.per_row === true).map((a) => ({ kind: "action", name: a.name ?? "" })),
|
|
15
|
+
]);
|
|
16
|
+
if (clash)
|
|
17
|
+
throw new GenerateError(clash);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Whether `value` looks like a schema at all: an object with a `tables` array. */
|
|
21
|
+
export function isSchema(value) {
|
|
22
|
+
return value !== null && typeof value === "object" && Array.isArray(value.tables);
|
|
23
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { CollectionSpec } from "./protocol.js";
|
|
2
|
+
import { State } from "./signal.js";
|
|
3
|
+
import type { LoadState, Model } from "./types.js";
|
|
4
|
+
import type { Vantage } from "./client.js";
|
|
5
|
+
type AnyModel = Model & Record<string, any>;
|
|
6
|
+
/** A window's cell spec: the collection's, with the window's size as its limit. */
|
|
7
|
+
export type CellSpec = CollectionSpec & {
|
|
8
|
+
$window?: boolean;
|
|
9
|
+
};
|
|
10
|
+
/** One key per description, whatever order its parts were set in. */
|
|
11
|
+
export declare function specKey({ table, where, search, sort, via, limit, $window }: CellSpec): string;
|
|
12
|
+
/** Shared state behind every Collection with the same description. Not exported from the package. */
|
|
13
|
+
export declare class CollectionCell {
|
|
14
|
+
private db;
|
|
15
|
+
readonly spec: CellSpec;
|
|
16
|
+
readonly key: string;
|
|
17
|
+
readonly ids: State<string[]>;
|
|
18
|
+
readonly loadState: State<LoadState>;
|
|
19
|
+
readonly total: State<number | null>;
|
|
20
|
+
readonly error: State<string | null>;
|
|
21
|
+
sub: number | null;
|
|
22
|
+
start: number;
|
|
23
|
+
pinned: boolean;
|
|
24
|
+
/** The server refused to open it; reads don't retry until a resync. */
|
|
25
|
+
refused: boolean;
|
|
26
|
+
touched: number;
|
|
27
|
+
wantedWindow?: {
|
|
28
|
+
start: number;
|
|
29
|
+
count: number;
|
|
30
|
+
};
|
|
31
|
+
private opening?;
|
|
32
|
+
private settled?;
|
|
33
|
+
private firstLoad;
|
|
34
|
+
private modelsCache?;
|
|
35
|
+
private windowWaiters;
|
|
36
|
+
constructor(db: Vantage<any>, spec: CellSpec, key: string);
|
|
37
|
+
/** Something read us: open if closed. After a final refusal of the connection, settle with its message. */
|
|
38
|
+
read(): void;
|
|
39
|
+
/** Open on the server, unless an open is already on its way. The ids shown so far stay until the reply. */
|
|
40
|
+
open(): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* The server dropped every sub (a resync): forget the open in flight, so
|
|
43
|
+
* the next one goes out, and a refusal, so `whenLoaded()` waits for the reopen.
|
|
44
|
+
*/
|
|
45
|
+
reset(): void;
|
|
46
|
+
/** Scrolled away from the start: the first reply's ids aren't the ones on screen. */
|
|
47
|
+
get scrolled(): boolean;
|
|
48
|
+
wanted(): boolean;
|
|
49
|
+
opened(sub: number): void;
|
|
50
|
+
loaded(sub: number, start: number, ids: string[], total: number | null, complete: boolean): void;
|
|
51
|
+
members(start: number, ids: string[], total: number | null, complete: boolean): void;
|
|
52
|
+
failed(message: string): void;
|
|
53
|
+
/** Resolves once rows `start..start+count` have arrived, or the cell failed (refused, or the connection halted). */
|
|
54
|
+
whenWindow(start: number, count: number): Promise<void>;
|
|
55
|
+
/** Whether a `whenLoaded()` is still waiting for its rows. */
|
|
56
|
+
get waitingForWindow(): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Failed, or the rows from `start` arrived: `count` of them, or fewer when
|
|
59
|
+
* the frame said `complete`. An estimated `total` never ends the window
|
|
60
|
+
* early. Reads state without tracking it.
|
|
61
|
+
*/
|
|
62
|
+
windowSettled(start: number, count: number): boolean;
|
|
63
|
+
/** Resolve every `whenWindow()` whose rows are now here. */
|
|
64
|
+
private wakeWindows;
|
|
65
|
+
drop(id: string): void;
|
|
66
|
+
whenLoaded(): Promise<void>;
|
|
67
|
+
models(): AnyModel[];
|
|
68
|
+
}
|
|
69
|
+
export {};
|