@yuneta/gobj-ui 7.26.21 → 7.26.23
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/gobj-ui.cjs.js +110 -15
- package/dist/gobj-ui.es.js +110 -15
- package/package.json +1 -1
- package/src/c_yui_schema_editor.js +46 -13
- package/src/schema_editor_reload.wiring.test.js +57 -0
- package/src/schema_import.js +50 -0
- package/src/schema_import.test.js +57 -0
- package/src/schema_model.js +24 -0
- package/src/schema_model.test.js +19 -0
- package/src/schema_validate.js +17 -0
- package/src/schema_validate.test.js +33 -0
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -25091,6 +25091,24 @@ function moved_orders(list) {
|
|
|
25091
25091
|
}
|
|
25092
25092
|
return writes;
|
|
25093
25093
|
}
|
|
25094
|
+
/***************************************************************
|
|
25095
|
+
* The TWO name rules of the treedb (SDK 7.26.9):
|
|
25096
|
+
*
|
|
25097
|
+
* a TOPIC name is a directory and a key: not empty, not "."
|
|
25098
|
+
* or "..", no '/' and no '`' (tranger2_name_is_identifier());
|
|
25099
|
+
* a COLUMN name (a column id, a hook, an fkey) is only a key:
|
|
25100
|
+
* not empty and no '`' (treedb_col_name_is_identifier()).
|
|
25101
|
+
*
|
|
25102
|
+
* Said here as in C, so the editor refuses what the treedb will.
|
|
25103
|
+
***************************************************************/
|
|
25104
|
+
function topic_name_breaks_the_rule(name) {
|
|
25105
|
+
let s = String(name === void 0 || name === null ? "" : name);
|
|
25106
|
+
return s === "" || s === "." || s === ".." || s.includes("/") || s.includes("`");
|
|
25107
|
+
}
|
|
25108
|
+
function col_name_breaks_the_rule(name) {
|
|
25109
|
+
let s = String(name === void 0 || name === null ? "" : name);
|
|
25110
|
+
return s === "" || s.includes("`");
|
|
25111
|
+
}
|
|
25094
25112
|
//#endregion
|
|
25095
25113
|
//#region src/treedb_topic_keys.js
|
|
25096
25114
|
/***********************************************************************
|
|
@@ -72670,6 +72688,8 @@ function validate_schema(treedb, options) {
|
|
|
72670
72688
|
if (treedb.topics.length === 0) add("warning", "schema has no topic", "", "", "");
|
|
72671
72689
|
for (let topic of treedb.topics) {
|
|
72672
72690
|
let col_names = topic.cols.map((c) => c.name);
|
|
72691
|
+
if (topic_name_breaks_the_rule(topic.name)) add("error", "topic name breaks the name rule", topic.name, "", topic.name);
|
|
72692
|
+
for (let col of topic.cols) if (col_name_breaks_the_rule(col.name)) add("error", "column name breaks the name rule", topic.name, col.name, col.name);
|
|
72673
72693
|
if (topic.cols.length === 0) add("error", "topic has no column", topic.name, "", "");
|
|
72674
72694
|
let pkey = topic.pkey || "id";
|
|
72675
72695
|
if (col_names.indexOf(pkey) < 0) add("error", "pkey names no column", topic.name, "", pkey);
|
|
@@ -72687,6 +72707,10 @@ function validate_schema(treedb, options) {
|
|
|
72687
72707
|
let hook = col_hook(col.record);
|
|
72688
72708
|
if (!hook || Object.keys(hook).length === 0) add("error", "hook has no mapping", topic.name, col.name, "");
|
|
72689
72709
|
else for (let [child_topic, fkey_col] of Object.entries(hook)) {
|
|
72710
|
+
if (col_name_breaks_the_rule(fkey_col)) {
|
|
72711
|
+
add("error", "column name breaks the name rule", topic.name, col.name, `${child_topic}.${fkey_col}`);
|
|
72712
|
+
continue;
|
|
72713
|
+
}
|
|
72690
72714
|
let child = topic_by_name[child_topic];
|
|
72691
72715
|
if (!child) {
|
|
72692
72716
|
add("error", "hook names no topic", topic.name, col.name, child_topic);
|
|
@@ -73594,6 +73618,52 @@ function plan_import(treedb, incoming, options) {
|
|
|
73594
73618
|
conflicts
|
|
73595
73619
|
};
|
|
73596
73620
|
}
|
|
73621
|
+
if (topic_name_breaks_the_rule(treedb_id)) conflicts.push({
|
|
73622
|
+
code: "treedb name breaks the name rule",
|
|
73623
|
+
topic: "",
|
|
73624
|
+
col: ""
|
|
73625
|
+
});
|
|
73626
|
+
for (let in_topic of incoming.topics) {
|
|
73627
|
+
let name = (in_topic || {}).id;
|
|
73628
|
+
if (name && topic_name_breaks_the_rule(name)) conflicts.push({
|
|
73629
|
+
code: "topic name breaks the name rule",
|
|
73630
|
+
topic: name,
|
|
73631
|
+
col: ""
|
|
73632
|
+
});
|
|
73633
|
+
let cols = (in_topic || {}).cols;
|
|
73634
|
+
if (Array.isArray(cols)) {
|
|
73635
|
+
for (let c of cols) if (!c || !c.id) conflicts.push({
|
|
73636
|
+
code: "column has no name",
|
|
73637
|
+
topic: name || "",
|
|
73638
|
+
col: ""
|
|
73639
|
+
});
|
|
73640
|
+
}
|
|
73641
|
+
for (let in_col of incoming_cols(in_topic || {})) {
|
|
73642
|
+
if (col_name_breaks_the_rule(in_col.name)) conflicts.push({
|
|
73643
|
+
code: "column name breaks the name rule",
|
|
73644
|
+
topic: name || "",
|
|
73645
|
+
col: in_col.name
|
|
73646
|
+
});
|
|
73647
|
+
let hook = (in_col.col || {}).hook;
|
|
73648
|
+
if (hook && typeof hook === "object" && !Array.isArray(hook)) for (let [child_topic, fkey_col] of Object.entries(hook)) {
|
|
73649
|
+
if (topic_name_breaks_the_rule(child_topic)) conflicts.push({
|
|
73650
|
+
code: "topic name breaks the name rule",
|
|
73651
|
+
topic: name || "",
|
|
73652
|
+
col: in_col.name
|
|
73653
|
+
});
|
|
73654
|
+
if (col_name_breaks_the_rule(fkey_col)) conflicts.push({
|
|
73655
|
+
code: "column name breaks the name rule",
|
|
73656
|
+
topic: name || "",
|
|
73657
|
+
col: in_col.name
|
|
73658
|
+
});
|
|
73659
|
+
}
|
|
73660
|
+
}
|
|
73661
|
+
}
|
|
73662
|
+
if (conflicts.length > 0) return {
|
|
73663
|
+
writes,
|
|
73664
|
+
summary,
|
|
73665
|
+
conflicts
|
|
73666
|
+
};
|
|
73597
73667
|
let stored_topics = {};
|
|
73598
73668
|
for (let topic of treedb && treedb.topics || []) stored_topics[topic.name] = topic;
|
|
73599
73669
|
let seen_topics = {};
|
|
@@ -74324,6 +74394,7 @@ var PRIVATE_DATA$1 = {
|
|
|
74324
74394
|
dialog: null,
|
|
74325
74395
|
dialog_gen: 0,
|
|
74326
74396
|
dialog_kind: "",
|
|
74397
|
+
dialog_content: null,
|
|
74327
74398
|
save_queue: null,
|
|
74328
74399
|
save_return: "",
|
|
74329
74400
|
save_wrote: false,
|
|
@@ -74387,14 +74458,27 @@ function mt_destroy$1(gobj) {
|
|
|
74387
74458
|
* Local Methods
|
|
74388
74459
|
***************************/
|
|
74389
74460
|
/************************************************************
|
|
74390
|
-
*
|
|
74391
|
-
*
|
|
74392
|
-
*
|
|
74393
|
-
*
|
|
74394
|
-
* a generic "cannot create topic".
|
|
74461
|
+
* A name the treedb refuses (the two name rules of
|
|
74462
|
+
* schema_model.js), said at once: the error carries its key as
|
|
74463
|
+
* a LITERAL (so a consumer's validate-locales sees it), and the
|
|
74464
|
+
* name field is marked aria-invalid for a reader.
|
|
74395
74465
|
************************************************************/
|
|
74396
|
-
function
|
|
74397
|
-
|
|
74466
|
+
function refuse_bad_name(gobj, $message) {
|
|
74467
|
+
mark_name_field(gobj, true);
|
|
74468
|
+
yui_shell_show_error(yui_shell_of(gobj), $message, { t: i18next.t });
|
|
74469
|
+
return -1;
|
|
74470
|
+
}
|
|
74471
|
+
/************************************************************
|
|
74472
|
+
* The name field says whether the LAST save refused its name:
|
|
74473
|
+
* set by every refusal of the name (empty, a rule, a duplicate),
|
|
74474
|
+
* cleared by a save that gets past them.
|
|
74475
|
+
************************************************************/
|
|
74476
|
+
function mark_name_field(gobj, invalid) {
|
|
74477
|
+
let priv = gobj.priv;
|
|
74478
|
+
let $value = priv.dialog_content ? priv.dialog_content.querySelector("[data-name=\"value\"]") : null;
|
|
74479
|
+
if (!$value) return;
|
|
74480
|
+
if (invalid) $value.setAttribute("aria-invalid", "true");
|
|
74481
|
+
else $value.removeAttribute("aria-invalid");
|
|
74398
74482
|
}
|
|
74399
74483
|
/***************************************************************
|
|
74400
74484
|
* The frame: a toolbar that never changes place, a notice line
|
|
@@ -75859,6 +75943,7 @@ function close_dialog(gobj) {
|
|
|
75859
75943
|
let dialog = priv.dialog;
|
|
75860
75944
|
priv.dialog = null;
|
|
75861
75945
|
priv.dialog_kind = "";
|
|
75946
|
+
priv.dialog_content = null;
|
|
75862
75947
|
priv.export_view = null;
|
|
75863
75948
|
try {
|
|
75864
75949
|
dialog.close();
|
|
@@ -75871,6 +75956,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix) {
|
|
|
75871
75956
|
close_dialog(gobj);
|
|
75872
75957
|
priv.dialog_gen = priv.model_gen;
|
|
75873
75958
|
priv.dialog_kind = logical;
|
|
75959
|
+
priv.dialog_content = $content;
|
|
75874
75960
|
priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
|
|
75875
75961
|
dialog: true,
|
|
75876
75962
|
title,
|
|
@@ -75880,6 +75966,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix) {
|
|
|
75880
75966
|
on_close: () => {
|
|
75881
75967
|
priv.dialog = null;
|
|
75882
75968
|
priv.dialog_kind = "";
|
|
75969
|
+
priv.dialog_content = null;
|
|
75883
75970
|
priv.export_view = null;
|
|
75884
75971
|
}
|
|
75885
75972
|
});
|
|
@@ -77905,17 +77992,21 @@ function ac_save_column(gobj, event, kw, src) {
|
|
|
77905
77992
|
}
|
|
77906
77993
|
let values = kw.values || {};
|
|
77907
77994
|
if (kw.creating && (0, _yuneta_gobj_js.empty_string)(String(values.value || "").trim())) {
|
|
77995
|
+
mark_name_field(gobj, true);
|
|
77908
77996
|
yui_shell_show_error(yui_shell_of(gobj), "a column needs a name", { t: i18next.t });
|
|
77909
77997
|
return -1;
|
|
77910
77998
|
}
|
|
77911
|
-
if (kw.creating &&
|
|
77912
|
-
|
|
77913
|
-
|
|
77914
|
-
|
|
77999
|
+
if (kw.creating && col_name_breaks_the_rule(String(values.value).trim())) return refuse_bad_name(gobj, (0, _yuneta_gobj_js.createElement2)([
|
|
78000
|
+
"span",
|
|
78001
|
+
{ i18n: "invalid column name" },
|
|
78002
|
+
(0, i18next.t)("invalid column name")
|
|
78003
|
+
]));
|
|
77915
78004
|
if (kw.creating && find_col(priv.model, priv.treedb_id, kw.topic, String(values.value).trim())) {
|
|
78005
|
+
mark_name_field(gobj, true);
|
|
77916
78006
|
yui_shell_show_error(yui_shell_of(gobj), "that column is already there", { t: i18next.t });
|
|
77917
78007
|
return -1;
|
|
77918
78008
|
}
|
|
78009
|
+
mark_name_field(gobj, false);
|
|
77919
78010
|
close_dialog(gobj);
|
|
77920
78011
|
return queue_writes(gobj, [{
|
|
77921
78012
|
op: kw.creating ? "create" : "update",
|
|
@@ -78163,17 +78254,21 @@ function ac_save_topic(gobj, event, kw, src) {
|
|
|
78163
78254
|
if (kw.creating) {
|
|
78164
78255
|
let name = String(values.value || "").trim();
|
|
78165
78256
|
if ((0, _yuneta_gobj_js.empty_string)(name)) {
|
|
78257
|
+
mark_name_field(gobj, true);
|
|
78166
78258
|
yui_shell_show_error(yui_shell_of(gobj), "a topic needs a name", { t: i18next.t });
|
|
78167
78259
|
return -1;
|
|
78168
78260
|
}
|
|
78169
|
-
if (
|
|
78170
|
-
|
|
78171
|
-
|
|
78172
|
-
|
|
78261
|
+
if (topic_name_breaks_the_rule(name)) return refuse_bad_name(gobj, (0, _yuneta_gobj_js.createElement2)([
|
|
78262
|
+
"span",
|
|
78263
|
+
{ i18n: "invalid topic name" },
|
|
78264
|
+
(0, i18next.t)("invalid topic name")
|
|
78265
|
+
]));
|
|
78173
78266
|
if (find_topic(priv.model, priv.treedb_id, name)) {
|
|
78267
|
+
mark_name_field(gobj, true);
|
|
78174
78268
|
yui_shell_show_error(yui_shell_of(gobj), "that topic is already there", { t: i18next.t });
|
|
78175
78269
|
return -1;
|
|
78176
78270
|
}
|
|
78271
|
+
mark_name_field(gobj, false);
|
|
78177
78272
|
}
|
|
78178
78273
|
close_dialog(gobj);
|
|
78179
78274
|
return queue_writes(gobj, [{
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -25086,6 +25086,24 @@ function moved_orders(list) {
|
|
|
25086
25086
|
}
|
|
25087
25087
|
return writes;
|
|
25088
25088
|
}
|
|
25089
|
+
/***************************************************************
|
|
25090
|
+
* The TWO name rules of the treedb (SDK 7.26.9):
|
|
25091
|
+
*
|
|
25092
|
+
* a TOPIC name is a directory and a key: not empty, not "."
|
|
25093
|
+
* or "..", no '/' and no '`' (tranger2_name_is_identifier());
|
|
25094
|
+
* a COLUMN name (a column id, a hook, an fkey) is only a key:
|
|
25095
|
+
* not empty and no '`' (treedb_col_name_is_identifier()).
|
|
25096
|
+
*
|
|
25097
|
+
* Said here as in C, so the editor refuses what the treedb will.
|
|
25098
|
+
***************************************************************/
|
|
25099
|
+
function topic_name_breaks_the_rule(name) {
|
|
25100
|
+
let s = String(name === void 0 || name === null ? "" : name);
|
|
25101
|
+
return s === "" || s === "." || s === ".." || s.includes("/") || s.includes("`");
|
|
25102
|
+
}
|
|
25103
|
+
function col_name_breaks_the_rule(name) {
|
|
25104
|
+
let s = String(name === void 0 || name === null ? "" : name);
|
|
25105
|
+
return s === "" || s.includes("`");
|
|
25106
|
+
}
|
|
25089
25107
|
//#endregion
|
|
25090
25108
|
//#region src/treedb_topic_keys.js
|
|
25091
25109
|
/***********************************************************************
|
|
@@ -72665,6 +72683,8 @@ function validate_schema(treedb, options) {
|
|
|
72665
72683
|
if (treedb.topics.length === 0) add("warning", "schema has no topic", "", "", "");
|
|
72666
72684
|
for (let topic of treedb.topics) {
|
|
72667
72685
|
let col_names = topic.cols.map((c) => c.name);
|
|
72686
|
+
if (topic_name_breaks_the_rule(topic.name)) add("error", "topic name breaks the name rule", topic.name, "", topic.name);
|
|
72687
|
+
for (let col of topic.cols) if (col_name_breaks_the_rule(col.name)) add("error", "column name breaks the name rule", topic.name, col.name, col.name);
|
|
72668
72688
|
if (topic.cols.length === 0) add("error", "topic has no column", topic.name, "", "");
|
|
72669
72689
|
let pkey = topic.pkey || "id";
|
|
72670
72690
|
if (col_names.indexOf(pkey) < 0) add("error", "pkey names no column", topic.name, "", pkey);
|
|
@@ -72682,6 +72702,10 @@ function validate_schema(treedb, options) {
|
|
|
72682
72702
|
let hook = col_hook(col.record);
|
|
72683
72703
|
if (!hook || Object.keys(hook).length === 0) add("error", "hook has no mapping", topic.name, col.name, "");
|
|
72684
72704
|
else for (let [child_topic, fkey_col] of Object.entries(hook)) {
|
|
72705
|
+
if (col_name_breaks_the_rule(fkey_col)) {
|
|
72706
|
+
add("error", "column name breaks the name rule", topic.name, col.name, `${child_topic}.${fkey_col}`);
|
|
72707
|
+
continue;
|
|
72708
|
+
}
|
|
72685
72709
|
let child = topic_by_name[child_topic];
|
|
72686
72710
|
if (!child) {
|
|
72687
72711
|
add("error", "hook names no topic", topic.name, col.name, child_topic);
|
|
@@ -73589,6 +73613,52 @@ function plan_import(treedb, incoming, options) {
|
|
|
73589
73613
|
conflicts
|
|
73590
73614
|
};
|
|
73591
73615
|
}
|
|
73616
|
+
if (topic_name_breaks_the_rule(treedb_id)) conflicts.push({
|
|
73617
|
+
code: "treedb name breaks the name rule",
|
|
73618
|
+
topic: "",
|
|
73619
|
+
col: ""
|
|
73620
|
+
});
|
|
73621
|
+
for (let in_topic of incoming.topics) {
|
|
73622
|
+
let name = (in_topic || {}).id;
|
|
73623
|
+
if (name && topic_name_breaks_the_rule(name)) conflicts.push({
|
|
73624
|
+
code: "topic name breaks the name rule",
|
|
73625
|
+
topic: name,
|
|
73626
|
+
col: ""
|
|
73627
|
+
});
|
|
73628
|
+
let cols = (in_topic || {}).cols;
|
|
73629
|
+
if (Array.isArray(cols)) {
|
|
73630
|
+
for (let c of cols) if (!c || !c.id) conflicts.push({
|
|
73631
|
+
code: "column has no name",
|
|
73632
|
+
topic: name || "",
|
|
73633
|
+
col: ""
|
|
73634
|
+
});
|
|
73635
|
+
}
|
|
73636
|
+
for (let in_col of incoming_cols(in_topic || {})) {
|
|
73637
|
+
if (col_name_breaks_the_rule(in_col.name)) conflicts.push({
|
|
73638
|
+
code: "column name breaks the name rule",
|
|
73639
|
+
topic: name || "",
|
|
73640
|
+
col: in_col.name
|
|
73641
|
+
});
|
|
73642
|
+
let hook = (in_col.col || {}).hook;
|
|
73643
|
+
if (hook && typeof hook === "object" && !Array.isArray(hook)) for (let [child_topic, fkey_col] of Object.entries(hook)) {
|
|
73644
|
+
if (topic_name_breaks_the_rule(child_topic)) conflicts.push({
|
|
73645
|
+
code: "topic name breaks the name rule",
|
|
73646
|
+
topic: name || "",
|
|
73647
|
+
col: in_col.name
|
|
73648
|
+
});
|
|
73649
|
+
if (col_name_breaks_the_rule(fkey_col)) conflicts.push({
|
|
73650
|
+
code: "column name breaks the name rule",
|
|
73651
|
+
topic: name || "",
|
|
73652
|
+
col: in_col.name
|
|
73653
|
+
});
|
|
73654
|
+
}
|
|
73655
|
+
}
|
|
73656
|
+
}
|
|
73657
|
+
if (conflicts.length > 0) return {
|
|
73658
|
+
writes,
|
|
73659
|
+
summary,
|
|
73660
|
+
conflicts
|
|
73661
|
+
};
|
|
73592
73662
|
let stored_topics = {};
|
|
73593
73663
|
for (let topic of treedb && treedb.topics || []) stored_topics[topic.name] = topic;
|
|
73594
73664
|
let seen_topics = {};
|
|
@@ -74319,6 +74389,7 @@ var PRIVATE_DATA$1 = {
|
|
|
74319
74389
|
dialog: null,
|
|
74320
74390
|
dialog_gen: 0,
|
|
74321
74391
|
dialog_kind: "",
|
|
74392
|
+
dialog_content: null,
|
|
74322
74393
|
save_queue: null,
|
|
74323
74394
|
save_return: "",
|
|
74324
74395
|
save_wrote: false,
|
|
@@ -74382,14 +74453,27 @@ function mt_destroy$1(gobj) {
|
|
|
74382
74453
|
* Local Methods
|
|
74383
74454
|
***************************/
|
|
74384
74455
|
/************************************************************
|
|
74385
|
-
*
|
|
74386
|
-
*
|
|
74387
|
-
*
|
|
74388
|
-
*
|
|
74389
|
-
* a generic "cannot create topic".
|
|
74456
|
+
* A name the treedb refuses (the two name rules of
|
|
74457
|
+
* schema_model.js), said at once: the error carries its key as
|
|
74458
|
+
* a LITERAL (so a consumer's validate-locales sees it), and the
|
|
74459
|
+
* name field is marked aria-invalid for a reader.
|
|
74390
74460
|
************************************************************/
|
|
74391
|
-
function
|
|
74392
|
-
|
|
74461
|
+
function refuse_bad_name(gobj, $message) {
|
|
74462
|
+
mark_name_field(gobj, true);
|
|
74463
|
+
yui_shell_show_error(yui_shell_of(gobj), $message, { t });
|
|
74464
|
+
return -1;
|
|
74465
|
+
}
|
|
74466
|
+
/************************************************************
|
|
74467
|
+
* The name field says whether the LAST save refused its name:
|
|
74468
|
+
* set by every refusal of the name (empty, a rule, a duplicate),
|
|
74469
|
+
* cleared by a save that gets past them.
|
|
74470
|
+
************************************************************/
|
|
74471
|
+
function mark_name_field(gobj, invalid) {
|
|
74472
|
+
let priv = gobj.priv;
|
|
74473
|
+
let $value = priv.dialog_content ? priv.dialog_content.querySelector("[data-name=\"value\"]") : null;
|
|
74474
|
+
if (!$value) return;
|
|
74475
|
+
if (invalid) $value.setAttribute("aria-invalid", "true");
|
|
74476
|
+
else $value.removeAttribute("aria-invalid");
|
|
74393
74477
|
}
|
|
74394
74478
|
/***************************************************************
|
|
74395
74479
|
* The frame: a toolbar that never changes place, a notice line
|
|
@@ -75854,6 +75938,7 @@ function close_dialog(gobj) {
|
|
|
75854
75938
|
let dialog = priv.dialog;
|
|
75855
75939
|
priv.dialog = null;
|
|
75856
75940
|
priv.dialog_kind = "";
|
|
75941
|
+
priv.dialog_content = null;
|
|
75857
75942
|
priv.export_view = null;
|
|
75858
75943
|
try {
|
|
75859
75944
|
dialog.close();
|
|
@@ -75866,6 +75951,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix) {
|
|
|
75866
75951
|
close_dialog(gobj);
|
|
75867
75952
|
priv.dialog_gen = priv.model_gen;
|
|
75868
75953
|
priv.dialog_kind = logical;
|
|
75954
|
+
priv.dialog_content = $content;
|
|
75869
75955
|
priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
|
|
75870
75956
|
dialog: true,
|
|
75871
75957
|
title,
|
|
@@ -75875,6 +75961,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix) {
|
|
|
75875
75961
|
on_close: () => {
|
|
75876
75962
|
priv.dialog = null;
|
|
75877
75963
|
priv.dialog_kind = "";
|
|
75964
|
+
priv.dialog_content = null;
|
|
75878
75965
|
priv.export_view = null;
|
|
75879
75966
|
}
|
|
75880
75967
|
});
|
|
@@ -77900,17 +77987,21 @@ function ac_save_column(gobj, event, kw, src) {
|
|
|
77900
77987
|
}
|
|
77901
77988
|
let values = kw.values || {};
|
|
77902
77989
|
if (kw.creating && empty_string(String(values.value || "").trim())) {
|
|
77990
|
+
mark_name_field(gobj, true);
|
|
77903
77991
|
yui_shell_show_error(yui_shell_of(gobj), "a column needs a name", { t });
|
|
77904
77992
|
return -1;
|
|
77905
77993
|
}
|
|
77906
|
-
if (kw.creating &&
|
|
77907
|
-
|
|
77908
|
-
|
|
77909
|
-
|
|
77994
|
+
if (kw.creating && col_name_breaks_the_rule(String(values.value).trim())) return refuse_bad_name(gobj, createElement2([
|
|
77995
|
+
"span",
|
|
77996
|
+
{ i18n: "invalid column name" },
|
|
77997
|
+
t("invalid column name")
|
|
77998
|
+
]));
|
|
77910
77999
|
if (kw.creating && find_col(priv.model, priv.treedb_id, kw.topic, String(values.value).trim())) {
|
|
78000
|
+
mark_name_field(gobj, true);
|
|
77911
78001
|
yui_shell_show_error(yui_shell_of(gobj), "that column is already there", { t });
|
|
77912
78002
|
return -1;
|
|
77913
78003
|
}
|
|
78004
|
+
mark_name_field(gobj, false);
|
|
77914
78005
|
close_dialog(gobj);
|
|
77915
78006
|
return queue_writes(gobj, [{
|
|
77916
78007
|
op: kw.creating ? "create" : "update",
|
|
@@ -78158,17 +78249,21 @@ function ac_save_topic(gobj, event, kw, src) {
|
|
|
78158
78249
|
if (kw.creating) {
|
|
78159
78250
|
let name = String(values.value || "").trim();
|
|
78160
78251
|
if (empty_string(name)) {
|
|
78252
|
+
mark_name_field(gobj, true);
|
|
78161
78253
|
yui_shell_show_error(yui_shell_of(gobj), "a topic needs a name", { t });
|
|
78162
78254
|
return -1;
|
|
78163
78255
|
}
|
|
78164
|
-
if (
|
|
78165
|
-
|
|
78166
|
-
|
|
78167
|
-
|
|
78256
|
+
if (topic_name_breaks_the_rule(name)) return refuse_bad_name(gobj, createElement2([
|
|
78257
|
+
"span",
|
|
78258
|
+
{ i18n: "invalid topic name" },
|
|
78259
|
+
t("invalid topic name")
|
|
78260
|
+
]));
|
|
78168
78261
|
if (find_topic(priv.model, priv.treedb_id, name)) {
|
|
78262
|
+
mark_name_field(gobj, true);
|
|
78169
78263
|
yui_shell_show_error(yui_shell_of(gobj), "that topic is already there", { t });
|
|
78170
78264
|
return -1;
|
|
78171
78265
|
}
|
|
78266
|
+
mark_name_field(gobj, false);
|
|
78172
78267
|
}
|
|
78173
78268
|
close_dialog(gobj);
|
|
78174
78269
|
return queue_writes(gobj, [{
|
package/package.json
CHANGED
|
@@ -140,6 +140,8 @@ import {
|
|
|
140
140
|
parse_fkey_ref,
|
|
141
141
|
next_order,
|
|
142
142
|
moved_orders,
|
|
143
|
+
topic_name_breaks_the_rule,
|
|
144
|
+
col_name_breaks_the_rule,
|
|
143
145
|
} from "./schema_model.js";
|
|
144
146
|
import {validate_schema, validate_model, has_errors} from "./schema_validate.js";
|
|
145
147
|
import {topic_descs} from "./schema_descs.js";
|
|
@@ -250,6 +252,7 @@ let PRIVATE_DATA = {
|
|
|
250
252
|
dialog: null,
|
|
251
253
|
dialog_gen: 0, /* the model_gen the open dialog was built on */
|
|
252
254
|
dialog_kind: "", /* the logical class of the open dialog */
|
|
255
|
+
dialog_content: null, /* the content of the open dialog (its name field is marked) */
|
|
253
256
|
|
|
254
257
|
/* A write, or a plan of them, in flight */
|
|
255
258
|
save_queue: null,
|
|
@@ -362,15 +365,36 @@ function mt_destroy(gobj)
|
|
|
362
365
|
|
|
363
366
|
|
|
364
367
|
/************************************************************
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* a generic "cannot create topic".
|
|
368
|
+
* A name the treedb refuses (the two name rules of
|
|
369
|
+
* schema_model.js), said at once: the error carries its key as
|
|
370
|
+
* a LITERAL (so a consumer's validate-locales sees it), and the
|
|
371
|
+
* name field is marked aria-invalid for a reader.
|
|
370
372
|
************************************************************/
|
|
371
|
-
function
|
|
373
|
+
function refuse_bad_name(gobj, $message)
|
|
372
374
|
{
|
|
373
|
-
|
|
375
|
+
mark_name_field(gobj, true);
|
|
376
|
+
yui_shell_show_error(yui_shell_of(gobj), $message, {t: t});
|
|
377
|
+
return -1;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/************************************************************
|
|
381
|
+
* The name field says whether the LAST save refused its name:
|
|
382
|
+
* set by every refusal of the name (empty, a rule, a duplicate),
|
|
383
|
+
* cleared by a save that gets past them.
|
|
384
|
+
************************************************************/
|
|
385
|
+
function mark_name_field(gobj, invalid)
|
|
386
|
+
{
|
|
387
|
+
let priv = gobj.priv;
|
|
388
|
+
let $value = priv.dialog_content?
|
|
389
|
+
priv.dialog_content.querySelector('[data-name="value"]') : null;
|
|
390
|
+
if(!$value) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if(invalid) {
|
|
394
|
+
$value.setAttribute("aria-invalid", "true");
|
|
395
|
+
} else {
|
|
396
|
+
$value.removeAttribute("aria-invalid");
|
|
397
|
+
}
|
|
374
398
|
}
|
|
375
399
|
|
|
376
400
|
/***************************************************************
|
|
@@ -1870,6 +1894,7 @@ function close_dialog(gobj)
|
|
|
1870
1894
|
let dialog = priv.dialog;
|
|
1871
1895
|
priv.dialog = null;
|
|
1872
1896
|
priv.dialog_kind = "";
|
|
1897
|
+
priv.dialog_content = null;
|
|
1873
1898
|
priv.export_view = null;
|
|
1874
1899
|
try {
|
|
1875
1900
|
dialog.close();
|
|
@@ -1885,6 +1910,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
|
|
|
1885
1910
|
close_dialog(gobj);
|
|
1886
1911
|
priv.dialog_gen = priv.model_gen;
|
|
1887
1912
|
priv.dialog_kind = logical;
|
|
1913
|
+
priv.dialog_content = $content;
|
|
1888
1914
|
priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
|
|
1889
1915
|
dialog: true,
|
|
1890
1916
|
title: title,
|
|
@@ -1894,6 +1920,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
|
|
|
1894
1920
|
on_close: () => {
|
|
1895
1921
|
priv.dialog = null;
|
|
1896
1922
|
priv.dialog_kind = "";
|
|
1923
|
+
priv.dialog_content = null;
|
|
1897
1924
|
priv.export_view = null;
|
|
1898
1925
|
}
|
|
1899
1926
|
});
|
|
@@ -4035,17 +4062,20 @@ function ac_save_column(gobj, event, kw, src)
|
|
|
4035
4062
|
|
|
4036
4063
|
let values = kw.values || {};
|
|
4037
4064
|
if(kw.creating && empty_string(String(values.value || "").trim())) {
|
|
4065
|
+
mark_name_field(gobj, true);
|
|
4038
4066
|
yui_shell_show_error(yui_shell_of(gobj), "a column needs a name", {t: t});
|
|
4039
4067
|
return -1;
|
|
4040
4068
|
}
|
|
4041
|
-
if(kw.creating &&
|
|
4042
|
-
|
|
4043
|
-
|
|
4069
|
+
if(kw.creating && col_name_breaks_the_rule(String(values.value).trim())) {
|
|
4070
|
+
return refuse_bad_name(gobj, createElement2(
|
|
4071
|
+
["span", {i18n: "invalid column name"}, t("invalid column name")]));
|
|
4044
4072
|
}
|
|
4045
4073
|
if(kw.creating && find_col(priv.model, priv.treedb_id, kw.topic, String(values.value).trim())) {
|
|
4074
|
+
mark_name_field(gobj, true);
|
|
4046
4075
|
yui_shell_show_error(yui_shell_of(gobj), "that column is already there", {t: t});
|
|
4047
4076
|
return -1;
|
|
4048
4077
|
}
|
|
4078
|
+
mark_name_field(gobj, false);
|
|
4049
4079
|
|
|
4050
4080
|
close_dialog(gobj);
|
|
4051
4081
|
return queue_writes(gobj, [{
|
|
@@ -4346,17 +4376,20 @@ function ac_save_topic(gobj, event, kw, src)
|
|
|
4346
4376
|
if(kw.creating) {
|
|
4347
4377
|
let name = String(values.value || "").trim();
|
|
4348
4378
|
if(empty_string(name)) {
|
|
4379
|
+
mark_name_field(gobj, true);
|
|
4349
4380
|
yui_shell_show_error(yui_shell_of(gobj), "a topic needs a name", {t: t});
|
|
4350
4381
|
return -1;
|
|
4351
4382
|
}
|
|
4352
|
-
if(
|
|
4353
|
-
|
|
4354
|
-
|
|
4383
|
+
if(topic_name_breaks_the_rule(name)) {
|
|
4384
|
+
return refuse_bad_name(gobj, createElement2(
|
|
4385
|
+
["span", {i18n: "invalid topic name"}, t("invalid topic name")]));
|
|
4355
4386
|
}
|
|
4356
4387
|
if(find_topic(priv.model, priv.treedb_id, name)) {
|
|
4388
|
+
mark_name_field(gobj, true);
|
|
4357
4389
|
yui_shell_show_error(yui_shell_of(gobj), "that topic is already there", {t: t});
|
|
4358
4390
|
return -1;
|
|
4359
4391
|
}
|
|
4392
|
+
mark_name_field(gobj, false);
|
|
4360
4393
|
}
|
|
4361
4394
|
|
|
4362
4395
|
close_dialog(gobj);
|
|
@@ -1716,3 +1716,60 @@ describe("the reason a load failed", () => {
|
|
|
1716
1716
|
expect($detail.getAttribute("data-i18n")).toBe("cannot reach the treedb");
|
|
1717
1717
|
});
|
|
1718
1718
|
});
|
|
1719
|
+
|
|
1720
|
+
|
|
1721
|
+
/*
|
|
1722
|
+
* The two name rules (REV-CLOUD V14.2): a name the treedb refuses is
|
|
1723
|
+
* said at once, with its key, and nothing is written; a '/' in a column
|
|
1724
|
+
* is not refused (a column is a key, not a directory).
|
|
1725
|
+
*/
|
|
1726
|
+
describe("names the treedb refuses", () => {
|
|
1727
|
+
test("a column x`y is refused with its key; kids/all is written", () => {
|
|
1728
|
+
const {editor, host} = build("names_col", "db/users");
|
|
1729
|
+
gobj_send_event(editor, "EV_SAVE_COLUMN", {
|
|
1730
|
+
creating: true, topic: "users", values: {value: "x`y", type: "string"}
|
|
1731
|
+
}, host);
|
|
1732
|
+
expect(take("update-node").length).toBe(0);
|
|
1733
|
+
expect(shown.length).toBe(1);
|
|
1734
|
+
expect(shown[0].getAttribute("data-i18n")).toBe("invalid column name");
|
|
1735
|
+
start_a_write(editor, host, "kids/all");
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
test("a topic a/b or .. is refused with its key", () => {
|
|
1739
|
+
const {editor, host} = build("names_topic", "db");
|
|
1740
|
+
for(const name of ["a/b", ".."]) {
|
|
1741
|
+
shown.length = 0;
|
|
1742
|
+
gobj_send_event(editor, "EV_SAVE_TOPIC", {
|
|
1743
|
+
creating: true, topic: "", values: {value: name}
|
|
1744
|
+
}, host);
|
|
1745
|
+
expect(take("update-node").length).toBe(0);
|
|
1746
|
+
expect(shown.length).toBe(1);
|
|
1747
|
+
expect(shown[0].getAttribute("data-i18n")).toBe("invalid topic name");
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
});
|
|
1751
|
+
|
|
1752
|
+
|
|
1753
|
+
/*
|
|
1754
|
+
* The name field says whether the LAST save refused its name (REV-CLOUD
|
|
1755
|
+
* V15.4): set by every refusal of the name, cleared by a save that gets
|
|
1756
|
+
* past them. It was set by the rule only, and never cleared.
|
|
1757
|
+
*/
|
|
1758
|
+
describe("aria-invalid of the name field", () => {
|
|
1759
|
+
test("set by a bad name and by a duplicate, cleared by a good one", () => {
|
|
1760
|
+
const {editor, host} = build("aria_topic", "db");
|
|
1761
|
+
gobj_send_event(editor, "EV_ADD_TOPIC", {}, host);
|
|
1762
|
+
const form = modals[modals.length - 1];
|
|
1763
|
+
const $value = form.$content.querySelector('[data-name="value"]');
|
|
1764
|
+
const save = (name) => {
|
|
1765
|
+
gobj_send_event(editor, "EV_SAVE_TOPIC", {
|
|
1766
|
+
creating: true, topic: "", values: {value: name}
|
|
1767
|
+
}, host);
|
|
1768
|
+
};
|
|
1769
|
+
save("a/b");
|
|
1770
|
+
expect($value.getAttribute("aria-invalid")).toBe("true");
|
|
1771
|
+
save("ok_name");
|
|
1772
|
+
expect($value.getAttribute("aria-invalid")).toBe(null);
|
|
1773
|
+
take("update-node");
|
|
1774
|
+
});
|
|
1775
|
+
});
|
package/src/schema_import.js
CHANGED
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
as_json,
|
|
32
32
|
is_empty_value,
|
|
33
33
|
fkey_ref,
|
|
34
|
+
topic_name_breaks_the_rule,
|
|
35
|
+
col_name_breaks_the_rule,
|
|
34
36
|
} from "./schema_model.js";
|
|
35
37
|
|
|
36
38
|
|
|
@@ -204,6 +206,54 @@ function plan_import(treedb, incoming, options)
|
|
|
204
206
|
return {writes: writes, summary: summary, conflicts: conflicts};
|
|
205
207
|
}
|
|
206
208
|
|
|
209
|
+
/*
|
|
210
|
+
* Names the treedb refuses (the two name rules of schema_model.js)
|
|
211
|
+
* stop the plan BEFORE any write: run through, the backend refused
|
|
212
|
+
* them half way, with the writes before them already done.
|
|
213
|
+
*/
|
|
214
|
+
if(topic_name_breaks_the_rule(treedb_id)) {
|
|
215
|
+
/* The treedb is a directory too (treedb_open_db() refuses it) */
|
|
216
|
+
conflicts.push({code: "treedb name breaks the name rule", topic: "", col: ""});
|
|
217
|
+
}
|
|
218
|
+
for(let in_topic of incoming.topics) {
|
|
219
|
+
let name = (in_topic || {}).id;
|
|
220
|
+
if(name && topic_name_breaks_the_rule(name)) {
|
|
221
|
+
conflicts.push({code: "topic name breaks the name rule", topic: name, col: ""});
|
|
222
|
+
}
|
|
223
|
+
let cols = (in_topic || {}).cols;
|
|
224
|
+
if(Array.isArray(cols)) {
|
|
225
|
+
/* incoming_cols() drops a column with no id: said, not dropped */
|
|
226
|
+
for(let c of cols) {
|
|
227
|
+
if(!c || !c.id) {
|
|
228
|
+
conflicts.push({code: "column has no name", topic: name || "", col: ""});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for(let in_col of incoming_cols(in_topic || {})) {
|
|
233
|
+
if(col_name_breaks_the_rule(in_col.name)) {
|
|
234
|
+
conflicts.push({code: "column name breaks the name rule", topic: name || "", col: in_col.name});
|
|
235
|
+
}
|
|
236
|
+
/* The hook map names a child topic and its fkey column: names
|
|
237
|
+
* the treedb refuses too, even when no column declares them. */
|
|
238
|
+
let hook = (in_col.col || {}).hook;
|
|
239
|
+
if(hook && typeof hook === "object" && !Array.isArray(hook)) {
|
|
240
|
+
for(let [child_topic, fkey_col] of Object.entries(hook)) {
|
|
241
|
+
if(topic_name_breaks_the_rule(child_topic)) {
|
|
242
|
+
conflicts.push({code: "topic name breaks the name rule",
|
|
243
|
+
topic: name || "", col: in_col.name});
|
|
244
|
+
}
|
|
245
|
+
if(col_name_breaks_the_rule(fkey_col)) {
|
|
246
|
+
conflicts.push({code: "column name breaks the name rule",
|
|
247
|
+
topic: name || "", col: in_col.name});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if(conflicts.length > 0) {
|
|
254
|
+
return {writes: writes, summary: summary, conflicts: conflicts};
|
|
255
|
+
}
|
|
256
|
+
|
|
207
257
|
let stored_topics = {};
|
|
208
258
|
for(let topic of ((treedb && treedb.topics) || [])) {
|
|
209
259
|
stored_topics[topic.name] = topic;
|
|
@@ -253,3 +253,60 @@ describe("the empty collections the store answers with", () => {
|
|
|
253
253
|
expect(plan_import(stored, incoming).writes).toEqual([]);
|
|
254
254
|
});
|
|
255
255
|
});
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
describe("names the treedb refuses stop the plan before any write (REV-CLOUD V14.2)", () => {
|
|
259
|
+
test("a topic a/b, a topic .., a column x`y: conflicts, no writes", () => {
|
|
260
|
+
let plan = plan_import(null, {
|
|
261
|
+
id: "db", schema_version: "1",
|
|
262
|
+
topics: [
|
|
263
|
+
{id: "a/b", pkey: "id", cols: {id: {id: "id", type: "string"}}},
|
|
264
|
+
{id: "..", pkey: "id", cols: {id: {id: "id", type: "string"}}},
|
|
265
|
+
{id: "ok", pkey: "id", cols: {"id": {id: "id", type: "string"},
|
|
266
|
+
"x`y": {id: "x`y", type: "string"}}},
|
|
267
|
+
]
|
|
268
|
+
});
|
|
269
|
+
expect(plan.writes).toEqual([]);
|
|
270
|
+
let codes = plan.conflicts.map((c) => c.code);
|
|
271
|
+
expect(codes.filter((c) => c === "topic name breaks the name rule").length).toBe(2);
|
|
272
|
+
expect(codes).toContain("column name breaks the name rule");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("a '/' in a column and a leading '.' in a topic are imported", () => {
|
|
276
|
+
let plan = plan_import(null, {
|
|
277
|
+
id: "db", schema_version: "1",
|
|
278
|
+
topics: [{id: ".hidden", pkey: "id",
|
|
279
|
+
cols: {"id": {id: "id", type: "string"}, "kids/all": {id: "kids/all", type: "dict"}}}]
|
|
280
|
+
});
|
|
281
|
+
expect(plan.conflicts).toEqual([]);
|
|
282
|
+
expect(plan.writes.length).toBeGreaterThan(0);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
describe("the import reads the hook map and the treedb id (REV-CLOUD V15.4)", () => {
|
|
288
|
+
test("a hook to an fkey x`y or to a topic a/b that no column declares: conflicts", () => {
|
|
289
|
+
let plan = plan_import(null, {
|
|
290
|
+
id: "db", schema_version: "1",
|
|
291
|
+
topics: [{id: "p", pkey: "id", cols: {
|
|
292
|
+
"id": {id: "id", type: "string"},
|
|
293
|
+
"kids": {id: "kids", type: "dict", flag: ["hook"], hook: {"k": "x`y", "a/b": "pid"}},
|
|
294
|
+
}}]
|
|
295
|
+
});
|
|
296
|
+
expect(plan.writes).toEqual([]);
|
|
297
|
+
let codes = plan.conflicts.map((c) => c.code);
|
|
298
|
+
expect(codes).toContain("column name breaks the name rule");
|
|
299
|
+
expect(codes).toContain("topic name breaks the name rule");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("a treedb a/b, and a column with no id in an array: conflicts, not dropped", () => {
|
|
303
|
+
let plan = plan_import(null, {
|
|
304
|
+
id: "a/b", schema_version: "1",
|
|
305
|
+
topics: [{id: "t", pkey: "id", cols: [{id: "id", type: "string"}, {id: "", type: "string"}]}]
|
|
306
|
+
});
|
|
307
|
+
expect(plan.writes).toEqual([]);
|
|
308
|
+
let codes = plan.conflicts.map((c) => c.code);
|
|
309
|
+
expect(codes).toContain("treedb name breaks the name rule");
|
|
310
|
+
expect(codes).toContain("column has no name");
|
|
311
|
+
});
|
|
312
|
+
});
|
package/src/schema_model.js
CHANGED
|
@@ -515,8 +515,32 @@ function moved_orders(list)
|
|
|
515
515
|
return writes;
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
+
/***************************************************************
|
|
519
|
+
* The TWO name rules of the treedb (SDK 7.26.9):
|
|
520
|
+
*
|
|
521
|
+
* a TOPIC name is a directory and a key: not empty, not "."
|
|
522
|
+
* or "..", no '/' and no '`' (tranger2_name_is_identifier());
|
|
523
|
+
* a COLUMN name (a column id, a hook, an fkey) is only a key:
|
|
524
|
+
* not empty and no '`' (treedb_col_name_is_identifier()).
|
|
525
|
+
*
|
|
526
|
+
* Said here as in C, so the editor refuses what the treedb will.
|
|
527
|
+
***************************************************************/
|
|
528
|
+
function topic_name_breaks_the_rule(name)
|
|
529
|
+
{
|
|
530
|
+
let s = String(name === undefined || name === null ? "" : name);
|
|
531
|
+
return s === "" || s === "." || s === ".." || s.includes("/") || s.includes("`");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function col_name_breaks_the_rule(name)
|
|
535
|
+
{
|
|
536
|
+
let s = String(name === undefined || name === null ? "" : name);
|
|
537
|
+
return s === "" || s.includes("`");
|
|
538
|
+
}
|
|
539
|
+
|
|
518
540
|
|
|
519
541
|
export {
|
|
542
|
+
topic_name_breaks_the_rule,
|
|
543
|
+
col_name_breaks_the_rule,
|
|
520
544
|
DEFAULT_ORDER,
|
|
521
545
|
REF_SEP,
|
|
522
546
|
parse_fkey_ref,
|
package/src/schema_model.test.js
CHANGED
|
@@ -256,3 +256,22 @@ describe("is_empty_value — what the store answers when a blob was never set",
|
|
|
256
256
|
expect(is_empty_value(false)).toBe(false);
|
|
257
257
|
});
|
|
258
258
|
});
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
describe("the two name rules", async () => {
|
|
262
|
+
const { topic_name_breaks_the_rule, col_name_breaks_the_rule } = await import("./schema_model.js");
|
|
263
|
+
test("as C: topic = tranger2_name_is_identifier(), column = treedb_col_name_is_identifier()", () => {
|
|
264
|
+
for(let n of ["", ".", "..", "a/b", "a`b"]) {
|
|
265
|
+
expect(topic_name_breaks_the_rule(n)).toBe(true);
|
|
266
|
+
}
|
|
267
|
+
for(let n of ["users", ".hidden", "a.b"]) {
|
|
268
|
+
expect(topic_name_breaks_the_rule(n)).toBe(false);
|
|
269
|
+
}
|
|
270
|
+
for(let n of ["", "a`b"]) {
|
|
271
|
+
expect(col_name_breaks_the_rule(n)).toBe(true);
|
|
272
|
+
}
|
|
273
|
+
for(let n of ["id", "kids/all", "..", ".x"]) {
|
|
274
|
+
expect(col_name_breaks_the_rule(n)).toBe(false);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
});
|
package/src/schema_validate.js
CHANGED
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
col_hook,
|
|
33
33
|
col_enum,
|
|
34
34
|
topic_pkey2s,
|
|
35
|
+
topic_name_breaks_the_rule,
|
|
36
|
+
col_name_breaks_the_rule,
|
|
35
37
|
} from "./schema_model.js";
|
|
36
38
|
|
|
37
39
|
|
|
@@ -100,6 +102,16 @@ function validate_schema(treedb, options)
|
|
|
100
102
|
for(let topic of treedb.topics) {
|
|
101
103
|
let col_names = topic.cols.map(c => c.name);
|
|
102
104
|
|
|
105
|
+
/* A name the treedb refuses: the topic is left out when it opens. */
|
|
106
|
+
if(topic_name_breaks_the_rule(topic.name)) {
|
|
107
|
+
add("error", "topic name breaks the name rule", topic.name, "", topic.name);
|
|
108
|
+
}
|
|
109
|
+
for(let col of topic.cols) {
|
|
110
|
+
if(col_name_breaks_the_rule(col.name)) {
|
|
111
|
+
add("error", "column name breaks the name rule", topic.name, col.name, col.name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
103
115
|
if(topic.cols.length === 0) {
|
|
104
116
|
add("error", "topic has no column", topic.name, "", "");
|
|
105
117
|
}
|
|
@@ -143,6 +155,11 @@ function validate_schema(treedb, options)
|
|
|
143
155
|
add("error", "hook has no mapping", topic.name, col.name, "");
|
|
144
156
|
} else {
|
|
145
157
|
for(let [child_topic, fkey_col] of Object.entries(hook)) {
|
|
158
|
+
if(col_name_breaks_the_rule(fkey_col)) {
|
|
159
|
+
add("error", "column name breaks the name rule", topic.name, col.name,
|
|
160
|
+
`${child_topic}.${fkey_col}`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
146
163
|
let child = topic_by_name[child_topic];
|
|
147
164
|
if(!child) {
|
|
148
165
|
add("error", "hook names no topic", topic.name, col.name,
|
|
@@ -287,3 +287,36 @@ describe("only one hook may name a fkey column", () => {
|
|
|
287
287
|
expect(dup[0].severity).toBe("error");
|
|
288
288
|
});
|
|
289
289
|
});
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
describe("the two name rules (REV-CLOUD V14.2)", () => {
|
|
293
|
+
test("a column with the delimiter is an error; a '/' in a column is fine", () => {
|
|
294
|
+
let bad = validate_schema(schema({extra_col:
|
|
295
|
+
{id: "db.users.x`y", value: "x`y", order: 3, type: "string",
|
|
296
|
+
flag: ["persistent"], topics: ["topics^db.users^cols"]}}));
|
|
297
|
+
expect(codes(bad)).toContain("column name breaks the name rule");
|
|
298
|
+
let fine = validate_schema(schema({extra_col:
|
|
299
|
+
{id: "db.users.kids/all", value: "kids/all", order: 3, type: "string",
|
|
300
|
+
flag: ["persistent"], topics: ["topics^db.users^cols"]}}));
|
|
301
|
+
expect(codes(fine)).not.toContain("column name breaks the name rule");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("a topic named with '/' or '..' is an error; a leading '.' is not", () => {
|
|
305
|
+
for(let [name, wanted] of [["a/b", true], ["..", true], [".hidden", false]]) {
|
|
306
|
+
let treedb = build_schema_model({
|
|
307
|
+
treedbs: [{id: "db", schema_version: 1}],
|
|
308
|
+
topics: [{id: `db.${name}`, value: name, order: 1, pkey: "id",
|
|
309
|
+
topic_version: 1, treedbs: ["treedbs^db^topics"]}],
|
|
310
|
+
cols: [{id: `db.${name}.id`, value: "id", order: 1, type: "string",
|
|
311
|
+
flag: ["persistent"], topics: [`topics^db.${name}^cols`]}],
|
|
312
|
+
}).treedbs[0];
|
|
313
|
+
expect(codes(validate_schema(treedb)).includes("topic name breaks the name rule"))
|
|
314
|
+
.toBe(wanted);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("a hook mapping to an fkey column with the delimiter is an error", () => {
|
|
319
|
+
let found = validate_schema(schema({hook: {users: "dep`artments"}}));
|
|
320
|
+
expect(codes(found)).toContain("column name breaks the name rule");
|
|
321
|
+
});
|
|
322
|
+
});
|