archgraph-argo 0.13.9 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
// transactions with a bounded busy retry. Keep EA's own SQLite handle open is fine —
|
|
12
12
|
// SQLite only locks during active transactions, an idle open connection does not block.
|
|
13
13
|
// - Rows are matched by Alias (schema id) / deterministic ea_guid, update-in-place only;
|
|
14
|
-
// existing t_diagramobjects/t_diagramlinks geometry
|
|
15
|
-
// missing members
|
|
14
|
+
// existing t_diagramobjects/t_diagramlinks geometry of STILL-VISIBLE members is never
|
|
15
|
+
// rewritten; the membership reconcile only INSERTs missing members and PRUNES stale
|
|
16
|
+
// schema-anchored members whose element/relationship is no longer in the view
|
|
17
|
+
// (pruneMembership, default true). Human-drawn (un-anchored) shapes are never pruned.
|
|
16
18
|
|
|
17
19
|
const { DatabaseSync } = require('node:sqlite');
|
|
18
20
|
const DEBUG = !!process.env.EA_QEA_DEBUG;
|
|
@@ -275,18 +277,36 @@ function ensureStyleToken(styleEx, keyValue) {
|
|
|
275
277
|
const token = String(keyValue).indexOf('=') >= 0 ? String(keyValue) + ';' : String(keyValue) + '=;';
|
|
276
278
|
return text ? token + text : token;
|
|
277
279
|
}
|
|
280
|
+
// Force a StyleEx token to a value (replaces an existing one, else prepends).
|
|
281
|
+
// Used for DLKO=1 (the "Freeze Visible" connector-display flag EA persists in the
|
|
282
|
+
// diagram's StyleEx): every projected diagram must default to Freeze Visible ON, even
|
|
283
|
+
// if EA previously stored DLKO=0 / dropped it after a human unchecked the box.
|
|
284
|
+
function setStyleToken(styleEx, key, value) {
|
|
285
|
+
const text = String(styleEx === null || styleEx === undefined ? '' : styleEx);
|
|
286
|
+
const re = new RegExp('(^|;|\\s)' + key + '=[^;]*', 'i');
|
|
287
|
+
if (re.test(text)) { return text.replace(re, (m, pre) => pre + key + '=' + value); }
|
|
288
|
+
const token = key + '=' + value + ';';
|
|
289
|
+
return text ? token + text : token;
|
|
290
|
+
}
|
|
278
291
|
|
|
279
292
|
// ---------------------------------------------------------------------------
|
|
280
293
|
// Core sync
|
|
281
294
|
// ---------------------------------------------------------------------------
|
|
282
|
-
// opts: { dryRun, allowDelete, snapshotDir }
|
|
295
|
+
// opts: { dryRun, allowDelete, snapshotDir, pruneMembership }
|
|
296
|
+
// pruneMembership (default true): the incremental membership reconcile is
|
|
297
|
+
// authoritative for schema-anchored members — a shape/connector whose canonical
|
|
298
|
+
// element/relationship is no longer in the view's included_elements/included_relationships
|
|
299
|
+
// is pruned from that diagram. Human-drawn shapes (no schema_id anchor) are never
|
|
300
|
+
// pruned, and still-in-view members keep their geometry untouched.
|
|
283
301
|
function syncGraphToQea(graph, qeaPath, opts) {
|
|
284
302
|
const o = opts || {};
|
|
303
|
+
const pruneMembership = o.pruneMembership !== false;
|
|
285
304
|
const stages = {};
|
|
286
305
|
const stats = {
|
|
287
306
|
added: { elements: 0, relationships: 0, diagrams: 0, diagramObjects: 0, diagramLinks: 0 },
|
|
288
307
|
updated: { elements: 0, relationships: 0, diagrams: 0 },
|
|
289
308
|
skipped: { elements: 0, relationships: 0, diagrams: 0 },
|
|
309
|
+
removed: { diagramObjects: 0, diagramLinks: 0 },
|
|
290
310
|
deleteCandidates: 0, deleted: 0,
|
|
291
311
|
};
|
|
292
312
|
const t0 = nowMs();
|
|
@@ -439,6 +459,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
439
459
|
const existingRels = db.prepare('SELECT Connector_ID, ea_guid, Name, Connector_Type, Stereotype, Notes, Direction, Start_Object_ID, End_Object_ID FROM t_connector').all();
|
|
440
460
|
for (const r of existingRels) { if (r.ea_guid) { relByGuid.set(String(r.ea_guid), r); } }
|
|
441
461
|
const newRels = [];
|
|
462
|
+
const relAliasToId = new Map();
|
|
442
463
|
for (const rel of graph.relationships || []) {
|
|
443
464
|
if (!rel || rel.id === undefined || rel.id === null) { continue; }
|
|
444
465
|
const alias = String(rel.id);
|
|
@@ -464,6 +485,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
464
485
|
End_Object_ID: Number(end),
|
|
465
486
|
};
|
|
466
487
|
if (existing) {
|
|
488
|
+
relAliasToId.set(alias, Number(existing.Connector_ID));
|
|
467
489
|
const changed = intended.Name !== (existing.Name || '') || (intended.Connector_Type || '') !== (existing.Connector_Type || '') ||
|
|
468
490
|
(intended.Stereotype || '') !== (existing.Stereotype || '') || intended.Notes !== (existing.Notes || '') ||
|
|
469
491
|
intended.Direction !== (existing.Direction || '') || Number(intended.Start_Object_ID) !== Number(existing.Start_Object_ID || 0) ||
|
|
@@ -477,7 +499,6 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
477
499
|
stats.added.relationships++;
|
|
478
500
|
}
|
|
479
501
|
}
|
|
480
|
-
const relAliasToId = new Map();
|
|
481
502
|
if (!o.dryRun) {
|
|
482
503
|
if (newRels.length > 0) {
|
|
483
504
|
insertMany(db, 't_connector', ['Name', 'Connector_Type', 'Stereotype', 'Notes', 'Direction', 'Start_Object_ID', 'End_Object_ID', 'ea_guid'], newRels);
|
|
@@ -530,7 +551,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
530
551
|
for (const view of graph.views || []) {
|
|
531
552
|
if (!view || view.view_id === undefined || view.view_id === null) { continue; }
|
|
532
553
|
const viewId = String(view.view_id);
|
|
533
|
-
const styleEx = 'schema_view_id=' + viewId + ';';
|
|
554
|
+
const styleEx = 'schema_view_id=' + viewId + ';DLKO=1;';
|
|
534
555
|
const parentObjectId = (function () {
|
|
535
556
|
if (view.parent_element_id !== undefined && view.parent_element_id !== null && view.parent_element_id !== '') {
|
|
536
557
|
const pid = elemIdByAliasAll.get(String(view.parent_element_id));
|
|
@@ -550,8 +571,12 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
550
571
|
if (existing) {
|
|
551
572
|
diagViewRows.set(viewId, existing);
|
|
552
573
|
// EA may have rewritten StyleEx and dropped the anchor — re-inject it while
|
|
553
|
-
// preserving EA's own formatting tokens so identity stays discoverable.
|
|
554
|
-
|
|
574
|
+
// preserving EA's own formatting tokens so identity stays discoverable. Also
|
|
575
|
+
// force DLKO=1 so every diagram defaults to "Freeze Visible" checked.
|
|
576
|
+
const anchoredStyleEx = ensureStyleToken(
|
|
577
|
+
setStyleToken(existing.StyleEx, 'DLKO', '1'),
|
|
578
|
+
'schema_view_id=' + viewId
|
|
579
|
+
);
|
|
555
580
|
const changed = intended.Name !== (existing.Name || '') || (existing.StyleEx || '') !== anchoredStyleEx;
|
|
556
581
|
if (DEBUG && changed) { console.error('DEBUG diagram chg', viewId, JSON.stringify({n:[intended.Name,(existing.Name||'')], style: !!parseStyleToken(existing.StyleEx,'schema_view_id')})); }
|
|
557
582
|
if (changed && !o.dryRun) {
|
|
@@ -593,18 +618,43 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
593
618
|
if (view && view.view_id !== undefined && view.view_id !== null) { upsertMeta(db, 'view', view.view_id, view); }
|
|
594
619
|
}
|
|
595
620
|
}
|
|
596
|
-
// memberships:
|
|
621
|
+
// memberships: insert missing members, and prune stale schema-anchored members
|
|
622
|
+
// whose element/relationship is no longer in the view (authoritative canonical
|
|
623
|
+
// membership reconcile). Still-in-view members keep their geometry untouched;
|
|
624
|
+
// human-drawn shapes (no schema anchor) are never pruned.
|
|
625
|
+
const canonicalElemObjIds = new Set(
|
|
626
|
+
[...elemIdByAliasAll.values()].filter((v) => v >= 0).map((v) => Number(v))
|
|
627
|
+
);
|
|
628
|
+
const canonicalRelCids = new Set(
|
|
629
|
+
[...relAliasToId.values()].filter((v) => v >= 0).map((v) => Number(v))
|
|
630
|
+
);
|
|
597
631
|
for (const view of graph.views || []) {
|
|
598
632
|
if (!view || view.view_id === undefined || view.view_id === null) { continue; }
|
|
599
633
|
const viewId = String(view.view_id);
|
|
600
634
|
const diagramId = diagIdForView(viewId);
|
|
601
635
|
if (diagramId === null) { continue; }
|
|
636
|
+
const incl = view.included_elements || [];
|
|
602
637
|
const placedObjs = new Set();
|
|
603
638
|
const objs = db.prepare('SELECT Object_ID FROM t_diagramobjects WHERE Diagram_ID=?').all(diagramId);
|
|
604
639
|
for (const r of objs) { placedObjs.add(Number(r.Object_ID)); }
|
|
640
|
+
|
|
641
|
+
if (pruneMembership) {
|
|
642
|
+
const wantedObjs = new Set();
|
|
643
|
+
for (const elId of incl) {
|
|
644
|
+
const oid = elemIdByAliasAll.get(String(elId));
|
|
645
|
+
if (oid !== undefined) { wantedObjs.add(Number(oid)); }
|
|
646
|
+
}
|
|
647
|
+
const stale = [...placedObjs].filter((oid) => !wantedObjs.has(oid) && canonicalElemObjIds.has(oid));
|
|
648
|
+
if (stale.length > 0 && !o.dryRun) {
|
|
649
|
+
const del = db.prepare('DELETE FROM t_diagramobjects WHERE Diagram_ID=? AND Object_ID=?');
|
|
650
|
+
for (const oid of stale) { del.run(diagramId, oid); }
|
|
651
|
+
}
|
|
652
|
+
stats.removed.diagramObjects += stale.length;
|
|
653
|
+
for (const oid of stale) { placedObjs.delete(oid); }
|
|
654
|
+
}
|
|
655
|
+
|
|
605
656
|
const nextSeq = objs.length;
|
|
606
657
|
const newObjs = [];
|
|
607
|
-
const incl = view.included_elements || [];
|
|
608
658
|
let seq = nextSeq;
|
|
609
659
|
for (const elId of incl) {
|
|
610
660
|
const oid = elemIdByAliasAll.get(String(elId));
|
|
@@ -625,11 +675,28 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
625
675
|
}
|
|
626
676
|
stats.added.diagramObjects += newObjs.length;
|
|
627
677
|
|
|
678
|
+
const relIncl = view.included_relationships || [];
|
|
628
679
|
const placedLinks = new Set();
|
|
629
680
|
const links = db.prepare('SELECT ConnectorID FROM t_diagramlinks WHERE DiagramID=?').all(diagramId);
|
|
630
681
|
for (const r of links) { placedLinks.add(Number(r.ConnectorID)); }
|
|
682
|
+
|
|
683
|
+
if (pruneMembership) {
|
|
684
|
+
const wantedRelCids = new Set();
|
|
685
|
+
for (const relId of relIncl) {
|
|
686
|
+
const cid = relAliasToId.get(String(relId));
|
|
687
|
+
if (cid !== undefined && cid >= 0) { wantedRelCids.add(Number(cid)); }
|
|
688
|
+
}
|
|
689
|
+
const staleLinks = [...placedLinks].filter((cid) => !wantedRelCids.has(cid) && canonicalRelCids.has(cid));
|
|
690
|
+
if (staleLinks.length > 0 && !o.dryRun) {
|
|
691
|
+
const del = db.prepare('DELETE FROM t_diagramlinks WHERE DiagramID=? AND ConnectorID=?');
|
|
692
|
+
for (const cid of staleLinks) { del.run(diagramId, cid); }
|
|
693
|
+
}
|
|
694
|
+
stats.removed.diagramLinks += staleLinks.length;
|
|
695
|
+
for (const cid of staleLinks) { placedLinks.delete(cid); }
|
|
696
|
+
}
|
|
697
|
+
|
|
631
698
|
const newLinks = [];
|
|
632
|
-
for (const relId of
|
|
699
|
+
for (const relId of relIncl) {
|
|
633
700
|
const cid = relAliasToId.get(String(relId));
|
|
634
701
|
if (cid === undefined || cid < 0) { continue; }
|
|
635
702
|
if (placedLinks.has(Number(cid))) { continue; }
|
|
@@ -17,7 +17,7 @@ const fs = require('node:fs');
|
|
|
17
17
|
const lib = require(path.join(__dirname, 'ea-qea-sync-lib.js'));
|
|
18
18
|
|
|
19
19
|
function parseArgs(argv) {
|
|
20
|
-
const args = { mode: 'sync', graph: '', qea: '', allowDelete: false, deleteConfirmFile: '', dryRun: false, snapshotDir: '', out: '', noBackup: false, intervalMs: 2000 };
|
|
20
|
+
const args = { mode: 'sync', graph: '', qea: '', allowDelete: false, deleteConfirmFile: '', dryRun: false, snapshotDir: '', out: '', noBackup: false, intervalMs: 2000, pruneMembership: true };
|
|
21
21
|
for (let i = 0; i < argv.length; i++) {
|
|
22
22
|
const a = argv[i];
|
|
23
23
|
const next = () => (i + 1 < argv.length ? argv[++i] : '');
|
|
@@ -30,6 +30,7 @@ function parseArgs(argv) {
|
|
|
30
30
|
else if (a === '--snapshot-dir') { args.snapshotDir = next(); }
|
|
31
31
|
else if (a === '--out') { args.out = next(); }
|
|
32
32
|
else if (a === '--no-backup') { args.noBackup = true; }
|
|
33
|
+
else if (a === '--no-prune') { args.pruneMembership = false; }
|
|
33
34
|
else if (a === '--interval') { args.intervalMs = Number(next()) || 2000; }
|
|
34
35
|
else if (a.startsWith('-')) { /* ignore unknown */ }
|
|
35
36
|
else if (args.modeSet === undefined) { /* positional not used */ }
|
|
@@ -98,7 +99,7 @@ function runOnce(args, graphPath, qeaPath) {
|
|
|
98
99
|
}
|
|
99
100
|
const res = args.mode === 'full'
|
|
100
101
|
? lib.fullProjection(graph, qeaPath, { dryRun: args.dryRun })
|
|
101
|
-
: lib.syncGraphToQea(graph, qeaPath, { dryRun: args.dryRun, allowDelete: confirmDelete(args) });
|
|
102
|
+
: lib.syncGraphToQea(graph, qeaPath, { dryRun: args.dryRun, allowDelete: confirmDelete(args), pruneMembership: args.pruneMembership });
|
|
102
103
|
const mode = args.dryRun ? 'dry-run' : 'sync';
|
|
103
104
|
console.log(JSON.stringify({ mode, graph: graphPath, qea: qeaPath, snapshot, result: res }, null, 2));
|
|
104
105
|
}
|
package/package.json
CHANGED