@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,234 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* schema_import.test.js
|
|
3
|
+
*
|
|
4
|
+
* The plan that makes a stored schema equal a given one, pinned.
|
|
5
|
+
*
|
|
6
|
+
* Two properties matter more than the rest and both are here: a
|
|
7
|
+
* plan that changes columns ALWAYS raises the topic version (else
|
|
8
|
+
* the import succeeds and nothing moves), and a plan run against
|
|
9
|
+
* the schema it was computed from is empty (else every import
|
|
10
|
+
* rewrites the whole store and every restart looks like a change).
|
|
11
|
+
***********************************************************************/
|
|
12
|
+
import { describe, test, expect } from "vitest";
|
|
13
|
+
import { build_schema_model } from "./schema_model.js";
|
|
14
|
+
import { schema_to_json } from "./schema_to_c.js";
|
|
15
|
+
import { plan_import, plan_deletes, same_value } from "./schema_import.js";
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
function model_of(cols_extra, topic_version)
|
|
19
|
+
{
|
|
20
|
+
return build_schema_model({
|
|
21
|
+
treedbs: [{id: "db", schema_version: 4}],
|
|
22
|
+
topics: [{id: "db.users", value: "users", order: 1, pkey: "id",
|
|
23
|
+
system_flag: "sf_string_key",
|
|
24
|
+
topic_version: topic_version === undefined ? 2 : topic_version,
|
|
25
|
+
treedbs: ["treedbs^db^topics"]}],
|
|
26
|
+
cols: [
|
|
27
|
+
{id: "db.users.id", value: "id", order: 1, header: "Id", fillspace: 20,
|
|
28
|
+
type: "string", flag: ["persistent", "required"],
|
|
29
|
+
topics: ["topics^db.users^cols"]},
|
|
30
|
+
{id: "db.users.name", value: "name", order: 2, header: "Name", fillspace: 10,
|
|
31
|
+
type: "string", flag: ["persistent"], topics: ["topics^db.users^cols"]},
|
|
32
|
+
].concat(cols_extra || []),
|
|
33
|
+
}).treedbs[0];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const INCOMING = {
|
|
37
|
+
id: "db",
|
|
38
|
+
schema_version: "4",
|
|
39
|
+
topics: [{
|
|
40
|
+
id: "users", pkey: "id", system_flag: "sf_string_key", topic_version: "2",
|
|
41
|
+
cols: {
|
|
42
|
+
id: {header: "Id", fillspace: 20, type: "string",
|
|
43
|
+
flag: ["persistent", "required"]},
|
|
44
|
+
name: {header: "Name", fillspace: 10, type: "string", flag: ["persistent"]},
|
|
45
|
+
}
|
|
46
|
+
}]
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function clone(o)
|
|
50
|
+
{
|
|
51
|
+
return JSON.parse(JSON.stringify(o));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
describe("a schema that is already there", () => {
|
|
56
|
+
test("plans nothing at all", () => {
|
|
57
|
+
const plan = plan_import(model_of(), INCOMING);
|
|
58
|
+
expect(plan.writes).toEqual([]);
|
|
59
|
+
expect(plan.conflicts).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("an export of the store re-imports as nothing — the round trip closes", () => {
|
|
63
|
+
const treedb = model_of();
|
|
64
|
+
const plan = plan_import(treedb, schema_to_json(treedb));
|
|
65
|
+
expect(plan.writes).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("flags in another order are the same flags", () => {
|
|
69
|
+
const incoming = clone(INCOMING);
|
|
70
|
+
incoming.topics[0].cols.id.flag = ["required", "persistent"];
|
|
71
|
+
expect(plan_import(model_of(), incoming).writes).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("a hook written as JSON text is the same hook", () => {
|
|
75
|
+
expect(same_value("hook", {a: "b"}, '{"a":"b"}')).toBe(true);
|
|
76
|
+
expect(same_value("flag", "fkey", ["fkey"])).toBe(true);
|
|
77
|
+
expect(same_value("header", "Id", "Id")).toBe(true);
|
|
78
|
+
expect(same_value("header", "Id", "Name")).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("adding a column", () => {
|
|
83
|
+
const incoming = clone(INCOMING);
|
|
84
|
+
incoming.topics[0].cols.email = {header: "Email", fillspace: 15, type: "string",
|
|
85
|
+
flag: ["persistent", "writable"]};
|
|
86
|
+
const plan = plan_import(model_of(), incoming);
|
|
87
|
+
|
|
88
|
+
test("creates it with the fkey and the order composed, not asked for", () => {
|
|
89
|
+
const create = plan.writes.find(w => w.op === "create");
|
|
90
|
+
expect(create.topic_name).toBe("cols");
|
|
91
|
+
expect(create.record.value).toBe("email");
|
|
92
|
+
expect(create.record.topics).toEqual(["topics^db.users^cols"]);
|
|
93
|
+
expect(create.record.order).toBe(3);
|
|
94
|
+
expect(create.record.header).toBe("Email");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("RAISES the topic version even though the incoming schema kept it", () => {
|
|
98
|
+
const update = plan.writes.find(w => w.topic_name === "topics");
|
|
99
|
+
expect(update.op).toBe("update");
|
|
100
|
+
expect(update.record.id).toBe("db.users");
|
|
101
|
+
expect(update.record.topic_version).toBe(3);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("raises the treedb's schema_version too", () => {
|
|
105
|
+
const treedb_write = plan.writes.find(w => w.topic_name === "treedbs");
|
|
106
|
+
expect(treedb_write.record.schema_version).toBe(5);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("the topic is written BEFORE the column that references it", () => {
|
|
110
|
+
const topic_at = plan.writes.findIndex(w => w.topic_name === "topics");
|
|
111
|
+
const col_at = plan.writes.findIndex(w => w.topic_name === "cols");
|
|
112
|
+
expect(topic_at).toBeLessThan(col_at);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("the summary counts what it did", () => {
|
|
116
|
+
expect(plan.summary.cols_created).toBe(1);
|
|
117
|
+
expect(plan.summary.topics_updated).toBe(1);
|
|
118
|
+
expect(plan.summary.cols_deleted).toBe(0);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("changing a column", () => {
|
|
123
|
+
test("only the fields that moved are written", () => {
|
|
124
|
+
const incoming = clone(INCOMING);
|
|
125
|
+
incoming.topics[0].cols.name.header = "Full name";
|
|
126
|
+
const plan = plan_import(model_of(), incoming);
|
|
127
|
+
const update = plan.writes.find(w => w.topic_name === "cols");
|
|
128
|
+
expect(update.record).toEqual({id: "db.users.name", header: "Full name"});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("a field the incoming schema DROPPED is cleared, not left behind", () => {
|
|
132
|
+
const incoming = clone(INCOMING);
|
|
133
|
+
delete incoming.topics[0].cols.name.fillspace;
|
|
134
|
+
const update = plan_import(model_of(), incoming).writes
|
|
135
|
+
.find(w => w.topic_name === "cols");
|
|
136
|
+
expect(update.record.fillspace).toBe("");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("dropping every flag clears the list rather than writing an empty string", () => {
|
|
140
|
+
const incoming = clone(INCOMING);
|
|
141
|
+
delete incoming.topics[0].cols.name.flag;
|
|
142
|
+
const update = plan_import(model_of(), incoming).writes
|
|
143
|
+
.find(w => w.topic_name === "cols");
|
|
144
|
+
expect(update.record.flag).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("reordering the columns writes their new order", () => {
|
|
148
|
+
const incoming = clone(INCOMING);
|
|
149
|
+
incoming.topics[0].cols = {
|
|
150
|
+
name: INCOMING.topics[0].cols.name,
|
|
151
|
+
id: INCOMING.topics[0].cols.id,
|
|
152
|
+
};
|
|
153
|
+
const plan = plan_import(model_of(), incoming);
|
|
154
|
+
const by_id = {};
|
|
155
|
+
for(const w of plan.writes.filter(w => w.topic_name === "cols")) {
|
|
156
|
+
by_id[w.record.id] = w.record.order;
|
|
157
|
+
}
|
|
158
|
+
expect(by_id["db.users.name"]).toBe(1);
|
|
159
|
+
expect(by_id["db.users.id"]).toBe(2);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("an incoming version ABOVE the stored one is honoured as it is", () => {
|
|
163
|
+
const incoming = clone(INCOMING);
|
|
164
|
+
incoming.topics[0].topic_version = "9";
|
|
165
|
+
incoming.topics[0].cols.name.header = "Full name";
|
|
166
|
+
const update = plan_import(model_of(), incoming).writes
|
|
167
|
+
.find(w => w.topic_name === "topics");
|
|
168
|
+
expect(update.record.topic_version).toBe("9");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("removing", () => {
|
|
173
|
+
const stored_extra = [{id: "db.users.legacy", value: "legacy", order: 3,
|
|
174
|
+
header: "Legacy", type: "string",
|
|
175
|
+
topics: ["topics^db.users^cols"]}];
|
|
176
|
+
|
|
177
|
+
test("a column the incoming schema does not declare is deleted", () => {
|
|
178
|
+
const plan = plan_import(model_of(stored_extra), INCOMING);
|
|
179
|
+
const del = plan_deletes(plan);
|
|
180
|
+
expect(del.length).toBe(1);
|
|
181
|
+
expect(del[0].record.id).toBe("db.users.legacy");
|
|
182
|
+
expect(plan.summary.cols_deleted).toBe(1);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("prune off MERGES instead: the extra column stays", () => {
|
|
186
|
+
const plan = plan_import(model_of(stored_extra), INCOMING, {prune: false});
|
|
187
|
+
expect(plan_deletes(plan)).toEqual([]);
|
|
188
|
+
expect(plan.writes).toEqual([]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("a whole topic that is gone takes its columns with it, columns first", () => {
|
|
192
|
+
const treedb = build_schema_model({
|
|
193
|
+
treedbs: [{id: "db", schema_version: 1}],
|
|
194
|
+
topics: [{id: "db.old", value: "old", order: 1, treedbs: ["treedbs^db^topics"]}],
|
|
195
|
+
cols: [{id: "db.old.x", value: "x", order: 1, type: "string",
|
|
196
|
+
topics: ["topics^db.old^cols"]}],
|
|
197
|
+
}).treedbs[0];
|
|
198
|
+
const plan = plan_import(treedb, {id: "db", schema_version: "1", topics: []});
|
|
199
|
+
const deletes = plan_deletes(plan);
|
|
200
|
+
expect(deletes.map(d => d.record.id)).toEqual(["db.old.x", "db.old"]);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("a treedb the store does not have yet", () => {
|
|
205
|
+
test("every topic and column is a create, and nothing is deleted", () => {
|
|
206
|
+
const plan = plan_import(null, INCOMING);
|
|
207
|
+
expect(plan.summary.topics_created).toBe(1);
|
|
208
|
+
expect(plan.summary.cols_created).toBe(2);
|
|
209
|
+
expect(plan_deletes(plan)).toEqual([]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("the new topic carries its fkey to the treedb", () => {
|
|
213
|
+
const create = plan_import(null, INCOMING).writes[0];
|
|
214
|
+
expect(create.record.treedbs).toEqual(["treedbs^db^topics"]);
|
|
215
|
+
expect(create.record.value).toBe("users");
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("what is not a schema", () => {
|
|
220
|
+
test("says so instead of planning writes", () => {
|
|
221
|
+
expect(plan_import(model_of(), null).conflicts[0].code).toBe("not a schema");
|
|
222
|
+
expect(plan_import(model_of(), {topics: "nope"}).conflicts[0].code).toBe("not a schema");
|
|
223
|
+
expect(plan_import(model_of(), {}).writes).toEqual([]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("a topic with no name is a conflict, not a nameless record", () => {
|
|
227
|
+
const plan = plan_import(model_of(), {id: "db", topics: [{cols: {}}]});
|
|
228
|
+
expect(plan.conflicts[0].code).toBe("topic has no name");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("no stored treedb and no id in the incoming schema plans nothing", () => {
|
|
232
|
+
expect(plan_import(null, {topics: []}).conflicts[0].code).toBe("schema has no id");
|
|
233
|
+
});
|
|
234
|
+
});
|