@yuneta/gobj-ui 7.23.169 → 7.23.170

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.
@@ -25638,6 +25638,361 @@ function row_matches(row, term) {
25638
25638
  });
25639
25639
  }
25640
25640
  //#endregion
25641
+ //#region src/schema_model.js
25642
+ /***********************************************************************
25643
+ * schema_model.js
25644
+ *
25645
+ * A SCHEMA out of the three meta-topics that store it.
25646
+ *
25647
+ * `treedb_system_schema` keeps every schema of a yuno as DATA, in
25648
+ * three flat topics — `treedbs`, `topics`, `cols` — linked by
25649
+ * fkeys. That is the right STORAGE and the wrong thing to put in
25650
+ * front of an operator: adding one column to one topic means
25651
+ * finding it in a table holding every column of every topic of
25652
+ * every treedb the yuno has.
25653
+ *
25654
+ * This module turns those three lists back into what they
25655
+ * describe: treedb -> topics -> columns, each in its declared
25656
+ * `order`. Pure, so the editor's model can be tested with no DOM
25657
+ * and no backend — the gclass fetches and draws, this decides
25658
+ * what the records MEAN.
25659
+ *
25660
+ * TWO THINGS IT EXISTS TO STATE ONCE:
25661
+ *
25662
+ * 1. A record is grouped by its FKEY, not by its id. The id is
25663
+ * qualified (`<treedb>.<topic>.<column>`) and reading it back
25664
+ * with a split on '.' works right up to the first treedb or
25665
+ * topic whose name carries one. The fkey is the link the store
25666
+ * itself uses; the id is only the fallback for a record whose
25667
+ * fkey never arrived.
25668
+ *
25669
+ * 2. A fkey reaches this code in FOUR shapes, because `nodes`
25670
+ * answers differently depending on the options it was called
25671
+ * with: the bare ref string, the expanded object, a list of
25672
+ * either, or a dict keyed by the ref. All four are the same
25673
+ * fact and parse_fkey_ref() flattens them to it.
25674
+ *
25675
+ * Copyright (c) 2026, ArtGins.
25676
+ * All Rights Reserved.
25677
+ ***********************************************************************/
25678
+ var DEFAULT_ORDER = 9999;
25679
+ /***************************************************************
25680
+ * parse_fkey_ref(value) -> [{topic_name, id, hook_name}, ...]
25681
+ *
25682
+ * Every shape a fkey column can arrive in, flattened to the
25683
+ * refs it names. Anything unrecognizable contributes nothing —
25684
+ * a schema drawn without one link beats a screen that throws.
25685
+ ***************************************************************/
25686
+ function parse_fkey_ref(value) {
25687
+ let refs = [];
25688
+ let push_one = (v) => {
25689
+ if (typeof v === "string") {
25690
+ let parts = v.split("^");
25691
+ if (parts.length !== 3) return;
25692
+ refs.push({
25693
+ topic_name: parts[0],
25694
+ id: parts[1],
25695
+ hook_name: parts[2]
25696
+ });
25697
+ return;
25698
+ }
25699
+ if (v && typeof v === "object") {
25700
+ let id = v.id;
25701
+ if (typeof id !== "string" || id.length === 0) return;
25702
+ refs.push({
25703
+ topic_name: v.topic_name || "",
25704
+ id,
25705
+ hook_name: v.hook_name || ""
25706
+ });
25707
+ }
25708
+ };
25709
+ if (value === null || value === void 0) return refs;
25710
+ if (typeof value === "string") {
25711
+ push_one(value);
25712
+ return refs;
25713
+ }
25714
+ if (Array.isArray(value)) {
25715
+ for (let v of value) push_one(v);
25716
+ return refs;
25717
+ }
25718
+ if (typeof value === "object") for (let [key, v] of Object.entries(value)) if (v && typeof v === "object" && typeof v.id === "string") push_one(v);
25719
+ else push_one(key);
25720
+ return refs;
25721
+ }
25722
+ /***************************************************************
25723
+ * The id of the FIRST parent a record names, or "" when it
25724
+ * names none. A record has one parent here — a column belongs to
25725
+ * one topic, a topic to one treedb — and a second ref would be a
25726
+ * store this editor cannot draw anyway.
25727
+ ***************************************************************/
25728
+ function parent_id(record, fkey_col) {
25729
+ let refs = parse_fkey_ref(record ? record[fkey_col] : null);
25730
+ return refs.length > 0 ? refs[0].id : "";
25731
+ }
25732
+ /***************************************************************
25733
+ * The name a record is KNOWN by, which is not what it is keyed
25734
+ * by: `topics` and `cols` key by the qualified id and carry the
25735
+ * bare name in `value` (its pkey2). An older store that still
25736
+ * keyed by rowid has no `value` worth showing, so the id stands
25737
+ * in — the same fallback as node_label().
25738
+ ***************************************************************/
25739
+ function record_name(record) {
25740
+ if (!record) return "";
25741
+ if (typeof record.value === "string" && record.value.length > 0) return record.value;
25742
+ return typeof record.id === "string" ? record.id : "";
25743
+ }
25744
+ /***************************************************************
25745
+ * The `order` of a record as a NUMBER, whatever the store put
25746
+ * there: an integer column read back from JSON may arrive as a
25747
+ * string, and comparing "10" with 9 sorts the schema wrong.
25748
+ ***************************************************************/
25749
+ function record_order(record) {
25750
+ let order = record ? record.order : void 0;
25751
+ if (typeof order === "number" && isFinite(order)) return order;
25752
+ if (typeof order === "string" && order.trim().length > 0) {
25753
+ let n = Number(order);
25754
+ if (isFinite(n)) return n;
25755
+ }
25756
+ return DEFAULT_ORDER;
25757
+ }
25758
+ /***************************************************************
25759
+ * Sort in place by `order`, ties broken by name so a schema
25760
+ * whose records all default to 9999 still draws the same way
25761
+ * twice.
25762
+ ***************************************************************/
25763
+ function sort_by_order(list) {
25764
+ list.sort((a, b) => {
25765
+ if (a.order !== b.order) return a.order - b.order;
25766
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
25767
+ });
25768
+ return list;
25769
+ }
25770
+ /***************************************************************
25771
+ * build_schema_model({treedbs, topics, cols}) -> model
25772
+ *
25773
+ * model = {
25774
+ * treedbs: [{
25775
+ * id, name, record,
25776
+ * schema_version, c_schema_version, system_schema_version,
25777
+ * topics: [{
25778
+ * id, name, order, record,
25779
+ * pkey, topic_version, system_topic, main_topic,
25780
+ * cols: [{id, name, order, record}]
25781
+ * }]
25782
+ * }],
25783
+ * orphan_topics: [...], topics whose treedb is not in the store
25784
+ * orphan_cols: [...] columns whose topic is not in the store
25785
+ * }
25786
+ *
25787
+ * An orphan is NOT dropped. A column whose topic was deleted is
25788
+ * exactly the kind of leftover an operator opens this editor to
25789
+ * find, and a model that hides it makes the editor lie.
25790
+ ***************************************************************/
25791
+ function build_schema_model(records) {
25792
+ let r = records || {};
25793
+ let treedb_rows = Array.isArray(r.treedbs) ? r.treedbs : [];
25794
+ let topic_rows = Array.isArray(r.topics) ? r.topics : [];
25795
+ let col_rows = Array.isArray(r.cols) ? r.cols : [];
25796
+ let treedbs = [];
25797
+ let by_treedb_id = {};
25798
+ for (let record of treedb_rows) {
25799
+ if (!record || typeof record.id !== "string") continue;
25800
+ let entry = {
25801
+ id: record.id,
25802
+ name: record.id,
25803
+ record,
25804
+ schema_version: record.schema_version,
25805
+ c_schema_version: record.c_schema_version,
25806
+ system_schema_version: record.system_schema_version,
25807
+ topics: []
25808
+ };
25809
+ treedbs.push(entry);
25810
+ by_treedb_id[entry.id] = entry;
25811
+ }
25812
+ let orphan_topics = [];
25813
+ let by_topic_id = {};
25814
+ for (let record of topic_rows) {
25815
+ if (!record || typeof record.id !== "string") continue;
25816
+ let entry = {
25817
+ id: record.id,
25818
+ name: record_name(record),
25819
+ order: record_order(record),
25820
+ record,
25821
+ treedb_id: parent_id(record, "treedbs"),
25822
+ pkey: record.pkey || "",
25823
+ pkey2s: record.pkey2s,
25824
+ system_flag: record.system_flag || "",
25825
+ tkey: record.tkey || "",
25826
+ topic_version: record.topic_version,
25827
+ system_topic: !!record.system_topic,
25828
+ main_topic: !!record.main_topic,
25829
+ cols: []
25830
+ };
25831
+ by_topic_id[entry.id] = entry;
25832
+ let parent = by_treedb_id[entry.treedb_id];
25833
+ if (parent) parent.topics.push(entry);
25834
+ else orphan_topics.push(entry);
25835
+ }
25836
+ let orphan_cols = [];
25837
+ for (let record of col_rows) {
25838
+ if (!record || typeof record.id !== "string") continue;
25839
+ let entry = {
25840
+ id: record.id,
25841
+ name: record_name(record),
25842
+ order: record_order(record),
25843
+ record,
25844
+ topic_id: parent_id(record, "topics")
25845
+ };
25846
+ let parent = by_topic_id[entry.topic_id];
25847
+ if (parent) parent.cols.push(entry);
25848
+ else orphan_cols.push(entry);
25849
+ }
25850
+ for (let treedb of treedbs) {
25851
+ sort_by_order(treedb.topics);
25852
+ for (let topic of treedb.topics) sort_by_order(topic.cols);
25853
+ }
25854
+ for (let topic of orphan_topics) sort_by_order(topic.cols);
25855
+ treedbs.sort((a, b) => {
25856
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
25857
+ });
25858
+ sort_by_order(orphan_topics);
25859
+ sort_by_order(orphan_cols);
25860
+ return {
25861
+ treedbs,
25862
+ orphan_topics,
25863
+ orphan_cols
25864
+ };
25865
+ }
25866
+ /***************************************************************
25867
+ * Is there a value here at all?
25868
+ *
25869
+ * The store answers a `blob` column that was never set with an
25870
+ * EMPTY COLLECTION, not with nothing: `enum: {}`, `hook: {}`,
25871
+ * `default: {}`. Read as "a value", those turn every export into
25872
+ * a schema full of empty objects — and `'hook': {}` in a literal
25873
+ * is a hook with no mapping, which is a link the treedb builds
25874
+ * and nothing writes.
25875
+ ***************************************************************/
25876
+ function is_empty_value(value) {
25877
+ if (value === null || value === void 0 || value === "") return true;
25878
+ if (Array.isArray(value)) return value.length === 0;
25879
+ if (typeof value === "object") return Object.keys(value).length === 0;
25880
+ return false;
25881
+ }
25882
+ /***************************************************************
25883
+ * The fields of a column that are DECLARED as one thing and
25884
+ * STORED as another.
25885
+ *
25886
+ * `hook`, `enum`, `default`, `pkey2s` and `properties` are `blob`
25887
+ * columns of the `cols` topic (treedb_system_schema.c), so what
25888
+ * comes back may be the value or the JSON text of the value,
25889
+ * depending on how it was written — a projection writes the
25890
+ * value, a hand edit through a plain text field writes the text.
25891
+ * `flag` is an array that a single-flag edit can leave as a bare
25892
+ * string. Reading these without saying so is how a hook drawn
25893
+ * from `{"users":"department"}` becomes no hook at all.
25894
+ ***************************************************************/
25895
+ function as_json(value) {
25896
+ if (typeof value !== "string") return value;
25897
+ let text = value.trim();
25898
+ if (text.length === 0) return null;
25899
+ try {
25900
+ return JSON.parse(text);
25901
+ } catch (e) {
25902
+ return value;
25903
+ }
25904
+ }
25905
+ function col_flags(record) {
25906
+ let flag = record ? record.flag : null;
25907
+ if (Array.isArray(flag)) return flag.filter((f) => typeof f === "string" && f.length > 0);
25908
+ if (typeof flag === "string" && flag.length > 0) {
25909
+ let parsed = as_json(flag);
25910
+ if (Array.isArray(parsed)) return parsed.filter((f) => typeof f === "string" && f.length > 0);
25911
+ return [flag];
25912
+ }
25913
+ return [];
25914
+ }
25915
+ function col_hook(record) {
25916
+ let hook = as_json(record ? record.hook : null);
25917
+ if (hook && typeof hook === "object" && !Array.isArray(hook)) return hook;
25918
+ return null;
25919
+ }
25920
+ function col_enum(record) {
25921
+ let e = as_json(record ? record.enum : null);
25922
+ if (Array.isArray(e)) return e;
25923
+ return [];
25924
+ }
25925
+ function topic_pkey2s(record) {
25926
+ let pkey2s = as_json(record ? record.pkey2s : null);
25927
+ if (Array.isArray(pkey2s)) return pkey2s.filter((k) => typeof k === "string" && k.length > 0);
25928
+ if (typeof pkey2s === "string" && pkey2s.length > 0) return [pkey2s];
25929
+ return [];
25930
+ }
25931
+ /***************************************************************
25932
+ * Lookups by id. The editor addresses a position by NAME (the
25933
+ * url carries `<treedb>/<topic>`), so both are offered.
25934
+ ***************************************************************/
25935
+ function find_treedb(model, treedb_id) {
25936
+ if (!model || !Array.isArray(model.treedbs)) return null;
25937
+ for (let treedb of model.treedbs) if (treedb.id === treedb_id) return treedb;
25938
+ return null;
25939
+ }
25940
+ function find_topic(model, treedb_id, topic_name) {
25941
+ let treedb = find_treedb(model, treedb_id);
25942
+ if (!treedb) return null;
25943
+ for (let topic of treedb.topics) if (topic.name === topic_name || topic.id === topic_name) return topic;
25944
+ return null;
25945
+ }
25946
+ function find_col(model, treedb_id, topic_name, col_name) {
25947
+ let topic = find_topic(model, treedb_id, topic_name);
25948
+ if (!topic) return null;
25949
+ for (let col of topic.cols) if (col.name === col_name || col.id === col_name) return col;
25950
+ return null;
25951
+ }
25952
+ /***************************************************************
25953
+ * The fkey a NEW record must carry to belong to `parent_id`.
25954
+ * Written here because the editor is the only writer that has to
25955
+ * compose one, and getting the hook name wrong links the record
25956
+ * to nothing while the write still succeeds.
25957
+ ***************************************************************/
25958
+ function fkey_ref(parent_topic, parent_id_, hook_name) {
25959
+ return `${parent_topic}^${parent_id_}^${hook_name}`;
25960
+ }
25961
+ /***************************************************************
25962
+ * Where a new sibling goes: after the last one. The store's own
25963
+ * default (9999) would put every hand-made column in the same
25964
+ * place and let their names decide the schema's order.
25965
+ ***************************************************************/
25966
+ function next_order(siblings) {
25967
+ let max = 0;
25968
+ if (Array.isArray(siblings)) for (let s of siblings) {
25969
+ let order = s && typeof s.order === "number" ? s.order : DEFAULT_ORDER;
25970
+ if (order < 9999 && order > max) max = order;
25971
+ }
25972
+ return max + 1;
25973
+ }
25974
+ /***************************************************************
25975
+ * Renumber a list AFTER a move, as the writes it takes: only the
25976
+ * records whose order actually changed, so a drag of one row in a
25977
+ * 40-column topic is not 40 writes.
25978
+ *
25979
+ * moved_orders(list) -> [{id, order}, ...]
25980
+ ***************************************************************/
25981
+ function moved_orders(list) {
25982
+ let writes = [];
25983
+ if (!Array.isArray(list)) return writes;
25984
+ for (let i = 0; i < list.length; i++) {
25985
+ let entry = list[i];
25986
+ let order = i + 1;
25987
+ if (!entry || typeof entry.id !== "string") continue;
25988
+ if (entry.order !== order) writes.push({
25989
+ id: entry.id,
25990
+ order
25991
+ });
25992
+ }
25993
+ return writes;
25994
+ }
25995
+ //#endregion
25641
25996
  //#region src/treedb_write_plan.js
