@substrat-run/cli 0.26.4 → 0.27.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/dist/cli.js +78 -3
- package/dist/cli.js.map +1 -1
- package/dist/model.d.ts +64 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +358 -0
- package/dist/model.js.map +1 -0
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +16 -22
- package/dist/preview.js.map +1 -1
- package/dist/problem.d.ts.map +1 -1
- package/dist/problem.js +40 -4
- package/dist/problem.js.map +1 -1
- package/dist/push.d.ts +40 -2
- package/dist/push.d.ts.map +1 -1
- package/dist/push.js +228 -24
- package/dist/push.js.map +1 -1
- package/package.json +3 -3
package/dist/model.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { webcrypto } from 'node:crypto';
|
|
5
|
+
/**
|
|
6
|
+
* A directory or the file itself → the `model.json` to read.
|
|
7
|
+
*
|
|
8
|
+
* A directory is the common case (`substrat model view .` from inside a vertical), and the
|
|
9
|
+
* artifact's location is a convention `tools/model-diff.mts` owns: the package root.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveModelPath(target) {
|
|
12
|
+
const abs = isAbsolute(target) ? target : resolve(process.cwd(), target);
|
|
13
|
+
const file = existsSync(abs) && statSync(abs).isDirectory() ? join(abs, 'model.json') : abs;
|
|
14
|
+
if (!existsSync(file)) {
|
|
15
|
+
throw new Error(`no model.json at ${file}\n` +
|
|
16
|
+
" A vertical's model.json is emitted beside its package.json by `pnpm lint:model`.\n" +
|
|
17
|
+
' Pass the directory that holds it, or the file itself.');
|
|
18
|
+
}
|
|
19
|
+
return file;
|
|
20
|
+
}
|
|
21
|
+
/** Parse a `model.json`, refusing anything that is not one rather than rendering an empty page. */
|
|
22
|
+
export function readModel(file) {
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
throw new Error(`${file} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
29
|
+
}
|
|
30
|
+
const entities = parsed?.entities;
|
|
31
|
+
if (!entities || typeof entities !== 'object' || Array.isArray(entities)) {
|
|
32
|
+
throw new Error(`${file} has no 'entities' object — is it a model.json?`);
|
|
33
|
+
}
|
|
34
|
+
for (const [name, entity] of Object.entries(entities)) {
|
|
35
|
+
if (!entity || typeof entity !== 'object' || typeof entity.table !== 'string') {
|
|
36
|
+
throw new Error(`${file}: entity '${name}' declares no table — is it a model.json?`);
|
|
37
|
+
}
|
|
38
|
+
// The list-shaped declarations, checked here rather than where they are read: a
|
|
39
|
+
// `"parents": "list"` that got past this surfaces as a TypeError from inside the
|
|
40
|
+
// layout, which reads as a bug in the renderer instead of as a malformed input.
|
|
41
|
+
for (const listed of ['parents', 'primaryKey', 'key', 'erasable']) {
|
|
42
|
+
const value = entity[listed];
|
|
43
|
+
if (value === undefined)
|
|
44
|
+
continue;
|
|
45
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== 'string')) {
|
|
46
|
+
throw new Error(`${file}: entity '${name}' declares '${listed}' as something other than a list of field names`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const lifecycles = parsed.lifecycles;
|
|
51
|
+
if (lifecycles !== undefined) {
|
|
52
|
+
if (!lifecycles || typeof lifecycles !== 'object' || Array.isArray(lifecycles)) {
|
|
53
|
+
throw new Error(`${file}: 'lifecycles' is not an object keyed by entity`);
|
|
54
|
+
}
|
|
55
|
+
for (const [entity, lc] of Object.entries(lifecycles)) {
|
|
56
|
+
const states = lc?.states;
|
|
57
|
+
if (!lc || typeof lc !== 'object' || !states || typeof states !== 'object' || Array.isArray(states)) {
|
|
58
|
+
throw new Error(`${file}: the lifecycle for '${entity}' declares no states map`);
|
|
59
|
+
}
|
|
60
|
+
for (const field of ['field', 'initial']) {
|
|
61
|
+
if (typeof lc[field] !== 'string') {
|
|
62
|
+
throw new Error(`${file}: the lifecycle for '${entity}' declares '${field}' as something other than a name`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const [state, def] of Object.entries(states)) {
|
|
66
|
+
if (!def || typeof def !== 'object' || Array.isArray(def)) {
|
|
67
|
+
throw new Error(`${file}: state '${state}' of the '${entity}' lifecycle is not an object`);
|
|
68
|
+
}
|
|
69
|
+
const on = def.on;
|
|
70
|
+
if (on !== undefined) {
|
|
71
|
+
if (!on || typeof on !== 'object' || Array.isArray(on)) {
|
|
72
|
+
throw new Error(`${file}: state '${state}' of the '${entity}' lifecycle declares 'on' as something other than a map`);
|
|
73
|
+
}
|
|
74
|
+
// The targets too, not only the container: a transition to `1` renders as a
|
|
75
|
+
// TypeError out of `escapeHtml`, which is the same failure one level down.
|
|
76
|
+
for (const [op, target] of Object.entries(on)) {
|
|
77
|
+
if (typeof target !== 'string') {
|
|
78
|
+
throw new Error(`${file}: transition '${op}' from state '${state}' of the '${entity}' lifecycle names no target state`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
87
|
+
const escapeHtml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
88
|
+
/** The field names of an entity, in declaration order — the order the model reads in. */
|
|
89
|
+
function fieldNames(entity) {
|
|
90
|
+
const props = entity.fields?.properties;
|
|
91
|
+
return props ? Object.keys(props) : [];
|
|
92
|
+
}
|
|
93
|
+
/** A field's JSON-Schema type, rendered short. `string`, `number | null`, `object`. */
|
|
94
|
+
function fieldType(entity, field) {
|
|
95
|
+
const props = entity.fields?.properties ?? {};
|
|
96
|
+
const schema = props[field];
|
|
97
|
+
if (!schema)
|
|
98
|
+
return '';
|
|
99
|
+
const t = schema['type'];
|
|
100
|
+
if (typeof t === 'string')
|
|
101
|
+
return t;
|
|
102
|
+
if (Array.isArray(t))
|
|
103
|
+
return t.filter((x) => typeof x === 'string').join(' | ');
|
|
104
|
+
if (Array.isArray(schema['anyOf'])) {
|
|
105
|
+
const parts = schema['anyOf'].map((s) => typeof s['type'] === 'string' ? s['type'] : '…');
|
|
106
|
+
return [...new Set(parts)].join(' | ');
|
|
107
|
+
}
|
|
108
|
+
if (schema['enum'])
|
|
109
|
+
return 'enum';
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
const primaryKeyOf = (entity) => entity.primaryKey ?? ['id'];
|
|
113
|
+
/**
|
|
114
|
+
* Depth of each entity: 0 for one with no parents, else one past its deepest parent.
|
|
115
|
+
*
|
|
116
|
+
* `parents` is an allowlist and a diamond is normal (a reservation hangs off both a
|
|
117
|
+
* resource and a member), so this is a longest-path layering, not a tree. A cycle would
|
|
118
|
+
* make that non-terminating — the kernel does not forbid one — so a node already on the
|
|
119
|
+
* current path is treated as depth 0 and the layout stays a drawing rather than a hang.
|
|
120
|
+
*/
|
|
121
|
+
function layerDepths(entities) {
|
|
122
|
+
const depths = new Map();
|
|
123
|
+
const walk = (name, path) => {
|
|
124
|
+
const cached = depths.get(name);
|
|
125
|
+
if (cached !== undefined)
|
|
126
|
+
return cached;
|
|
127
|
+
if (path.has(name))
|
|
128
|
+
return 0;
|
|
129
|
+
const parents = (entities[name]?.parents ?? []).filter((p) => p in entities);
|
|
130
|
+
const next = new Set([...path, name]);
|
|
131
|
+
const depth = parents.length === 0 ? 0 : Math.max(...parents.map((p) => walk(p, next))) + 1;
|
|
132
|
+
depths.set(name, depth);
|
|
133
|
+
return depth;
|
|
134
|
+
};
|
|
135
|
+
for (const name of Object.keys(entities))
|
|
136
|
+
walk(name, new Set());
|
|
137
|
+
return depths;
|
|
138
|
+
}
|
|
139
|
+
const BOX_H = 46;
|
|
140
|
+
const ROW_GAP = 104;
|
|
141
|
+
const COL_GAP = 28;
|
|
142
|
+
const PAD = 20;
|
|
143
|
+
/** Where a depth band wraps — near enough a laptop's window that the labels stay readable. */
|
|
144
|
+
const MAX_WIDTH = 1080;
|
|
145
|
+
/**
|
|
146
|
+
* The ER diagram, as inline SVG with no `xmlns` — inside HTML it is parsed as SVG already,
|
|
147
|
+
* and the attribute's value is a URL, which the "nothing external" assertion reads as a
|
|
148
|
+
* reference whether a browser would fetch it or not.
|
|
149
|
+
*/
|
|
150
|
+
function renderDiagram(entities) {
|
|
151
|
+
const names = Object.keys(entities);
|
|
152
|
+
if (names.length === 0)
|
|
153
|
+
return '<p class="empty">This model declares no entities.</p>';
|
|
154
|
+
const depths = layerDepths(entities);
|
|
155
|
+
const rows = [];
|
|
156
|
+
for (const name of names) {
|
|
157
|
+
const d = depths.get(name) ?? 0;
|
|
158
|
+
(rows[d] ??= []).push(name);
|
|
159
|
+
}
|
|
160
|
+
// A depth band wider than MAX_WIDTH wraps onto further lines rather than growing the
|
|
161
|
+
// canvas: the SVG is scaled to the page width, so one 20-entity row would render every
|
|
162
|
+
// label too small to read. A wrapped line stays inside its band, and depth strictly
|
|
163
|
+
// increases along a parent edge, so an arrow still always points at an earlier line.
|
|
164
|
+
const boxes = new Map();
|
|
165
|
+
let width = 0;
|
|
166
|
+
let line = 0;
|
|
167
|
+
rows.forEach((row) => {
|
|
168
|
+
let x = PAD;
|
|
169
|
+
row.forEach((name, i) => {
|
|
170
|
+
const label = Math.max(name.length, (entities[name]?.table ?? '').length);
|
|
171
|
+
const w = Math.max(132, label * 8 + 28);
|
|
172
|
+
if (i > 0 && x + w + PAD > MAX_WIDTH) {
|
|
173
|
+
line += 1;
|
|
174
|
+
x = PAD;
|
|
175
|
+
}
|
|
176
|
+
boxes.set(name, { name, table: entities[name]?.table ?? '', x, y: PAD + line * ROW_GAP, w, h: BOX_H });
|
|
177
|
+
x += w + COL_GAP;
|
|
178
|
+
width = Math.max(width, x - COL_GAP + PAD);
|
|
179
|
+
});
|
|
180
|
+
line += 1;
|
|
181
|
+
});
|
|
182
|
+
const height = PAD * 2 + (line - 1) * ROW_GAP + BOX_H;
|
|
183
|
+
const edges = [];
|
|
184
|
+
for (const name of names) {
|
|
185
|
+
const child = boxes.get(name);
|
|
186
|
+
if (!child)
|
|
187
|
+
continue;
|
|
188
|
+
for (const parentName of entities[name]?.parents ?? []) {
|
|
189
|
+
const parent = boxes.get(parentName);
|
|
190
|
+
if (!parent)
|
|
191
|
+
continue; // a parent outside this model — the entity list below still names it
|
|
192
|
+
const x1 = child.x + child.w / 2;
|
|
193
|
+
const y1 = child.y;
|
|
194
|
+
const x2 = parent.x + parent.w / 2;
|
|
195
|
+
const y2 = parent.y + parent.h;
|
|
196
|
+
const mid = (y1 + y2) / 2;
|
|
197
|
+
edges.push(`<path class="edge" d="M ${x1} ${y1} C ${x1} ${mid}, ${x2} ${mid}, ${x2} ${y2}" marker-end="url(#arrow)">` +
|
|
198
|
+
`<title>${escapeHtml(name)} hangs off ${escapeHtml(parentName)}</title></path>`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const nodes = [...boxes.values()].map((b) => `<g class="node"><rect x="${b.x}" y="${b.y}" width="${b.w}" height="${b.h}" rx="8" />` +
|
|
202
|
+
`<text class="node-name" x="${b.x + b.w / 2}" y="${b.y + 20}">${escapeHtml(b.name)}</text>` +
|
|
203
|
+
`<text class="node-table" x="${b.x + b.w / 2}" y="${b.y + 36}">${escapeHtml(b.table)}</text></g>`);
|
|
204
|
+
return (`<svg class="er" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img" ` +
|
|
205
|
+
`aria-label="Entity relationship diagram">` +
|
|
206
|
+
`<defs><marker id="arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" ` +
|
|
207
|
+
`orient="auto-start-reverse"><path d="M 0 0 L 8 4 L 0 8 z" /></marker></defs>` +
|
|
208
|
+
`${edges.join('')}${nodes.join('')}</svg>`);
|
|
209
|
+
}
|
|
210
|
+
/** One entity's fields, with the primary key, the natural key and the erasable fields marked. */
|
|
211
|
+
function renderEntity(name, entity) {
|
|
212
|
+
const pk = new Set(primaryKeyOf(entity));
|
|
213
|
+
const key = new Set(entity.key ?? []);
|
|
214
|
+
const erasable = new Set(entity.erasable ?? []);
|
|
215
|
+
const required = new Set(entity.fields?.required ?? []);
|
|
216
|
+
const rows = fieldNames(entity).map((field) => {
|
|
217
|
+
const marks = [
|
|
218
|
+
pk.has(field) ? '<span class="mark pk" title="primary key">PK</span>' : '',
|
|
219
|
+
key.has(field) ? '<span class="mark key" title="natural key">KEY</span>' : '',
|
|
220
|
+
erasable.has(field) ? '<span class="mark erasable" title="reachable by an erasure">ERASABLE</span>' : '',
|
|
221
|
+
]
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
.join(' ');
|
|
224
|
+
return (`<tr><td class="field">${escapeHtml(field)}${required.has(field) ? '' : '<span class="opt">?</span>'}</td>` +
|
|
225
|
+
`<td class="type">${escapeHtml(fieldType(entity, field))}</td><td class="marks">${marks}</td></tr>`);
|
|
226
|
+
});
|
|
227
|
+
const parents = (entity.parents ?? []).map((p) => `<span class="pill">${escapeHtml(p)}</span>`).join(' ');
|
|
228
|
+
const meta = [
|
|
229
|
+
`<span class="meta-item">table <code>${escapeHtml(entity.table)}</code></span>`,
|
|
230
|
+
`<span class="meta-item">primary key <code>${escapeHtml(primaryKeyOf(entity).join(', '))}</code></span>`,
|
|
231
|
+
entity.key ? `<span class="meta-item">key <code>${escapeHtml(entity.key.join(', '))}</code></span>` : '',
|
|
232
|
+
parents ? `<span class="meta-item">hangs off ${parents}</span>` : '',
|
|
233
|
+
]
|
|
234
|
+
.filter(Boolean)
|
|
235
|
+
.join('');
|
|
236
|
+
return (`<section class="entity" id="entity-${escapeHtml(name)}"><h3>${escapeHtml(name)}</h3>` +
|
|
237
|
+
`<div class="meta">${meta}</div>` +
|
|
238
|
+
`<table class="fields"><tbody>${rows.join('')}</tbody></table></section>`);
|
|
239
|
+
}
|
|
240
|
+
/** The declared state machines, when the model carries any (#844). */
|
|
241
|
+
function renderLifecycles(lifecycles) {
|
|
242
|
+
const blocks = Object.entries(lifecycles).map(([entity, lc]) => {
|
|
243
|
+
const states = Object.entries(lc.states).map(([state, def]) => {
|
|
244
|
+
const edges = Object.entries(def.on ?? {}).map(([op, target]) => `<li><code>${escapeHtml(op)}</code> → ${escapeHtml(target)}</li>`);
|
|
245
|
+
const initial = state === lc.initial ? '<span class="mark pk" title="initial state">INITIAL</span>' : '';
|
|
246
|
+
return (`<div class="state"><h4>${escapeHtml(state)} ${initial}</h4>` +
|
|
247
|
+
`${edges.length ? `<ul>${edges.join('')}</ul>` : '<p class="empty">terminal</p>'}</div>`);
|
|
248
|
+
});
|
|
249
|
+
return (`<section class="entity"><h3>${escapeHtml(entity)}<span class="opt"> · ${escapeHtml(lc.field)}</span></h3>` +
|
|
250
|
+
`<div class="states">${states.join('')}</div></section>`);
|
|
251
|
+
});
|
|
252
|
+
return `<h2>Lifecycles</h2><div class="entities">${blocks.join('')}</div>`;
|
|
253
|
+
}
|
|
254
|
+
const STYLE = `
|
|
255
|
+
:root { color-scheme: light dark; --fg: #16181d; --dim: #5b6272; --line: #d7dbe3; --bg: #fbfbfd;
|
|
256
|
+
--card: #ffffff; --accent: #2f5bd7; --warn: #b2542b; }
|
|
257
|
+
@media (prefers-color-scheme: dark) {
|
|
258
|
+
:root { --fg: #e8eaf0; --dim: #9aa3b5; --line: #333844; --bg: #14161b; --card: #1b1e25;
|
|
259
|
+
--accent: #7fa2ff; --warn: #e2986a; }
|
|
260
|
+
}
|
|
261
|
+
* { box-sizing: border-box; }
|
|
262
|
+
body { margin: 0; padding: 32px; background: var(--bg); color: var(--fg);
|
|
263
|
+
font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
264
|
+
header { margin-bottom: 24px; }
|
|
265
|
+
h1 { margin: 0 0 4px; font-size: 22px; }
|
|
266
|
+
h2 { margin: 32px 0 12px; font-size: 16px; text-transform: uppercase; letter-spacing: .08em; color: var(--dim); }
|
|
267
|
+
h3 { margin: 0 0 8px; font-size: 15px; }
|
|
268
|
+
h4 { margin: 0 0 4px; font-size: 13px; }
|
|
269
|
+
.sub { color: var(--dim); font-size: 13px; }
|
|
270
|
+
code { font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
271
|
+
.diagram { overflow-x: auto; padding: 8px 0; }
|
|
272
|
+
svg.er { max-width: 100%; height: auto; }
|
|
273
|
+
.er rect { fill: var(--card); stroke: var(--line); stroke-width: 1.5; }
|
|
274
|
+
.er text { text-anchor: middle; fill: var(--fg); font: 13px ui-sans-serif, system-ui, sans-serif; }
|
|
275
|
+
.er .node-table { fill: var(--dim); font: 11px ui-monospace, Menlo, monospace; }
|
|
276
|
+
.er .edge { fill: none; stroke: var(--accent); stroke-width: 1.5; }
|
|
277
|
+
.er marker path { fill: var(--accent); }
|
|
278
|
+
.entities { display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
|
|
279
|
+
.entity { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; }
|
|
280
|
+
.meta { color: var(--dim); font-size: 12px; margin-bottom: 10px; }
|
|
281
|
+
.meta-item { display: inline-block; margin-right: 12px; }
|
|
282
|
+
.pill { display: inline-block; border: 1px solid var(--line); border-radius: 999px; padding: 0 8px; }
|
|
283
|
+
table.fields { width: 100%; border-collapse: collapse; }
|
|
284
|
+
table.fields td { border-top: 1px solid var(--line); padding: 4px 0; vertical-align: top; }
|
|
285
|
+
td.field { font: 12px ui-monospace, Menlo, monospace; }
|
|
286
|
+
td.type { color: var(--dim); font: 12px ui-monospace, Menlo, monospace; width: 30%; }
|
|
287
|
+
td.marks { text-align: right; white-space: nowrap; }
|
|
288
|
+
.opt { color: var(--dim); }
|
|
289
|
+
.mark { font-size: 10px; letter-spacing: .06em; border-radius: 4px; padding: 1px 5px; margin-left: 4px;
|
|
290
|
+
border: 1px solid var(--line); color: var(--dim); }
|
|
291
|
+
.mark.pk { color: var(--accent); border-color: var(--accent); }
|
|
292
|
+
.mark.erasable { color: var(--warn); border-color: var(--warn); }
|
|
293
|
+
.states { display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
|
|
294
|
+
.state ul { margin: 0; padding-left: 16px; }
|
|
295
|
+
.empty { color: var(--dim); }
|
|
296
|
+
footer { margin-top: 32px; color: var(--dim); font-size: 12px; }
|
|
297
|
+
`;
|
|
298
|
+
/** Render the whole view. Pure — the file-system half is `writeModelView`. */
|
|
299
|
+
export function renderModelHtml(model, opts) {
|
|
300
|
+
const entities = model.entities;
|
|
301
|
+
const names = Object.keys(entities);
|
|
302
|
+
const cards = names.map((name) => renderEntity(name, entities[name]));
|
|
303
|
+
const lifecycles = model.lifecycles && Object.keys(model.lifecycles).length ? renderLifecycles(model.lifecycles) : '';
|
|
304
|
+
const erasable = names.filter((n) => (entities[n]?.erasable ?? []).length > 0).length;
|
|
305
|
+
return `<!doctype html>
|
|
306
|
+
<html lang="en">
|
|
307
|
+
<head>
|
|
308
|
+
<meta charset="utf-8" />
|
|
309
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
310
|
+
<title>Entity model — ${escapeHtml(basename(dirname(opts.source)) || 'substrat')}</title>
|
|
311
|
+
<style>${STYLE}</style>
|
|
312
|
+
</head>
|
|
313
|
+
<body>
|
|
314
|
+
<!-- Rendered by \`substrat model view\` from ${escapeHtml(opts.source)} — regenerate, do not edit. -->
|
|
315
|
+
<header>
|
|
316
|
+
<h1>Entity model</h1>
|
|
317
|
+
<p class="sub">${names.length} entities · ${erasable} with erasable fields · from <code>${escapeHtml(opts.source)}</code></p>
|
|
318
|
+
</header>
|
|
319
|
+
<div class="diagram">${renderDiagram(entities)}</div>
|
|
320
|
+
<h2>Entities</h2>
|
|
321
|
+
<div class="entities">${cards.join('')}</div>
|
|
322
|
+
${lifecycles}
|
|
323
|
+
<footer>An arrow points from an entity to a parent it may hang off — the allowlist <code>ctx.link</code> checks.
|
|
324
|
+
Marks: PK primary key · KEY natural key · ERASABLE reachable by an erasure. <code>?</code> marks an optional field.</footer>
|
|
325
|
+
</body>
|
|
326
|
+
</html>
|
|
327
|
+
`;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Default output path: a temp file named for the model's directory.
|
|
331
|
+
*
|
|
332
|
+
* Deliberately not beside `model.json`: a view written into the project would be an
|
|
333
|
+
* un-gated generated file in someone's repo, and this one is a thing you look at, not a
|
|
334
|
+
* thing you commit. Stable across runs, so a re-render replaces the tab you already have
|
|
335
|
+
* open rather than leaving a trail of files behind.
|
|
336
|
+
*
|
|
337
|
+
* The directory's basename alone is not enough to be stable AND distinct — a monorepo with
|
|
338
|
+
* `apps/a/api` and `apps/b/api` would have the second render silently replace the first
|
|
339
|
+
* one's view — so the full resolved directory is hashed into the name.
|
|
340
|
+
*/
|
|
341
|
+
export async function defaultOutPath(modelFile) {
|
|
342
|
+
const dir = dirname(resolve(modelFile));
|
|
343
|
+
const label = basename(dir).replace(/[^a-zA-Z0-9._-]/g, '-') || 'model';
|
|
344
|
+
const digest = await webcrypto.subtle.digest('SHA-256', new TextEncoder().encode(dir));
|
|
345
|
+
const short = Buffer.from(digest).toString('hex').slice(0, 8);
|
|
346
|
+
return join(tmpdir(), 'substrat-model', `${label}-${short}.html`);
|
|
347
|
+
}
|
|
348
|
+
/** Read, render, write. Returns the absolute path — the thing worth printing. */
|
|
349
|
+
export async function writeModelView(target, opts = {}) {
|
|
350
|
+
const modelFile = resolveModelPath(target);
|
|
351
|
+
const model = readModel(modelFile);
|
|
352
|
+
const html = renderModelHtml(model, { source: modelFile });
|
|
353
|
+
const file = opts.out ? resolve(process.cwd(), opts.out) : await defaultOutPath(modelFile);
|
|
354
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
355
|
+
writeFileSync(file, html);
|
|
356
|
+
return { file, entities: Object.keys(model.entities).length };
|
|
357
|
+
}
|
|
358
|
+
//# sourceMappingURL=model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
|
package/dist/preview.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAkBD;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,GAAG,OAAO,CAAC,cAAc,CAAC,CAoB1B;AAED;wEACwE;AACxE,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAQtC;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACvC,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAOxB;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,MAAM,CAazD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAQhF"}
|
package/dist/preview.js
CHANGED
|
@@ -11,28 +11,22 @@
|
|
|
11
11
|
* tenant-scoped push token CI already carries.
|
|
12
12
|
*/
|
|
13
13
|
import { warnIfStale } from './version.js';
|
|
14
|
-
import {
|
|
15
|
-
|
|
14
|
+
import { parseJsonBody } from './http.js';
|
|
15
|
+
import { failureMessage } from './problem.js';
|
|
16
|
+
/**
|
|
17
|
+
* One request, and one reader for what a refusal said (#971).
|
|
18
|
+
*
|
|
19
|
+
* `failureMessage` is the CLI's shared reader: a problem document, the deprecated
|
|
20
|
+
* `{ error }` duplicate, and the pre-#113 `{ error, issues }` Zod refusal all arrive as
|
|
21
|
+
* the same message here as they do from `push` or `promote` — so a preview 400 names the
|
|
22
|
+
* field it refused, and which command a builder ran stops changing the shape of the answer.
|
|
23
|
+
*/
|
|
24
|
+
async function request(action, url, header, init) {
|
|
16
25
|
const res = await fetch(url, { ...init, headers: { 'content-type': 'application/json', ...header } });
|
|
17
26
|
warnIfStale(res.headers);
|
|
18
27
|
const body = await res.text();
|
|
19
|
-
if (!res.ok)
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const parsed = JSON.parse(body);
|
|
23
|
-
message = parsed.error ?? message;
|
|
24
|
-
// A control-plane Zod refusal (`{ error: 'invalid request', issues }`) is useless
|
|
25
|
-
// without the issues: 'invalid request' alone gives the operator nothing to fix.
|
|
26
|
-
// Append the failing path(s) so a preview 400 names the field instead of hiding it.
|
|
27
|
-
if (Array.isArray(parsed.issues) && parsed.issues.length > 0) {
|
|
28
|
-
message += ` — ${JSON.stringify(parsed.issues)}`;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
// Not JSON — the raw body is the message.
|
|
33
|
-
}
|
|
34
|
-
throw new Error(`${res.status}: ${message}${explainPlatformFault(res.status, message)}`);
|
|
35
|
-
}
|
|
28
|
+
if (!res.ok)
|
|
29
|
+
throw new Error(failureMessage(action, res.status, body));
|
|
36
30
|
return parseJsonBody(body, url);
|
|
37
31
|
}
|
|
38
32
|
/**
|
|
@@ -43,7 +37,7 @@ async function request(url, header, init) {
|
|
|
43
37
|
*/
|
|
44
38
|
export async function createPreview(opts) {
|
|
45
39
|
const base = opts.controlPlaneUrl.replace(/\/$/, '');
|
|
46
|
-
return request(`${base}/verticals/${encodeURIComponent(opts.slug)}/previews`, opts.header, {
|
|
40
|
+
return request('preview create failed', `${base}/verticals/${encodeURIComponent(opts.slug)}/previews`, opts.header, {
|
|
47
41
|
method: 'POST',
|
|
48
42
|
body: JSON.stringify({
|
|
49
43
|
tag: opts.tag,
|
|
@@ -61,11 +55,11 @@ export async function createPreview(opts) {
|
|
|
61
55
|
* PR-close job never fails because the preview was already removed. */
|
|
62
56
|
export async function deletePreview(opts) {
|
|
63
57
|
const base = opts.controlPlaneUrl.replace(/\/$/, '');
|
|
64
|
-
return request(`${base}/verticals/${encodeURIComponent(opts.slug)}/previews/${encodeURIComponent(opts.tag)}`, opts.header, { method: 'DELETE' });
|
|
58
|
+
return request('preview delete failed', `${base}/verticals/${encodeURIComponent(opts.slug)}/previews/${encodeURIComponent(opts.tag)}`, opts.header, { method: 'DELETE' });
|
|
65
59
|
}
|
|
66
60
|
export async function listPreviews(opts) {
|
|
67
61
|
const base = opts.controlPlaneUrl.replace(/\/$/, '');
|
|
68
|
-
return request(`${base}/verticals/${encodeURIComponent(opts.slug)}/previews`, opts.header);
|
|
62
|
+
return request('preview list failed', `${base}/verticals/${encodeURIComponent(opts.slug)}/previews`, opts.header);
|
|
69
63
|
}
|
|
70
64
|
/** Render previews as an aligned table (the `substrat preview ls` output). */
|
|
71
65
|
export function formatPreviews(rows) {
|
package/dist/preview.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preview.js","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"preview.js","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"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"}
|
package/dist/problem.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"problem.d.ts","sourceRoot":"","sources":["../src/problem.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"problem.d.ts","sourceRoot":"","sources":["../src/problem.ts"],"names":[],"mappings":"AA2BA,yCAAyC;AACzC,MAAM,WAAW,cAAc;IAC7B,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACnD;AAyBD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CA4CxD;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMnF"}
|
package/dist/problem.js
CHANGED
|
@@ -14,13 +14,40 @@
|
|
|
14
14
|
*
|
|
15
15
|
* 1. a problem document — `detail` (else `title`), the `code`, and the field errors;
|
|
16
16
|
* 2. `{ error }` — the pre-#113 body, still what an older deployed control plane and
|
|
17
|
-
* several hand-rolled `onError`s answer with
|
|
17
|
+
* several hand-rolled `onError`s answer with, including its top-level `issues` array
|
|
18
|
+
* (the raw Zod refusal: `{ error: 'invalid request', issues }`, where the `error`
|
|
19
|
+
* alone names nothing a builder can fix);
|
|
18
20
|
* 3. anything else — a slice of the raw body, which is at least the truth.
|
|
19
21
|
*/
|
|
20
22
|
import { problem as problemSchema } from '@substrat-run/contracts';
|
|
21
23
|
import { explainPlatformFault } from './http.js';
|
|
22
24
|
/** How much of an unrecognised body is worth printing before it stops being a message. */
|
|
23
25
|
const RAW_BODY_LIMIT = 300;
|
|
26
|
+
/**
|
|
27
|
+
* The pre-#113 validation body's field complaints, in the shape a problem document uses.
|
|
28
|
+
*
|
|
29
|
+
* A control plane that has not adopted `toProblem` answers a Zod refusal with
|
|
30
|
+
* `{ error: 'invalid request', issues: [...] }` — the sentence says nothing and the array
|
|
31
|
+
* says everything. `issues` entries are Zod's own (`path: ['tag']`, `message`), so they
|
|
32
|
+
* map onto `errors` one for one; an entry in some other shape is kept as its own text
|
|
33
|
+
* rather than dropped, because a message nobody can read still beats one nobody sees.
|
|
34
|
+
*/
|
|
35
|
+
function legacyIssues(parsed) {
|
|
36
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
37
|
+
return undefined;
|
|
38
|
+
const raw = parsed.issues;
|
|
39
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
40
|
+
return undefined;
|
|
41
|
+
return raw.map((issue) => {
|
|
42
|
+
if (issue !== null && typeof issue === 'object') {
|
|
43
|
+
const o = issue;
|
|
44
|
+
const path = Array.isArray(o.path) ? o.path.join('.') : typeof o.path === 'string' ? o.path : '';
|
|
45
|
+
if (typeof o.message === 'string' && o.message)
|
|
46
|
+
return { path, message: o.message };
|
|
47
|
+
}
|
|
48
|
+
return { path: '', message: typeof issue === 'string' ? issue : JSON.stringify(issue) };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
24
51
|
/**
|
|
25
52
|
* Parse a response body into what it actually says.
|
|
26
53
|
*
|
|
@@ -41,14 +68,18 @@ export function readProblem(body) {
|
|
|
41
68
|
catch {
|
|
42
69
|
return { detail: raw.slice(0, RAW_BODY_LIMIT) };
|
|
43
70
|
}
|
|
71
|
+
// The field complaints, wherever this body happens to carry them: `errors` is the
|
|
72
|
+
// contract's member, `issues` the pre-#113 one, and a body may carry either.
|
|
73
|
+
const issues = legacyIssues(parsed);
|
|
44
74
|
const strict = problemSchema.safeParse(parsed);
|
|
45
75
|
if (strict.success) {
|
|
46
76
|
const p = strict.data;
|
|
77
|
+
const errors = p.errors && p.errors.length > 0 ? p.errors : issues;
|
|
47
78
|
return {
|
|
48
79
|
detail: p.detail ?? p.error ?? p.title,
|
|
49
80
|
code: p.code,
|
|
50
81
|
title: p.title,
|
|
51
|
-
...(
|
|
82
|
+
...(errors ? { errors } : {}),
|
|
52
83
|
};
|
|
53
84
|
}
|
|
54
85
|
// Not a whole problem document. Read the members that ARE there, in the order of
|
|
@@ -58,8 +89,13 @@ export function readProblem(body) {
|
|
|
58
89
|
const o = parsed;
|
|
59
90
|
const str = (k) => (typeof o[k] === 'string' && o[k] ? o[k] : undefined);
|
|
60
91
|
const detail = str('detail') ?? str('error') ?? str('message') ?? str('title');
|
|
61
|
-
if (detail !== undefined) {
|
|
62
|
-
return {
|
|
92
|
+
if (detail !== undefined || issues) {
|
|
93
|
+
return {
|
|
94
|
+
detail: detail ?? '',
|
|
95
|
+
...(str('code') ? { code: str('code') } : {}),
|
|
96
|
+
...(str('title') ? { title: str('title') } : {}),
|
|
97
|
+
...(issues ? { errors: issues } : {}),
|
|
98
|
+
};
|
|
63
99
|
}
|
|
64
100
|
}
|
|
65
101
|
return { detail: raw.slice(0, RAW_BODY_LIMIT) };
|
package/dist/problem.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"problem.js","sourceRoot":"","sources":["../src/problem.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"problem.js","sourceRoot":"","sources":["../src/problem.ts"],"names":[],"mappings":"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"}
|