archgraph-argo 0.14.0 → 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;
|
|
@@ -290,14 +292,21 @@ function setStyleToken(styleEx, key, value) {
|
|
|
290
292
|
// ---------------------------------------------------------------------------
|
|
291
293
|
// Core sync
|
|
292
294
|
// ---------------------------------------------------------------------------
|
|
293
|
-
// 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.
|
|
294
301
|
function syncGraphToQea(graph, qeaPath, opts) {
|
|
295
302
|
const o = opts || {};
|
|
303
|
+
const pruneMembership = o.pruneMembership !== false;
|
|
296
304
|
const stages = {};
|
|
297
305
|
const stats = {
|
|
298
306
|
added: { elements: 0, relationships: 0, diagrams: 0, diagramObjects: 0, diagramLinks: 0 },
|
|
299
307
|
updated: { elements: 0, relationships: 0, diagrams: 0 },
|
|
300
308
|
skipped: { elements: 0, relationships: 0, diagrams: 0 },
|
|
309
|
+
removed: { diagramObjects: 0, diagramLinks: 0 },
|
|
301
310
|
deleteCandidates: 0, deleted: 0,
|
|
302
311
|
};
|
|
303
312
|
const t0 = nowMs();
|
|
@@ -450,6 +459,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
450
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();
|
|
451
460
|
for (const r of existingRels) { if (r.ea_guid) { relByGuid.set(String(r.ea_guid), r); } }
|
|
452
461
|
const newRels = [];
|
|
462
|
+
const relAliasToId = new Map();
|
|
453
463
|
for (const rel of graph.relationships || []) {
|
|
454
464
|
if (!rel || rel.id === undefined || rel.id === null) { continue; }
|
|
455
465
|
const alias = String(rel.id);
|
|
@@ -475,6 +485,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
475
485
|
End_Object_ID: Number(end),
|
|
476
486
|
};
|
|
477
487
|
if (existing) {
|
|
488
|
+
relAliasToId.set(alias, Number(existing.Connector_ID));
|
|
478
489
|
const changed = intended.Name !== (existing.Name || '') || (intended.Connector_Type || '') !== (existing.Connector_Type || '') ||
|
|
479
490
|
(intended.Stereotype || '') !== (existing.Stereotype || '') || intended.Notes !== (existing.Notes || '') ||
|
|
480
491
|
intended.Direction !== (existing.Direction || '') || Number(intended.Start_Object_ID) !== Number(existing.Start_Object_ID || 0) ||
|
|
@@ -488,7 +499,6 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
488
499
|
stats.added.relationships++;
|
|
489
500
|
}
|
|
490
501
|
}
|
|
491
|
-
const relAliasToId = new Map();
|
|
492
502
|
if (!o.dryRun) {
|
|
493
503
|
if (newRels.length > 0) {
|
|
494
504
|
insertMany(db, 't_connector', ['Name', 'Connector_Type', 'Stereotype', 'Notes', 'Direction', 'Start_Object_ID', 'End_Object_ID', 'ea_guid'], newRels);
|
|
@@ -608,18 +618,43 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
608
618
|
if (view && view.view_id !== undefined && view.view_id !== null) { upsertMeta(db, 'view', view.view_id, view); }
|
|
609
619
|
}
|
|
610
620
|
}
|
|
611
|
-
// 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
|
+
);
|
|
612
631
|
for (const view of graph.views || []) {
|
|
613
632
|
if (!view || view.view_id === undefined || view.view_id === null) { continue; }
|
|
614
633
|
const viewId = String(view.view_id);
|
|
615
634
|
const diagramId = diagIdForView(viewId);
|
|
616
635
|
if (diagramId === null) { continue; }
|
|
636
|
+
const incl = view.included_elements || [];
|
|
617
637
|
const placedObjs = new Set();
|
|
618
638
|
const objs = db.prepare('SELECT Object_ID FROM t_diagramobjects WHERE Diagram_ID=?').all(diagramId);
|
|
619
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
|
+
|
|
620
656
|
const nextSeq = objs.length;
|
|
621
657
|
const newObjs = [];
|
|
622
|
-
const incl = view.included_elements || [];
|
|
623
658
|
let seq = nextSeq;
|
|
624
659
|
for (const elId of incl) {
|
|
625
660
|
const oid = elemIdByAliasAll.get(String(elId));
|
|
@@ -640,11 +675,28 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
640
675
|
}
|
|
641
676
|
stats.added.diagramObjects += newObjs.length;
|
|
642
677
|
|
|
678
|
+
const relIncl = view.included_relationships || [];
|
|
643
679
|
const placedLinks = new Set();
|
|
644
680
|
const links = db.prepare('SELECT ConnectorID FROM t_diagramlinks WHERE DiagramID=?').all(diagramId);
|
|
645
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
|
+
|
|
646
698
|
const newLinks = [];
|
|
647
|
-
for (const relId of
|
|
699
|
+
for (const relId of relIncl) {
|
|
648
700
|
const cid = relAliasToId.get(String(relId));
|
|
649
701
|
if (cid === undefined || cid < 0) { continue; }
|
|
650
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