25642
25997
  /***********************************************************************
25643
25998
  * treedb_write_plan.js
@@ -25699,6 +26054,43 @@ function plan_treedb_writes(flags) {
25699
26054
  form_toolbar: null
25700
26055
  };
25701
26056
  }
26057
+ /************************************************************
26058
+ * Does column `col` travel back to treedb on a write?
26059
+ *
26060
+ * The form SHOWS every field and sends back only what the topic
26061
+ * accepts: the writable cols, the fkeys (a link is edited by
26062
+ * linking) and the pkey, which is not written but is what
26063
+ * ADDRESSES the record.
26064
+ *
26065
+ * It matters because `treedb_update_node()` does not check
26066
+ * `writable` -- it writes any col it is handed -- so a read-only
26067
+ * field travelling back is written with whatever the form made of
26068
+ * it, and the fields that describe a record are exactly the ones
26069
+ * that do not survive a round trip through a widget: a `time` is an
26070
+ * integer rendered as a `datetime-local`, with no seconds, so every
26071
+ * save moved it back up to 59 s. Nothing looked wrong until the
26072
+ * stored timestamp had moved.
26073
+ *
26074
+ * `is_file` and not the type: a `file` column IS an fkey (it is
26075
+ * flagged ['fkey','file']) but answers `type: "file"`, and the write
26076
+ * goes out with `autolink`, which rebuilds the links from what the
26077
+ * record carries. Dropped, a read-only `file` column -- the one only
26078
+ * a load fills -- was UNLINKED by every save of any other field.
26079
+ *
26080
+ * The pkey2s go back for the same reason as the pkey: a secondary key
26081
+ * names the INSTANCE the update is for (`yunos.yuno_release` in the
26082
+ * agent), and it is `persistent, required`, not `writable`. Dropped,
26083
+ * the update reached the backend with no pkey2 and C_NODE resolved it
26084
+ * to the PRIMARY instance -- right by chance while the table lists
26085
+ * primaries, wrong from a form opened on a row of `instances`.
26086
+ ************************************************************/
26087
+ function col_goes_back_to_treedb(desc, col) {
26088
+ let d = desc || {};
26089
+ let pkey = d.pkey || "id";
26090
+ const field_desc = treedb_get_field_desc(col);
26091
+ if (field_desc.is_writable || field_desc.type === "fkey" || field_desc.is_file || col.id === pkey) return true;
26092
+ return topic_pkey2s(d).includes(col.id);
26093
+ }
25702
26094
  //#endregion
