@yuneta/gobj-ui 7.23.195 → 7.23.197
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 +42 -11
- package/dist/gobj-ui.cjs.js +336 -161
- package/dist/gobj-ui.es.js +336 -161
- package/package.json +1 -1
- package/src/c_yui_schema_editor.js +57 -122
- package/src/schema_to_c.js +49 -9
- package/src/schema_to_c.test.js +21 -3
- package/src/schema_to_diagram.js +301 -0
- package/src/schema_to_diagram.test.js +107 -0
- package/src/schema_validate.js +5 -17
- package/src/schema_validate.test.js +4 -24
- package/src/schema_version_bump.test.js +14 -71
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* schema_to_diagram.js
|
|
3
|
+
*
|
|
4
|
+
* The graph of a treedb schema, drawn as text.
|
|
5
|
+
*
|
|
6
|
+
* It is the comment that heads every `treedb_schema_<db>.c`: one
|
|
7
|
+
* box per topic, in the order the schema declares them, a row per
|
|
8
|
+
* column, and every hook -> fkey link drawn as a line from the
|
|
9
|
+
* hook's row (`◀`) to the fkey's row. It is DERIVED from the
|
|
10
|
+
* schema, never drawn by hand, so the file that holds the literal
|
|
11
|
+
* can be replaced whole with an export and its picture is never
|
|
12
|
+
* behind it.
|
|
13
|
+
*
|
|
14
|
+
* Each link runs down its own LANE to the right of the boxes, the
|
|
15
|
+
* shorter links nearer to them, so no search is needed to lay it
|
|
16
|
+
* out: the only ambiguity left is where a line crosses a lane that
|
|
17
|
+
* is in use, and that is drawn as a junction.
|
|
18
|
+
*
|
|
19
|
+
* The input is the schema as its literal holds it (what
|
|
20
|
+
* schema_to_json() answers, or the parsed literal itself):
|
|
21
|
+
* {id, schema_version, topics: [{id, pkey2s, tkey, main_topic,
|
|
22
|
+
* cols: {name: {type, flag, hook}}}]}
|
|
23
|
+
*
|
|
24
|
+
* Copyright (c) 2026, ArtGins.
|
|
25
|
+
* All Rights Reserved.
|
|
26
|
+
***********************************************************************/
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/* Where the boxes start, and their narrowest inside: the widths of
|
|
30
|
+
* the diagrams drawn by hand before this, so a generated one reads
|
|
31
|
+
* like them. */
|
|
32
|
+
const INDENT = 8;
|
|
33
|
+
const MIN_INNER = 27;
|
|
34
|
+
|
|
35
|
+
/* From a box's right edge to its first lane, and between lanes. */
|
|
36
|
+
const LANE_GAP = 4;
|
|
37
|
+
|
|
38
|
+
const LEGEND = [
|
|
39
|
+
"{} dict hook (N unique children)",
|
|
40
|
+
"[] list hook (n not-unique children)",
|
|
41
|
+
"() string hook (1 unique child)",
|
|
42
|
+
"(↖) 1 fkey (1 parent)",
|
|
43
|
+
"[↖] n fkeys (n parents)",
|
|
44
|
+
"{↖} N fkeys (N parents)",
|
|
45
|
+
"",
|
|
46
|
+
"(2) pkey2 - secondary key",
|
|
47
|
+
"(t) tkey - time key",
|
|
48
|
+
"* field required",
|
|
49
|
+
"= field inherited",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/* A cell of a line is a set of the directions it joins; the glyph is
|
|
53
|
+
* what those directions draw. */
|
|
54
|
+
const L = 1, R = 2, U = 4, D = 8;
|
|
55
|
+
const GLYPH = {
|
|
56
|
+
[L|R]: "─", [L]: "─", [R]: "─",
|
|
57
|
+
[U|D]: "│", [U]: "│", [D]: "│",
|
|
58
|
+
[R|D]: "┌", [L|D]: "┐", [R|U]: "└", [L|U]: "┘",
|
|
59
|
+
[L|R|D]: "┬", [L|R|U]: "┴", [U|D|R]: "├", [U|D|L]: "┤",
|
|
60
|
+
[L|R|U|D]: "┼",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
/***************************************************************
|
|
65
|
+
* A value that may be one string or a list of them, as a list.
|
|
66
|
+
***************************************************************/
|
|
67
|
+
function as_list(value)
|
|
68
|
+
{
|
|
69
|
+
if(typeof value === "string") {
|
|
70
|
+
return value ? [value] : [];
|
|
71
|
+
}
|
|
72
|
+
if(Array.isArray(value)) {
|
|
73
|
+
let list = [];
|
|
74
|
+
for(let v of value) {
|
|
75
|
+
list.push(...as_list(v));
|
|
76
|
+
}
|
|
77
|
+
return list;
|
|
78
|
+
}
|
|
79
|
+
if(value && typeof value === "object") {
|
|
80
|
+
let list = [];
|
|
81
|
+
for(let v of Object.values(value)) {
|
|
82
|
+
list.push(...as_list(v));
|
|
83
|
+
}
|
|
84
|
+
return list;
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/***************************************************************
|
|
90
|
+
* The columns of a topic as [name, desc] in declared order: a
|
|
91
|
+
* dict in the literals, a list of {id} in some stores.
|
|
92
|
+
***************************************************************/
|
|
93
|
+
function topic_cols(topic)
|
|
94
|
+
{
|
|
95
|
+
let cols = topic.cols || {};
|
|
96
|
+
if(Array.isArray(cols)) {
|
|
97
|
+
return cols.map((c) => [c.id, c]);
|
|
98
|
+
}
|
|
99
|
+
return Object.entries(cols);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/***************************************************************
|
|
103
|
+
* What a link column looks like at its end of the line.
|
|
104
|
+
***************************************************************/
|
|
105
|
+
function hook_marker(type)
|
|
106
|
+
{
|
|
107
|
+
if(type === "dict" || type === "object") {
|
|
108
|
+
return "{}";
|
|
109
|
+
}
|
|
110
|
+
if(type === "array" || type === "list") {
|
|
111
|
+
return "[]";
|
|
112
|
+
}
|
|
113
|
+
return "()";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function fkey_marker(type)
|
|
117
|
+
{
|
|
118
|
+
if(type === "dict" || type === "object") {
|
|
119
|
+
return "{↖}";
|
|
120
|
+
}
|
|
121
|
+
if(type === "array" || type === "list") {
|
|
122
|
+
return "[↖]";
|
|
123
|
+
}
|
|
124
|
+
return "(↖)";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/***************************************************************
|
|
128
|
+
* One row of a box: the text inside it, and whether it is a
|
|
129
|
+
* link column (written against the right edge, where its line
|
|
130
|
+
* leaves the box).
|
|
131
|
+
***************************************************************/
|
|
132
|
+
function col_row(topic, name, desc, pkey2s)
|
|
133
|
+
{
|
|
134
|
+
let flags = as_list(desc.flag);
|
|
135
|
+
if(flags.indexOf("hook") >= 0) {
|
|
136
|
+
return {text: `${name} ${hook_marker(desc.type)}`, link: true};
|
|
137
|
+
}
|
|
138
|
+
if(flags.indexOf("fkey") >= 0) {
|
|
139
|
+
return {text: `${name} ${fkey_marker(desc.type)}`, link: true};
|
|
140
|
+
}
|
|
141
|
+
let mark = flags.indexOf("required") >= 0 ? "*" :
|
|
142
|
+
flags.indexOf("inherit") >= 0 ? "=" : " ";
|
|
143
|
+
let text = `${mark} ${name}`;
|
|
144
|
+
if(pkey2s.indexOf(name) >= 0) {
|
|
145
|
+
text += " (2)";
|
|
146
|
+
}
|
|
147
|
+
if(topic.tkey && topic.tkey === name) {
|
|
148
|
+
text += " (t)";
|
|
149
|
+
}
|
|
150
|
+
return {text: text, link: false};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/***************************************************************
|
|
154
|
+
* schema_to_diagram(schema) -> the text, one string with a
|
|
155
|
+
* newline per line and no trailing spaces.
|
|
156
|
+
***************************************************************/
|
|
157
|
+
function schema_to_diagram(schema)
|
|
158
|
+
{
|
|
159
|
+
if(!schema || !Array.isArray(schema.topics)) {
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/*
|
|
164
|
+
* The boxes, and where each column landed
|
|
165
|
+
*/
|
|
166
|
+
let boxes = [];
|
|
167
|
+
let inner = MIN_INNER;
|
|
168
|
+
for(let topic of schema.topics) {
|
|
169
|
+
let pkey2s = as_list(topic.pkey2s);
|
|
170
|
+
let rows = topic_cols(topic).map(([name, desc]) =>
|
|
171
|
+
Object.assign({name: name}, col_row(topic, name, desc || {}, pkey2s))
|
|
172
|
+
);
|
|
173
|
+
for(let row of rows) {
|
|
174
|
+
inner = Math.max(inner, row.text.length + 2);
|
|
175
|
+
}
|
|
176
|
+
let title = topic.id + (topic.main_topic ? " (main_topic)" : "");
|
|
177
|
+
inner = Math.max(inner, title.length);
|
|
178
|
+
boxes.push({topic: topic, title: title, rows: rows});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let lines = [];
|
|
182
|
+
let where = {}; /* "topic`col" -> line index */
|
|
183
|
+
let edge = INDENT + inner + 1;
|
|
184
|
+
|
|
185
|
+
lines.push(`${schema.id || ""} (schema_version ${schema.schema_version || "?"})`);
|
|
186
|
+
lines.push("");
|
|
187
|
+
for(let l of LEGEND) {
|
|
188
|
+
lines.push(l);
|
|
189
|
+
}
|
|
190
|
+
lines.push("");
|
|
191
|
+
lines.push("");
|
|
192
|
+
|
|
193
|
+
for(let box of boxes) {
|
|
194
|
+
let pad = Math.max(0, Math.floor((inner + 2 - box.title.length) / 2));
|
|
195
|
+
lines.push(" ".repeat(INDENT + pad) + box.title);
|
|
196
|
+
lines.push(" ".repeat(INDENT) + "┌" + "─".repeat(inner) + "┐");
|
|
197
|
+
for(let row of box.rows) {
|
|
198
|
+
let text = row.link ?
|
|
199
|
+
row.text.padStart(inner - 1) + " " :
|
|
200
|
+
row.text.padEnd(inner);
|
|
201
|
+
where[`${box.topic.id}\`${row.name}`] = lines.length;
|
|
202
|
+
lines.push(" ".repeat(INDENT) + "│" + text + "│");
|
|
203
|
+
}
|
|
204
|
+
lines.push(" ".repeat(INDENT) + "└" + "─".repeat(inner) + "┘");
|
|
205
|
+
lines.push("");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/*
|
|
209
|
+
* The links: from a hook's row to the row of the fkey it fills
|
|
210
|
+
*/
|
|
211
|
+
let links = [];
|
|
212
|
+
for(let topic of schema.topics) {
|
|
213
|
+
for(let [name, desc] of topic_cols(topic)) {
|
|
214
|
+
desc = desc || {};
|
|
215
|
+
if(as_list(desc.flag).indexOf("hook") < 0) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
let hook = desc.hook;
|
|
219
|
+
if(!hook || typeof hook !== "object" || Array.isArray(hook)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
for(let [child_topic, fkey_col] of Object.entries(hook)) {
|
|
223
|
+
let from = where[`${topic.id}\`${name}`];
|
|
224
|
+
let to = where[`${child_topic}\`${fkey_col}`];
|
|
225
|
+
if(from === undefined || to === undefined) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
links.push({from: from, to: to,
|
|
229
|
+
top: Math.min(from, to), bottom: Math.max(from, to)});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* Shorter links nearer the boxes: a lane holds links whose rows
|
|
235
|
+
* do not overlap. */
|
|
236
|
+
links.sort((a, b) => (a.bottom - a.top) - (b.bottom - b.top) || a.top - b.top);
|
|
237
|
+
let lanes = [];
|
|
238
|
+
for(let link of links) {
|
|
239
|
+
let k = 0;
|
|
240
|
+
for(;; k++) {
|
|
241
|
+
let used = lanes[k] || [];
|
|
242
|
+
let free = used.every((o) => link.bottom < o.top || o.bottom < link.top);
|
|
243
|
+
if(free) {
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
lanes[k] = (lanes[k] || []).concat([link]);
|
|
248
|
+
link.x = edge + LANE_GAP + LANE_GAP * k;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/*
|
|
252
|
+
* Draw them on a grid of direction bits, then as glyphs
|
|
253
|
+
*/
|
|
254
|
+
let bits = {};
|
|
255
|
+
let fixed = {};
|
|
256
|
+
let join = (y, x, b) => {
|
|
257
|
+
let key = `${y},${x}`;
|
|
258
|
+
bits[key] = (bits[key] || 0) | b;
|
|
259
|
+
};
|
|
260
|
+
let hline = (y, x1, x2) => {
|
|
261
|
+
for(let x = x1; x <= x2; x++) {
|
|
262
|
+
join(y, x, (x > x1 ? L : 0) | (x < x2 ? R : 0));
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
let vline = (x, y1, y2) => {
|
|
266
|
+
for(let y = y1; y <= y2; y++) {
|
|
267
|
+
join(y, x, (y > y1 ? U : 0) | (y < y2 ? D : 0));
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
for(let link of links) {
|
|
271
|
+
hline(link.from, edge + 2, link.x);
|
|
272
|
+
hline(link.to, edge + 2, link.x);
|
|
273
|
+
vline(link.x, link.top, link.bottom);
|
|
274
|
+
fixed[`${link.from},${edge + 2}`] = "◀";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let out = lines.map((line, y) => {
|
|
278
|
+
let chars = Array.from(line);
|
|
279
|
+
for(let key of Object.keys(bits)) {
|
|
280
|
+
let [ky, kx] = key.split(",").map(Number);
|
|
281
|
+
if(ky !== y) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
while(chars.length <= kx) {
|
|
285
|
+
chars.push(" ");
|
|
286
|
+
}
|
|
287
|
+
chars[kx] = fixed[key] || GLYPH[bits[key]] || " ";
|
|
288
|
+
}
|
|
289
|
+
return chars.join("").replace(/\s+$/, "");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
while(out.length && out[out.length - 1] === "") {
|
|
293
|
+
out.pop();
|
|
294
|
+
}
|
|
295
|
+
return out.join("\n");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
export {
|
|
300
|
+
schema_to_diagram,
|
|
301
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* schema_to_diagram.test.js
|
|
3
|
+
*
|
|
4
|
+
* The graph of a schema as text, pinned on a small schema that has
|
|
5
|
+
* every case: a self-referent hook, a hook to another topic, one
|
|
6
|
+
* hook filling two topics, a pkey2, a tkey, required and inherited
|
|
7
|
+
* fields, and the main topic.
|
|
8
|
+
***********************************************************************/
|
|
9
|
+
import { describe, test, expect } from "vitest";
|
|
10
|
+
import { schema_to_diagram } from "./schema_to_diagram.js";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const SCHEMA = {
|
|
14
|
+
id: "treedb_sample",
|
|
15
|
+
schema_version: "4",
|
|
16
|
+
topics: [
|
|
17
|
+
{
|
|
18
|
+
id: "departments",
|
|
19
|
+
pkey: "id",
|
|
20
|
+
main_topic: true,
|
|
21
|
+
cols: {
|
|
22
|
+
id: {type: "string", flag: ["persistent", "required"]},
|
|
23
|
+
departments: {type: "dict", flag: ["hook"], hook: {departments: "department_id"}},
|
|
24
|
+
department_id: {type: "string", flag: ["fkey"]},
|
|
25
|
+
users: {type: "dict", flag: ["hook"], hook: {users: "departments"}},
|
|
26
|
+
managers: {type: "list", flag: ["hook"],
|
|
27
|
+
hook: {users: "manager", departments: "manager"}},
|
|
28
|
+
manager: {type: "array", flag: ["fkey"]},
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "users",
|
|
33
|
+
pkey: "id",
|
|
34
|
+
pkey2s: "name",
|
|
35
|
+
tkey: "tm",
|
|
36
|
+
cols: {
|
|
37
|
+
id: {type: "string", flag: "required"},
|
|
38
|
+
name: {type: "string", flag: ["persistent"]},
|
|
39
|
+
enabled: {type: "boolean", flag: ["persistent", "inherit"]},
|
|
40
|
+
tm: {type: "integer", flag: ["time"]},
|
|
41
|
+
departments: {type: "array", flag: ["fkey"]},
|
|
42
|
+
manager: {type: "array", flag: ["fkey"]},
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const EXPECTED = `treedb_sample (schema_version 4)
|
|
49
|
+
|
|
50
|
+
{} dict hook (N unique children)
|
|
51
|
+
[] list hook (n not-unique children)
|
|
52
|
+
() string hook (1 unique child)
|
|
53
|
+
(↖) 1 fkey (1 parent)
|
|
54
|
+
[↖] n fkeys (n parents)
|
|
55
|
+
{↖} N fkeys (N parents)
|
|
56
|
+
|
|
57
|
+
(2) pkey2 - secondary key
|
|
58
|
+
(t) tkey - time key
|
|
59
|
+
* field required
|
|
60
|
+
= field inherited
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
departments (main_topic)
|
|
64
|
+
┌───────────────────────────┐
|
|
65
|
+
│* id │
|
|
66
|
+
│ departments {} │ ◀─┐
|
|
67
|
+
│ department_id (↖) │ ──┘
|
|
68
|
+
│ users {} │ ◀─────┐
|
|
69
|
+
│ managers [] │ ◀─┬───┼───┐
|
|
70
|
+
│ manager [↖] │ ──┘ │ │
|
|
71
|
+
└───────────────────────────┘ │ │
|
|
72
|
+
│ │
|
|
73
|
+
users │ │
|
|
74
|
+
┌───────────────────────────┐ │ │
|
|
75
|
+
│* id │ │ │
|
|
76
|
+
│ name (2) │ │ │
|
|
77
|
+
│= enabled │ │ │
|
|
78
|
+
│ tm (t) │ │ │
|
|
79
|
+
│ departments [↖] │ ──────┘ │
|
|
80
|
+
│ manager [↖] │ ──────────┘
|
|
81
|
+
└───────────────────────────┘`;
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
describe("schema_to_diagram", () => {
|
|
85
|
+
const diagram = schema_to_diagram(SCHEMA);
|
|
86
|
+
|
|
87
|
+
test("the whole picture", () => {
|
|
88
|
+
expect(diagram).toBe(EXPECTED);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("no line ends in a space", () => {
|
|
92
|
+
for(const line of diagram.split("\n")) {
|
|
93
|
+
expect(line).toBe(line.trimEnd());
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("a link to a column that does not exist is not drawn", () => {
|
|
98
|
+
const broken = JSON.parse(JSON.stringify(SCHEMA));
|
|
99
|
+
broken.topics[0].cols.users.hook = {users: "nowhere"};
|
|
100
|
+
expect(schema_to_diagram(broken)).not.toContain("◀─────┐");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("nothing to draw is an empty string, not a throw", () => {
|
|
104
|
+
expect(schema_to_diagram(null)).toBe("");
|
|
105
|
+
expect(schema_to_diagram({id: "x"})).toBe("");
|
|
106
|
+
});
|
|
107
|
+
});
|
package/src/schema_validate.js
CHANGED
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
* operator drew does nothing at all. Restarting on
|
|
17
17
|
* this is an outage with no gain.
|
|
18
18
|
* `warning` the schema opens and something is not what it looks
|
|
19
|
-
* like
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* like.
|
|
20
|
+
*
|
|
21
|
+
* A version is not checked here: an edit is a DRAFT and moves no
|
|
22
|
+
* version, and C_TREEDB's save-schema raises the versions of what
|
|
23
|
+
* changed when it publishes it.
|
|
23
24
|
*
|
|
24
25
|
* Every `code` is an i18n key: the caller renders, this decides.
|
|
25
26
|
*
|
|
@@ -69,8 +70,6 @@ function validate_schema(treedb, options)
|
|
|
69
70
|
{
|
|
70
71
|
let findings = [];
|
|
71
72
|
let opts = options || {};
|
|
72
|
-
let written = Array.isArray(opts.written_topics) ? opts.written_topics : [];
|
|
73
|
-
let baseline = opts.baseline || {};
|
|
74
73
|
|
|
75
74
|
if(!treedb || !Array.isArray(treedb.topics)) {
|
|
76
75
|
return findings;
|
|
@@ -117,17 +116,6 @@ function validate_schema(treedb, options)
|
|
|
117
116
|
}
|
|
118
117
|
}
|
|
119
118
|
|
|
120
|
-
/* A version that did not move republishes nothing: the stored
|
|
121
|
-
* topic_cols.json masks the whole edit and the restart looks
|
|
122
|
-
* like it worked. */
|
|
123
|
-
if(written.indexOf(topic.id) >= 0) {
|
|
124
|
-
let before = baseline[topic.id];
|
|
125
|
-
if(before !== undefined && String(before) === String(topic.topic_version)) {
|
|
126
|
-
add("warning", "topic version not bumped", topic.name, "",
|
|
127
|
-
String(topic.topic_version));
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
119
|
for(let col of topic.cols) {
|
|
132
120
|
let flags = col_flags(col.record);
|
|
133
121
|
let type = col.record ? col.record.type : "";
|
|
@@ -221,34 +221,14 @@ describe("the column itself", () => {
|
|
|
221
221
|
});
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
-
describe("
|
|
225
|
-
test("a written topic whose topic_version
|
|
224
|
+
describe("a version is not the validator's", () => {
|
|
225
|
+
test("a written topic whose topic_version did not move is NOT reported", () => {
|
|
226
|
+
/* An edit is a draft; save-schema raises the version. */
|
|
226
227
|
const f = validate_schema(schema(), {
|
|
227
228
|
written_topics: ["db.departments"],
|
|
228
229
|
baseline: {"db.departments": 1}
|
|
229
230
|
});
|
|
230
|
-
|
|
231
|
-
expect(v.length).toBe(1);
|
|
232
|
-
expect(v[0].topic).toBe("departments");
|
|
233
|
-
expect(v[0].severity).toBe("warning");
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
test("a bumped version says nothing", () => {
|
|
237
|
-
expect(validate_schema(schema({dep_version: 2}), {
|
|
238
|
-
written_topics: ["db.departments"],
|
|
239
|
-
baseline: {"db.departments": 1}
|
|
240
|
-
})).toEqual([]);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test("a topic nobody wrote is not asked about its version", () => {
|
|
244
|
-
expect(validate_schema(schema(), {baseline: {"db.departments": 1}})).toEqual([]);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
test("a version compared across types still matches (2 vs '2')", () => {
|
|
248
|
-
expect(validate_schema(schema({dep_version: "1"}), {
|
|
249
|
-
written_topics: ["db.departments"],
|
|
250
|
-
baseline: {"db.departments": 1}
|
|
251
|
-
}).length).toBe(1);
|
|
231
|
+
expect(f.filter(x => x.code === "topic version not bumped")).toEqual([]);
|
|
252
232
|
});
|
|
253
233
|
});
|
|
254
234
|
|
|
@@ -1,92 +1,35 @@
|
|
|
1
1
|
/***********************************************************************
|
|
2
2
|
* schema_version_bump.test.js
|
|
3
3
|
*
|
|
4
|
-
* "This topic was written
|
|
4
|
+
* "This topic was written in this session and is not saved."
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* offered a Raise button that wrote another version and was
|
|
12
|
-
* measured against itself again.
|
|
13
|
-
*
|
|
14
|
-
* The banner could not be dismissed. One operator pressed it
|
|
15
|
-
* thirty-three times on a single column delete, and that is why
|
|
16
|
-
* schema versions climbed by tens with the schema unchanged.
|
|
6
|
+
* An edit is a DRAFT (M36 of yunetas' 2026-09-21 review): it moves
|
|
7
|
+
* no version, and the host's save-schema raises the versions of what
|
|
8
|
+
* changed. So "written" alone is the whole question -- the version is
|
|
9
|
+
* no longer measured against a baseline, which is also why the old
|
|
10
|
+
* "Raise" button and the loop it fed are gone.
|
|
17
11
|
*
|
|
18
12
|
* Copyright (c) 2026, ArtGins.
|
|
19
13
|
* All Rights Reserved.
|
|
20
14
|
***********************************************************************/
|
|
21
15
|
import {describe, it, expect} from "vitest";
|
|
22
|
-
import {
|
|
16
|
+
import {topic_is_draft} from "./c_yui_schema_editor.js";
|
|
23
17
|
|
|
24
18
|
const TOPIC = {id: "device_groups", topic_version: 5};
|
|
25
19
|
|
|
26
|
-
describe("
|
|
20
|
+
describe("topic_is_draft", () => {
|
|
27
21
|
it("says nothing about a topic this session never wrote", () => {
|
|
28
|
-
expect(
|
|
22
|
+
expect(topic_is_draft({}, TOPIC)).toBe(false);
|
|
29
23
|
});
|
|
30
24
|
|
|
31
|
-
it("
|
|
32
|
-
expect(
|
|
25
|
+
it("a written topic is a draft, whatever its version says", () => {
|
|
26
|
+
expect(topic_is_draft({device_groups: true}, TOPIC)).toBe(true);
|
|
27
|
+
expect(topic_is_draft({device_groups: true}, {id: "device_groups", topic_version: 6}))
|
|
33
28
|
.toBe(true);
|
|
34
29
|
});
|
|
35
30
|
|
|
36
|
-
it("STOPS asking once the version has moved past the baseline", () => {
|
|
37
|
-
/* The case the loop got wrong: the write raised it to 6 and the
|
|
38
|
-
* baseline still says 5, so there is nothing left to raise. */
|
|
39
|
-
expect(needs_version_bump(
|
|
40
|
-
{device_groups: true}, {device_groups: 5}, {id: "device_groups", topic_version: 6}
|
|
41
|
-
)).toBe(false);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("compares as text, so 5 and '5' are the same version", () => {
|
|
45
|
-
expect(needs_version_bump(
|
|
46
|
-
{device_groups: true}, {device_groups: "5"}, TOPIC
|
|
47
|
-
)).toBe(true);
|
|
48
|
-
expect(needs_version_bump(
|
|
49
|
-
{device_groups: true}, {device_groups: 5}, {id: "device_groups", topic_version: "5"}
|
|
50
|
-
)).toBe(true);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
31
|
it("answers false rather than throwing on a missing half", () => {
|
|
54
|
-
expect(
|
|
55
|
-
expect(
|
|
56
|
-
expect(needs_version_bump({device_groups: true}, {device_groups: 5}, null)).toBe(false);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("a topic with no baseline entry is not owed a bump", () => {
|
|
60
|
-
expect(needs_version_bump({device_groups: true}, {}, TOPIC)).toBe(false);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe("the loop it came from", () => {
|
|
65
|
-
/*
|
|
66
|
-
* What the editor does per bump: raise the version by one. With the
|
|
67
|
-
* baseline left alone, ONE press settles it; with the baseline
|
|
68
|
-
* re-taken each time -- the old bug -- it never settles.
|
|
69
|
-
*/
|
|
70
|
-
function press_raise(state) {
|
|
71
|
-
return {written: state.written, baseline: state.baseline,
|
|
72
|
-
topic: {id: "t", topic_version: Number(state.topic.topic_version) + 1}};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
it("settles after a single press", () => {
|
|
76
|
-
let st = {written: {t: true}, baseline: {t: 5}, topic: {id: "t", topic_version: 5}};
|
|
77
|
-
expect(needs_version_bump(st.written, st.baseline, st.topic)).toBe(true);
|
|
78
|
-
st = press_raise(st);
|
|
79
|
-
expect(needs_version_bump(st.written, st.baseline, st.topic)).toBe(false);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("never settles if the baseline is re-taken with the version", () => {
|
|
83
|
-
let st = {written: {t: true}, baseline: {t: 5}, topic: {id: "t", topic_version: 5}};
|
|
84
|
-
let presses = 0;
|
|
85
|
-
while(needs_version_bump(st.written, st.baseline, st.topic) && presses < 40) {
|
|
86
|
-
st = press_raise(st);
|
|
87
|
-
st.baseline = {t: st.topic.topic_version}; /* the bug */
|
|
88
|
-
presses++;
|
|
89
|
-
}
|
|
90
|
-
expect(presses).toBe(40); /* it ran to the guard: no way out */
|
|
32
|
+
expect(topic_is_draft(null, TOPIC)).toBe(false);
|
|
33
|
+
expect(topic_is_draft({device_groups: true}, null)).toBe(false);
|
|
91
34
|
});
|
|
92
35
|
});
|