@yuneta/gobj-ui 6.0.0 → 6.1.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 +95 -2
- package/dist/gobj-ui.cjs.js +5374 -476
- package/dist/gobj-ui.es.js +5374 -477
- package/index.js +1 -0
- package/package.json +1 -1
- package/src/c_yui_schema_editor.css +133 -0
- package/src/c_yui_schema_editor.js +3217 -0
- package/src/c_yui_treedb_schema.js +22 -3
- package/src/schema_descs.js +164 -0
- package/src/schema_descs.test.js +145 -0
- package/src/schema_flags.js +237 -0
- package/src/schema_flags.test.js +133 -0
- package/src/schema_import.js +413 -0
- package/src/schema_import.test.js +234 -0
- package/src/schema_model.js +512 -0
- package/src/schema_model.test.js +233 -0
- package/src/schema_to_c.js +367 -0
- package/src/schema_to_c.test.js +218 -0
- package/src/schema_validate.js +289 -0
- package/src/schema_validate.test.js +265 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* schema_validate.test.js
|
|
3
|
+
*
|
|
4
|
+
* What must be caught BEFORE the restart, pinned.
|
|
5
|
+
*
|
|
6
|
+
* Each of these is a way to spend an outage finding out something
|
|
7
|
+
* the records already said: a link that saves nothing, a topic
|
|
8
|
+
* that will not open, or — the expensive one — a restart that
|
|
9
|
+
* succeeds and changes nothing because the version did not move.
|
|
10
|
+
***********************************************************************/
|
|
11
|
+
import { describe, test, expect } from "vitest";
|
|
12
|
+
import { build_schema_model } from "./schema_model.js";
|
|
13
|
+
import { validate_schema, validate_model, has_errors } from "./schema_validate.js";
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/* A treedb of two topics linked by departments.users -> users.departments,
|
|
17
|
+
which is the shape every test below breaks in one place. */
|
|
18
|
+
function schema(overrides)
|
|
19
|
+
{
|
|
20
|
+
let o = overrides || {};
|
|
21
|
+
const records = {
|
|
22
|
+
treedbs: [{id: "db", schema_version: 1}],
|
|
23
|
+
topics: [
|
|
24
|
+
{id: "db.departments", value: "departments", order: 1, pkey: "id",
|
|
25
|
+
topic_version: o.dep_version === undefined ? 1 : o.dep_version,
|
|
26
|
+
treedbs: ["treedbs^db^topics"]},
|
|
27
|
+
{id: "db.users", value: "users", order: 2, pkey: o.users_pkey || "id",
|
|
28
|
+
topic_version: 1, pkey2s: o.users_pkey2s,
|
|
29
|
+
treedbs: ["treedbs^db^topics"]},
|
|
30
|
+
],
|
|
31
|
+
cols: [
|
|
32
|
+
{id: "db.departments.id", value: "id", order: 1, type: "string",
|
|
33
|
+
flag: ["persistent"], topics: ["topics^db.departments^cols"]},
|
|
34
|
+
{id: "db.departments.users", value: "users", order: 2,
|
|
35
|
+
type: o.hook_type === undefined ? "dict" : o.hook_type,
|
|
36
|
+
flag: ["hook"], hook: o.hook === undefined ? {users: "departments"} : o.hook,
|
|
37
|
+
topics: ["topics^db.departments^cols"]},
|
|
38
|
+
{id: "db.users.id", value: "id", order: 1, type: "string",
|
|
39
|
+
flag: ["persistent"], topics: ["topics^db.users^cols"]},
|
|
40
|
+
{id: "db.users.departments", value: "departments", order: 2,
|
|
41
|
+
type: o.fkey_type === undefined ? "array" : o.fkey_type,
|
|
42
|
+
flag: o.fkey_flag === undefined ? ["fkey"] : o.fkey_flag,
|
|
43
|
+
topics: ["topics^db.users^cols"]},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
if(o.extra_col) {
|
|
47
|
+
records.cols.push(o.extra_col);
|
|
48
|
+
}
|
|
49
|
+
return build_schema_model(records).treedbs[0];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function codes(findings)
|
|
53
|
+
{
|
|
54
|
+
return findings.map(f => f.code);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
describe("a schema with nothing wrong", () => {
|
|
59
|
+
test("says nothing", () => {
|
|
60
|
+
expect(validate_schema(schema())).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("no treedb at all is not a crash", () => {
|
|
64
|
+
expect(validate_schema(null)).toEqual([]);
|
|
65
|
+
expect(validate_schema(undefined, {})).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("the topic will not open", () => {
|
|
70
|
+
test("a pkey naming no column", () => {
|
|
71
|
+
const f = validate_schema(schema({users_pkey: "user_id"}));
|
|
72
|
+
expect(codes(f)).toContain("pkey names no column");
|
|
73
|
+
expect(f[0].detail).toBe("user_id");
|
|
74
|
+
expect(has_errors(f)).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("a pkey2 naming no column", () => {
|
|
78
|
+
expect(codes(validate_schema(schema({users_pkey2s: ["name"]}))))
|
|
79
|
+
.toContain("pkey2 names no column");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("a pkey2 declared as a bare string is read the same way", () => {
|
|
83
|
+
expect(codes(validate_schema(schema({users_pkey2s: "name"}))))
|
|
84
|
+
.toContain("pkey2 names no column");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("a topic with no column", () => {
|
|
88
|
+
const model = build_schema_model({
|
|
89
|
+
treedbs: [{id: "db"}],
|
|
90
|
+
topics: [{id: "db.empty", value: "empty", treedbs: ["treedbs^db^topics"]}],
|
|
91
|
+
cols: [],
|
|
92
|
+
});
|
|
93
|
+
expect(codes(validate_schema(model.treedbs[0]))).toContain("topic has no column");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("the link does nothing, and the write succeeded", () => {
|
|
98
|
+
test("a hook with no mapping", () => {
|
|
99
|
+
expect(codes(validate_schema(schema({hook: null})))).toContain("hook has no mapping");
|
|
100
|
+
expect(codes(validate_schema(schema({hook: {}})))).toContain("hook has no mapping");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("a hook naming a topic that is not in this treedb", () => {
|
|
104
|
+
expect(codes(validate_schema(schema({hook: {clients: "departments"}}))))
|
|
105
|
+
.toContain("hook names no topic");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("a hook naming a column the child does not have", () => {
|
|
109
|
+
const f = validate_schema(schema({hook: {users: "department_id"}}));
|
|
110
|
+
expect(codes(f)).toContain("hook names no fkey column");
|
|
111
|
+
expect(f[0].detail).toBe("users.department_id");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("a hook whose target column is NOT flagged fkey", () => {
|
|
115
|
+
expect(codes(validate_schema(schema({fkey_flag: ["persistent"]}))))
|
|
116
|
+
.toContain("hook target is not a fkey");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("a hook typed as a scalar cannot hold children", () => {
|
|
120
|
+
expect(codes(validate_schema(schema({hook_type: "string"}))))
|
|
121
|
+
.toContain("hook must be a collection");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("a fkey typed as an integer", () => {
|
|
125
|
+
expect(codes(validate_schema(schema({fkey_type: "integer"}))))
|
|
126
|
+
.toContain("fkey has a bad type");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("a fkey no hook names is a WARNING — the parent side went away", () => {
|
|
130
|
+
const f = validate_schema(schema({hook: {users: "departments"},
|
|
131
|
+
extra_col: {id: "db.users.boss", value: "boss", order: 3, type: "string",
|
|
132
|
+
flag: ["fkey"], topics: ["topics^db.users^cols"]}}));
|
|
133
|
+
const orphan = f.filter(x => x.code === "fkey with no hook");
|
|
134
|
+
expect(orphan.length).toBe(1);
|
|
135
|
+
expect(orphan[0].col).toBe("boss");
|
|
136
|
+
expect(orphan[0].severity).toBe("warning");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("the column itself", () => {
|
|
141
|
+
test("no type", () => {
|
|
142
|
+
expect(codes(validate_schema(schema({
|
|
143
|
+
extra_col: {id: "db.users.x", value: "x", order: 9,
|
|
144
|
+
topics: ["topics^db.users^cols"]}
|
|
145
|
+
})))).toContain("column has no type");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("a type the treedb's own validator does not know", () => {
|
|
149
|
+
expect(codes(validate_schema(schema({
|
|
150
|
+
extra_col: {id: "db.users.x", value: "x", order: 9, type: "text",
|
|
151
|
+
topics: ["topics^db.users^cols"]}
|
|
152
|
+
})))).toContain("unknown column type");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("flagged enum with no enum list", () => {
|
|
156
|
+
expect(codes(validate_schema(schema({
|
|
157
|
+
extra_col: {id: "db.users.x", value: "x", order: 9, type: "string",
|
|
158
|
+
flag: ["enum"], topics: ["topics^db.users^cols"]}
|
|
159
|
+
})))).toContain("enum column has no enum");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("an enum stored as JSON TEXT counts as an enum", () => {
|
|
163
|
+
expect(codes(validate_schema(schema({
|
|
164
|
+
extra_col: {id: "db.users.x", value: "x", order: 9, type: "string",
|
|
165
|
+
flag: ["enum"], enum: '["a","b"]',
|
|
166
|
+
topics: ["topics^db.users^cols"]}
|
|
167
|
+
})))).not.toContain("enum column has no enum");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("a hook stored as JSON TEXT is still a hook mapping", () => {
|
|
171
|
+
expect(validate_schema(schema({hook: '{"users":"departments"}'}))).toEqual([]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("a flag stored as a bare string is still a flag", () => {
|
|
175
|
+
expect(codes(validate_schema(schema({fkey_flag: "fkey"}))))
|
|
176
|
+
.not.toContain("hook target is not a fkey");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("the version that did not move", () => {
|
|
181
|
+
test("a written topic whose topic_version is unchanged is reported", () => {
|
|
182
|
+
const f = validate_schema(schema(), {
|
|
183
|
+
written_topics: ["db.departments"],
|
|
184
|
+
baseline: {"db.departments": 1}
|
|
185
|
+
});
|
|
186
|
+
const v = f.filter(x => x.code === "topic version not bumped");
|
|
187
|
+
expect(v.length).toBe(1);
|
|
188
|
+
expect(v[0].topic).toBe("departments");
|
|
189
|
+
expect(v[0].severity).toBe("warning");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("a bumped version says nothing", () => {
|
|
193
|
+
expect(validate_schema(schema({dep_version: 2}), {
|
|
194
|
+
written_topics: ["db.departments"],
|
|
195
|
+
baseline: {"db.departments": 1}
|
|
196
|
+
})).toEqual([]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("a topic nobody wrote is not asked about its version", () => {
|
|
200
|
+
expect(validate_schema(schema(), {baseline: {"db.departments": 1}})).toEqual([]);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("a version compared across types still matches (2 vs '2')", () => {
|
|
204
|
+
expect(validate_schema(schema({dep_version: "1"}), {
|
|
205
|
+
written_topics: ["db.departments"],
|
|
206
|
+
baseline: {"db.departments": 1}
|
|
207
|
+
}).length).toBe(1);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("errors come first, and orphans are the model's business", () => {
|
|
212
|
+
test("an error outranks a warning in the list", () => {
|
|
213
|
+
const f = validate_schema(schema({users_pkey: "nope",
|
|
214
|
+
extra_col: {id: "db.users.boss", value: "boss", order: 3, type: "string",
|
|
215
|
+
flag: ["fkey"], topics: ["topics^db.users^cols"]}}));
|
|
216
|
+
expect(f[0].severity).toBe("error");
|
|
217
|
+
expect(f[f.length - 1].severity).toBe("warning");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("validate_model reports what belongs to nothing", () => {
|
|
221
|
+
const model = build_schema_model({
|
|
222
|
+
treedbs: [{id: "db"}],
|
|
223
|
+
topics: [{id: "gone.users", value: "users", treedbs: ["treedbs^gone^topics"]}],
|
|
224
|
+
cols: [{id: "db.gone.x", value: "x", topics: ["topics^db.gone^cols"]}],
|
|
225
|
+
});
|
|
226
|
+
const c = codes(validate_model(model));
|
|
227
|
+
expect(c).toContain("topic belongs to no treedb");
|
|
228
|
+
expect(c).toContain("column belongs to no topic");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("has_errors ignores warnings", () => {
|
|
232
|
+
expect(has_errors([{severity: "warning"}])).toBe(false);
|
|
233
|
+
expect(has_errors([{severity: "warning"}, {severity: "error"}])).toBe(true);
|
|
234
|
+
expect(has_errors(null)).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("only one hook may name a fkey column", () => {
|
|
239
|
+
test("two hooks on the same fkey is what the treedb refuses to open", () => {
|
|
240
|
+
const model = build_schema_model({
|
|
241
|
+
treedbs: [{id: "db"}],
|
|
242
|
+
topics: [
|
|
243
|
+
{id: "db.a", value: "a", order: 1, treedbs: ["treedbs^db^topics"]},
|
|
244
|
+
{id: "db.b", value: "b", order: 2, treedbs: ["treedbs^db^topics"]},
|
|
245
|
+
{id: "db.c", value: "c", order: 3, treedbs: ["treedbs^db^topics"]},
|
|
246
|
+
],
|
|
247
|
+
cols: [
|
|
248
|
+
{id: "db.a.id", value: "id", order: 1, type: "string", topics: ["topics^db.a^cols"]},
|
|
249
|
+
{id: "db.a.cs", value: "cs", order: 2, type: "dict", flag: ["hook"],
|
|
250
|
+
hook: {c: "parent"}, topics: ["topics^db.a^cols"]},
|
|
251
|
+
{id: "db.b.id", value: "id", order: 1, type: "string", topics: ["topics^db.b^cols"]},
|
|
252
|
+
{id: "db.b.cs", value: "cs", order: 2, type: "dict", flag: ["hook"],
|
|
253
|
+
hook: {c: "parent"}, topics: ["topics^db.b^cols"]},
|
|
254
|
+
{id: "db.c.id", value: "id", order: 1, type: "string", topics: ["topics^db.c^cols"]},
|
|
255
|
+
{id: "db.c.parent", value: "parent", order: 2, type: "array", flag: ["fkey"],
|
|
256
|
+
topics: ["topics^db.c^cols"]},
|
|
257
|
+
],
|
|
258
|
+
});
|
|
259
|
+
const f = validate_schema(model.treedbs[0]);
|
|
260
|
+
const dup = f.filter(x => x.code === "fkey named by two hooks");
|
|
261
|
+
expect(dup.length).toBe(1);
|
|
262
|
+
expect(dup[0].col).toBe("parent");
|
|
263
|
+
expect(dup[0].severity).toBe("error");
|
|
264
|
+
});
|
|
265
|
+
});
|