25703
26095
  //#region src/delete_impact.js
25704
26096
  /***********************************************************************
@@ -28046,35 +28438,6 @@ function get_schema_col(gobj, id) {
28046
28438
  return null;
28047
28439
  }
28048
28440
  /************************************************************
28049
- * Does column `col` travel back to treedb on a write?
28050
- *
28051
- * The form SHOWS every field and sends back only what the topic
28052
- * accepts: the writable cols, the fkeys (a link is edited by
28053
- * linking) and the pkey, which is not written but is what
28054
- * ADDRESSES the record.
28055
- *
28056
- * It matters because `treedb_update_node()` does not check
28057
- * `writable` -- it writes any col it is handed -- so a read-only
28058
- * field travelling back is written with whatever the form made of
28059
- * it, and the fields that describe a record are exactly the ones
28060
- * that do not survive a round trip through a widget: a `time` is an
28061
- * integer rendered as a `datetime-local`, with no seconds, so every
28062
- * save moved it back up to 59 s. Nothing looked wrong until the
28063
- * stored timestamp had moved.
28064
- *
28065
- * `is_file` and not the type: a `file` column IS an fkey (it is
28066
- * flagged ['fkey','file']) but answers `type: "file"`, and the write
28067
- * goes out with `autolink`, which rebuilds the links from what the
28068
- * record carries. Dropped, a read-only `file` column -- the one only
28069
- * a load fills -- was UNLINKED by every save of any other field.
28070
- ************************************************************/
28071
- function col_goes_back_to_treedb(gobj, col) {
28072
- let pkey = gobj_read_attr(gobj, "desc").pkey || "id";
28073
- const field_desc = treedb_get_field_desc(col);
28074
- if (field_desc.is_writable || field_desc.type === "fkey" || field_desc.is_file || col.id === pkey) return true;
28075
- return false;
28076
- }
28077
- /************************************************************
28078
28441
  * The record the FORM hands over, without the columns that do
28079
28442
  * not go back to treedb (see col_goes_back_to_treedb()). The
28080
28443
  * form has already encoded the values, so only the columns are
@@ -28084,7 +28447,7 @@ function strip_read_only_cols(gobj, record) {
28084
28447
  let row = {};
28085
28448
  for (let field_name of Object.keys(record || {})) {
28086
28449
  let col = get_schema_col(gobj, field_name);
28087
- if (col && !col_goes_back_to_treedb(gobj, col)) continue;
28450
+ if (col && !col_goes_back_to_treedb(gobj_read_attr(gobj, "desc"), col)) continue;
28088
28451
  row[field_name] = record[field_name];
28089
28452
  }
28090
28453
  return row;
@@ -28100,7 +28463,7 @@ function transform__form_record_2_treedb_record(gobj, kw, operation) {
28100
28463
  if (empty_string(field_name)) continue;
28101
28464
  let col = get_schema_col(gobj, field_name);
28102
28465
  if (!col) continue;
28103
- if (!col_goes_back_to_treedb(gobj, col)) continue;
28466
+ if (!col_goes_back_to_treedb(gobj_read_attr(gobj, "desc"), col)) continue;
28104
28467
  let value = kw[field_name];
28105
28468
  row[field_name] = transform__form_value_2_treedb_value(gobj, col, value, operation);
28106
28469
  }
@@ -69018,361 +69381,6 @@ function register_c_yui_treedb_schema() {
69018
69381
  return create_gclass$2(GCLASS_NAME$2);
69019
69382
  }
69020
69383
  //#endregion
69021
- //#region src/schema_model.js
69022
- /***********************************************************************
69023
- * schema_model.js
69024
- *
69025
- * A SCHEMA out of the three meta-topics that store it.
69026
- *
69027
- * `treedb_system_schema` keeps every schema of a yuno as DATA, in
69028
- * three flat topics — `treedbs`, `topics`, `cols` — linked by
69029
- * fkeys. That is the right STORAGE and the wrong thing to put in
69030
- * front of an operator: adding one column to one topic means
69031
- * finding it in a table holding every column of every topic of
69032
- * every treedb the yuno has.
69033
- *
69034
- * This module turns those three lists back into what they
69035
- * describe: treedb -> topics -> columns, each in its declared
69036
- * `order`. Pure, so the editor's model can be tested with no DOM
69037
- * and no backend — the gclass fetches and draws, this decides
69038
- * what the records MEAN.
69039
- *
69040
- * TWO THINGS IT EXISTS TO STATE ONCE:
69041
- *
69042
- * 1. A record is grouped by its FKEY, not by its id. The id is
69043
- * qualified (`<treedb>.<topic>.<column>`) and reading it back
69044
- * with a split on '.' works right up to the first treedb or
69045
- * topic whose name carries one. The fkey is the link the store
69046
- * itself uses; the id is only the fallback for a record whose
69047
- * fkey never arrived.
69048
- *
69049
- * 2. A fkey reaches this code in FOUR shapes, because `nodes`
69050
- * answers differently depending on the options it was called
69051
- * with: the bare ref string, the expanded object, a list of
69052
- * either, or a dict keyed by the ref. All four are the same
69053
- * fact and parse_fkey_ref() flattens them to it.
69054
- *
69055
- * Copyright (c) 2026, ArtGins.
69056
- * All Rights Reserved.
69057
- ***********************************************************************/
69058
- var DEFAULT_ORDER = 9999;
69059
- /***************************************************************
69060
- * parse_fkey_ref(value) -> [{topic_name, id, hook_name}, ...]
69061
- *
69062
- * Every shape a fkey column can arrive in, flattened to the
69063
- * refs it names. Anything unrecognizable contributes nothing —
69064
- * a schema drawn without one link beats a screen that throws.
69065
- ***************************************************************/
69066
- function parse_fkey_ref(value) {
69067
- let refs = [];
69068
- let push_one = (v) => {
69069
- if (typeof v === "string") {
69070
- let parts = v.split("^");
69071
- if (parts.length !== 3) return;
69072
- refs.push({
69073
- topic_name: parts[0],
69074
- id: parts[1],
69075
- hook_name: parts[2]
69076
- });
69077
- return;
69078
- }
69079
- if (v && typeof v === "object") {
69080
- let id = v.id;
69081
- if (typeof id !== "string" || id.length === 0) return;
69082
- refs.push({
69083
- topic_name: v.topic_name || "",
69084
- id,
69085
- hook_name: v.hook_name || ""
69086
- });
69087
- }
69088
- };
69089
- if (value === null || value === void 0) return refs;
69090
- if (typeof value === "string") {
69091
- push_one(value);
69092
- return refs;
69093
- }
69094
- if (Array.isArray(value)) {
69095
- for (let v of value) push_one(v);
69096
- return refs;
69097
- }
69098
- if (typeof value === "object") for (let [key, v] of Object.entries(value)) if (v && typeof v === "object" && typeof v.id === "string") push_one(v);
69099
- else push_one(key);
69100
- return refs;
69101
- }
69102
- /***************************************************************
69103
- * The id of the FIRST parent a record names, or "" when it
69104
- * names none. A record has one parent here — a column belongs to
69105
- * one topic, a topic to one treedb — and a second ref would be a
69106
- * store this editor cannot draw anyway.
69107
- ***************************************************************/
69108
- function parent_id(record, fkey_col) {
69109
- let refs = parse_fkey_ref(record ? record[fkey_col] : null);
69110
- return refs.length > 0 ? refs[0].id : "";
69111
- }
69112
- /***************************************************************
69113
- * The name a record is KNOWN by, which is not what it is keyed
69114
- * by: `topics` and `cols` key by the qualified id and carry the
69115
- * bare name in `value` (its pkey2). An older store that still
69116
- * keyed by rowid has no `value` worth showing, so the id stands
69117
- * in — the same fallback as node_label().
69118
- ***************************************************************/
69119
- function record_name(record) {
69120
- if (!record) return "";
69121
- if (typeof record.value === "string" && record.value.length > 0) return record.value;
69122
- return typeof record.id === "string" ? record.id : "";
69123
- }
69124
- /***************************************************************
69125
- * The `order` of a record as a NUMBER, whatever the store put
69126
- * there: an integer column read back from JSON may arrive as a
69127
- * string, and comparing "10" with 9 sorts the schema wrong.
69128
- ***************************************************************/
69129
- function record_order(record) {
69130
- let order = record ? record.order : void 0;
69131
- if (typeof order === "number" && isFinite(order)) return order;
69132
- if (typeof order === "string" && order.trim().length > 0) {
69133
- let n = Number(order);
69134
- if (isFinite(n)) return n;
69135
- }
69136
- return DEFAULT_ORDER;
69137
- }
69138
- /***************************************************************
69139
- * Sort in place by `order`, ties broken by name so a schema
69140
- * whose records all default to 9999 still draws the same way
69141
- * twice.
69142
- ***************************************************************/
69143
- function sort_by_order(list) {
69144
- list.sort((a, b) => {
69145
- if (a.order !== b.order) return a.order - b.order;
69146
- return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
69147
- });
69148
- return list;
69149
- }
69150
- /***************************************************************
69151
- * build_schema_model({treedbs, topics, cols}) -> model
69152
- *
69153
- * model = {
69154
- * treedbs: [{
69155
- * id, name, record,
69156
- * schema_version, c_schema_version, system_schema_version,
69157
- * topics: [{
69158
- * id, name, order, record,
69159
- * pkey, topic_version, system_topic, main_topic,
69160
- * cols: [{id, name, order, record}]
69161
- * }]
69162
- * }],
69163
- * orphan_topics: [...], topics whose treedb is not in the store
69164
- * orphan_cols: [...] columns whose topic is not in the store
69165
- * }
69166
- *
69167
- * An orphan is NOT dropped. A column whose topic was deleted is
69168
- * exactly the kind of leftover an operator opens this editor to
69169
- * find, and a model that hides it makes the editor lie.
69170
- ***************************************************************/
69171
- function build_schema_model(records) {
69172
- let r = records || {};
69173
- let treedb_rows = Array.isArray(r.treedbs) ? r.treedbs : [];
69174
- let topic_rows = Array.isArray(r.topics) ? r.topics : [];
69175
- let col_rows = Array.isArray(r.cols) ? r.cols : [];
69176
- let treedbs = [];
69177
- let by_treedb_id = {};
69178
- for (let record of treedb_rows) {
69179
- if (!record || typeof record.id !== "string") continue;
69180
- let entry = {
69181
- id: record.id,
69182
- name: record.id,
69183
- record,
69184
- schema_version: record.schema_version,
69185
- c_schema_version: record.c_schema_version,
69186
- system_schema_version: record.system_schema_version,
69187
- topics: []
69188
- };
69189
- treedbs.push(entry);
69190
- by_treedb_id[entry.id] = entry;
69191
- }
69192
- let orphan_topics = [];
69193
- let by_topic_id = {};
69194
- for (let record of topic_rows) {
69195
- if (!record || typeof record.id !== "string") continue;
69196
- let entry = {
69197
- id: record.id,
69198
- name: record_name(record),
69199
- order: record_order(record),
69200
- record,
69201
- treedb_id: parent_id(record, "treedbs"),
69202
- pkey: record.pkey || "",
69203
- pkey2s: record.pkey2s,
69204
- system_flag: record.system_flag || "",
69205
- tkey: record.tkey || "",
69206
- topic_version: record.topic_version,
69207
- system_topic: !!record.system_topic,
69208
- main_topic: !!record.main_topic,
69209
- cols: []
69210
- };
69211
- by_topic_id[entry.id] = entry;
69212
- let parent = by_treedb_id[entry.treedb_id];
69213
- if (parent) parent.topics.push(entry);
69214
- else orphan_topics.push(entry);
69215
- }
69216
- let orphan_cols = [];
69217
- for (let record of col_rows) {
69218
- if (!record || typeof record.id !== "string") continue;
69219
- let entry = {
69220
- id: record.id,
69221
- name: record_name(record),
69222
- order: record_order(record),
69223
- record,
69224
- topic_id: parent_id(record, "topics")
69225
- };
69226
- let parent = by_topic_id[entry.topic_id];
69227
- if (parent) parent.cols.push(entry);
69228
- else orphan_cols.push(entry);
69229
- }
69230
- for (let treedb of treedbs) {
69231
- sort_by_order(treedb.topics);
69232
- for (let topic of treedb.topics) sort_by_order(topic.cols);
69233
- }
69234
- for (let topic of orphan_topics) sort_by_order(topic.cols);
69235
- treedbs.sort((a, b) => {
69236
- return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
69237
- });
69238
- sort_by_order(orphan_topics);
69239
- sort_by_order(orphan_cols);
69240
- return {
69241
- treedbs,
69242
- orphan_topics,
69243
- orphan_cols
69244
- };
69245
- }
69246
- /***************************************************************
69247
- * Is there a value here at all?
69248
- *
69249
- * The store answers a `blob` column that was never set with an
69250
- * EMPTY COLLECTION, not with nothing: `enum: {}`, `hook: {}`,
69251
- * `default: {}`. Read as "a value", those turn every export into
69252
- * a schema full of empty objects — and `'hook': {}` in a literal
69253
- * is a hook with no mapping, which is a link the treedb builds
69254
- * and nothing writes.
69255
- ***************************************************************/
69256
- function is_empty_value(value) {
69257
- if (value === null || value === void 0 || value === "") return true;
69258
- if (Array.isArray(value)) return value.length === 0;
69259
- if (typeof value === "object") return Object.keys(value).length === 0;
69260
- return false;
69261
- }
69262
- /***************************************************************
69263
- * The fields of a column that are DECLARED as one thing and
69264
- * STORED as another.
69265
- *
69266
- * `hook`, `enum`, `default`, `pkey2s` and `properties` are `blob`
69267
- * columns of the `cols` topic (treedb_system_schema.c), so what
69268
- * comes back may be the value or the JSON text of the value,
69269
- * depending on how it was written — a projection writes the
69270
- * value, a hand edit through a plain text field writes the text.
69271
- * `flag` is an array that a single-flag edit can leave as a bare
69272
- * string. Reading these without saying so is how a hook drawn
69273
- * from `{"users":"department"}` becomes no hook at all.
69274
- ***************************************************************/
69275
- function as_json(value) {
69276
- if (typeof value !== "string") return value;
69277
- let text = value.trim();
69278
- if (text.length === 0) return null;
69279
- try {
69280
- return JSON.parse(text);
69281
- } catch (e) {
69282
- return value;
69283
- }
69284
- }
69285
- function col_flags(record) {
69286
- let flag = record ? record.flag : null;
69287
- if (Array.isArray(flag)) return flag.filter((f) => typeof f === "string" && f.length > 0);
69288
- if (typeof flag === "string" && flag.length > 0) {
69289
- let parsed = as_json(flag);
69290
- if (Array.isArray(parsed)) return parsed.filter((f) => typeof f === "string" && f.length > 0);
69291
- return [flag];
69292
- }
69293
- return [];
69294
- }
69295
- function col_hook(record) {
69296
- let hook = as_json(record ? record.hook : null);
69297
- if (hook && typeof hook === "object" && !Array.isArray(hook)) return hook;
69298
- return null;
69299
- }
69300
- function col_enum(record) {
69301
- let e = as_json(record ? record.enum : null);
69302
- if (Array.isArray(e)) return e;
69303
- return [];
69304
- }
69305
- function topic_pkey2s(record) {
69306
- let pkey2s = as_json(record ? record.pkey2s : null);
69307
- if (Array.isArray(pkey2s)) return pkey2s.filter((k) => typeof k === "string" && k.length > 0);
69308
- if (typeof pkey2s === "string" && pkey2s.length > 0) return [pkey2s];
69309
- return [];
69310
- }
69311
- /***************************************************************
69312
- * Lookups by id. The editor addresses a position by NAME (the
69313
- * url carries `<treedb>/<topic>`), so both are offered.
69314
- ***************************************************************/
69315
- function find_treedb(model, treedb_id) {
69316
- if (!model || !Array.isArray(model.treedbs)) return null;
69317
- for (let treedb of model.treedbs) if (treedb.id === treedb_id) return treedb;
69318
- return null;
69319
- }
69320
- function find_topic(model, treedb_id, topic_name) {
69321
- let treedb = find_treedb(model, treedb_id);
69322
- if (!treedb) return null;
69323
- for (let topic of treedb.topics) if (topic.name === topic_name || topic.id === topic_name) return topic;
69324
- return null;
69325
- }
69326
- function find_col(model, treedb_id, topic_name, col_name) {
69327
- let topic = find_topic(model, treedb_id, topic_name);
69328
- if (!topic) return null;
69329
- for (let col of topic.cols) if (col.name === col_name || col.id === col_name) return col;
69330
- return null;
69331
- }
69332
- /***************************************************************
69333
- * The fkey a NEW record must carry to belong to `parent_id`.
69334
- * Written here because the editor is the only writer that has to
69335
- * compose one, and getting the hook name wrong links the record
69336
- * to nothing while the write still succeeds.
69337
- ***************************************************************/
69338
- function fkey_ref(parent_topic, parent_id_, hook_name) {
69339
- return `${parent_topic}^${parent_id_}^${hook_name}`;
69340
- }
69341
- /***************************************************************
69342
- * Where a new sibling goes: after the last one. The store's own
69343
- * default (9999) would put every hand-made column in the same
69344
- * place and let their names decide the schema's order.
69345
- ***************************************************************/
69346
- function next_order(siblings) {
69347
- let max = 0;
69348
- if (Array.isArray(siblings)) for (let s of siblings) {
69349
- let order = s && typeof s.order === "number" ? s.order : DEFAULT_ORDER;
69350
- if (order < 9999 && order > max) max = order;
69351
- }
69352
- return max + 1;
69353
- }
69354
- /***************************************************************
69355
- * Renumber a list AFTER a move, as the writes it takes: only the
69356
- * records whose order actually changed, so a drag of one row in a
69357
- * 40-column topic is not 40 writes.
69358
- *
69359
- * moved_orders(list) -> [{id, order}, ...]
69360
- ***************************************************************/
69361
- function moved_orders(list) {
69362
- let writes = [];
69363
- if (!Array.isArray(list)) return writes;
69364
- for (let i = 0; i < list.length; i++) {
69365
- let entry = list[i];
69366
- let order = i + 1;
69367
- if (!entry || typeof entry.id !== "string") continue;
69368
- if (entry.order !== order) writes.push({
69369
- id: entry.id,
69370
- order
69371
- });
69372
- }
69373
- return writes;
69374
- }
69375
- //#endregion
69376
69384
  //#region src/schema_validate.js
69377
69385
  /***********************************************************************
69378
69386
  * schema_validate.js