@yuneta/gobj-ui 7.23.169 → 7.23.171
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 +24 -0
- package/dist/gobj-ui.cjs.js +581 -421
- package/dist/gobj-ui.es.js +581 -421
- package/package.json +2 -2
- package/src/c_yui_treedb_schema.js +26 -5
- package/src/c_yui_treedb_topic_with_form.js +5 -39
- package/src/c_yui_treedb_topics.css +3 -0
- package/src/c_yui_treedb_topics.js +57 -35
- package/src/treedb_topic_keys.js +122 -0
- package/src/treedb_topic_keys.test.js +79 -0
- package/src/treedb_write_plan.js +47 -1
- package/src/treedb_write_plan.test.js +53 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yuneta/gobj-ui",
|
|
3
|
-
"version": "7.23.
|
|
3
|
+
"version": "7.23.171",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/gobj-ui.cjs.js",
|
|
6
6
|
"module": "dist/gobj-ui.es.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@rollup/plugin-terser": "^1.0.0",
|
|
32
32
|
"@vitest/coverage-v8": "^4.1.10",
|
|
33
|
-
"@yuneta/gobj-js": "^7.
|
|
33
|
+
"@yuneta/gobj-js": "^7.21.0",
|
|
34
34
|
"vite": "^8.2.1",
|
|
35
35
|
"vitest": "^4.1.10"
|
|
36
36
|
},
|
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
yui_graph_update_zoom,
|
|
69
69
|
} from "./yui_graph_camera.js";
|
|
70
70
|
import {ensure_pinch_zoom_patch} from "./g6_touch_gestures.js";
|
|
71
|
+
import {col_key_roles} from "./treedb_topic_keys.js";
|
|
71
72
|
|
|
72
73
|
import {getStrokeColor} from "./lib_graph.js";
|
|
73
74
|
|
|
@@ -260,12 +261,20 @@ function hook_mark(type)
|
|
|
260
261
|
* (↖) 1 fkey (1 parent)
|
|
261
262
|
* [↖] n fkeys (n parents)
|
|
262
263
|
* {↖} N fkeys (N parents)
|
|
264
|
+
* (2) a secondary key (pkey2)
|
|
265
|
+
* (t) the time key (tkey)
|
|
263
266
|
* * required
|
|
264
267
|
* # the primary key
|
|
265
268
|
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
+
* The key marks lead the mark column, and a pkey2 is drawn in
|
|
270
|
+
* bold like the pkey: a topic with secondary keys keeps several
|
|
271
|
+
* instances under one id, which is the first thing to know
|
|
272
|
+
* about it. `(t)` is new to the notation: the literals' legend
|
|
273
|
+
* carries it too.
|
|
274
|
+
*
|
|
275
|
+
* The flags are read here rather than through
|
|
276
|
+
* treedb_get_field_desc's single `type`, which keeps only the
|
|
277
|
+
* last flag it saw.
|
|
269
278
|
************************************************************/
|
|
270
279
|
function schema_rows(desc)
|
|
271
280
|
{
|
|
@@ -280,6 +289,14 @@ function schema_rows(desc)
|
|
|
280
289
|
let flags = Array.isArray(col.flag)? col.flag : [];
|
|
281
290
|
let is_hook = flags.indexOf("hook") >= 0;
|
|
282
291
|
let is_fkey = flags.indexOf("fkey") >= 0;
|
|
292
|
+
let roles = col_key_roles(desc, col.id);
|
|
293
|
+
let key_marks = [];
|
|
294
|
+
if(roles.includes("pkey2")) {
|
|
295
|
+
key_marks.push("(2)");
|
|
296
|
+
}
|
|
297
|
+
if(roles.includes("tkey")) {
|
|
298
|
+
key_marks.push("(t)");
|
|
299
|
+
}
|
|
283
300
|
let mark = "";
|
|
284
301
|
if(is_hook) {
|
|
285
302
|
mark = hook_mark(col.type);
|
|
@@ -290,13 +307,17 @@ function schema_rows(desc)
|
|
|
290
307
|
.replace(")", "↖)");
|
|
291
308
|
mark = mark? (mark + " " + fmark) : fmark;
|
|
292
309
|
}
|
|
310
|
+
if(key_marks.length) {
|
|
311
|
+
mark = mark? (key_marks.join(" ") + " " + mark) : key_marks.join(" ");
|
|
312
|
+
}
|
|
293
313
|
rows.push({
|
|
294
314
|
name: col.id,
|
|
295
315
|
type: col.type || "",
|
|
296
316
|
mark: mark,
|
|
297
317
|
is_hook: is_hook,
|
|
298
318
|
is_fkey: is_fkey,
|
|
299
|
-
is_pkey: (
|
|
319
|
+
is_pkey: roles.includes("pkey"),
|
|
320
|
+
is_pkey2: roles.includes("pkey2"),
|
|
300
321
|
required: (flags.indexOf("required") >= 0 || flags.indexOf("notnull") >= 0),
|
|
301
322
|
});
|
|
302
323
|
}
|
|
@@ -367,7 +388,7 @@ function build_card_innerHTML(topic, rows, color, dark)
|
|
|
367
388
|
let rows_html = "";
|
|
368
389
|
for(let row of rows) {
|
|
369
390
|
let lead = row.is_pkey? "#" : (row.required? "*" : "");
|
|
370
|
-
let name_weight = (row.is_pkey || row.
|
|
391
|
+
let name_weight = (row.is_pkey || row.is_pkey2)? "700" : (row.required? "600" : "400");
|
|
371
392
|
let name_color = (row.is_hook || row.is_fkey)? title_color : text_color;
|
|
372
393
|
rows_html +=
|
|
373
394
|
` <div class="TREEDB_SCHEMA_FIELD" style="
|
|
@@ -96,7 +96,9 @@ import {t} from "i18next";
|
|
|
96
96
|
|
|
97
97
|
import {yui_toolbar_icon} from "./yui_toolbar.js";
|
|
98
98
|
|
|
99
|
-
import {
|
|
99
|
+
import {
|
|
100
|
+
plan_treedb_writes, READONLY_FORM_TOOLBAR, col_goes_back_to_treedb
|
|
101
|
+
} from "./treedb_write_plan.js";
|
|
100
102
|
import {
|
|
101
103
|
yui_file_read, yui_files_manifest, yui_file_id_label, yui_file_size_label
|
|
102
104
|
} from "./yui_file_field.js";
|
|
@@ -3262,42 +3264,6 @@ function get_schema_col(gobj, id)
|
|
|
3262
3264
|
return null;
|
|
3263
3265
|
}
|
|
3264
3266
|
|
|
3265
|
-
/************************************************************
|
|
3266
|
-
* Does column `col` travel back to treedb on a write?
|
|
3267
|
-
*
|
|
3268
|
-
* The form SHOWS every field and sends back only what the topic
|
|
3269
|
-
* accepts: the writable cols, the fkeys (a link is edited by
|
|
3270
|
-
* linking) and the pkey, which is not written but is what
|
|
3271
|
-
* ADDRESSES the record.
|
|
3272
|
-
*
|
|
3273
|
-
* It matters because `treedb_update_node()` does not check
|
|
3274
|
-
* `writable` -- it writes any col it is handed -- so a read-only
|
|
3275
|
-
* field travelling back is written with whatever the form made of
|
|
3276
|
-
* it, and the fields that describe a record are exactly the ones
|
|
3277
|
-
* that do not survive a round trip through a widget: a `time` is an
|
|
3278
|
-
* integer rendered as a `datetime-local`, with no seconds, so every
|
|
3279
|
-
* save moved it back up to 59 s. Nothing looked wrong until the
|
|
3280
|
-
* stored timestamp had moved.
|
|
3281
|
-
*
|
|
3282
|
-
* `is_file` and not the type: a `file` column IS an fkey (it is
|
|
3283
|
-
* flagged ['fkey','file']) but answers `type: "file"`, and the write
|
|
3284
|
-
* goes out with `autolink`, which rebuilds the links from what the
|
|
3285
|
-
* record carries. Dropped, a read-only `file` column -- the one only
|
|
3286
|
-
* a load fills -- was UNLINKED by every save of any other field.
|
|
3287
|
-
************************************************************/
|
|
3288
|
-
function col_goes_back_to_treedb(gobj, col)
|
|
3289
|
-
{
|
|
3290
|
-
let desc = gobj_read_attr(gobj, "desc");
|
|
3291
|
-
let pkey = desc.pkey || "id";
|
|
3292
|
-
const field_desc = treedb_get_field_desc(col);
|
|
3293
|
-
|
|
3294
|
-
if(field_desc.is_writable || field_desc.type === "fkey" ||
|
|
3295
|
-
field_desc.is_file || col.id === pkey) {
|
|
3296
|
-
return true;
|
|
3297
|
-
}
|
|
3298
|
-
return false;
|
|
3299
|
-
}
|
|
3300
|
-
|
|
3301
3267
|
/************************************************************
|
|
3302
3268
|
* The record the FORM hands over, without the columns that do
|
|
3303
3269
|
* not go back to treedb (see col_goes_back_to_treedb()). The
|
|
@@ -3309,7 +3275,7 @@ function strip_read_only_cols(gobj, record)
|
|
|
3309
3275
|
let row = {};
|
|
3310
3276
|
for(let field_name of Object.keys(record || {})) {
|
|
3311
3277
|
let col = get_schema_col(gobj, field_name);
|
|
3312
|
-
if(col && !col_goes_back_to_treedb(gobj, col)) {
|
|
3278
|
+
if(col && !col_goes_back_to_treedb(gobj_read_attr(gobj, "desc"), col)) {
|
|
3313
3279
|
continue;
|
|
3314
3280
|
}
|
|
3315
3281
|
row[field_name] = record[field_name];
|
|
@@ -3336,7 +3302,7 @@ function transform__form_record_2_treedb_record(gobj, kw, operation)
|
|
|
3336
3302
|
continue;
|
|
3337
3303
|
}
|
|
3338
3304
|
|
|
3339
|
-
if(!col_goes_back_to_treedb(gobj, col)) {
|
|
3305
|
+
if(!col_goes_back_to_treedb(gobj_read_attr(gobj, "desc"), col)) {
|
|
3340
3306
|
continue;
|
|
3341
3307
|
}
|
|
3342
3308
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Management of TreeDB's topics with Bulma tabs
|
|
5
5
|
*
|
|
6
|
-
* Copyright (c) 2025, ArtGins.
|
|
6
|
+
* Copyright (c) 2025-2026, ArtGins.
|
|
7
7
|
* All Rights Reserved.
|
|
8
8
|
***********************************************************************/
|
|
9
9
|
|
|
@@ -66,6 +66,9 @@ import {
|
|
|
66
66
|
} from "./c_yui_shell.js";
|
|
67
67
|
import {nodes_answer} from "./nodes_answer.js";
|
|
68
68
|
import {yui_toolbar_icon} from "./yui_toolbar.js";
|
|
69
|
+
import {
|
|
70
|
+
desc_pkey2s, desc_tkey, col_key_roles, system_flag_names
|
|
71
|
+
} from "./treedb_topic_keys.js";
|
|
69
72
|
|
|
70
73
|
import {t} from "i18next";
|
|
71
74
|
|
|
@@ -688,6 +691,17 @@ function topic_card_meta(desc)
|
|
|
688
691
|
]]);
|
|
689
692
|
meta.push(['div', {class: 'TREEDB_TOPIC_CARD_META'}, head]);
|
|
690
693
|
|
|
694
|
+
/* The secondary keys come before the relations: they change what
|
|
695
|
+
* a record of this topic IS (several instances under one id). */
|
|
696
|
+
let pkey2s = desc_pkey2s(desc);
|
|
697
|
+
if(pkey2s.length) {
|
|
698
|
+
let list = pkey2s.join(", ");
|
|
699
|
+
meta.push(['div', {class: 'TREEDB_TOPIC_CARD_REL TREEDB_TOPIC_CARD_PKEY2S', title: list}, [
|
|
700
|
+
['span', {class: 'TREEDB_TOPIC_CARD_REL_LABEL', i18n: 'pkey2s'}, 'pkey2s'],
|
|
701
|
+
['span', {class: 'TREEDB_TOPIC_CARD_REL_LIST'}, list]
|
|
702
|
+
]]);
|
|
703
|
+
}
|
|
704
|
+
|
|
691
705
|
let relation = (cls, key, arrow, names) => {
|
|
692
706
|
let list = names.join(", ");
|
|
693
707
|
return ['div', {class: `TREEDB_TOPIC_CARD_REL ${cls}`, title: list}, [
|
|
@@ -1021,9 +1035,15 @@ function show_topic_info(gobj, topic)
|
|
|
1021
1035
|
|
|
1022
1036
|
/************************************************************
|
|
1023
1037
|
* Build the read-only topic-info panel from a topic desc: the
|
|
1024
|
-
* topic name, its
|
|
1038
|
+
* topic name, its KEYS, and a table of columns (name / type /
|
|
1025
1039
|
* key relationship). Everything is guarded — a malformed desc
|
|
1026
1040
|
* renders a shorter panel, never throws.
|
|
1041
|
+
*
|
|
1042
|
+
* The keys are what an operator has to know before touching a
|
|
1043
|
+
* record, so they are all said, and the empty ones too: a
|
|
1044
|
+
* topic with `pkey2s` keeps several instances under one id,
|
|
1045
|
+
* and an empty `tkey` means the time of a record is the time
|
|
1046
|
+
* it was appended -- a row missing is not the same as "none".
|
|
1027
1047
|
************************************************************/
|
|
1028
1048
|
function build_topic_info_panel(gobj, $info, topic, desc)
|
|
1029
1049
|
{
|
|
@@ -1032,38 +1052,39 @@ function build_topic_info_panel(gobj, $info, topic, desc)
|
|
|
1032
1052
|
}
|
|
1033
1053
|
|
|
1034
1054
|
let pkey = desc.pkey || "";
|
|
1055
|
+
let pkey2s = desc_pkey2s(desc);
|
|
1056
|
+
let tkey = desc_tkey(desc);
|
|
1035
1057
|
|
|
1036
|
-
/* Topic metadata
|
|
1037
|
-
*
|
|
1038
|
-
* present; the version is emphasised as a tag. */
|
|
1039
|
-
let fmt_flag = (v) => {
|
|
1040
|
-
if(is_array(v)) {
|
|
1041
|
-
return v.join(", ");
|
|
1042
|
-
}
|
|
1043
|
-
if(is_object(v)) {
|
|
1044
|
-
return Object.keys(v).join(", ");
|
|
1045
|
-
}
|
|
1046
|
-
return (v === undefined || v === null) ? "" : String(v);
|
|
1047
|
-
};
|
|
1058
|
+
/* Topic metadata: version / system / pkey / pkey2s / tkey. The
|
|
1059
|
+
* version and the secondary keys are emphasised as tags. */
|
|
1048
1060
|
let $meta = [];
|
|
1049
|
-
let push_meta = (key,
|
|
1050
|
-
if(value === undefined || value === null || value === "") {
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
let $val = highlight
|
|
1054
|
-
? ["span", {class: "tag is-info"}, `${value}`]
|
|
1055
|
-
: ["code", {}, `${value}`];
|
|
1061
|
+
let push_meta = (key, $val) => {
|
|
1056
1062
|
$meta.push(
|
|
1057
|
-
["tr", {}, [
|
|
1063
|
+
["tr", {class: `TREEDB_TOPIC_INFO_${key.toUpperCase()}`}, [
|
|
1058
1064
|
["th", {i18n: key, style: "width:11rem;"}, key],
|
|
1059
1065
|
["td", {}, [$val]]
|
|
1060
1066
|
]]
|
|
1061
1067
|
);
|
|
1062
1068
|
};
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1069
|
+
if(desc.topic_version !== undefined && desc.topic_version !== null && desc.topic_version !== "") {
|
|
1070
|
+
push_meta("version", ["span", {class: "tag is-info"}, `${desc.topic_version}`]);
|
|
1071
|
+
}
|
|
1072
|
+
let sf_names = system_flag_names(desc.system_flag);
|
|
1073
|
+
if(sf_names.length > 0) {
|
|
1074
|
+
push_meta("system", ["code", {title: `${desc.system_flag}`}, sf_names.join(" | ")]);
|
|
1075
|
+
}
|
|
1076
|
+
if(pkey) {
|
|
1077
|
+
push_meta("pkey", ["code", {}, pkey]);
|
|
1078
|
+
}
|
|
1079
|
+
if(pkey2s.length > 0) {
|
|
1080
|
+
push_meta("pkey2s", ["span", {class: "tags"},
|
|
1081
|
+
pkey2s.map(name => ["span", {class: "tag is-warning"}, name])
|
|
1082
|
+
]);
|
|
1083
|
+
}
|
|
1084
|
+
push_meta("tkey", tkey
|
|
1085
|
+
? ["code", {}, tkey]
|
|
1086
|
+
: ["span", {class: "has-text-grey", i18n: "append time"}, "append time"]
|
|
1087
|
+
);
|
|
1067
1088
|
|
|
1068
1089
|
let $rows = [];
|
|
1069
1090
|
let cols = is_array(desc.cols) ? desc.cols : [];
|
|
@@ -1072,19 +1093,20 @@ function build_topic_info_panel(gobj, $info, topic, desc)
|
|
|
1072
1093
|
continue;
|
|
1073
1094
|
}
|
|
1074
1095
|
let type = col.type || (is_array(col.flag) ? col.flag.join(", ") : (col.flag || ""));
|
|
1075
|
-
let
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
rel = "→ " + Object.keys(col.fkey).join(", ");
|
|
1096
|
+
let roles = col_key_roles(desc, col.id);
|
|
1097
|
+
let parts = roles.slice();
|
|
1098
|
+
if(is_object(col.fkey)) {
|
|
1099
|
+
parts.push("→ " + Object.keys(col.fkey).join(", "));
|
|
1080
1100
|
} else if(is_object(col.hook)) {
|
|
1081
|
-
|
|
1101
|
+
parts.push("hook → " + Object.keys(col.hook).join(", "));
|
|
1082
1102
|
}
|
|
1103
|
+
let rel = parts.join(" · ");
|
|
1104
|
+
let is_key = roles.length > 0;
|
|
1083
1105
|
$rows.push(
|
|
1084
|
-
["tr", {}, [
|
|
1085
|
-
["td", {}, [["code", {}, `${col.id}`]]],
|
|
1106
|
+
["tr", is_key ? {class: "TREEDB_TOPIC_INFO_KEY_COL"} : {}, [
|
|
1107
|
+
["td", {}, [["code", is_key ? {class: "has-text-weight-bold"} : {}, `${col.id}`]]],
|
|
1086
1108
|
["td", {}, `${type}`],
|
|
1087
|
-
["td", {}, `${rel}`]
|
|
1109
|
+
["td", is_key ? {class: "has-text-weight-semibold"} : {}, `${rel}`]
|
|
1088
1110
|
]]
|
|
1089
1111
|
);
|
|
1090
1112
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* treedb_topic_keys.js
|
|
3
|
+
*
|
|
4
|
+
* The KEYS of a treedb topic, read from its desc: the pkey, the
|
|
5
|
+
* secondary keys (`pkey2s`) and the time key (`tkey`), plus the
|
|
6
|
+
* names behind its `system_flag` number.
|
|
7
|
+
*
|
|
8
|
+
* Every view that describes a topic -- the schema diagram, the
|
|
9
|
+
* topic-info panel, the topic cards -- must say them, and say them
|
|
10
|
+
* the same way. A topic with `pkey2s` is not a topic with one more
|
|
11
|
+
* column: one id holds several INSTANCES, an update has to carry
|
|
12
|
+
* the pkey2 or it lands on the primary, and `delete-instance`
|
|
13
|
+
* exists. `tkey` names where the time of every record comes from.
|
|
14
|
+
* The views said neither, so this is where they ask.
|
|
15
|
+
*
|
|
16
|
+
* Pure, so it is tested with no DOM.
|
|
17
|
+
*
|
|
18
|
+
* Copyright (c) 2026, ArtGins.
|
|
19
|
+
* All Rights Reserved.
|
|
20
|
+
***********************************************************************/
|
|
21
|
+
import {topic_pkey2s} from "./schema_model.js";
|
|
22
|
+
|
|
23
|
+
/* The names of the system_flag bits, in bit order: the `sf_names`
|
|
24
|
+
* table of timeranger2.c, which the C enum points at. Keep the two in
|
|
25
|
+
* step -- an empty slot is a bit with no name. */
|
|
26
|
+
const SF_NAMES = [
|
|
27
|
+
"sf_string_key", // 0x0001
|
|
28
|
+
"sf_rowid_key", // 0x0002
|
|
29
|
+
"sf_int_key", // 0x0004
|
|
30
|
+
"", // 0x0008
|
|
31
|
+
"sf_zip_record", // 0x0010
|
|
32
|
+
"sf_cipher_record", // 0x0020
|
|
33
|
+
"", // 0x0040
|
|
34
|
+
"", // 0x0080
|
|
35
|
+
"sf_t_ms", // 0x0100
|
|
36
|
+
"sf_tm_ms", // 0x0200
|
|
37
|
+
"sf_deleted_instance", // 0x0400
|
|
38
|
+
"sf_immutable_record", // 0x0800
|
|
39
|
+
"sf_loading_from_disk", // 0x1000
|
|
40
|
+
"", // 0x2000
|
|
41
|
+
"", // 0x4000
|
|
42
|
+
"", // 0x8000
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
/************************************************************
|
|
47
|
+
* The secondary keys of a topic, always a list. The desc
|
|
48
|
+
* carries a list; a schema literal may declare a bare
|
|
49
|
+
* string.
|
|
50
|
+
************************************************************/
|
|
51
|
+
function desc_pkey2s(desc)
|
|
52
|
+
{
|
|
53
|
+
return topic_pkey2s(desc);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/************************************************************
|
|
57
|
+
* The time key of a topic, "" when it has none -- and then
|
|
58
|
+
* the time of a record is the time it was appended.
|
|
59
|
+
************************************************************/
|
|
60
|
+
function desc_tkey(desc)
|
|
61
|
+
{
|
|
62
|
+
let tkey = desc ? desc.tkey : "";
|
|
63
|
+
return (typeof tkey === "string") ? tkey : "";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/************************************************************
|
|
67
|
+
* Which keys of its topic a column IS, in the order the
|
|
68
|
+
* views print them: "pkey", "pkey2", "tkey". Empty for a
|
|
69
|
+
* column that is none.
|
|
70
|
+
************************************************************/
|
|
71
|
+
function col_key_roles(desc, col_id)
|
|
72
|
+
{
|
|
73
|
+
let roles = [];
|
|
74
|
+
if(!desc || typeof col_id !== "string" || col_id.length === 0) {
|
|
75
|
+
return roles;
|
|
76
|
+
}
|
|
77
|
+
if(col_id === (desc.pkey || "id")) {
|
|
78
|
+
roles.push("pkey");
|
|
79
|
+
}
|
|
80
|
+
if(desc_pkey2s(desc).includes(col_id)) {
|
|
81
|
+
roles.push("pkey2");
|
|
82
|
+
}
|
|
83
|
+
if(desc_tkey(desc) === col_id) {
|
|
84
|
+
roles.push("tkey");
|
|
85
|
+
}
|
|
86
|
+
return roles;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/************************************************************
|
|
90
|
+
* The names of a system_flag. The desc carries the NUMBER
|
|
91
|
+
* (`1`), a schema literal carries the names
|
|
92
|
+
* ("sf_string_key|sf_t_ms"); both come back as a list. A
|
|
93
|
+
* set bit with no name is shown by its value, never
|
|
94
|
+
* dropped.
|
|
95
|
+
************************************************************/
|
|
96
|
+
function system_flag_names(system_flag)
|
|
97
|
+
{
|
|
98
|
+
if(Array.isArray(system_flag)) {
|
|
99
|
+
return system_flag.filter(n => typeof n === "string" && n.length > 0);
|
|
100
|
+
}
|
|
101
|
+
if(typeof system_flag === "string") {
|
|
102
|
+
return system_flag.split(/[|,]/).map(n => n.trim()).filter(n => n.length > 0);
|
|
103
|
+
}
|
|
104
|
+
if(typeof system_flag !== "number" || !Number.isInteger(system_flag) || system_flag <= 0) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
let names = [];
|
|
108
|
+
for(let bit = 0; bit < SF_NAMES.length; bit++) {
|
|
109
|
+
let mask = 1 << bit;
|
|
110
|
+
if(system_flag & mask) {
|
|
111
|
+
names.push(SF_NAMES[bit] || `0x${mask.toString(16).padStart(4, "0")}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return names;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export {
|
|
118
|
+
desc_pkey2s,
|
|
119
|
+
desc_tkey,
|
|
120
|
+
col_key_roles,
|
|
121
|
+
system_flag_names,
|
|
122
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* treedb_topic_keys.test.js
|
|
3
|
+
*
|
|
4
|
+
* The keys of a topic, read from its desc, pinned: the shapes the
|
|
5
|
+
* backend and the schema literals really send.
|
|
6
|
+
***********************************************************************/
|
|
7
|
+
import { describe, test, expect } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
desc_pkey2s, desc_tkey, col_key_roles, system_flag_names
|
|
10
|
+
} from "./treedb_topic_keys.js";
|
|
11
|
+
|
|
12
|
+
/* The `binaries` topic of treedb_yuneta_agent, as `descs` answers it. */
|
|
13
|
+
const BINARIES = {
|
|
14
|
+
topic_name: "binaries", pkey: "id", tkey: "", system_flag: 1,
|
|
15
|
+
topic_version: 3, pkey2s: ["version"],
|
|
16
|
+
cols: [{id: "id"}, {id: "version"}, {id: "size"}]
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("desc_pkey2s", () => {
|
|
20
|
+
test("a list, as the desc carries it", () => {
|
|
21
|
+
expect(desc_pkey2s(BINARIES)).toEqual(["version"]);
|
|
22
|
+
});
|
|
23
|
+
test("a bare string, as a schema literal declares it", () => {
|
|
24
|
+
expect(desc_pkey2s({pkey2s: "yuno_release"})).toEqual(["yuno_release"]);
|
|
25
|
+
});
|
|
26
|
+
test("absent or empty is no secondary key", () => {
|
|
27
|
+
expect(desc_pkey2s({})).toEqual([]);
|
|
28
|
+
expect(desc_pkey2s({pkey2s: ""})).toEqual([]);
|
|
29
|
+
expect(desc_pkey2s(null)).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("desc_tkey", () => {
|
|
34
|
+
test("empty is the append time", () => {
|
|
35
|
+
expect(desc_tkey(BINARIES)).toBe("");
|
|
36
|
+
expect(desc_tkey({})).toBe("");
|
|
37
|
+
expect(desc_tkey(null)).toBe("");
|
|
38
|
+
});
|
|
39
|
+
test("a named column", () => {
|
|
40
|
+
expect(desc_tkey({tkey: "tm"})).toBe("tm");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("col_key_roles", () => {
|
|
45
|
+
test("the pkey, a pkey2, and a column that is neither", () => {
|
|
46
|
+
expect(col_key_roles(BINARIES, "id")).toEqual(["pkey"]);
|
|
47
|
+
expect(col_key_roles(BINARIES, "version")).toEqual(["pkey2"]);
|
|
48
|
+
expect(col_key_roles(BINARIES, "size")).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
test("a column can be a pkey2 AND the tkey", () => {
|
|
51
|
+
expect(col_key_roles({pkey: "id", pkey2s: ["tm"], tkey: "tm"}, "tm"))
|
|
52
|
+
.toEqual(["pkey2", "tkey"]);
|
|
53
|
+
});
|
|
54
|
+
test("an empty tkey marks no column", () => {
|
|
55
|
+
expect(col_key_roles({pkey: "id", tkey: ""}, "")).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
test("the pkey defaults to id", () => {
|
|
58
|
+
expect(col_key_roles({}, "id")).toEqual(["pkey"]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("system_flag_names", () => {
|
|
63
|
+
test("the number the desc carries", () => {
|
|
64
|
+
expect(system_flag_names(1)).toEqual(["sf_string_key"]);
|
|
65
|
+
expect(system_flag_names(0x0002 | 0x0100)).toEqual(["sf_rowid_key", "sf_t_ms"]);
|
|
66
|
+
});
|
|
67
|
+
test("a set bit with no name is shown by its value", () => {
|
|
68
|
+
expect(system_flag_names(0x0009)).toEqual(["sf_string_key", "0x0008"]);
|
|
69
|
+
});
|
|
70
|
+
test("the names a schema literal carries", () => {
|
|
71
|
+
expect(system_flag_names("sf_string_key|sf_t_ms")).toEqual(["sf_string_key", "sf_t_ms"]);
|
|
72
|
+
expect(system_flag_names(["sf_int_key"])).toEqual(["sf_int_key"]);
|
|
73
|
+
});
|
|
74
|
+
test("nothing, zero or garbage is no name", () => {
|
|
75
|
+
expect(system_flag_names(0)).toEqual([]);
|
|
76
|
+
expect(system_flag_names(undefined)).toEqual([]);
|
|
77
|
+
expect(system_flag_names(1.5)).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
});
|
package/src/treedb_write_plan.js
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
* All Rights Reserved.
|
|
20
20
|
***********************************************************************/
|
|
21
21
|
|
|
22
|
+
import {treedb_get_field_desc} from "@yuneta/gobj-js";
|
|
23
|
+
import {topic_pkey2s} from "./schema_model.js";
|
|
24
|
+
|
|
22
25
|
/* What a read-only form keeps: reading a record includes taking it with
|
|
23
26
|
* you, so `copy` stays and the write half goes. */
|
|
24
27
|
const READONLY_FORM_TOOLBAR = ["copy"];
|
|
@@ -70,4 +73,47 @@ function plan_treedb_writes(flags)
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
/************************************************************
|
|
77
|
+
* Does column `col` travel back to treedb on a write?
|
|
78
|
+
*
|
|
79
|
+
* The form SHOWS every field and sends back only what the topic
|
|
80
|
+
* accepts: the writable cols, the fkeys (a link is edited by
|
|
81
|
+
* linking) and the pkey, which is not written but is what
|
|
82
|
+
* ADDRESSES the record.
|
|
83
|
+
*
|
|
84
|
+
* It matters because `treedb_update_node()` does not check
|
|
85
|
+
* `writable` -- it writes any col it is handed -- so a read-only
|
|
86
|
+
* field travelling back is written with whatever the form made of
|
|
87
|
+
* it, and the fields that describe a record are exactly the ones
|
|
88
|
+
* that do not survive a round trip through a widget: a `time` is an
|
|
89
|
+
* integer rendered as a `datetime-local`, with no seconds, so every
|
|
90
|
+
* save moved it back up to 59 s. Nothing looked wrong until the
|
|
91
|
+
* stored timestamp had moved.
|
|
92
|
+
*
|
|
93
|
+
* `is_file` and not the type: a `file` column IS an fkey (it is
|
|
94
|
+
* flagged ['fkey','file']) but answers `type: "file"`, and the write
|
|
95
|
+
* goes out with `autolink`, which rebuilds the links from what the
|
|
96
|
+
* record carries. Dropped, a read-only `file` column -- the one only
|
|
97
|
+
* a load fills -- was UNLINKED by every save of any other field.
|
|
98
|
+
*
|
|
99
|
+
* The pkey2s go back for the same reason as the pkey: a secondary key
|
|
100
|
+
* names the INSTANCE the update is for (`yunos.yuno_release` in the
|
|
101
|
+
* agent), and it is `persistent, required`, not `writable`. Dropped,
|
|
102
|
+
* the update reached the backend with no pkey2 and C_NODE resolved it
|
|
103
|
+
* to the PRIMARY instance -- right by chance while the table lists
|
|
104
|
+
* primaries, wrong from a form opened on a row of `instances`.
|
|
105
|
+
************************************************************/
|
|
106
|
+
function col_goes_back_to_treedb(desc, col)
|
|
107
|
+
{
|
|
108
|
+
let d = desc || {};
|
|
109
|
+
let pkey = d.pkey || "id";
|
|
110
|
+
const field_desc = treedb_get_field_desc(col);
|
|
111
|
+
|
|
112
|
+
if(field_desc.is_writable || field_desc.type === "fkey" ||
|
|
113
|
+
field_desc.is_file || col.id === pkey) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return topic_pkey2s(d).includes(col.id);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export {plan_treedb_writes, READONLY_FORM_TOOLBAR, col_goes_back_to_treedb};
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* affordance back by accident.
|
|
9
9
|
***********************************************************************/
|
|
10
10
|
import { describe, test, expect } from "vitest";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
plan_treedb_writes, READONLY_FORM_TOOLBAR, col_goes_back_to_treedb
|
|
13
|
+
} from "./treedb_write_plan.js";
|
|
12
14
|
|
|
13
15
|
const WRITES = ["edition_mode", "new_button", "delete_button", "paste_button", "in_row_icons"];
|
|
14
16
|
|
|
@@ -81,3 +83,53 @@ describe("readonly beats every with_*", () => {
|
|
|
81
83
|
expect(plan_treedb_writes({readonly: "yes"}).new_button).toBe(false);
|
|
82
84
|
});
|
|
83
85
|
});
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
/***********************************************************************
|
|
89
|
+
* Which columns of a form go back in the write. The shape is the
|
|
90
|
+
* agent's `yunos`: a pkey, a secondary key that is required but not
|
|
91
|
+
* writable, and the read-only columns that never survive a widget.
|
|
92
|
+
***********************************************************************/
|
|
93
|
+
const YUNOS_DESC = {
|
|
94
|
+
pkey: "id",
|
|
95
|
+
pkey2s: ["yuno_release"],
|
|
96
|
+
};
|
|
97
|
+
const COL = {
|
|
98
|
+
id: {id: "id", type: "string", flag: ["persistent", "required", "rowid"]},
|
|
99
|
+
yuno_release: {id: "yuno_release", type: "string", flag: ["persistent", "required"]},
|
|
100
|
+
yuno_name: {id: "yuno_name", type: "string", flag: ["persistent", "writable"]},
|
|
101
|
+
date: {id: "date", type: "integer", flag: ["persistent", "time"]},
|
|
102
|
+
realm_id: {id: "realm_id", type: "string", flag: ["persistent", "fkey"]},
|
|
103
|
+
photo: {id: "photo", type: "string", flag: ["persistent", "fkey", "file"]},
|
|
104
|
+
yunos: {id: "yunos", type: "object", flag: ["hook"], hook: {yunos: "realm_id"}},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
describe("what goes back to treedb from a form", () => {
|
|
108
|
+
test("a writable column, an fkey and a file column go back", () => {
|
|
109
|
+
expect(col_goes_back_to_treedb(YUNOS_DESC, COL.yuno_name)).toBe(true);
|
|
110
|
+
expect(col_goes_back_to_treedb(YUNOS_DESC, COL.realm_id)).toBe(true);
|
|
111
|
+
expect(col_goes_back_to_treedb(YUNOS_DESC, COL.photo)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("a read-only time and a hook stay: they do not survive the widget", () => {
|
|
115
|
+
expect(col_goes_back_to_treedb(YUNOS_DESC, COL.date)).toBe(false);
|
|
116
|
+
expect(col_goes_back_to_treedb(YUNOS_DESC, COL.yunos)).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("the pkey goes back although it is not writable: it addresses the record", () => {
|
|
120
|
+
expect(col_goes_back_to_treedb(YUNOS_DESC, COL.id)).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("a pkey2 goes back although it is not writable: it names the INSTANCE", () => {
|
|
124
|
+
expect(col_goes_back_to_treedb(YUNOS_DESC, COL.yuno_release)).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("a pkey2s declared as a bare string (the C literal) counts the same", () => {
|
|
128
|
+
expect(col_goes_back_to_treedb({pkey: "id", pkey2s: "yuno_release"}, COL.yuno_release)).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("without pkey2s in the desc the same column stays read-only", () => {
|
|
132
|
+
expect(col_goes_back_to_treedb({pkey: "id"}, COL.yuno_release)).toBe(false);
|
|
133
|
+
expect(col_goes_back_to_treedb(null, COL.yuno_release)).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
});
|