@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,218 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* schema_to_c.test.js
|
|
3
|
+
*
|
|
4
|
+
* The stored schema written back as its C literal, pinned.
|
|
5
|
+
*
|
|
6
|
+
* The test that matters is the ROUND TRIP: the emitted text is put
|
|
7
|
+
* through the same two steps the yuno puts it through — the C
|
|
8
|
+
* compiler's unescaping and helper_quote2doublequote() — and the
|
|
9
|
+
* JSON that comes out must be the JSON that went in. Everything
|
|
10
|
+
* else about the format is cosmetic; that is the part that decides
|
|
11
|
+
* whether the paste compiles and opens.
|
|
12
|
+
***********************************************************************/
|
|
13
|
+
import { describe, test, expect } from "vitest";
|
|
14
|
+
import { build_schema_model } from "./schema_model.js";
|
|
15
|
+
import { schema_to_json, schema_to_c } from "./schema_to_c.js";
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
/***************************************************************
|
|
19
|
+
* What the yuno does to the literal before parsing it:
|
|
20
|
+
* 1. the C compiler resolves `\<newline>` continuations and
|
|
21
|
+
* the `\\` / `\"` escapes,
|
|
22
|
+
* 2. helper_quote2doublequote() turns EVERY ' into ",
|
|
23
|
+
* 3. jansson parses what is left.
|
|
24
|
+
***************************************************************/
|
|
25
|
+
function load_like_the_yuno(source)
|
|
26
|
+
{
|
|
27
|
+
const start = source.indexOf('"\\\n');
|
|
28
|
+
const end = source.lastIndexOf('";');
|
|
29
|
+
expect(start).toBeGreaterThan(0);
|
|
30
|
+
expect(end).toBeGreaterThan(start);
|
|
31
|
+
|
|
32
|
+
let body = source.slice(start + 3, end);
|
|
33
|
+
|
|
34
|
+
/* Line continuations: a line ends with `\n\` + newline, which the
|
|
35
|
+
compiler turns into one newline. */
|
|
36
|
+
let text = body.split("\\n\\\n").join("\n");
|
|
37
|
+
text = text.replace(/\\n\\$/, "");
|
|
38
|
+
|
|
39
|
+
/* C escapes, innermost last: `\\` is a backslash, `\"` a quote. */
|
|
40
|
+
let out = "";
|
|
41
|
+
for(let i = 0; i < text.length; i++) {
|
|
42
|
+
if(text[i] === "\\" && i + 1 < text.length) {
|
|
43
|
+
const next = text[i + 1];
|
|
44
|
+
if(next === "\\") {
|
|
45
|
+
out += "\\";
|
|
46
|
+
i++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if(next === '"') {
|
|
50
|
+
out += '"';
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
out += text[i];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* helper_quote2doublequote(): every single quote, no exception. */
|
|
59
|
+
out = out.split("'").join('"');
|
|
60
|
+
|
|
61
|
+
return JSON.parse(out);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
const MODEL = build_schema_model({
|
|
66
|
+
treedbs: [{id: "treedb_sample", schema_version: 4}],
|
|
67
|
+
topics: [
|
|
68
|
+
{id: "treedb_sample.departments", value: "departments", order: 1, pkey: "id",
|
|
69
|
+
system_flag: "sf_string_key", topic_version: 2,
|
|
70
|
+
treedbs: ["treedbs^treedb_sample^topics"]},
|
|
71
|
+
{id: "treedb_sample.users", value: "users", order: 2, pkey: "id",
|
|
72
|
+
system_flag: "sf_string_key", topic_version: 3, pkey2s: "name",
|
|
73
|
+
treedbs: ["treedbs^treedb_sample^topics"]},
|
|
74
|
+
],
|
|
75
|
+
cols: [
|
|
76
|
+
{id: "treedb_sample.departments.id", value: "id", order: 1, header: "Id",
|
|
77
|
+
fillspace: 20, type: "string", flag: ["persistent", "required"],
|
|
78
|
+
topics: ["topics^treedb_sample.departments^cols"]},
|
|
79
|
+
{id: "treedb_sample.departments.users", value: "users", order: 2,
|
|
80
|
+
header: "Users", fillspace: 20, type: "dict", flag: ["hook"],
|
|
81
|
+
hook: {users: "departments"},
|
|
82
|
+
topics: ["topics^treedb_sample.departments^cols"]},
|
|
83
|
+
{id: "treedb_sample.users.id", value: "id", order: 1, header: "Id",
|
|
84
|
+
fillspace: 20, type: "string", flag: ["persistent"],
|
|
85
|
+
topics: ["topics^treedb_sample.users^cols"]},
|
|
86
|
+
{id: "treedb_sample.users.name", value: "name", order: 2, header: "Name",
|
|
87
|
+
fillspace: 10, type: "string", flag: ["persistent", "writable"],
|
|
88
|
+
topics: ["topics^treedb_sample.users^cols"]},
|
|
89
|
+
{id: "treedb_sample.users.departments", value: "departments", order: 3,
|
|
90
|
+
header: "Departments", fillspace: 20, type: "array", flag: ["fkey"],
|
|
91
|
+
topics: ["topics^treedb_sample.users^cols"]},
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
const TREEDB = MODEL.treedbs[0];
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
describe("schema_to_json", () => {
|
|
98
|
+
const json = schema_to_json(TREEDB);
|
|
99
|
+
|
|
100
|
+
test("the shape of the literal: topics a LIST, cols a DICT", () => {
|
|
101
|
+
expect(Array.isArray(json.topics)).toBe(true);
|
|
102
|
+
expect(json.topics.map(t => t.id)).toEqual(["departments", "users"]);
|
|
103
|
+
expect(Array.isArray(json.topics[0].cols)).toBe(false);
|
|
104
|
+
expect(Object.keys(json.topics[1].cols)).toEqual(["id", "name", "departments"]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("a topic is named by its NAME, a column by its own", () => {
|
|
108
|
+
expect(json.topics[0].id).toBe("departments");
|
|
109
|
+
expect(json.topics[0].cols.users.type).toBe("dict");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("no storage field survives the export", () => {
|
|
113
|
+
for(const key of ["value", "treedbs", "order"]) {
|
|
114
|
+
expect(json.topics[0][key]).toBeUndefined();
|
|
115
|
+
}
|
|
116
|
+
for(const key of ["value", "topics", "order"]) {
|
|
117
|
+
expect(json.topics[0].cols.id[key]).toBeUndefined();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("a single pkey2 is written bare, as the literals write it", () => {
|
|
122
|
+
expect(json.topics[1].pkey2s).toBe("name");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("the keys come out in the order the .c files declare them", () => {
|
|
126
|
+
expect(Object.keys(json.topics[1]).slice(0, 3)).toEqual(["id", "pkey", "pkey2s"]);
|
|
127
|
+
expect(Object.keys(json.topics[0].cols.id))
|
|
128
|
+
.toEqual(["header", "fillspace", "type", "flag"]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("no treedb is null, not a throw", () => {
|
|
132
|
+
expect(schema_to_json(null)).toBe(null);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("schema_to_c — the round trip", () => {
|
|
137
|
+
const source = schema_to_c(TREEDB);
|
|
138
|
+
|
|
139
|
+
test("it is a C array declaration with the conventional name", () => {
|
|
140
|
+
expect(source.startsWith("static char treedb_schema_sample[]= \"\\\n")).toBe(true);
|
|
141
|
+
expect(source.trimEnd().endsWith('";')).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("every line carries the continuation", () => {
|
|
145
|
+
const body = source.split("\n").slice(1, -2);
|
|
146
|
+
for(const line of body) {
|
|
147
|
+
expect(line.endsWith("\\n\\")).toBe(true);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("loaded the way the yuno loads it, it is the schema it came from", () => {
|
|
152
|
+
expect(load_like_the_yuno(source)).toEqual(schema_to_json(TREEDB));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("the caller can name the array and set the continuation column", () => {
|
|
156
|
+
const custom = schema_to_c(TREEDB, {var_name: "my_schema", pad: 40});
|
|
157
|
+
expect(custom.startsWith("static char my_schema[]=")).toBe(true);
|
|
158
|
+
expect(load_like_the_yuno(custom)).toEqual(schema_to_json(TREEDB));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("a version is quoted, a fillspace is not — as the literals write them", () => {
|
|
162
|
+
expect(source).toContain("'schema_version': '4'");
|
|
163
|
+
expect(source).toContain("'topic_version': '2'");
|
|
164
|
+
expect(source).toContain("'fillspace': 20");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("a boolean stays a boolean", () => {
|
|
168
|
+
const model = build_schema_model({
|
|
169
|
+
treedbs: [{id: "treedb_x", schema_version: 1}],
|
|
170
|
+
topics: [{id: "treedb_x.t", value: "t", system_topic: true,
|
|
171
|
+
treedbs: ["treedbs^treedb_x^topics"]}],
|
|
172
|
+
cols: [{id: "treedb_x.t.id", value: "id", type: "string",
|
|
173
|
+
topics: ["topics^treedb_x.t^cols"]}],
|
|
174
|
+
});
|
|
175
|
+
const src = schema_to_c(model.treedbs[0]);
|
|
176
|
+
expect(src).toContain("'system_topic': true");
|
|
177
|
+
expect(load_like_the_yuno(src).topics[0].system_topic).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("no treedb is an empty string, not a broken declaration", () => {
|
|
181
|
+
expect(schema_to_c(null)).toBe("");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("the characters that cannot be written as themselves", () => {
|
|
186
|
+
function with_header(header)
|
|
187
|
+
{
|
|
188
|
+
const model = build_schema_model({
|
|
189
|
+
treedbs: [{id: "treedb_x", schema_version: 1}],
|
|
190
|
+
topics: [{id: "treedb_x.t", value: "t", treedbs: ["treedbs^treedb_x^topics"]}],
|
|
191
|
+
cols: [{id: "treedb_x.t.id", value: "id", type: "string", header: header,
|
|
192
|
+
topics: ["topics^treedb_x.t^cols"]}],
|
|
193
|
+
});
|
|
194
|
+
return schema_to_c(model.treedbs[0]);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
test("a SINGLE QUOTE survives — the pass that rewrites them cannot see \\u0027", () => {
|
|
198
|
+
const src = with_header("Client's name");
|
|
199
|
+
expect(src).toContain("\\\\u0027");
|
|
200
|
+
expect(load_like_the_yuno(src).topics[0].cols.id.header).toBe("Client's name");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("a double quote survives both layers", () => {
|
|
204
|
+
const src = with_header('Say "hi"');
|
|
205
|
+
expect(load_like_the_yuno(src).topics[0].cols.id.header).toBe('Say "hi"');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("a backslash survives both layers", () => {
|
|
209
|
+
const src = with_header("a\\b");
|
|
210
|
+
expect(load_like_the_yuno(src).topics[0].cols.id.header).toBe("a\\b");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("all three at once", () => {
|
|
214
|
+
const header = 'a\\b "c" d\'e';
|
|
215
|
+
expect(load_like_the_yuno(with_header(header)).topics[0].cols.id.header)
|
|
216
|
+
.toBe(header);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* schema_validate.js
|
|
3
|
+
*
|
|
4
|
+
* What is wrong with a schema, BEFORE the yuno is restarted to
|
|
5
|
+
* read it.
|
|
6
|
+
*
|
|
7
|
+
* Applying a schema is restarting the yuno that owns it. A schema
|
|
8
|
+
* the treedb refuses therefore costs an outage to discover, and
|
|
9
|
+
* the message arrives in that yuno's log, on the node, minutes
|
|
10
|
+
* after the edit that caused it. Everything checked here is
|
|
11
|
+
* checkable from the records alone.
|
|
12
|
+
*
|
|
13
|
+
* TWO CLASSES OF FINDING, and the difference matters:
|
|
14
|
+
*
|
|
15
|
+
* `error` the treedb will refuse the topic, or the link the
|
|
16
|
+
* operator drew does nothing at all. Restarting on
|
|
17
|
+
* this is an outage with no gain.
|
|
18
|
+
* `warning` the schema opens and something is not what it looks
|
|
19
|
+
* like — an unbumped `topic_version` masking the whole
|
|
20
|
+
* edit is the one that costs the most time, because
|
|
21
|
+
* the restart SUCCEEDS and the change is simply not
|
|
22
|
+
* there.
|
|
23
|
+
*
|
|
24
|
+
* Every `code` is an i18n key: the caller renders, this decides.
|
|
25
|
+
*
|
|
26
|
+
* Copyright (c) 2026, ArtGins.
|
|
27
|
+
* All Rights Reserved.
|
|
28
|
+
***********************************************************************/
|
|
29
|
+
import {
|
|
30
|
+
col_flags,
|
|
31
|
+
col_hook,
|
|
32
|
+
col_enum,
|
|
33
|
+
topic_pkey2s,
|
|
34
|
+
} from "./schema_model.js";
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
/* What a column may be typed as: the `type` enum of the `cols` topic
|
|
38
|
+
* (treedb_system_schema.c). A type outside it is refused by the
|
|
39
|
+
* validator the treedb builds from that same enum. */
|
|
40
|
+
const COL_TYPES = [
|
|
41
|
+
"string", "integer", "object", "dict", "array", "list",
|
|
42
|
+
"real", "boolean", "blob"
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/* What a HOOK may be typed as: a hook holds its children, so it is a
|
|
46
|
+
* collection. `{}` (dict) for N unique children, `[]` (list) for n. */
|
|
47
|
+
const HOOK_TYPES = ["dict", "object", "list", "array"];
|
|
48
|
+
|
|
49
|
+
/* What a FKEY may be typed as: one parent is a string, n parents a
|
|
50
|
+
* collection. */
|
|
51
|
+
const FKEY_TYPES = ["string", "dict", "object", "list", "array"];
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
/***************************************************************
|
|
55
|
+
* validate_schema(treedb, options) -> [finding, ...]
|
|
56
|
+
*
|
|
57
|
+
* treedb one entry of build_schema_model().treedbs
|
|
58
|
+
* options {
|
|
59
|
+
* written_topics: [topic id, ...] topics this session wrote
|
|
60
|
+
* baseline: {topic id: topic_version} version at load time
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* finding {severity, code, treedb, topic, col, detail}
|
|
64
|
+
*
|
|
65
|
+
* Findings come out worst-first, so a caller that shows three
|
|
66
|
+
* lines shows the three that matter.
|
|
67
|
+
***************************************************************/
|
|
68
|
+
function validate_schema(treedb, options)
|
|
69
|
+
{
|
|
70
|
+
let findings = [];
|
|
71
|
+
let opts = options || {};
|
|
72
|
+
let written = Array.isArray(opts.written_topics) ? opts.written_topics : [];
|
|
73
|
+
let baseline = opts.baseline || {};
|
|
74
|
+
|
|
75
|
+
if(!treedb || !Array.isArray(treedb.topics)) {
|
|
76
|
+
return findings;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let add = (severity, code, topic, col, detail) => {
|
|
80
|
+
findings.push({
|
|
81
|
+
severity: severity,
|
|
82
|
+
code: code,
|
|
83
|
+
treedb: treedb.id,
|
|
84
|
+
topic: topic || "",
|
|
85
|
+
col: col || "",
|
|
86
|
+
detail: detail || ""
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/* Which topics exist, and which columns each one has: a hook is
|
|
91
|
+
* checked against the CHILD topic, so both are needed up front. */
|
|
92
|
+
let topic_by_name = {};
|
|
93
|
+
for(let topic of treedb.topics) {
|
|
94
|
+
topic_by_name[topic.name] = topic;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if(treedb.topics.length === 0) {
|
|
98
|
+
add("warning", "schema has no topic", "", "", "");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for(let topic of treedb.topics) {
|
|
102
|
+
let col_names = topic.cols.map(c => c.name);
|
|
103
|
+
|
|
104
|
+
if(topic.cols.length === 0) {
|
|
105
|
+
add("error", "topic has no column", topic.name, "", "");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* The pkey is the column every record is stored under: naming
|
|
109
|
+
* one that is not there is a topic that does not open. */
|
|
110
|
+
let pkey = topic.pkey || "id";
|
|
111
|
+
if(col_names.indexOf(pkey) < 0) {
|
|
112
|
+
add("error", "pkey names no column", topic.name, "", pkey);
|
|
113
|
+
}
|
|
114
|
+
for(let pkey2 of topic_pkey2s(topic.record)) {
|
|
115
|
+
if(col_names.indexOf(pkey2) < 0) {
|
|
116
|
+
add("error", "pkey2 names no column", topic.name, "", pkey2);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
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
|
+
for(let col of topic.cols) {
|
|
132
|
+
let flags = col_flags(col.record);
|
|
133
|
+
let type = col.record ? col.record.type : "";
|
|
134
|
+
let is_hook = flags.indexOf("hook") >= 0;
|
|
135
|
+
let is_fkey = flags.indexOf("fkey") >= 0;
|
|
136
|
+
|
|
137
|
+
if(!type) {
|
|
138
|
+
add("error", "column has no type", topic.name, col.name, "");
|
|
139
|
+
} else if(COL_TYPES.indexOf(type) < 0) {
|
|
140
|
+
add("error", "unknown column type", topic.name, col.name, type);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if(flags.indexOf("enum") >= 0 && col_enum(col.record).length === 0) {
|
|
144
|
+
add("error", "enum column has no enum", topic.name, col.name, "");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if(is_hook) {
|
|
148
|
+
if(HOOK_TYPES.indexOf(type) < 0) {
|
|
149
|
+
add("error", "hook must be a collection", topic.name, col.name, type);
|
|
150
|
+
}
|
|
151
|
+
let hook = col_hook(col.record);
|
|
152
|
+
if(!hook || Object.keys(hook).length === 0) {
|
|
153
|
+
/* A hook with no mapping links nothing, and the write
|
|
154
|
+
* that made it succeeded. */
|
|
155
|
+
add("error", "hook has no mapping", topic.name, col.name, "");
|
|
156
|
+
} else {
|
|
157
|
+
for(let [child_topic, fkey_col] of Object.entries(hook)) {
|
|
158
|
+
let child = topic_by_name[child_topic];
|
|
159
|
+
if(!child) {
|
|
160
|
+
add("error", "hook names no topic", topic.name, col.name,
|
|
161
|
+
child_topic);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
let child_col = null;
|
|
165
|
+
for(let c of child.cols) {
|
|
166
|
+
if(c.name === fkey_col) {
|
|
167
|
+
child_col = c;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if(!child_col) {
|
|
172
|
+
add("error", "hook names no fkey column", topic.name, col.name,
|
|
173
|
+
`${child_topic}.${fkey_col}`);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if(col_flags(child_col.record).indexOf("fkey") < 0) {
|
|
177
|
+
/* The column exists and is not a fkey: the link
|
|
178
|
+
* saves nothing and the graph draws no edge. */
|
|
179
|
+
add("error", "hook target is not a fkey", topic.name, col.name,
|
|
180
|
+
`${child_topic}.${fkey_col}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if(is_fkey && FKEY_TYPES.indexOf(type) < 0) {
|
|
187
|
+
add("error", "fkey has a bad type", topic.name, col.name, type);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* How many hooks name each fkey column. Both answers are a
|
|
193
|
+
* finding: NONE and the parent side of the link was renamed or
|
|
194
|
+
* deleted, so the references are written by nobody; TWO and the
|
|
195
|
+
* treedb refuses to open the schema ("Only can be one fkey",
|
|
196
|
+
* tr_treedb.c) because the column cannot hold both parents. */
|
|
197
|
+
let hooked = {};
|
|
198
|
+
for(let topic of treedb.topics) {
|
|
199
|
+
for(let col of topic.cols) {
|
|
200
|
+
let hook = col_hook(col.record);
|
|
201
|
+
if(!hook) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
for(let [child_topic, fkey_col] of Object.entries(hook)) {
|
|
205
|
+
let key = `${child_topic}.${fkey_col}`;
|
|
206
|
+
hooked[key] = (hooked[key] || 0) + 1;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for(let topic of treedb.topics) {
|
|
211
|
+
for(let col of topic.cols) {
|
|
212
|
+
if(col_flags(col.record).indexOf("fkey") < 0) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
let count = hooked[`${topic.name}.${col.name}`] || 0;
|
|
216
|
+
if(count === 0) {
|
|
217
|
+
add("warning", "fkey with no hook", topic.name, col.name, "");
|
|
218
|
+
} else if(count > 1) {
|
|
219
|
+
add("error", "fkey named by two hooks", topic.name, col.name, String(count));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
findings.sort((a, b) => {
|
|
225
|
+
if(a.severity !== b.severity) {
|
|
226
|
+
return a.severity === "error" ? -1 : 1;
|
|
227
|
+
}
|
|
228
|
+
return 0;
|
|
229
|
+
});
|
|
230
|
+
return findings;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/***************************************************************
|
|
234
|
+
* The same over a whole model, plus what belongs to no treedb:
|
|
235
|
+
* an orphan is a leftover of a deletion and the editor is where
|
|
236
|
+
* it is found.
|
|
237
|
+
***************************************************************/
|
|
238
|
+
function validate_model(model, options)
|
|
239
|
+
{
|
|
240
|
+
let findings = [];
|
|
241
|
+
|
|
242
|
+
if(!model) {
|
|
243
|
+
return findings;
|
|
244
|
+
}
|
|
245
|
+
for(let treedb of (model.treedbs || [])) {
|
|
246
|
+
findings = findings.concat(validate_schema(treedb, options));
|
|
247
|
+
}
|
|
248
|
+
for(let topic of (model.orphan_topics || [])) {
|
|
249
|
+
findings.push({
|
|
250
|
+
severity: "warning", code: "topic belongs to no treedb",
|
|
251
|
+
treedb: "", topic: topic.name, col: "", detail: topic.id
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
for(let col of (model.orphan_cols || [])) {
|
|
255
|
+
findings.push({
|
|
256
|
+
severity: "warning", code: "column belongs to no topic",
|
|
257
|
+
treedb: "", topic: "", col: col.name, detail: col.id
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return findings;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/***************************************************************
|
|
264
|
+
* Is there anything that should stop an Apply? Warnings do not:
|
|
265
|
+
* the operator may know better, and an unbumped version is worth
|
|
266
|
+
* saying and not worth refusing.
|
|
267
|
+
***************************************************************/
|
|
268
|
+
function has_errors(findings)
|
|
269
|
+
{
|
|
270
|
+
if(!Array.isArray(findings)) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
for(let f of findings) {
|
|
274
|
+
if(f && f.severity === "error") {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
export {
|
|
283
|
+
COL_TYPES,
|
|
284
|
+
HOOK_TYPES,
|
|
285
|
+
FKEY_TYPES,
|
|
286
|
+
validate_schema,
|
|
287
|
+
validate_model,
|
|
288
|
+
has_errors,
|
|
289
|
+
};
|