@yuneta/gobj-ui 7.23.170 → 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 +542 -390
- package/dist/gobj-ui.es.js +542 -390
- package/package.json +1 -1
- package/src/c_yui_treedb_schema.js +26 -5
- 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/dist/gobj-ui.cjs.js
CHANGED
|
@@ -23051,13 +23051,455 @@ function nodes_answer(data) {
|
|
|
23051
23051
|
};
|
|
23052
23052
|
}
|
|
23053
23053
|
//#endregion
|
|
23054
|
+
//#region src/schema_model.js
|
|
23055
|
+
/***********************************************************************
|
|
23056
|
+
* schema_model.js
|
|
23057
|
+
*
|
|
23058
|
+
* A SCHEMA out of the three meta-topics that store it.
|
|
23059
|
+
*
|
|
23060
|
+
* `treedb_system_schema` keeps every schema of a yuno as DATA, in
|
|
23061
|
+
* three flat topics — `treedbs`, `topics`, `cols` — linked by
|
|
23062
|
+
* fkeys. That is the right STORAGE and the wrong thing to put in
|
|
23063
|
+
* front of an operator: adding one column to one topic means
|
|
23064
|
+
* finding it in a table holding every column of every topic of
|
|
23065
|
+
* every treedb the yuno has.
|
|
23066
|
+
*
|
|
23067
|
+
* This module turns those three lists back into what they
|
|
23068
|
+
* describe: treedb -> topics -> columns, each in its declared
|
|
23069
|
+
* `order`. Pure, so the editor's model can be tested with no DOM
|
|
23070
|
+
* and no backend — the gclass fetches and draws, this decides
|
|
23071
|
+
* what the records MEAN.
|
|
23072
|
+
*
|
|
23073
|
+
* TWO THINGS IT EXISTS TO STATE ONCE:
|
|
23074
|
+
*
|
|
23075
|
+
* 1. A record is grouped by its FKEY, not by its id. The id is
|
|
23076
|
+
* qualified (`<treedb>.<topic>.<column>`) and reading it back
|
|
23077
|
+
* with a split on '.' works right up to the first treedb or
|
|
23078
|
+
* topic whose name carries one. The fkey is the link the store
|
|
23079
|
+
* itself uses; the id is only the fallback for a record whose
|
|
23080
|
+
* fkey never arrived.
|
|
23081
|
+
*
|
|
23082
|
+
* 2. A fkey reaches this code in FOUR shapes, because `nodes`
|
|
23083
|
+
* answers differently depending on the options it was called
|
|
23084
|
+
* with: the bare ref string, the expanded object, a list of
|
|
23085
|
+
* either, or a dict keyed by the ref. All four are the same
|
|
23086
|
+
* fact and parse_fkey_ref() flattens them to it.
|
|
23087
|
+
*
|
|
23088
|
+
* Copyright (c) 2026, ArtGins.
|
|
23089
|
+
* All Rights Reserved.
|
|
23090
|
+
***********************************************************************/
|
|
23091
|
+
var DEFAULT_ORDER = 9999;
|
|
23092
|
+
/***************************************************************
|
|
23093
|
+
* parse_fkey_ref(value) -> [{topic_name, id, hook_name}, ...]
|
|
23094
|
+
*
|
|
23095
|
+
* Every shape a fkey column can arrive in, flattened to the
|
|
23096
|
+
* refs it names. Anything unrecognizable contributes nothing —
|
|
23097
|
+
* a schema drawn without one link beats a screen that throws.
|
|
23098
|
+
***************************************************************/
|
|
23099
|
+
function parse_fkey_ref(value) {
|
|
23100
|
+
let refs = [];
|
|
23101
|
+
let push_one = (v) => {
|
|
23102
|
+
if (typeof v === "string") {
|
|
23103
|
+
let parts = v.split("^");
|
|
23104
|
+
if (parts.length !== 3) return;
|
|
23105
|
+
refs.push({
|
|
23106
|
+
topic_name: parts[0],
|
|
23107
|
+
id: parts[1],
|
|
23108
|
+
hook_name: parts[2]
|
|
23109
|
+
});
|
|
23110
|
+
return;
|
|
23111
|
+
}
|
|
23112
|
+
if (v && typeof v === "object") {
|
|
23113
|
+
let id = v.id;
|
|
23114
|
+
if (typeof id !== "string" || id.length === 0) return;
|
|
23115
|
+
refs.push({
|
|
23116
|
+
topic_name: v.topic_name || "",
|
|
23117
|
+
id,
|
|
23118
|
+
hook_name: v.hook_name || ""
|
|
23119
|
+
});
|
|
23120
|
+
}
|
|
23121
|
+
};
|
|
23122
|
+
if (value === null || value === void 0) return refs;
|
|
23123
|
+
if (typeof value === "string") {
|
|
23124
|
+
push_one(value);
|
|
23125
|
+
return refs;
|
|
23126
|
+
}
|
|
23127
|
+
if (Array.isArray(value)) {
|
|
23128
|
+
for (let v of value) push_one(v);
|
|
23129
|
+
return refs;
|
|
23130
|
+
}
|
|
23131
|
+
if (typeof value === "object") for (let [key, v] of Object.entries(value)) if (v && typeof v === "object" && typeof v.id === "string") push_one(v);
|
|
23132
|
+
else push_one(key);
|
|
23133
|
+
return refs;
|
|
23134
|
+
}
|
|
23135
|
+
/***************************************************************
|
|
23136
|
+
* The id of the FIRST parent a record names, or "" when it
|
|
23137
|
+
* names none. A record has one parent here — a column belongs to
|
|
23138
|
+
* one topic, a topic to one treedb — and a second ref would be a
|
|
23139
|
+
* store this editor cannot draw anyway.
|
|
23140
|
+
***************************************************************/
|
|
23141
|
+
function parent_id(record, fkey_col) {
|
|
23142
|
+
let refs = parse_fkey_ref(record ? record[fkey_col] : null);
|
|
23143
|
+
return refs.length > 0 ? refs[0].id : "";
|
|
23144
|
+
}
|
|
23145
|
+
/***************************************************************
|
|
23146
|
+
* The name a record is KNOWN by, which is not what it is keyed
|
|
23147
|
+
* by: `topics` and `cols` key by the qualified id and carry the
|
|
23148
|
+
* bare name in `value` (its pkey2). An older store that still
|
|
23149
|
+
* keyed by rowid has no `value` worth showing, so the id stands
|
|
23150
|
+
* in — the same fallback as node_label().
|
|
23151
|
+
***************************************************************/
|
|
23152
|
+
function record_name(record) {
|
|
23153
|
+
if (!record) return "";
|
|
23154
|
+
if (typeof record.value === "string" && record.value.length > 0) return record.value;
|
|
23155
|
+
return typeof record.id === "string" ? record.id : "";
|
|
23156
|
+
}
|
|
23157
|
+
/***************************************************************
|
|
23158
|
+
* The `order` of a record as a NUMBER, whatever the store put
|
|
23159
|
+
* there: an integer column read back from JSON may arrive as a
|
|
23160
|
+
* string, and comparing "10" with 9 sorts the schema wrong.
|
|
23161
|
+
***************************************************************/
|
|
23162
|
+
function record_order(record) {
|
|
23163
|
+
let order = record ? record.order : void 0;
|
|
23164
|
+
if (typeof order === "number" && isFinite(order)) return order;
|
|
23165
|
+
if (typeof order === "string" && order.trim().length > 0) {
|
|
23166
|
+
let n = Number(order);
|
|
23167
|
+
if (isFinite(n)) return n;
|
|
23168
|
+
}
|
|
23169
|
+
return DEFAULT_ORDER;
|
|
23170
|
+
}
|
|
23171
|
+
/***************************************************************
|
|
23172
|
+
* Sort in place by `order`, ties broken by name so a schema
|
|
23173
|
+
* whose records all default to 9999 still draws the same way
|
|
23174
|
+
* twice.
|
|
23175
|
+
***************************************************************/
|
|
23176
|
+
function sort_by_order(list) {
|
|
23177
|
+
list.sort((a, b) => {
|
|
23178
|
+
if (a.order !== b.order) return a.order - b.order;
|
|
23179
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
23180
|
+
});
|
|
23181
|
+
return list;
|
|
23182
|
+
}
|
|
23183
|
+
/***************************************************************
|
|
23184
|
+
* build_schema_model({treedbs, topics, cols}) -> model
|
|
23185
|
+
*
|
|
23186
|
+
* model = {
|
|
23187
|
+
* treedbs: [{
|
|
23188
|
+
* id, name, record,
|
|
23189
|
+
* schema_version, c_schema_version, system_schema_version,
|
|
23190
|
+
* topics: [{
|
|
23191
|
+
* id, name, order, record,
|
|
23192
|
+
* pkey, topic_version, system_topic, main_topic,
|
|
23193
|
+
* cols: [{id, name, order, record}]
|
|
23194
|
+
* }]
|
|
23195
|
+
* }],
|
|
23196
|
+
* orphan_topics: [...], topics whose treedb is not in the store
|
|
23197
|
+
* orphan_cols: [...] columns whose topic is not in the store
|
|
23198
|
+
* }
|
|
23199
|
+
*
|
|
23200
|
+
* An orphan is NOT dropped. A column whose topic was deleted is
|
|
23201
|
+
* exactly the kind of leftover an operator opens this editor to
|
|
23202
|
+
* find, and a model that hides it makes the editor lie.
|
|
23203
|
+
***************************************************************/
|
|
23204
|
+
function build_schema_model(records) {
|
|
23205
|
+
let r = records || {};
|
|
23206
|
+
let treedb_rows = Array.isArray(r.treedbs) ? r.treedbs : [];
|
|
23207
|
+
let topic_rows = Array.isArray(r.topics) ? r.topics : [];
|
|
23208
|
+
let col_rows = Array.isArray(r.cols) ? r.cols : [];
|
|
23209
|
+
let treedbs = [];
|
|
23210
|
+
let by_treedb_id = {};
|
|
23211
|
+
for (let record of treedb_rows) {
|
|
23212
|
+
if (!record || typeof record.id !== "string") continue;
|
|
23213
|
+
let entry = {
|
|
23214
|
+
id: record.id,
|
|
23215
|
+
name: record.id,
|
|
23216
|
+
record,
|
|
23217
|
+
schema_version: record.schema_version,
|
|
23218
|
+
c_schema_version: record.c_schema_version,
|
|
23219
|
+
system_schema_version: record.system_schema_version,
|
|
23220
|
+
topics: []
|
|
23221
|
+
};
|
|
23222
|
+
treedbs.push(entry);
|
|
23223
|
+
by_treedb_id[entry.id] = entry;
|
|
23224
|
+
}
|
|
23225
|
+
let orphan_topics = [];
|
|
23226
|
+
let by_topic_id = {};
|
|
23227
|
+
for (let record of topic_rows) {
|
|
23228
|
+
if (!record || typeof record.id !== "string") continue;
|
|
23229
|
+
let entry = {
|
|
23230
|
+
id: record.id,
|
|
23231
|
+
name: record_name(record),
|
|
23232
|
+
order: record_order(record),
|
|
23233
|
+
record,
|
|
23234
|
+
treedb_id: parent_id(record, "treedbs"),
|
|
23235
|
+
pkey: record.pkey || "",
|
|
23236
|
+
pkey2s: record.pkey2s,
|
|
23237
|
+
system_flag: record.system_flag || "",
|
|
23238
|
+
tkey: record.tkey || "",
|
|
23239
|
+
topic_version: record.topic_version,
|
|
23240
|
+
system_topic: !!record.system_topic,
|
|
23241
|
+
main_topic: !!record.main_topic,
|
|
23242
|
+
cols: []
|
|
23243
|
+
};
|
|
23244
|
+
by_topic_id[entry.id] = entry;
|
|
23245
|
+
let parent = by_treedb_id[entry.treedb_id];
|
|
23246
|
+
if (parent) parent.topics.push(entry);
|
|
23247
|
+
else orphan_topics.push(entry);
|
|
23248
|
+
}
|
|
23249
|
+
let orphan_cols = [];
|
|
23250
|
+
for (let record of col_rows) {
|
|
23251
|
+
if (!record || typeof record.id !== "string") continue;
|
|
23252
|
+
let entry = {
|
|
23253
|
+
id: record.id,
|
|
23254
|
+
name: record_name(record),
|
|
23255
|
+
order: record_order(record),
|
|
23256
|
+
record,
|
|
23257
|
+
topic_id: parent_id(record, "topics")
|
|
23258
|
+
};
|
|
23259
|
+
let parent = by_topic_id[entry.topic_id];
|
|
23260
|
+
if (parent) parent.cols.push(entry);
|
|
23261
|
+
else orphan_cols.push(entry);
|
|
23262
|
+
}
|
|
23263
|
+
for (let treedb of treedbs) {
|
|
23264
|
+
sort_by_order(treedb.topics);
|
|
23265
|
+
for (let topic of treedb.topics) sort_by_order(topic.cols);
|
|
23266
|
+
}
|
|
23267
|
+
for (let topic of orphan_topics) sort_by_order(topic.cols);
|
|
23268
|
+
treedbs.sort((a, b) => {
|
|
23269
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
23270
|
+
});
|
|
23271
|
+
sort_by_order(orphan_topics);
|
|
23272
|
+
sort_by_order(orphan_cols);
|
|
23273
|
+
return {
|
|
23274
|
+
treedbs,
|
|
23275
|
+
orphan_topics,
|
|
23276
|
+
orphan_cols
|
|
23277
|
+
};
|
|
23278
|
+
}
|
|
23279
|
+
/***************************************************************
|
|
23280
|
+
* Is there a value here at all?
|
|
23281
|
+
*
|
|
23282
|
+
* The store answers a `blob` column that was never set with an
|
|
23283
|
+
* EMPTY COLLECTION, not with nothing: `enum: {}`, `hook: {}`,
|
|
23284
|
+
* `default: {}`. Read as "a value", those turn every export into
|
|
23285
|
+
* a schema full of empty objects — and `'hook': {}` in a literal
|
|
23286
|
+
* is a hook with no mapping, which is a link the treedb builds
|
|
23287
|
+
* and nothing writes.
|
|
23288
|
+
***************************************************************/
|
|
23289
|
+
function is_empty_value(value) {
|
|
23290
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
23291
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
23292
|
+
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
23293
|
+
return false;
|
|
23294
|
+
}
|
|
23295
|
+
/***************************************************************
|
|
23296
|
+
* The fields of a column that are DECLARED as one thing and
|
|
23297
|
+
* STORED as another.
|
|
23298
|
+
*
|
|
23299
|
+
* `hook`, `enum`, `default`, `pkey2s` and `properties` are `blob`
|
|
23300
|
+
* columns of the `cols` topic (treedb_system_schema.c), so what
|
|
23301
|
+
* comes back may be the value or the JSON text of the value,
|
|
23302
|
+
* depending on how it was written — a projection writes the
|
|
23303
|
+
* value, a hand edit through a plain text field writes the text.
|
|
23304
|
+
* `flag` is an array that a single-flag edit can leave as a bare
|
|
23305
|
+
* string. Reading these without saying so is how a hook drawn
|
|
23306
|
+
* from `{"users":"department"}` becomes no hook at all.
|
|
23307
|
+
***************************************************************/
|
|
23308
|
+
function as_json(value) {
|
|
23309
|
+
if (typeof value !== "string") return value;
|
|
23310
|
+
let text = value.trim();
|
|
23311
|
+
if (text.length === 0) return null;
|
|
23312
|
+
try {
|
|
23313
|
+
return JSON.parse(text);
|
|
23314
|
+
} catch (e) {
|
|
23315
|
+
return value;
|
|
23316
|
+
}
|
|
23317
|
+
}
|
|
23318
|
+
function col_flags(record) {
|
|
23319
|
+
let flag = record ? record.flag : null;
|
|
23320
|
+
if (Array.isArray(flag)) return flag.filter((f) => typeof f === "string" && f.length > 0);
|
|
23321
|
+
if (typeof flag === "string" && flag.length > 0) {
|
|
23322
|
+
let parsed = as_json(flag);
|
|
23323
|
+
if (Array.isArray(parsed)) return parsed.filter((f) => typeof f === "string" && f.length > 0);
|
|
23324
|
+
return [flag];
|
|
23325
|
+
}
|
|
23326
|
+
return [];
|
|
23327
|
+
}
|
|
23328
|
+
function col_hook(record) {
|
|
23329
|
+
let hook = as_json(record ? record.hook : null);
|
|
23330
|
+
if (hook && typeof hook === "object" && !Array.isArray(hook)) return hook;
|
|
23331
|
+
return null;
|
|
23332
|
+
}
|
|
23333
|
+
function col_enum(record) {
|
|
23334
|
+
let e = as_json(record ? record.enum : null);
|
|
23335
|
+
if (Array.isArray(e)) return e;
|
|
23336
|
+
return [];
|
|
23337
|
+
}
|
|
23338
|
+
function topic_pkey2s(record) {
|
|
23339
|
+
let pkey2s = as_json(record ? record.pkey2s : null);
|
|
23340
|
+
if (Array.isArray(pkey2s)) return pkey2s.filter((k) => typeof k === "string" && k.length > 0);
|
|
23341
|
+
if (typeof pkey2s === "string" && pkey2s.length > 0) return [pkey2s];
|
|
23342
|
+
return [];
|
|
23343
|
+
}
|
|
23344
|
+
/***************************************************************
|
|
23345
|
+
* Lookups by id. The editor addresses a position by NAME (the
|
|
23346
|
+
* url carries `<treedb>/<topic>`), so both are offered.
|
|
23347
|
+
***************************************************************/
|
|
23348
|
+
function find_treedb(model, treedb_id) {
|
|
23349
|
+
if (!model || !Array.isArray(model.treedbs)) return null;
|
|
23350
|
+
for (let treedb of model.treedbs) if (treedb.id === treedb_id) return treedb;
|
|
23351
|
+
return null;
|
|
23352
|
+
}
|
|
23353
|
+
function find_topic(model, treedb_id, topic_name) {
|
|
23354
|
+
let treedb = find_treedb(model, treedb_id);
|
|
23355
|
+
if (!treedb) return null;
|
|
23356
|
+
for (let topic of treedb.topics) if (topic.name === topic_name || topic.id === topic_name) return topic;
|
|
23357
|
+
return null;
|
|
23358
|
+
}
|
|
23359
|
+
function find_col(model, treedb_id, topic_name, col_name) {
|
|
23360
|
+
let topic = find_topic(model, treedb_id, topic_name);
|
|
23361
|
+
if (!topic) return null;
|
|
23362
|
+
for (let col of topic.cols) if (col.name === col_name || col.id === col_name) return col;
|
|
23363
|
+
return null;
|
|
23364
|
+
}
|
|
23365
|
+
/***************************************************************
|
|
23366
|
+
* The fkey a NEW record must carry to belong to `parent_id`.
|
|
23367
|
+
* Written here because the editor is the only writer that has to
|
|
23368
|
+
* compose one, and getting the hook name wrong links the record
|
|
23369
|
+
* to nothing while the write still succeeds.
|
|
23370
|
+
***************************************************************/
|
|
23371
|
+
function fkey_ref(parent_topic, parent_id_, hook_name) {
|
|
23372
|
+
return `${parent_topic}^${parent_id_}^${hook_name}`;
|
|
23373
|
+
}
|
|
23374
|
+
/***************************************************************
|
|
23375
|
+
* Where a new sibling goes: after the last one. The store's own
|
|
23376
|
+
* default (9999) would put every hand-made column in the same
|
|
23377
|
+
* place and let their names decide the schema's order.
|
|
23378
|
+
***************************************************************/
|
|
23379
|
+
function next_order(siblings) {
|
|
23380
|
+
let max = 0;
|
|
23381
|
+
if (Array.isArray(siblings)) for (let s of siblings) {
|
|
23382
|
+
let order = s && typeof s.order === "number" ? s.order : DEFAULT_ORDER;
|
|
23383
|
+
if (order < 9999 && order > max) max = order;
|
|
23384
|
+
}
|
|
23385
|
+
return max + 1;
|
|
23386
|
+
}
|
|
23387
|
+
/***************************************************************
|
|
23388
|
+
* Renumber a list AFTER a move, as the writes it takes: only the
|
|
23389
|
+
* records whose order actually changed, so a drag of one row in a
|
|
23390
|
+
* 40-column topic is not 40 writes.
|
|
23391
|
+
*
|
|
23392
|
+
* moved_orders(list) -> [{id, order}, ...]
|
|
23393
|
+
***************************************************************/
|
|
23394
|
+
function moved_orders(list) {
|
|
23395
|
+
let writes = [];
|
|
23396
|
+
if (!Array.isArray(list)) return writes;
|
|
23397
|
+
for (let i = 0; i < list.length; i++) {
|
|
23398
|
+
let entry = list[i];
|
|
23399
|
+
let order = i + 1;
|
|
23400
|
+
if (!entry || typeof entry.id !== "string") continue;
|
|
23401
|
+
if (entry.order !== order) writes.push({
|
|
23402
|
+
id: entry.id,
|
|
23403
|
+
order
|
|
23404
|
+
});
|
|
23405
|
+
}
|
|
23406
|
+
return writes;
|
|
23407
|
+
}
|
|
23408
|
+
//#endregion
|
|
23409
|
+
//#region src/treedb_topic_keys.js
|
|
23410
|
+
/***********************************************************************
|
|
23411
|
+
* treedb_topic_keys.js
|
|
23412
|
+
*
|
|
23413
|
+
* The KEYS of a treedb topic, read from its desc: the pkey, the
|
|
23414
|
+
* secondary keys (`pkey2s`) and the time key (`tkey`), plus the
|
|
23415
|
+
* names behind its `system_flag` number.
|
|
23416
|
+
*
|
|
23417
|
+
* Every view that describes a topic -- the schema diagram, the
|
|
23418
|
+
* topic-info panel, the topic cards -- must say them, and say them
|
|
23419
|
+
* the same way. A topic with `pkey2s` is not a topic with one more
|
|
23420
|
+
* column: one id holds several INSTANCES, an update has to carry
|
|
23421
|
+
* the pkey2 or it lands on the primary, and `delete-instance`
|
|
23422
|
+
* exists. `tkey` names where the time of every record comes from.
|
|
23423
|
+
* The views said neither, so this is where they ask.
|
|
23424
|
+
*
|
|
23425
|
+
* Pure, so it is tested with no DOM.
|
|
23426
|
+
*
|
|
23427
|
+
* Copyright (c) 2026, ArtGins.
|
|
23428
|
+
* All Rights Reserved.
|
|
23429
|
+
***********************************************************************/
|
|
23430
|
+
var SF_NAMES = [
|
|
23431
|
+
"sf_string_key",
|
|
23432
|
+
"sf_rowid_key",
|
|
23433
|
+
"sf_int_key",
|
|
23434
|
+
"",
|
|
23435
|
+
"sf_zip_record",
|
|
23436
|
+
"sf_cipher_record",
|
|
23437
|
+
"",
|
|
23438
|
+
"",
|
|
23439
|
+
"sf_t_ms",
|
|
23440
|
+
"sf_tm_ms",
|
|
23441
|
+
"sf_deleted_instance",
|
|
23442
|
+
"sf_immutable_record",
|
|
23443
|
+
"sf_loading_from_disk",
|
|
23444
|
+
"",
|
|
23445
|
+
"",
|
|
23446
|
+
""
|
|
23447
|
+
];
|
|
23448
|
+
/************************************************************
|
|
23449
|
+
* The secondary keys of a topic, always a list. The desc
|
|
23450
|
+
* carries a list; a schema literal may declare a bare
|
|
23451
|
+
* string.
|
|
23452
|
+
************************************************************/
|
|
23453
|
+
function desc_pkey2s(desc) {
|
|
23454
|
+
return topic_pkey2s(desc);
|
|
23455
|
+
}
|
|
23456
|
+
/************************************************************
|
|
23457
|
+
* The time key of a topic, "" when it has none -- and then
|
|
23458
|
+
* the time of a record is the time it was appended.
|
|
23459
|
+
************************************************************/
|
|
23460
|
+
function desc_tkey(desc) {
|
|
23461
|
+
let tkey = desc ? desc.tkey : "";
|
|
23462
|
+
return typeof tkey === "string" ? tkey : "";
|
|
23463
|
+
}
|
|
23464
|
+
/************************************************************
|
|
23465
|
+
* Which keys of its topic a column IS, in the order the
|
|
23466
|
+
* views print them: "pkey", "pkey2", "tkey". Empty for a
|
|
23467
|
+
* column that is none.
|
|
23468
|
+
************************************************************/
|
|
23469
|
+
function col_key_roles(desc, col_id) {
|
|
23470
|
+
let roles = [];
|
|
23471
|
+
if (!desc || typeof col_id !== "string" || col_id.length === 0) return roles;
|
|
23472
|
+
if (col_id === (desc.pkey || "id")) roles.push("pkey");
|
|
23473
|
+
if (desc_pkey2s(desc).includes(col_id)) roles.push("pkey2");
|
|
23474
|
+
if (desc_tkey(desc) === col_id) roles.push("tkey");
|
|
23475
|
+
return roles;
|
|
23476
|
+
}
|
|
23477
|
+
/************************************************************
|
|
23478
|
+
* The names of a system_flag. The desc carries the NUMBER
|
|
23479
|
+
* (`1`), a schema literal carries the names
|
|
23480
|
+
* ("sf_string_key|sf_t_ms"); both come back as a list. A
|
|
23481
|
+
* set bit with no name is shown by its value, never
|
|
23482
|
+
* dropped.
|
|
23483
|
+
************************************************************/
|
|
23484
|
+
function system_flag_names(system_flag) {
|
|
23485
|
+
if (Array.isArray(system_flag)) return system_flag.filter((n) => typeof n === "string" && n.length > 0);
|
|
23486
|
+
if (typeof system_flag === "string") return system_flag.split(/[|,]/).map((n) => n.trim()).filter((n) => n.length > 0);
|
|
23487
|
+
if (typeof system_flag !== "number" || !Number.isInteger(system_flag) || system_flag <= 0) return [];
|
|
23488
|
+
let names = [];
|
|
23489
|
+
for (let bit = 0; bit < SF_NAMES.length; bit++) {
|
|
23490
|
+
let mask = 1 << bit;
|
|
23491
|
+
if (system_flag & mask) names.push(SF_NAMES[bit] || `0x${mask.toString(16).padStart(4, "0")}`);
|
|
23492
|
+
}
|
|
23493
|
+
return names;
|
|
23494
|
+
}
|
|
23495
|
+
//#endregion
|
|
23054
23496
|
//#region src/c_yui_treedb_topics.js
|
|
23055
23497
|
/***********************************************************************
|
|
23056
23498
|
* c_yui_treedb_topics.js
|
|
23057
23499
|
*
|
|
23058
23500
|
* Management of TreeDB's topics with Bulma tabs
|
|
23059
23501
|
*
|
|
23060
|
-
* Copyright (c) 2025, ArtGins.
|
|
23502
|
+
* Copyright (c) 2025-2026, ArtGins.
|
|
23061
23503
|
* All Rights Reserved.
|
|
23062
23504
|
***********************************************************************/
|
|
23063
23505
|
/***************************************************************
|
|
@@ -23571,6 +24013,29 @@ function topic_card_meta(desc) {
|
|
|
23571
24013
|
{ class: "TREEDB_TOPIC_CARD_META" },
|
|
23572
24014
|
head
|
|
23573
24015
|
]);
|
|
24016
|
+
let pkey2s = desc_pkey2s(desc);
|
|
24017
|
+
if (pkey2s.length) {
|
|
24018
|
+
let list = pkey2s.join(", ");
|
|
24019
|
+
meta.push([
|
|
24020
|
+
"div",
|
|
24021
|
+
{
|
|
24022
|
+
class: "TREEDB_TOPIC_CARD_REL TREEDB_TOPIC_CARD_PKEY2S",
|
|
24023
|
+
title: list
|
|
24024
|
+
},
|
|
24025
|
+
[[
|
|
24026
|
+
"span",
|
|
24027
|
+
{
|
|
24028
|
+
class: "TREEDB_TOPIC_CARD_REL_LABEL",
|
|
24029
|
+
i18n: "pkey2s"
|
|
24030
|
+
},
|
|
24031
|
+
"pkey2s"
|
|
24032
|
+
], [
|
|
24033
|
+
"span",
|
|
24034
|
+
{ class: "TREEDB_TOPIC_CARD_REL_LIST" },
|
|
24035
|
+
list
|
|
24036
|
+
]]
|
|
24037
|
+
]);
|
|
24038
|
+
}
|
|
23574
24039
|
let relation = (cls, key, arrow, names) => {
|
|
23575
24040
|
let list = names.join(", ");
|
|
23576
24041
|
return [
|
|
@@ -23834,33 +24299,26 @@ function show_topic_info(gobj, topic) {
|
|
|
23834
24299
|
}
|
|
23835
24300
|
/************************************************************
|
|
23836
24301
|
* Build the read-only topic-info panel from a topic desc: the
|
|
23837
|
-
* topic name, its
|
|
24302
|
+
* topic name, its KEYS, and a table of columns (name / type /
|
|
23838
24303
|
* key relationship). Everything is guarded — a malformed desc
|
|
23839
24304
|
* renders a shorter panel, never throws.
|
|
24305
|
+
*
|
|
24306
|
+
* The keys are what an operator has to know before touching a
|
|
24307
|
+
* record, so they are all said, and the empty ones too: a
|
|
24308
|
+
* topic with `pkey2s` keeps several instances under one id,
|
|
24309
|
+
* and an empty `tkey` means the time of a record is the time
|
|
24310
|
+
* it was appended -- a row missing is not the same as "none".
|
|
23840
24311
|
************************************************************/
|
|
23841
24312
|
function build_topic_info_panel(gobj, $info, topic, desc) {
|
|
23842
24313
|
while ($info.firstChild) $info.removeChild($info.firstChild);
|
|
23843
24314
|
let pkey = desc.pkey || "";
|
|
23844
|
-
let
|
|
23845
|
-
|
|
23846
|
-
if ((0, _yuneta_gobj_js.is_object)(v)) return Object.keys(v).join(", ");
|
|
23847
|
-
return v === void 0 || v === null ? "" : String(v);
|
|
23848
|
-
};
|
|
24315
|
+
let pkey2s = desc_pkey2s(desc);
|
|
24316
|
+
let tkey = desc_tkey(desc);
|
|
23849
24317
|
let $meta = [];
|
|
23850
|
-
let push_meta = (key,
|
|
23851
|
-
if (value === void 0 || value === null || value === "") return;
|
|
23852
|
-
let $val = highlight ? [
|
|
23853
|
-
"span",
|
|
23854
|
-
{ class: "tag is-info" },
|
|
23855
|
-
`${value}`
|
|
23856
|
-
] : [
|
|
23857
|
-
"code",
|
|
23858
|
-
{},
|
|
23859
|
-
`${value}`
|
|
23860
|
-
];
|
|
24318
|
+
let push_meta = (key, $val) => {
|
|
23861
24319
|
$meta.push([
|
|
23862
24320
|
"tr",
|
|
23863
|
-
{},
|
|
24321
|
+
{ class: `TREEDB_TOPIC_INFO_${key.toUpperCase()}` },
|
|
23864
24322
|
[[
|
|
23865
24323
|
"th",
|
|
23866
24324
|
{
|
|
@@ -23875,29 +24333,64 @@ function build_topic_info_panel(gobj, $info, topic, desc) {
|
|
|
23875
24333
|
]]
|
|
23876
24334
|
]);
|
|
23877
24335
|
};
|
|
23878
|
-
push_meta("version",
|
|
23879
|
-
|
|
23880
|
-
|
|
23881
|
-
|
|
24336
|
+
if (desc.topic_version !== void 0 && desc.topic_version !== null && desc.topic_version !== "") push_meta("version", [
|
|
24337
|
+
"span",
|
|
24338
|
+
{ class: "tag is-info" },
|
|
24339
|
+
`${desc.topic_version}`
|
|
24340
|
+
]);
|
|
24341
|
+
let sf_names = system_flag_names(desc.system_flag);
|
|
24342
|
+
if (sf_names.length > 0) push_meta("system", [
|
|
24343
|
+
"code",
|
|
24344
|
+
{ title: `${desc.system_flag}` },
|
|
24345
|
+
sf_names.join(" | ")
|
|
24346
|
+
]);
|
|
24347
|
+
if (pkey) push_meta("pkey", [
|
|
24348
|
+
"code",
|
|
24349
|
+
{},
|
|
24350
|
+
pkey
|
|
24351
|
+
]);
|
|
24352
|
+
if (pkey2s.length > 0) push_meta("pkey2s", [
|
|
24353
|
+
"span",
|
|
24354
|
+
{ class: "tags" },
|
|
24355
|
+
pkey2s.map((name) => [
|
|
24356
|
+
"span",
|
|
24357
|
+
{ class: "tag is-warning" },
|
|
24358
|
+
name
|
|
24359
|
+
])
|
|
24360
|
+
]);
|
|
24361
|
+
push_meta("tkey", tkey ? [
|
|
24362
|
+
"code",
|
|
24363
|
+
{},
|
|
24364
|
+
tkey
|
|
24365
|
+
] : [
|
|
24366
|
+
"span",
|
|
24367
|
+
{
|
|
24368
|
+
class: "has-text-grey",
|
|
24369
|
+
i18n: "append time"
|
|
24370
|
+
},
|
|
24371
|
+
"append time"
|
|
24372
|
+
]);
|
|
23882
24373
|
let $rows = [];
|
|
23883
24374
|
let cols = (0, _yuneta_gobj_js.is_array)(desc.cols) ? desc.cols : [];
|
|
23884
24375
|
for (let col of cols) {
|
|
23885
24376
|
if (!col || !col.id || col.id.charAt(0) === "_") continue;
|
|
23886
24377
|
let type = col.type || ((0, _yuneta_gobj_js.is_array)(col.flag) ? col.flag.join(", ") : col.flag || "");
|
|
23887
|
-
let
|
|
23888
|
-
|
|
23889
|
-
|
|
23890
|
-
else if ((0, _yuneta_gobj_js.is_object)(col.hook))
|
|
24378
|
+
let roles = col_key_roles(desc, col.id);
|
|
24379
|
+
let parts = roles.slice();
|
|
24380
|
+
if ((0, _yuneta_gobj_js.is_object)(col.fkey)) parts.push("→ " + Object.keys(col.fkey).join(", "));
|
|
24381
|
+
else if ((0, _yuneta_gobj_js.is_object)(col.hook)) parts.push("hook → " + Object.keys(col.hook).join(", "));
|
|
24382
|
+
let rel = parts.join(" · ");
|
|
24383
|
+
let is_key = roles.length > 0;
|
|
23891
24384
|
$rows.push([
|
|
23892
24385
|
"tr",
|
|
23893
|
-
{},
|
|
24386
|
+
is_key ? { class: "TREEDB_TOPIC_INFO_KEY_COL" } : {},
|
|
23894
24387
|
[
|
|
23895
24388
|
[
|
|
23896
24389
|
"td",
|
|
23897
24390
|
{},
|
|
23898
24391
|
[[
|
|
23899
24392
|
"code",
|
|
23900
|
-
{},
|
|
24393
|
+
is_key ? { class: "has-text-weight-bold" } : {},
|
|
23901
24394
|
`${col.id}`
|
|
23902
24395
|
]]
|
|
23903
24396
|
],
|
|
@@ -23908,7 +24401,7 @@ function build_topic_info_panel(gobj, $info, topic, desc) {
|
|
|
23908
24401
|
],
|
|
23909
24402
|
[
|
|
23910
24403
|
"td",
|
|
23911
|
-
{},
|
|
24404
|
+
is_key ? { class: "has-text-weight-semibold" } : {},
|
|
23912
24405
|
`${rel}`
|
|
23913
24406
|
]
|
|
23914
24407
|
]
|
|
@@ -25643,361 +26136,6 @@ function row_matches(row, term) {
|
|
|
25643
26136
|
});
|
|
25644
26137
|
}
|
|
25645
26138
|
//#endregion
|
|
25646
|
-
//#region src/schema_model.js
|
|
25647
|
-
/***********************************************************************
|
|
25648
|
-
* schema_model.js
|
|
25649
|
-
*
|
|
25650
|
-
* A SCHEMA out of the three meta-topics that store it.
|
|
25651
|
-
*
|
|
25652
|
-
* `treedb_system_schema` keeps every schema of a yuno as DATA, in
|
|
25653
|
-
* three flat topics — `treedbs`, `topics`, `cols` — linked by
|
|
25654
|
-
* fkeys. That is the right STORAGE and the wrong thing to put in
|
|
25655
|
-
* front of an operator: adding one column to one topic means
|
|
25656
|
-
* finding it in a table holding every column of every topic of
|
|
25657
|
-
* every treedb the yuno has.
|
|
25658
|
-
*
|
|
25659
|
-
* This module turns those three lists back into what they
|
|
25660
|
-
* describe: treedb -> topics -> columns, each in its declared
|
|
25661
|
-
* `order`. Pure, so the editor's model can be tested with no DOM
|
|
25662
|
-
* and no backend — the gclass fetches and draws, this decides
|
|
25663
|
-
* what the records MEAN.
|
|
25664
|
-
*
|
|
25665
|
-
* TWO THINGS IT EXISTS TO STATE ONCE:
|
|
25666
|
-
*
|
|
25667
|
-
* 1. A record is grouped by its FKEY, not by its id. The id is
|
|
25668
|
-
* qualified (`<treedb>.<topic>.<column>`) and reading it back
|
|
25669
|
-
* with a split on '.' works right up to the first treedb or
|
|
25670
|
-
* topic whose name carries one. The fkey is the link the store
|
|
25671
|
-
* itself uses; the id is only the fallback for a record whose
|
|
25672
|
-
* fkey never arrived.
|
|
25673
|
-
*
|
|
25674
|
-
* 2. A fkey reaches this code in FOUR shapes, because `nodes`
|
|
25675
|
-
* answers differently depending on the options it was called
|
|
25676
|
-
* with: the bare ref string, the expanded object, a list of
|
|
25677
|
-
* either, or a dict keyed by the ref. All four are the same
|
|
25678
|
-
* fact and parse_fkey_ref() flattens them to it.
|
|
25679
|
-
*
|
|
25680
|
-
* Copyright (c) 2026, ArtGins.
|
|
25681
|
-
* All Rights Reserved.
|
|
25682
|
-
***********************************************************************/
|
|
25683
|
-
var DEFAULT_ORDER = 9999;
|
|
25684
|
-
/***************************************************************
|
|
25685
|
-
* parse_fkey_ref(value) -> [{topic_name, id, hook_name}, ...]
|
|
25686
|
-
*
|
|
25687
|
-
* Every shape a fkey column can arrive in, flattened to the
|
|
25688
|
-
* refs it names. Anything unrecognizable contributes nothing —
|
|
25689
|
-
* a schema drawn without one link beats a screen that throws.
|
|
25690
|
-
***************************************************************/
|
|
25691
|
-
function parse_fkey_ref(value) {
|
|
25692
|
-
let refs = [];
|
|
25693
|
-
let push_one = (v) => {
|
|
25694
|
-
if (typeof v === "string") {
|
|
25695
|
-
let parts = v.split("^");
|
|
25696
|
-
if (parts.length !== 3) return;
|
|
25697
|
-
refs.push({
|
|
25698
|
-
topic_name: parts[0],
|
|
25699
|
-
id: parts[1],
|
|
25700
|
-
hook_name: parts[2]
|
|
25701
|
-
});
|
|
25702
|
-
return;
|
|
25703
|
-
}
|
|
25704
|
-
if (v && typeof v === "object") {
|
|
25705
|
-
let id = v.id;
|
|
25706
|
-
if (typeof id !== "string" || id.length === 0) return;
|
|
25707
|
-
refs.push({
|
|
25708
|
-
topic_name: v.topic_name || "",
|
|
25709
|
-
id,
|
|
25710
|
-
hook_name: v.hook_name || ""
|
|
25711
|
-
});
|
|
25712
|
-
}
|
|
25713
|
-
};
|
|
25714
|
-
if (value === null || value === void 0) return refs;
|
|
25715
|
-
if (typeof value === "string") {
|
|
25716
|
-
push_one(value);
|
|
25717
|
-
return refs;
|
|
25718
|
-
}
|
|
25719
|
-
if (Array.isArray(value)) {
|
|
25720
|
-
for (let v of value) push_one(v);
|
|
25721
|
-
return refs;
|
|
25722
|
-
}
|
|
25723
|
-
if (typeof value === "object") for (let [key, v] of Object.entries(value)) if (v && typeof v === "object" && typeof v.id === "string") push_one(v);
|
|
25724
|
-
else push_one(key);
|
|
25725
|
-
return refs;
|
|
25726
|
-
}
|
|
25727
|
-
/***************************************************************
|
|
25728
|
-
* The id of the FIRST parent a record names, or "" when it
|
|
25729
|
-
* names none. A record has one parent here — a column belongs to
|
|
25730
|
-
* one topic, a topic to one treedb — and a second ref would be a
|
|
25731
|
-
* store this editor cannot draw anyway.
|
|
25732
|
-
***************************************************************/
|
|
25733
|
-
function parent_id(record, fkey_col) {
|
|
25734
|
-
let refs = parse_fkey_ref(record ? record[fkey_col] : null);
|
|
25735
|
-
return refs.length > 0 ? refs[0].id : "";
|
|
25736
|
-
}
|
|
25737
|
-
/***************************************************************
|
|
25738
|
-
* The name a record is KNOWN by, which is not what it is keyed
|
|
25739
|
-
* by: `topics` and `cols` key by the qualified id and carry the
|
|
25740
|
-
* bare name in `value` (its pkey2). An older store that still
|
|
25741
|
-
* keyed by rowid has no `value` worth showing, so the id stands
|
|
25742
|
-
* in — the same fallback as node_label().
|
|
25743
|
-
***************************************************************/
|
|
25744
|
-
function record_name(record) {
|
|
25745
|
-
if (!record) return "";
|
|
25746
|
-
if (typeof record.value === "string" && record.value.length > 0) return record.value;
|
|
25747
|
-
return typeof record.id === "string" ? record.id : "";
|
|
25748
|
-
}
|
|
25749
|
-
/***************************************************************
|
|
25750
|
-
* The `order` of a record as a NUMBER, whatever the store put
|
|
25751
|
-
* there: an integer column read back from JSON may arrive as a
|
|
25752
|
-
* string, and comparing "10" with 9 sorts the schema wrong.
|
|
25753
|
-
***************************************************************/
|
|
25754
|
-
function record_order(record) {
|
|
25755
|
-
let order = record ? record.order : void 0;
|
|
25756
|
-
if (typeof order === "number" && isFinite(order)) return order;
|
|
25757
|
-
if (typeof order === "string" && order.trim().length > 0) {
|
|
25758
|
-
let n = Number(order);
|
|
25759
|
-
if (isFinite(n)) return n;
|
|
25760
|
-
}
|
|
25761
|
-
return DEFAULT_ORDER;
|
|
25762
|
-
}
|
|
25763
|
-
/***************************************************************
|
|
25764
|
-
* Sort in place by `order`, ties broken by name so a schema
|
|
25765
|
-
* whose records all default to 9999 still draws the same way
|
|
25766
|
-
* twice.
|
|
25767
|
-
***************************************************************/
|
|
25768
|
-
function sort_by_order(list) {
|
|
25769
|
-
list.sort((a, b) => {
|
|
25770
|
-
if (a.order !== b.order) return a.order - b.order;
|
|
25771
|
-
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
25772
|
-
});
|
|
25773
|
-
return list;
|
|
25774
|
-
}
|
|
25775
|
-
/***************************************************************
|
|
25776
|
-
* build_schema_model({treedbs, topics, cols}) -> model
|
|
25777
|
-
*
|
|
25778
|
-
* model = {
|
|
25779
|
-
* treedbs: [{
|
|
25780
|
-
* id, name, record,
|
|
25781
|
-
* schema_version, c_schema_version, system_schema_version,
|
|
25782
|
-
* topics: [{
|
|
25783
|
-
* id, name, order, record,
|
|
25784
|
-
* pkey, topic_version, system_topic, main_topic,
|
|
25785
|
-
* cols: [{id, name, order, record}]
|
|
25786
|
-
* }]
|
|
25787
|
-
* }],
|
|
25788
|
-
* orphan_topics: [...], topics whose treedb is not in the store
|
|
25789
|
-
* orphan_cols: [...] columns whose topic is not in the store
|
|
25790
|
-
* }
|
|
25791
|
-
*
|
|
25792
|
-
* An orphan is NOT dropped. A column whose topic was deleted is
|
|
25793
|
-
* exactly the kind of leftover an operator opens this editor to
|
|
25794
|
-
* find, and a model that hides it makes the editor lie.
|
|
25795
|
-
***************************************************************/
|
|
25796
|
-
function build_schema_model(records) {
|
|
25797
|
-
let r = records || {};
|
|
25798
|
-
let treedb_rows = Array.isArray(r.treedbs) ? r.treedbs : [];
|
|
25799
|
-
let topic_rows = Array.isArray(r.topics) ? r.topics : [];
|
|
25800
|
-
let col_rows = Array.isArray(r.cols) ? r.cols : [];
|
|
25801
|
-
let treedbs = [];
|
|
25802
|
-
let by_treedb_id = {};
|
|
25803
|
-
for (let record of treedb_rows) {
|
|
25804
|
-
if (!record || typeof record.id !== "string") continue;
|
|
25805
|
-
let entry = {
|
|
25806
|
-
id: record.id,
|
|
25807
|
-
name: record.id,
|
|
25808
|
-
record,
|
|
25809
|
-
schema_version: record.schema_version,
|
|
25810
|
-
c_schema_version: record.c_schema_version,
|
|
25811
|
-
system_schema_version: record.system_schema_version,
|
|
25812
|
-
topics: []
|
|
25813
|
-
};
|
|
25814
|
-
treedbs.push(entry);
|
|
25815
|
-
by_treedb_id[entry.id] = entry;
|
|
25816
|
-
}
|
|
25817
|
-
let orphan_topics = [];
|
|
25818
|
-
let by_topic_id = {};
|
|
25819
|
-
for (let record of topic_rows) {
|
|
25820
|
-
if (!record || typeof record.id !== "string") continue;
|
|
25821
|
-
let entry = {
|
|
25822
|
-
id: record.id,
|
|
25823
|
-
name: record_name(record),
|
|
25824
|
-
order: record_order(record),
|
|
25825
|
-
record,
|
|
25826
|
-
treedb_id: parent_id(record, "treedbs"),
|
|
25827
|
-
pkey: record.pkey || "",
|
|
25828
|
-
pkey2s: record.pkey2s,
|
|
25829
|
-
system_flag: record.system_flag || "",
|
|
25830
|
-
tkey: record.tkey || "",
|
|
25831
|
-
topic_version: record.topic_version,
|
|
25832
|
-
system_topic: !!record.system_topic,
|
|
25833
|
-
main_topic: !!record.main_topic,
|
|
25834
|
-
cols: []
|
|
25835
|
-
};
|
|
25836
|
-
by_topic_id[entry.id] = entry;
|
|
25837
|
-
let parent = by_treedb_id[entry.treedb_id];
|
|
25838
|
-
if (parent) parent.topics.push(entry);
|
|
25839
|
-
else orphan_topics.push(entry);
|
|
25840
|
-
}
|
|
25841
|
-
let orphan_cols = [];
|
|
25842
|
-
for (let record of col_rows) {
|
|
25843
|
-
if (!record || typeof record.id !== "string") continue;
|
|
25844
|
-
let entry = {
|
|
25845
|
-
id: record.id,
|
|
25846
|
-
name: record_name(record),
|
|
25847
|
-
order: record_order(record),
|
|
25848
|
-
record,
|
|
25849
|
-
topic_id: parent_id(record, "topics")
|
|
25850
|
-
};
|
|
25851
|
-
let parent = by_topic_id[entry.topic_id];
|
|
25852
|
-
if (parent) parent.cols.push(entry);
|
|
25853
|
-
else orphan_cols.push(entry);
|
|
25854
|
-
}
|
|
25855
|
-
for (let treedb of treedbs) {
|
|
25856
|
-
sort_by_order(treedb.topics);
|
|
25857
|
-
for (let topic of treedb.topics) sort_by_order(topic.cols);
|
|
25858
|
-
}
|
|
25859
|
-
for (let topic of orphan_topics) sort_by_order(topic.cols);
|
|
25860
|
-
treedbs.sort((a, b) => {
|
|
25861
|
-
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
25862
|
-
});
|
|
25863
|
-
sort_by_order(orphan_topics);
|
|
25864
|
-
sort_by_order(orphan_cols);
|
|
25865
|
-
return {
|
|
25866
|
-
treedbs,
|
|
25867
|
-
orphan_topics,
|
|
25868
|
-
orphan_cols
|
|
25869
|
-
};
|
|
25870
|
-
}
|
|
25871
|
-
/***************************************************************
|
|
25872
|
-
* Is there a value here at all?
|
|
25873
|
-
*
|
|
25874
|
-
* The store answers a `blob` column that was never set with an
|
|
25875
|
-
* EMPTY COLLECTION, not with nothing: `enum: {}`, `hook: {}`,
|
|
25876
|
-
* `default: {}`. Read as "a value", those turn every export into
|
|
25877
|
-
* a schema full of empty objects — and `'hook': {}` in a literal
|
|
25878
|
-
* is a hook with no mapping, which is a link the treedb builds
|
|
25879
|
-
* and nothing writes.
|
|
25880
|
-
***************************************************************/
|
|
25881
|
-
function is_empty_value(value) {
|
|
25882
|
-
if (value === null || value === void 0 || value === "") return true;
|
|
25883
|
-
if (Array.isArray(value)) return value.length === 0;
|
|
25884
|
-
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
25885
|
-
return false;
|
|
25886
|
-
}
|
|
25887
|
-
/***************************************************************
|
|
25888
|
-
* The fields of a column that are DECLARED as one thing and
|
|
25889
|
-
* STORED as another.
|
|
25890
|
-
*
|
|
25891
|
-
* `hook`, `enum`, `default`, `pkey2s` and `properties` are `blob`
|
|
25892
|
-
* columns of the `cols` topic (treedb_system_schema.c), so what
|
|
25893
|
-
* comes back may be the value or the JSON text of the value,
|
|
25894
|
-
* depending on how it was written — a projection writes the
|
|
25895
|
-
* value, a hand edit through a plain text field writes the text.
|
|
25896
|
-
* `flag` is an array that a single-flag edit can leave as a bare
|
|
25897
|
-
* string. Reading these without saying so is how a hook drawn
|
|
25898
|
-
* from `{"users":"department"}` becomes no hook at all.
|
|
25899
|
-
***************************************************************/
|
|
25900
|
-
function as_json(value) {
|
|
25901
|
-
if (typeof value !== "string") return value;
|
|
25902
|
-
let text = value.trim();
|
|
25903
|
-
if (text.length === 0) return null;
|
|
25904
|
-
try {
|
|
25905
|
-
return JSON.parse(text);
|
|
25906
|
-
} catch (e) {
|
|
25907
|
-
return value;
|
|
25908
|
-
}
|
|
25909
|
-
}
|
|
25910
|
-
function col_flags(record) {
|
|
25911
|
-
let flag = record ? record.flag : null;
|
|
25912
|
-
if (Array.isArray(flag)) return flag.filter((f) => typeof f === "string" && f.length > 0);
|
|
25913
|
-
if (typeof flag === "string" && flag.length > 0) {
|
|
25914
|
-
let parsed = as_json(flag);
|
|
25915
|
-
if (Array.isArray(parsed)) return parsed.filter((f) => typeof f === "string" && f.length > 0);
|
|
25916
|
-
return [flag];
|
|
25917
|
-
}
|
|
25918
|
-
return [];
|
|
25919
|
-
}
|
|
25920
|
-
function col_hook(record) {
|
|
25921
|
-
let hook = as_json(record ? record.hook : null);
|
|
25922
|
-
if (hook && typeof hook === "object" && !Array.isArray(hook)) return hook;
|
|
25923
|
-
return null;
|
|
25924
|
-
}
|
|
25925
|
-
function col_enum(record) {
|
|
25926
|
-
let e = as_json(record ? record.enum : null);
|
|
25927
|
-
if (Array.isArray(e)) return e;
|
|
25928
|
-
return [];
|
|
25929
|
-
}
|
|
25930
|
-
function topic_pkey2s(record) {
|
|
25931
|
-
let pkey2s = as_json(record ? record.pkey2s : null);
|
|
25932
|
-
if (Array.isArray(pkey2s)) return pkey2s.filter((k) => typeof k === "string" && k.length > 0);
|
|
25933
|
-
if (typeof pkey2s === "string" && pkey2s.length > 0) return [pkey2s];
|
|
25934
|
-
return [];
|
|
25935
|
-
}
|
|
25936
|
-
/***************************************************************
|
|
25937
|
-
* Lookups by id. The editor addresses a position by NAME (the
|
|
25938
|
-
* url carries `<treedb>/<topic>`), so both are offered.
|
|
25939
|
-
***************************************************************/
|
|
25940
|
-
function find_treedb(model, treedb_id) {
|
|
25941
|
-
if (!model || !Array.isArray(model.treedbs)) return null;
|
|
25942
|
-
for (let treedb of model.treedbs) if (treedb.id === treedb_id) return treedb;
|
|
25943
|
-
return null;
|
|
25944
|
-
}
|
|
25945
|
-
function find_topic(model, treedb_id, topic_name) {
|
|
25946
|
-
let treedb = find_treedb(model, treedb_id);
|
|
25947
|
-
if (!treedb) return null;
|
|
25948
|
-
for (let topic of treedb.topics) if (topic.name === topic_name || topic.id === topic_name) return topic;
|
|
25949
|
-
return null;
|
|
25950
|
-
}
|
|
25951
|
-
function find_col(model, treedb_id, topic_name, col_name) {
|
|
25952
|
-
let topic = find_topic(model, treedb_id, topic_name);
|
|
25953
|
-
if (!topic) return null;
|
|
25954
|
-
for (let col of topic.cols) if (col.name === col_name || col.id === col_name) return col;
|
|
25955
|
-
return null;
|
|
25956
|
-
}
|
|
25957
|
-
/***************************************************************
|
|
25958
|
-
* The fkey a NEW record must carry to belong to `parent_id`.
|
|
25959
|
-
* Written here because the editor is the only writer that has to
|
|
25960
|
-
* compose one, and getting the hook name wrong links the record
|
|
25961
|
-
* to nothing while the write still succeeds.
|
|
25962
|
-
***************************************************************/
|
|
25963
|
-
function fkey_ref(parent_topic, parent_id_, hook_name) {
|
|
25964
|
-
return `${parent_topic}^${parent_id_}^${hook_name}`;
|
|
25965
|
-
}
|
|
25966
|
-
/***************************************************************
|
|
25967
|
-
* Where a new sibling goes: after the last one. The store's own
|
|
25968
|
-
* default (9999) would put every hand-made column in the same
|
|
25969
|
-
* place and let their names decide the schema's order.
|
|
25970
|
-
***************************************************************/
|
|
25971
|
-
function next_order(siblings) {
|
|
25972
|
-
let max = 0;
|
|
25973
|
-
if (Array.isArray(siblings)) for (let s of siblings) {
|
|
25974
|
-
let order = s && typeof s.order === "number" ? s.order : DEFAULT_ORDER;
|
|
25975
|
-
if (order < 9999 && order > max) max = order;
|
|
25976
|
-
}
|
|
25977
|
-
return max + 1;
|
|
25978
|
-
}
|
|
25979
|
-
/***************************************************************
|
|
25980
|
-
* Renumber a list AFTER a move, as the writes it takes: only the
|
|
25981
|
-
* records whose order actually changed, so a drag of one row in a
|
|
25982
|
-
* 40-column topic is not 40 writes.
|
|
25983
|
-
*
|
|
25984
|
-
* moved_orders(list) -> [{id, order}, ...]
|
|
25985
|
-
***************************************************************/
|
|
25986
|
-
function moved_orders(list) {
|
|
25987
|
-
let writes = [];
|
|
25988
|
-
if (!Array.isArray(list)) return writes;
|
|
25989
|
-
for (let i = 0; i < list.length; i++) {
|
|
25990
|
-
let entry = list[i];
|
|
25991
|
-
let order = i + 1;
|
|
25992
|
-
if (!entry || typeof entry.id !== "string") continue;
|
|
25993
|
-
if (entry.order !== order) writes.push({
|
|
25994
|
-
id: entry.id,
|
|
25995
|
-
order
|
|
25996
|
-
});
|
|
25997
|
-
}
|
|
25998
|
-
return writes;
|
|
25999
|
-
}
|
|
26000
|
-
//#endregion
|
|
26001
26139
|
//#region src/treedb_write_plan.js
|
|
26002
26140
|
/***********************************************************************
|
|
26003
26141
|
* treedb_write_plan.js
|
|
@@ -68843,12 +68981,20 @@ function hook_mark(type) {
|
|
|
68843
68981
|
* (↖) 1 fkey (1 parent)
|
|
68844
68982
|
* [↖] n fkeys (n parents)
|
|
68845
68983
|
* {↖} N fkeys (N parents)
|
|
68984
|
+
* (2) a secondary key (pkey2)
|
|
68985
|
+
* (t) the time key (tkey)
|
|
68846
68986
|
* * required
|
|
68847
68987
|
* # the primary key
|
|
68848
68988
|
*
|
|
68849
|
-
*
|
|
68850
|
-
*
|
|
68851
|
-
*
|
|
68989
|
+
* The key marks lead the mark column, and a pkey2 is drawn in
|
|
68990
|
+
* bold like the pkey: a topic with secondary keys keeps several
|
|
68991
|
+
* instances under one id, which is the first thing to know
|
|
68992
|
+
* about it. `(t)` is new to the notation: the literals' legend
|
|
68993
|
+
* carries it too.
|
|
68994
|
+
*
|
|
68995
|
+
* The flags are read here rather than through
|
|
68996
|
+
* treedb_get_field_desc's single `type`, which keeps only the
|
|
68997
|
+
* last flag it saw.
|
|
68852
68998
|
************************************************************/
|
|
68853
68999
|
function schema_rows(desc) {
|
|
68854
69000
|
let rows = [];
|
|
@@ -68858,19 +69004,25 @@ function schema_rows(desc) {
|
|
|
68858
69004
|
let flags = Array.isArray(col.flag) ? col.flag : [];
|
|
68859
69005
|
let is_hook = flags.indexOf("hook") >= 0;
|
|
68860
69006
|
let is_fkey = flags.indexOf("fkey") >= 0;
|
|
69007
|
+
let roles = col_key_roles(desc, col.id);
|
|
69008
|
+
let key_marks = [];
|
|
69009
|
+
if (roles.includes("pkey2")) key_marks.push("(2)");
|
|
69010
|
+
if (roles.includes("tkey")) key_marks.push("(t)");
|
|
68861
69011
|
let mark = "";
|
|
68862
69012
|
if (is_hook) mark = hook_mark(col.type);
|
|
68863
69013
|
if (is_fkey) {
|
|
68864
69014
|
let fmark = hook_mark(col.type).replace("}", "↖}").replace("]", "↖]").replace(")", "↖)");
|
|
68865
69015
|
mark = mark ? mark + " " + fmark : fmark;
|
|
68866
69016
|
}
|
|
69017
|
+
if (key_marks.length) mark = mark ? key_marks.join(" ") + " " + mark : key_marks.join(" ");
|
|
68867
69018
|
rows.push({
|
|
68868
69019
|
name: col.id,
|
|
68869
69020
|
type: col.type || "",
|
|
68870
69021
|
mark,
|
|
68871
69022
|
is_hook,
|
|
68872
69023
|
is_fkey,
|
|
68873
|
-
is_pkey:
|
|
69024
|
+
is_pkey: roles.includes("pkey"),
|
|
69025
|
+
is_pkey2: roles.includes("pkey2"),
|
|
68874
69026
|
required: flags.indexOf("required") >= 0 || flags.indexOf("notnull") >= 0
|
|
68875
69027
|
});
|
|
68876
69028
|
}
|
|
@@ -68917,7 +69069,7 @@ function build_card_innerHTML(topic, rows, color, dark) {
|
|
|
68917
69069
|
let rows_html = "";
|
|
68918
69070
|
for (let row of rows) {
|
|
68919
69071
|
let lead = row.is_pkey ? "#" : row.required ? "*" : "";
|
|
68920
|
-
let name_weight = row.is_pkey || row.required ? "600" : "400";
|
|
69072
|
+
let name_weight = row.is_pkey || row.is_pkey2 ? "700" : row.required ? "600" : "400";
|
|
68921
69073
|
let name_color = row.is_hook || row.is_fkey ? title_color : text_color;
|
|
68922
69074
|
rows_html += ` <div class="TREEDB_SCHEMA_FIELD" style="
|
|
68923
69075
|
display: flex; align-items: center; gap: 6px;
|