api_binding_ui 2.0.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -50
- package/SKILL.md +29 -34
- package/dist/app/index-0n23yy5r.js +367 -0
- package/dist/app/index-aaxy714b.css +1 -0
- package/dist/app/index.html +7 -0
- package/dist/cli.js +969 -0
- package/dist/codegen/generate.d.ts +22 -0
- package/dist/codegen/helpers.d.ts +2 -0
- package/dist/framework/contract.d.ts +38 -23
- package/dist/framework/expr.d.ts +26 -27
- package/dist/framework/file.d.ts +28 -0
- package/dist/framework/store.d.ts +3 -15
- package/dist/{index-3q1vx729.js → index-jj8twdw9.js} +158 -173
- package/dist/index-jj8twdw9.js.map +16 -0
- package/dist/index.d.ts +6 -8
- package/dist/index.js +461 -298
- package/dist/index.js.map +6 -8
- package/dist/studio/BindingStudio.d.ts +6 -11
- package/dist/studio/SettingsPanel.d.ts +3 -7
- package/dist/studio/index.css +0 -149
- package/dist/studio/index.d.ts +1 -2
- package/dist/studio/index.js +228 -437
- package/dist/studio/index.js.map +8 -10
- package/dist/studio/useBindings.d.ts +7 -5
- package/dist/studio/useCatalog.d.ts +2 -0
- package/package.json +65 -53
- package/spec/FRAMEWORK.md +106 -201
- package/dist/framework/runner.d.ts +0 -36
- package/dist/index-3q1vx729.js.map +0 -16
- package/dist/runtime/BindingProvider.d.ts +0 -42
- package/dist/runtime/BindingScreen.d.ts +0 -23
- package/dist/runtime/hooks.d.ts +0 -22
- package/dist/runtime/screenStore.d.ts +0 -7
- package/dist/studio/BindingWidget.d.ts +0 -9
- package/dist/studio/useSettings.d.ts +0 -8
package/dist/index.js
CHANGED
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
2
|
catalogFromOpenApi2,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
CatalogLoadError2,
|
|
4
|
+
catalogFromText2,
|
|
5
|
+
loadCatalog2,
|
|
6
|
+
ID2,
|
|
7
|
+
parseBindingsFile2,
|
|
8
|
+
screenOf2,
|
|
9
|
+
fromFile2,
|
|
10
|
+
toFile2,
|
|
11
|
+
memoryStore2,
|
|
12
|
+
httpStore2,
|
|
13
|
+
flat,
|
|
6
14
|
createEngine2,
|
|
7
15
|
validateRegistry2,
|
|
8
|
-
validateBindings2
|
|
9
|
-
|
|
10
|
-
BindingProvider2
|
|
11
|
-
} from "./index-3q1vx729.js";
|
|
16
|
+
validateBindings2
|
|
17
|
+
} from "./index-jj8twdw9.js";
|
|
12
18
|
// src/framework/expr.ts
|
|
13
|
-
var PIPE = /^([A-Za-z_][\w-]*)\((.*)\)$/;
|
|
14
19
|
var cache = new Map;
|
|
15
|
-
function parseArg(raw) {
|
|
16
|
-
const t = raw.trim();
|
|
17
|
-
if (!t)
|
|
18
|
-
return;
|
|
19
|
-
try {
|
|
20
|
-
return JSON.parse(t);
|
|
21
|
-
} catch {
|
|
22
|
-
return t;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
20
|
function segments(path) {
|
|
26
21
|
return path.replace(/\[(\*|\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
27
22
|
}
|
|
@@ -29,320 +24,488 @@ function parse(expr) {
|
|
|
29
24
|
const hit = cache.get(expr);
|
|
30
25
|
if (hit)
|
|
31
26
|
return hit;
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
27
|
+
const e = expr.trim();
|
|
28
|
+
let parsed;
|
|
29
|
+
if (e.startsWith("$")) {
|
|
30
|
+
const segs = segments(e);
|
|
31
|
+
const [h, id] = segs;
|
|
32
|
+
if (h === "$item")
|
|
33
|
+
parsed = { head: { type: "item" }, path: segs.slice(1) };
|
|
34
|
+
else if ((h === "$context" || h === "$steps") && id)
|
|
35
|
+
parsed = { head: { type: h === "$context" ? "context" : "steps", id }, path: segs.slice(2) };
|
|
36
|
+
else
|
|
37
|
+
throw new Error(`Unknown source "${h}" in "${expr}".`);
|
|
38
|
+
} else {
|
|
39
|
+
const colon = e.indexOf(":");
|
|
40
|
+
if (colon < 1)
|
|
41
|
+
throw new Error(`"${expr}" must read "<componentId>:<slot>".`);
|
|
42
|
+
const [slot, ...path] = segments(e.slice(colon + 1));
|
|
43
|
+
if (!slot)
|
|
44
|
+
throw new Error(`"${expr}" names no slot.`);
|
|
45
|
+
parsed = { head: { type: "slot", component: e.slice(0, colon), slot }, path };
|
|
46
|
+
}
|
|
44
47
|
cache.set(expr, parsed);
|
|
45
48
|
return parsed;
|
|
46
49
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
// src/codegen/helpers.ts
|
|
51
|
+
var HELPERS = `import { useMutation, useQuery } from "@tanstack/react-query";
|
|
52
|
+
|
|
53
|
+
/** Data for a slot. \`bound\` is false when no API feeds it; keep the UI's own value then. */
|
|
54
|
+
export interface Data<T> { bound: boolean; data: T | undefined; loading: boolean; error: unknown }
|
|
55
|
+
/** An action. \`run\` rejects when a call fails; it resolves at once when no API is connected. */
|
|
56
|
+
export interface Action { bound: boolean; run: (payload?: any) => Promise<void>; running: boolean; error: unknown }
|
|
57
|
+
/** A per-item value, such as a cell in a row. */
|
|
58
|
+
export interface Cell<T> { bound: boolean; read: (item: any) => T | undefined }
|
|
59
|
+
|
|
60
|
+
export const UNBOUND_DATA: Data<never> = { bound: false, data: undefined, loading: false, error: undefined };
|
|
61
|
+
export const UNBOUND_ACTION: Action = { bound: false, run: async () => {}, running: false, error: undefined };
|
|
62
|
+
export const UNBOUND_CELL: Cell<never> = { bound: false, read: () => undefined };
|
|
63
|
+
|
|
64
|
+
export class ApiError extends Error {
|
|
65
|
+
constructor(readonly status: number, readonly data: unknown) { super(\`API call failed (\${status})\`); this.name = "ApiError"; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Reads a path; "*" maps the rest of the path over a list. */
|
|
69
|
+
export function at(value: unknown, path: readonly string[]): any {
|
|
70
|
+
let v: any = value;
|
|
71
|
+
for (let i = 0; i < path.length; i++) {
|
|
72
|
+
if (v == null) return undefined;
|
|
73
|
+
if (path[i] === "*") return Array.isArray(v) ? v.map(x => at(x, path.slice(i + 1))) : undefined;
|
|
58
74
|
v = v[path[i]];
|
|
59
75
|
}
|
|
60
76
|
return v;
|
|
61
77
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
root = scope.context?.[head[1]];
|
|
69
|
-
else if (head[0] === "$steps")
|
|
70
|
-
root = scope.steps?.[head[1]];
|
|
71
|
-
else if (head[0].startsWith("$"))
|
|
72
|
-
root = scope.providers?.[head[0].slice(1)];
|
|
73
|
-
else
|
|
74
|
-
root = scope.slot(head[0], head[1]);
|
|
75
|
-
let value = walk(root, path);
|
|
76
|
-
for (const p of pipes) {
|
|
77
|
-
const fn = scope.transforms?.[p.name];
|
|
78
|
-
if (!fn)
|
|
79
|
-
throw new Error(`Unknown transform "${p.name}" in "${expr}"`);
|
|
80
|
-
value = fn(value, ...p.args);
|
|
81
|
-
}
|
|
82
|
-
return value;
|
|
83
|
-
}
|
|
84
|
-
// src/framework/runner.ts
|
|
85
|
-
class BindingError extends Error {
|
|
86
|
-
detail;
|
|
87
|
-
constructor(message, detail) {
|
|
88
|
-
super(message);
|
|
89
|
-
this.detail = detail;
|
|
90
|
-
this.name = "BindingError";
|
|
78
|
+
|
|
79
|
+
/** The body of a client response; throws on a non-2xx status. Passes through clients that return the body. */
|
|
80
|
+
export function ok(res: any): any {
|
|
81
|
+
if (res && typeof res === "object" && typeof res.status === "number" && "data" in res && "headers" in res) {
|
|
82
|
+
if (res.status < 200 || res.status >= 300) throw new ApiError(res.status, res.data);
|
|
83
|
+
return res.data;
|
|
91
84
|
}
|
|
85
|
+
return res;
|
|
92
86
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
87
|
+
|
|
88
|
+
/** True when every required input has a value. */
|
|
89
|
+
export const has = (inputs: Record<string, unknown>) => Object.values(inputs).every(v => v !== undefined);
|
|
90
|
+
|
|
91
|
+
/** Throws when a required input has no value. */
|
|
92
|
+
export function need(operation: string, inputs: Record<string, unknown>) {
|
|
93
|
+
const missing = Object.keys(inputs).filter(k => inputs[k] === undefined);
|
|
94
|
+
if (missing.length) throw new Error(\`\${operation}: missing \${missing.join(", ")}\`);
|
|
99
95
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
else if (where === "header")
|
|
115
|
-
out.headers[keys[0]] = String(value);
|
|
116
|
-
else if (where === "body") {
|
|
117
|
-
if (!keys.length)
|
|
118
|
-
out.body = value;
|
|
119
|
-
else
|
|
120
|
-
setPointer(body ??= {}, keys, value);
|
|
121
|
-
}
|
|
96
|
+
|
|
97
|
+
/** Drops undefined values. */
|
|
98
|
+
export const defined = (o: Record<string, unknown>): any => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
|
|
99
|
+
|
|
100
|
+
export const path = (v: unknown): any => (v === undefined ? undefined : encodeURIComponent(String(v)));
|
|
101
|
+
|
|
102
|
+
/** A body from a base value and [keys, value] entries; undefined values are skipped. */
|
|
103
|
+
export function assemble(base: unknown, entries: Array<[string[], unknown]>): any {
|
|
104
|
+
const out: any = base && typeof base === "object" ? { ...base } : {};
|
|
105
|
+
for (const [keys, value] of entries) {
|
|
106
|
+
if (value === undefined) continue;
|
|
107
|
+
let node = out;
|
|
108
|
+
keys.slice(0, -1).forEach(k => { node = node[k] = node[k] && typeof node[k] === "object" ? { ...node[k] } : {}; });
|
|
109
|
+
node[keys[keys.length - 1]] = value;
|
|
122
110
|
}
|
|
123
|
-
if (body)
|
|
124
|
-
out.body = { ...out.body, ...body };
|
|
125
|
-
const missing = op.parameters.filter((p) => p.required && !((p.pointer in request) && evaluate(request[p.pointer], scope) !== undefined));
|
|
126
|
-
if (missing.length)
|
|
127
|
-
throw new BindingError(`Missing required input: ${missing.map((p) => p.name).join(", ")}.`);
|
|
128
111
|
return out;
|
|
129
112
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
function mapItems(data, binding, scope) {
|
|
136
|
-
if (!binding.map || !Object.keys(binding.map).length)
|
|
137
|
-
return data;
|
|
138
|
-
const one = (item) => ({
|
|
139
|
-
...item && typeof item === "object" ? item : {},
|
|
140
|
-
...Object.fromEntries(Object.entries(binding.map).map(([k, e]) => [k, evaluate(e, { ...scope, item })]))
|
|
141
|
-
});
|
|
113
|
+
|
|
114
|
+
/** Adds mapped fields to each item, keeping the fields the API returned. */
|
|
115
|
+
export function mapItems(data: any, map: (item: any) => Record<string, unknown>): any {
|
|
116
|
+
const one = (item: any) => ({ ...(item && typeof item === "object" ? item : {}), ...map(item) });
|
|
142
117
|
return Array.isArray(data) ? data.map(one) : data == null ? data : one(data);
|
|
143
118
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
119
|
+
|
|
120
|
+
export const list = (v: unknown): unknown[] => (Array.isArray(v) ? v : v === undefined ? [] : [v]);
|
|
121
|
+
|
|
122
|
+
export function finish(failed: unknown[]) {
|
|
123
|
+
if (failed.length === 1) throw failed[0];
|
|
124
|
+
if (failed.length) throw new AggregateError(failed, \`\${failed.length} API calls failed\`);
|
|
147
125
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
const op = operations.get(step.operation);
|
|
153
|
-
if (!op)
|
|
154
|
-
throw new BindingError(`Operation "${step.operation}" is not in the catalog.`);
|
|
155
|
-
const items = step.forEach ? evaluate(step.forEach, scope) : [scope.item];
|
|
156
|
-
const list = Array.isArray(items) ? items : [items];
|
|
157
|
-
const responses = [];
|
|
158
|
-
let failed = false;
|
|
159
|
-
for (const item of list) {
|
|
160
|
-
try {
|
|
161
|
-
responses.push(await fetcher(buildRequest(op, step.request, { ...scope, item, steps: results }), op));
|
|
162
|
-
} catch (error) {
|
|
163
|
-
failed = true;
|
|
164
|
-
outcomes.push({ step: step.id, ok: false, error });
|
|
165
|
-
if (step.onError === "stop")
|
|
166
|
-
return outcomes;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
const response = step.forEach ? responses : responses[0];
|
|
170
|
-
results[step.id] = { response };
|
|
171
|
-
if (!failed)
|
|
172
|
-
outcomes.push({ step: step.id, ok: true, response });
|
|
173
|
-
}
|
|
174
|
-
return outcomes;
|
|
126
|
+
|
|
127
|
+
export function useData<T>(key: readonly unknown[], fetch: () => Promise<unknown>, select: (data: any) => T, options: { enabled: boolean; everySeconds?: number }): Data<T> {
|
|
128
|
+
const q = useQuery({ queryKey: key, queryFn: fetch, select, enabled: options.enabled, refetchInterval: options.everySeconds ? options.everySeconds * 1000 : false });
|
|
129
|
+
return { bound: true, data: q.data, loading: q.isLoading, error: q.error ?? undefined };
|
|
175
130
|
}
|
|
176
|
-
// src/runtime/BindingScreen.tsx
|
|
177
|
-
import * as React from "react";
|
|
178
131
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const listeners = new Set;
|
|
183
|
-
return {
|
|
184
|
-
get: (b, s) => values.get(`${b}.${s}`),
|
|
185
|
-
set(b, s, value) {
|
|
186
|
-
const k = `${b}.${s}`;
|
|
187
|
-
if (Object.is(values.get(k), value))
|
|
188
|
-
return;
|
|
189
|
-
values.set(k, value);
|
|
190
|
-
listeners.forEach((l) => l());
|
|
191
|
-
},
|
|
192
|
-
subscribe(l) {
|
|
193
|
-
listeners.add(l);
|
|
194
|
-
return () => {
|
|
195
|
-
listeners.delete(l);
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
};
|
|
132
|
+
export function useAction(run: (payload?: any) => Promise<void>): Action {
|
|
133
|
+
const m = useMutation({ mutationFn: run });
|
|
134
|
+
return { bound: true, run: payload => m.mutateAsync(payload), running: m.isPending, error: m.error ?? undefined };
|
|
199
135
|
}
|
|
136
|
+
`;
|
|
200
137
|
|
|
201
|
-
// src/
|
|
202
|
-
|
|
203
|
-
var
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return
|
|
138
|
+
// src/codegen/generate.ts
|
|
139
|
+
var GENERATED_MARK = "@generated";
|
|
140
|
+
var HEADER = `// ${GENERATED_MARK} Do not edit: change the bindings and regenerate.
|
|
141
|
+
`;
|
|
142
|
+
var words = (s) => s.split(/[.\-_]+/).filter(Boolean);
|
|
143
|
+
var pascal = (s) => words(s).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
|
|
144
|
+
var camel = (s) => {
|
|
145
|
+
const p = pascal(s);
|
|
146
|
+
return p[0].toLowerCase() + p.slice(1);
|
|
147
|
+
};
|
|
148
|
+
var str = (v) => JSON.stringify(v);
|
|
149
|
+
var IDENT = /^[A-Za-z_$][\w$]*$/;
|
|
150
|
+
var prop = (k) => IDENT.test(k) ? k : str(k);
|
|
151
|
+
var orvalName = (id) => IDENT.test(id) ? id[0].toLowerCase() + id.slice(1) : camel(id);
|
|
152
|
+
function tsType(schema, many = false, depth = 0) {
|
|
153
|
+
const one = () => {
|
|
154
|
+
if (!schema || typeof schema !== "object" || depth > 3)
|
|
155
|
+
return "any";
|
|
156
|
+
const s = schema;
|
|
157
|
+
if (Array.isArray(s.enum) && s.enum.length && s.enum.every((v) => typeof v === "string"))
|
|
158
|
+
return s.enum.map(str).join(" | ");
|
|
159
|
+
switch (s.type) {
|
|
160
|
+
case "string":
|
|
161
|
+
return "string";
|
|
162
|
+
case "integer":
|
|
163
|
+
case "number":
|
|
164
|
+
return "number";
|
|
165
|
+
case "boolean":
|
|
166
|
+
return "boolean";
|
|
167
|
+
case "array":
|
|
168
|
+
return `Array<${tsType(s.items, false, depth + 1)}>`;
|
|
169
|
+
case "object": {
|
|
170
|
+
const props = Object.entries(s.properties ?? {});
|
|
171
|
+
if (!props.length)
|
|
172
|
+
return "Record<string, any>";
|
|
173
|
+
return `{ ${props.map(([k, v]) => `${prop(k)}?: ${tsType(v, false, depth + 1)}`).join("; ")}; [key: string]: any }`;
|
|
174
|
+
}
|
|
175
|
+
default:
|
|
176
|
+
return "any";
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
return many ? `Array<${one()}>` : one();
|
|
210
180
|
}
|
|
211
|
-
function
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
181
|
+
function compileExpr(expr, cx, scope, at) {
|
|
182
|
+
let p;
|
|
183
|
+
try {
|
|
184
|
+
p = parse(expr);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
cx.issue("expr.invalid", e.message, at);
|
|
187
|
+
return "undefined";
|
|
188
|
+
}
|
|
189
|
+
const path = p.path.map(str);
|
|
190
|
+
const h = p.head;
|
|
191
|
+
const read = (root, keys) => {
|
|
192
|
+
cx.use.add("at");
|
|
193
|
+
return `at(${root}, [${keys.join(", ")}])`;
|
|
194
|
+
};
|
|
195
|
+
if (h.type === "item") {
|
|
196
|
+
if (!scope.item) {
|
|
197
|
+
cx.issue("expr.no-item", `"${expr}" reads $item, which this slot does not have.`, at);
|
|
198
|
+
return "undefined";
|
|
199
|
+
}
|
|
200
|
+
return path.length ? read("item", path) : "item";
|
|
201
|
+
}
|
|
202
|
+
if (h.type === "context") {
|
|
203
|
+
if (!cx.screen.context.some((c) => c.id === h.id)) {
|
|
204
|
+
cx.issue("expr.unknown-context", `"${expr}": ${cx.screen.id} declares no context "${h.id}".`, at);
|
|
205
|
+
return "undefined";
|
|
206
|
+
}
|
|
207
|
+
return read("state.context", [str(h.id), ...path]);
|
|
208
|
+
}
|
|
209
|
+
if (h.type === "steps") {
|
|
210
|
+
if (!scope.steps?.has(h.id)) {
|
|
211
|
+
cx.issue("expr.unknown-step", `"${expr}" reads step "${h.id}", which does not run before it.`, at);
|
|
212
|
+
return "undefined";
|
|
213
|
+
}
|
|
214
|
+
return read("steps", [str(h.id), ...path]);
|
|
215
|
+
}
|
|
216
|
+
if (!cx.produces.get(h.component)?.has(h.slot)) {
|
|
217
|
+
cx.issue("expr.unknown-slot", `"${expr}" reads a value that is not produced on ${cx.screen.id}.`, at);
|
|
218
|
+
return "undefined";
|
|
219
|
+
}
|
|
220
|
+
return read("state", [str(cx.keys.get(h.component)), str(h.slot), ...path]);
|
|
217
221
|
}
|
|
218
|
-
var
|
|
219
|
-
function
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
222
|
+
var decode = (k) => k.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
223
|
+
function compileCall(op, request, cx, scope, at) {
|
|
224
|
+
const e = (x) => compileExpr(x, cx, scope, at);
|
|
225
|
+
const known = new Set(op.parameters.map((p) => p.pointer));
|
|
226
|
+
const body = [];
|
|
227
|
+
let whole;
|
|
228
|
+
const query = [];
|
|
229
|
+
const headers = [];
|
|
230
|
+
for (const [pointer, x] of Object.entries(request)) {
|
|
231
|
+
const [, where, ...rest] = pointer.split("/");
|
|
232
|
+
const keys = rest.map(decode);
|
|
233
|
+
if (where === "body") {
|
|
234
|
+
if (!op.body) {
|
|
235
|
+
cx.issue("request.no-body", `${op.id} takes no body, but "${pointer}" is set.`, at);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (!keys.length)
|
|
239
|
+
whole = e(x);
|
|
240
|
+
else
|
|
241
|
+
body.push(`[[${keys.map(str).join(", ")}], ${e(x)}]`);
|
|
242
|
+
} else if (!known.has(pointer))
|
|
243
|
+
cx.issue("request.unknown-input", `${op.id} has no input "${pointer}".`, at);
|
|
244
|
+
else if (where === "query")
|
|
245
|
+
query.push(`${prop(keys[0])}: ${e(x)}`);
|
|
246
|
+
else if (where === "header")
|
|
247
|
+
headers.push(`${str(keys[0])}: ${e(x)}`);
|
|
248
|
+
}
|
|
249
|
+
const args = [...op.path.matchAll(/\{([^}]+)\}/g)].map((m) => {
|
|
250
|
+
const x = request[`/path/${m[1]}`];
|
|
251
|
+
if (x === undefined) {
|
|
252
|
+
cx.issue("request.missing-path", `${op.id} needs "${m[1]}" in its path; it is not connected.`, at);
|
|
253
|
+
return "undefined";
|
|
254
|
+
}
|
|
255
|
+
cx.use.add("path");
|
|
256
|
+
return `path(${e(x)})`;
|
|
231
257
|
});
|
|
258
|
+
if (op.body) {
|
|
259
|
+
const set = new Set(Object.keys(request).filter((k) => k.startsWith("/body/")).map((k) => decode(k.split("/")[2])));
|
|
260
|
+
const gaps = whole ? [] : (flat(op.body.schema).required ?? []).filter((f) => !set.has(f));
|
|
261
|
+
if (gaps.length)
|
|
262
|
+
cx.warn("request.required-body", `${op.id} requires ${gaps.join(", ")} in its body; not connected.`, at);
|
|
263
|
+
cx.use.add("assemble");
|
|
264
|
+
args.push(`assemble(${whole ?? "undefined"}, [${body.join(", ")}])`);
|
|
265
|
+
}
|
|
266
|
+
if (op.parameters.some((p) => p.pointer.startsWith("/query/"))) {
|
|
267
|
+
cx.use.add("defined");
|
|
268
|
+
args.push(`defined({ ${query.join(", ")} })`);
|
|
269
|
+
}
|
|
270
|
+
const key = `[${[str(op.id), ...args].join(", ")}]`;
|
|
271
|
+
if (headers.length) {
|
|
272
|
+
cx.use.add("defined");
|
|
273
|
+
args.push(`{ headers: defined({ ${headers.join(", ")} }) }`);
|
|
274
|
+
}
|
|
275
|
+
const required = op.parameters.filter((p) => p.required).map((p) => `${prop(p.name)}: ${request[p.pointer] === undefined ? "undefined" : e(request[p.pointer])}`);
|
|
276
|
+
return { fn: orvalName(op.id), call: `${orvalName(op.id)}(${args.join(", ")})`, key, required: required.length ? `{ ${required.join(", ")} }` : "" };
|
|
232
277
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function useSlot(bindId, slot) {
|
|
246
|
-
const runtime = useBindingRuntime();
|
|
247
|
-
const screen = useScreen();
|
|
248
|
-
const binding = runtime.binding(screen.id, bindId, slot);
|
|
249
|
-
const [nonce, setNonce] = React2.useState(0);
|
|
250
|
-
const [state, setState] = React2.useState({ key: "", status: "loading" });
|
|
251
|
-
const snapshot = React2.useCallback(() => {
|
|
252
|
-
if (!binding)
|
|
253
|
-
return "";
|
|
254
|
-
try {
|
|
255
|
-
const scope = scopeOf(runtime, screen);
|
|
256
|
-
if (binding.type === "expression")
|
|
257
|
-
return JSON.stringify(evaluate(binding.value, scope) ?? null);
|
|
258
|
-
if (binding.type !== "operation")
|
|
259
|
-
return "";
|
|
260
|
-
const op = runtime.operations.get(binding.operation);
|
|
261
|
-
return op ? requestKey(buildRequest(op, binding.request, scope)) : WAIT + "operation missing";
|
|
262
|
-
} catch (e) {
|
|
263
|
-
return WAIT + (e instanceof Error ? e.message : String(e));
|
|
278
|
+
var selectPath = (selector) => segments(selector.replace(/^\$\.?/, "").replace(/\[\*\]$/, ""));
|
|
279
|
+
function compileSteps(steps, cx, at, ops, imports) {
|
|
280
|
+
const done = new Set;
|
|
281
|
+
const blocks = [];
|
|
282
|
+
let usesPayload = false;
|
|
283
|
+
cx.use.add("ok");
|
|
284
|
+
cx.use.add("finish");
|
|
285
|
+
for (const st of steps) {
|
|
286
|
+
const op = ops.get(st.operation);
|
|
287
|
+
if (!op) {
|
|
288
|
+
cx.issue("catalog.operation-missing", `${st.operation} is not in the API docs.`, at);
|
|
289
|
+
continue;
|
|
264
290
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
291
|
+
const scope = { item: true, steps: done };
|
|
292
|
+
const call = compileCall(op, st.request, cx, scope, at);
|
|
293
|
+
imports.add(call.fn);
|
|
294
|
+
const each = st.forEach ? compileExpr(st.forEach, cx, scope, at) : undefined;
|
|
295
|
+
if (each)
|
|
296
|
+
cx.use.add("list");
|
|
297
|
+
else
|
|
298
|
+
usesPayload = true;
|
|
299
|
+
if (call.required)
|
|
300
|
+
cx.use.add("need");
|
|
301
|
+
const readsItem = /\bitem\b/.test(call.call + call.required);
|
|
302
|
+
blocks.push([
|
|
303
|
+
` {`,
|
|
304
|
+
` const responses: unknown[] = [];`,
|
|
305
|
+
` for (const item of ${each ? `list(${each})` : "[payload]"}) {`,
|
|
306
|
+
readsItem ? "" : ` void item;`,
|
|
307
|
+
` try {`,
|
|
308
|
+
call.required ? ` need(${str(op.id)}, ${call.required});` : "",
|
|
309
|
+
` responses.push(ok(await ${call.call}));`,
|
|
310
|
+
` } catch (error) {`,
|
|
311
|
+
st.onError === "stop" ? ` throw error;` : ` failed.push(error);`,
|
|
312
|
+
` }`,
|
|
313
|
+
` }`,
|
|
314
|
+
` steps[${str(st.id)}] = { response: ${each ? "responses" : "responses[0]"} };`,
|
|
315
|
+
` }`
|
|
316
|
+
].filter(Boolean).join(`
|
|
317
|
+
`));
|
|
318
|
+
done.add(st.id);
|
|
319
|
+
}
|
|
320
|
+
return [
|
|
321
|
+
`async (${usesPayload ? "payload" : "_payload"}?: any) => {`,
|
|
322
|
+
` const steps: Record<string, unknown> = {};`,
|
|
323
|
+
` const failed: unknown[] = [];`,
|
|
324
|
+
...blocks,
|
|
325
|
+
` finish(failed);`,
|
|
326
|
+
` }`
|
|
327
|
+
].join(`
|
|
328
|
+
`);
|
|
329
|
+
}
|
|
330
|
+
function generate(file, catalog) {
|
|
331
|
+
const issues = [];
|
|
332
|
+
const { fw, index, issues: fileIssues } = fromFile2(file, catalog);
|
|
333
|
+
const valid = validateRegistry2(fw);
|
|
334
|
+
issues.push(...fileIssues, ...valid.issues);
|
|
335
|
+
const ops = new Map(catalog.operations.map((o) => [o.id, o]));
|
|
336
|
+
const DESC = new Map(fw.descriptors.map((d) => [d.kind, d]));
|
|
337
|
+
const client = file.output?.client;
|
|
338
|
+
if (!client)
|
|
339
|
+
issues.push({ severity: "error", screen: "", code: "output.missing", message: `Set "output": { "dir", "client" } in the bindings file.` });
|
|
340
|
+
const files = [{ path: "screen-helpers.ts", content: HEADER + HELPERS }];
|
|
341
|
+
const exports = [];
|
|
342
|
+
for (const screen of valid.registry.screens) {
|
|
343
|
+
const name = pascal(screen.id);
|
|
344
|
+
const keys = new Map;
|
|
345
|
+
const taken = new Map;
|
|
346
|
+
for (const c of screen.components) {
|
|
347
|
+
const k = camel(c.bindId.slice(screen.id.length + 1));
|
|
348
|
+
if (taken.has(k))
|
|
349
|
+
issues.push({ severity: "error", screen: screen.id, component: c.bindId, code: "codegen.name-clash", message: `"${c.bindId}" and "${taken.get(k)}" both become "${k}".` });
|
|
350
|
+
taken.set(k, c.bindId);
|
|
351
|
+
keys.set(c.bindId, k);
|
|
280
352
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return () => {
|
|
289
|
-
live = false;
|
|
353
|
+
const cx = {
|
|
354
|
+
screen,
|
|
355
|
+
keys,
|
|
356
|
+
use: new Set,
|
|
357
|
+
produces: new Map(screen.components.map((c) => [c.bindId, new Set(DESC.get(c.kind).slots.filter((s) => s.role === "produce").map((s) => s.id))])),
|
|
358
|
+
issue: (code, message, at) => issues.push({ severity: "error", screen: screen.id, code, message, ...at }),
|
|
359
|
+
warn: (code, message, at) => issues.push({ severity: "warning", screen: screen.id, code, message, ...at })
|
|
290
360
|
};
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const runtime = useBindingRuntime();
|
|
303
|
-
const screen = useScreen();
|
|
304
|
-
const binding = runtime.binding(screen.id, bindId, slot);
|
|
305
|
-
return React2.useCallback((item) => binding?.type === "expression" ? evaluate(binding.value, scopeOf(runtime, screen, item)) : undefined, [binding, runtime, screen]);
|
|
306
|
-
}
|
|
307
|
-
function useTrigger(bindId, slot) {
|
|
308
|
-
const runtime = useBindingRuntime();
|
|
309
|
-
const screen = useScreen();
|
|
310
|
-
const binding = runtime.binding(screen.id, bindId, slot);
|
|
311
|
-
const [running, setRunning] = React2.useState(false);
|
|
312
|
-
const run = React2.useCallback(async (item) => {
|
|
313
|
-
if (binding?.type !== "steps")
|
|
314
|
-
return [];
|
|
315
|
-
setRunning(true);
|
|
316
|
-
try {
|
|
317
|
-
return await runSteps(binding.steps, runtime.operations, scopeOf(runtime, screen, item), runtime.fetcher);
|
|
318
|
-
} finally {
|
|
319
|
-
setRunning(false);
|
|
361
|
+
const bindings = new Map;
|
|
362
|
+
for (const b of index[screen.id] ?? []) {
|
|
363
|
+
if (!keys.has(b.component)) {
|
|
364
|
+
cx.issue("binding.unknown-component", `A binding names "${b.component}", which is not registered.`, b);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (b.state !== "confirmed") {
|
|
368
|
+
issues.push({ severity: "warning", screen: screen.id, component: b.component, slot: b.slot, code: "binding.draft", message: `${b.component} · ${b.slot} is a draft; it is not generated.` });
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
bindings.set(`${b.component}|${b.slot}`, b);
|
|
320
372
|
}
|
|
321
|
-
|
|
322
|
-
|
|
373
|
+
const imports = new Set;
|
|
374
|
+
const types = new Set;
|
|
375
|
+
const body = [];
|
|
376
|
+
const ret = [];
|
|
377
|
+
const apiType = [];
|
|
378
|
+
const stateType = [];
|
|
379
|
+
for (const c of screen.components) {
|
|
380
|
+
const d = DESC.get(c.kind);
|
|
381
|
+
const k = keys.get(c.bindId);
|
|
382
|
+
const produce = d.slots.filter((s) => s.role === "produce");
|
|
383
|
+
if (produce.length)
|
|
384
|
+
stateType.push(` ${prop(k)}?: { ${produce.map((s) => `${prop(s.id)}?: ${tsType(s.schema, s.many)}`).join("; ")} };`);
|
|
385
|
+
const out = [];
|
|
386
|
+
const outT = [];
|
|
387
|
+
const unbound = (slot, v) => {
|
|
388
|
+
cx.use.add(v);
|
|
389
|
+
out.push(`${prop(slot)}: ${v}`);
|
|
390
|
+
};
|
|
391
|
+
for (const s of d.slots.filter((x) => x.role !== "produce")) {
|
|
392
|
+
const b = bindings.get(`${c.bindId}|${s.id}`);
|
|
393
|
+
const at = { component: c.bindId, slot: s.id };
|
|
394
|
+
const v = `${k}${pascal(s.id)}`;
|
|
395
|
+
if (s.role === "trigger") {
|
|
396
|
+
types.add("Action");
|
|
397
|
+
outT.push(`${prop(s.id)}: Action`);
|
|
398
|
+
if (b?.type !== "steps") {
|
|
399
|
+
unbound(s.id, "UNBOUND_ACTION");
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
cx.use.add("useAction");
|
|
403
|
+
body.push(` const ${v} = useAction(${compileSteps(b.steps, cx, at, ops, imports)});`);
|
|
404
|
+
out.push(`${prop(s.id)}: ${v}`);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const T = tsType(s.schema, s.many);
|
|
408
|
+
if (s.scope) {
|
|
409
|
+
types.add("Cell");
|
|
410
|
+
outT.push(`${prop(s.id)}: Cell<${T}>`);
|
|
411
|
+
if (b?.type !== "expression") {
|
|
412
|
+
unbound(s.id, "UNBOUND_CELL");
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
out.push(`${prop(s.id)}: { bound: true, read: (item: any) => ${compileExpr(b.value, cx, { item: true }, at)} }`);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
types.add("Data");
|
|
419
|
+
outT.push(`${prop(s.id)}: Data<${T}>`);
|
|
420
|
+
if (b?.type === "expression") {
|
|
421
|
+
out.push(`${prop(s.id)}: { bound: true, data: ${compileExpr(b.value, cx, { item: false }, at)}, loading: false, error: undefined }`);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const op = b?.type === "operation" ? ops.get(b.operation) : undefined;
|
|
425
|
+
if (b?.type === "operation" && !op)
|
|
426
|
+
cx.issue("catalog.operation-missing", `${b.operation} is not in the API docs.`, at);
|
|
427
|
+
if (b?.type !== "operation" || !op) {
|
|
428
|
+
unbound(s.id, "UNBOUND_DATA");
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const call = compileCall(op, b.request, cx, { item: false }, at);
|
|
432
|
+
imports.add(call.fn);
|
|
433
|
+
["useData", "ok"].forEach((h) => cx.use.add(h));
|
|
434
|
+
if (call.required)
|
|
435
|
+
cx.use.add("has");
|
|
436
|
+
const sel = selectPath(b.select).map(str);
|
|
437
|
+
if (sel.length)
|
|
438
|
+
cx.use.add("at");
|
|
439
|
+
const picked = sel.length ? `at(data, [${sel.join(", ")}])` : "data";
|
|
440
|
+
const map = Object.entries(b.map ?? {});
|
|
441
|
+
if (map.length)
|
|
442
|
+
cx.use.add("mapItems");
|
|
443
|
+
const select = map.length ? `mapItems(${picked}, (item: any) => ({ ${map.map(([f, x]) => `${prop(f)}: ${compileExpr(x, cx, { item: true }, at)}`).join(", ")} }))` : picked;
|
|
444
|
+
const every = typeof b.load === "object" ? `, everySeconds: ${b.load.everySeconds}` : "";
|
|
445
|
+
body.push(` const ${v} = useData<${T}>(${call.key}, async () => ok(await ${call.call}), (data: any) => ${select}, { enabled: ${call.required ? `has(${call.required})` : "true"}${every} });`);
|
|
446
|
+
out.push(`${prop(s.id)}: ${v}`);
|
|
447
|
+
}
|
|
448
|
+
if (out.length) {
|
|
449
|
+
ret.push(` ${prop(k)}: { ${out.join(", ")} },`);
|
|
450
|
+
apiType.push(` ${prop(k)}: { ${outT.join("; ")} };`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (screen.context.length)
|
|
454
|
+
stateType.push(` context?: { ${screen.context.map((c) => `${prop(c.id)}?: ${"schema" in c.shape ? tsType(c.shape.schema, c.many) : c.many ? "Array<any>" : "any"}`).join("; ")} };`);
|
|
455
|
+
const usesState = [...body, ...ret].some((l) => /\bstate\b/.test(l));
|
|
456
|
+
const lines = [
|
|
457
|
+
HEADER.trimEnd(),
|
|
458
|
+
imports.size ? `import { ${[...imports].sort().join(", ")} } from ${str(client ?? "")};` : null,
|
|
459
|
+
cx.use.size ? `import { ${[...cx.use].sort().join(", ")} } from "./screen-helpers";` : null,
|
|
460
|
+
types.size ? `import type { ${[...types].sort().join(", ")} } from "./screen-helpers";` : null,
|
|
461
|
+
"",
|
|
462
|
+
`/** ${screen.label}: values its components produce, and what it was opened with. */`,
|
|
463
|
+
stateType.length ? `export interface ${name}State {
|
|
464
|
+
${stateType.join(`
|
|
465
|
+
`)}
|
|
466
|
+
}` : `export type ${name}State = Record<string, never>;`,
|
|
467
|
+
"",
|
|
468
|
+
`export interface ${name}Api {
|
|
469
|
+
${apiType.join(`
|
|
470
|
+
`)}
|
|
471
|
+
}`,
|
|
472
|
+
"",
|
|
473
|
+
`/** API data and actions for ${screen.label} (${screen.id}). */`,
|
|
474
|
+
`export function use${name}Api(${usesState ? "state" : "_state"}: ${name}State = {}): ${name}Api {`,
|
|
475
|
+
...body,
|
|
476
|
+
` return {`,
|
|
477
|
+
...ret,
|
|
478
|
+
` };`,
|
|
479
|
+
`}`,
|
|
480
|
+
""
|
|
481
|
+
].filter((l) => l !== null);
|
|
482
|
+
files.push({ path: `${screen.id}.ts`, content: lines.join(`
|
|
483
|
+
`) });
|
|
484
|
+
exports.push(`export * from "./${screen.id}";`);
|
|
485
|
+
}
|
|
486
|
+
files.push({ path: "index.ts", content: HEADER + exports.join(`
|
|
487
|
+
`) + `
|
|
488
|
+
` });
|
|
489
|
+
return { files, issues };
|
|
323
490
|
}
|
|
324
491
|
export {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
BindingProvider2 as BindingProvider,
|
|
328
|
-
BindingScreen,
|
|
329
|
-
buildRequest,
|
|
492
|
+
CatalogLoadError2 as CatalogLoadError,
|
|
493
|
+
ID2 as ID,
|
|
330
494
|
catalogFromOpenApi2 as catalogFromOpenApi,
|
|
495
|
+
catalogFromText2 as catalogFromText,
|
|
331
496
|
createEngine2 as createEngine,
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
useSlot,
|
|
342
|
-
useTrigger,
|
|
497
|
+
fromFile2 as fromFile,
|
|
498
|
+
generate,
|
|
499
|
+
httpStore2 as httpStore,
|
|
500
|
+
loadCatalog2 as loadCatalog,
|
|
501
|
+
memoryStore2 as memoryStore,
|
|
502
|
+
parseBindingsFile2 as parseBindingsFile,
|
|
503
|
+
parse as parseExpression,
|
|
504
|
+
screenOf2 as screenOf,
|
|
505
|
+
toFile2 as toFile,
|
|
343
506
|
validateBindings2 as validateBindings,
|
|
344
507
|
validateRegistry2 as validateRegistry
|
|
345
508
|
};
|
|
346
509
|
|
|
347
|
-
//# debugId=
|
|
510
|
+
//# debugId=BEA7AA51ED34223764756E2164756E21
|
|
348
511
|
//# sourceMappingURL=index.js.map
|