archgraph-argo 0.14.0 → 0.15.1
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;
|
|
@@ -20,6 +22,10 @@ const crypto = require('node:crypto');
|
|
|
20
22
|
|
|
21
23
|
const SYNC_PACKAGE_NAME = 'ArchGraph Sync';
|
|
22
24
|
const DIAGRAM_TYPE = 'Logical';
|
|
25
|
+
// Attributes/Operations "Show Compartments" (Elements tab) default unchecked: EA stores
|
|
26
|
+
// these in t_diagram.PDATA as HideAtts / HideOps (hide compartment = 1). Projected
|
|
27
|
+
// diagrams hide both compartments by default while preserving EA's other PDATA tokens.
|
|
28
|
+
const DIAGRAM_DISPLAY_DEFAULT = 'HideAtts=1;HideOps=1;';
|
|
23
29
|
const META_TABLE = 'kg_sync_meta'; // {kind,key,sha,payload} — Node export/reconcile store
|
|
24
30
|
const BUSY_TIMEOUT_MS = 15000;
|
|
25
31
|
const CHUNK = 200;
|
|
@@ -290,14 +296,21 @@ function setStyleToken(styleEx, key, value) {
|
|
|
290
296
|
// ---------------------------------------------------------------------------
|
|
291
297
|
// Core sync
|
|
292
298
|
// ---------------------------------------------------------------------------
|
|
293
|
-
// opts: { dryRun, allowDelete, snapshotDir }
|
|
299
|
+
// opts: { dryRun, allowDelete, snapshotDir, pruneMembership }
|
|
300
|
+
// pruneMembership (default true): the incremental membership reconcile is
|
|
301
|
+
// authoritative for schema-anchored members — a shape/connector whose canonical
|
|
302
|
+
// element/relationship is no longer in the view's included_elements/included_relationships
|
|
303
|
+
// is pruned from that diagram. Human-drawn shapes (no schema_id anchor) are never
|
|
304
|
+
// pruned, and still-in-view members keep their geometry untouched.
|
|
294
305
|
function syncGraphToQea(graph, qeaPath, opts) {
|
|
295
306
|
const o = opts || {};
|
|
307
|
+
const pruneMembership = o.pruneMembership !== false;
|
|
296
308
|
const stages = {};
|
|
297
309
|
const stats = {
|
|
298
310
|
added: { elements: 0, relationships: 0, diagrams: 0, diagramObjects: 0, diagramLinks: 0 },
|
|
299
311
|
updated: { elements: 0, relationships: 0, diagrams: 0 },
|
|
300
312
|
skipped: { elements: 0, relationships: 0, diagrams: 0 },
|
|
313
|
+
removed: { diagramObjects: 0, diagramLinks: 0 },
|
|
301
314
|
deleteCandidates: 0, deleted: 0,
|
|
302
315
|
};
|
|
303
316
|
const t0 = nowMs();
|
|
@@ -450,6 +463,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
450
463
|
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
464
|
for (const r of existingRels) { if (r.ea_guid) { relByGuid.set(String(r.ea_guid), r); } }
|
|
452
465
|
const newRels = [];
|
|
466
|
+
const relAliasToId = new Map();
|
|
453
467
|
for (const rel of graph.relationships || []) {
|
|
454
468
|
if (!rel || rel.id === undefined || rel.id === null) { continue; }
|
|
455
469
|
const alias = String(rel.id);
|
|
@@ -475,6 +489,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
475
489
|
End_Object_ID: Number(end),
|
|
476
490
|
};
|
|
477
491
|
if (existing) {
|
|
492
|
+
relAliasToId.set(alias, Number(existing.Connector_ID));
|
|
478
493
|
const changed = intended.Name !== (existing.Name || '') || (intended.Connector_Type || '') !== (existing.Connector_Type || '') ||
|
|
479
494
|
(intended.Stereotype || '') !== (existing.Stereotype || '') || intended.Notes !== (existing.Notes || '') ||
|
|
480
495
|
intended.Direction !== (existing.Direction || '') || Number(intended.Start_Object_ID) !== Number(existing.Start_Object_ID || 0) ||
|
|
@@ -488,7 +503,6 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
488
503
|
stats.added.relationships++;
|
|
489
504
|
}
|
|
490
505
|
}
|
|
491
|
-
const relAliasToId = new Map();
|
|
492
506
|
if (!o.dryRun) {
|
|
493
507
|
if (newRels.length > 0) {
|
|
494
508
|
insertMany(db, 't_connector', ['Name', 'Connector_Type', 'Stereotype', 'Notes', 'Direction', 'Start_Object_ID', 'End_Object_ID', 'ea_guid'], newRels);
|
|
@@ -528,7 +542,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
528
542
|
// it touches an open project and DROPS unknown tokens like schema_view_id — if we
|
|
529
543
|
// only matched by the token we would re-INSERT the same deterministic ea_guid and
|
|
530
544
|
// crash on t_diagram's UNIQUE(ea_guid) (projection failure: Neo4j ok, EA stale).
|
|
531
|
-
const existingDiags = db.prepare('SELECT Diagram_ID, Package_ID, Name, StyleEx, ea_guid FROM t_diagram WHERE Package_ID=?').all(syncId);
|
|
545
|
+
const existingDiags = db.prepare('SELECT Diagram_ID, Package_ID, Name, StyleEx, PDATA, ea_guid FROM t_diagram WHERE Package_ID=?').all(syncId);
|
|
532
546
|
const diagByView = new Map();
|
|
533
547
|
const diagByGuid = new Map();
|
|
534
548
|
for (const d of existingDiags) {
|
|
@@ -557,6 +571,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
557
571
|
ParentID: parentObjectId,
|
|
558
572
|
Notes: '', // EA .qea 不保留多段 Notes;视图内容经 kg_sync_meta 保真
|
|
559
573
|
StyleEx: styleEx,
|
|
574
|
+
PDATA: DIAGRAM_DISPLAY_DEFAULT,
|
|
560
575
|
};
|
|
561
576
|
if (existing) {
|
|
562
577
|
diagViewRows.set(viewId, existing);
|
|
@@ -567,11 +582,17 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
567
582
|
setStyleToken(existing.StyleEx, 'DLKO', '1'),
|
|
568
583
|
'schema_view_id=' + viewId
|
|
569
584
|
);
|
|
570
|
-
|
|
585
|
+
// Default Attributes/Operations compartments unchecked: force HideAtts=1 /
|
|
586
|
+
// HideOps=1 into PDATA while preserving EA's other display tokens.
|
|
587
|
+
const anchoredPdata = setStyleToken(
|
|
588
|
+
setStyleToken(existing.PDATA, 'HideAtts', '1'),
|
|
589
|
+
'HideOps', '1'
|
|
590
|
+
);
|
|
591
|
+
const changed = intended.Name !== (existing.Name || '') || (existing.StyleEx || '') !== anchoredStyleEx || (existing.PDATA || '') !== anchoredPdata;
|
|
571
592
|
if (DEBUG && changed) { console.error('DEBUG diagram chg', viewId, JSON.stringify({n:[intended.Name,(existing.Name||'')], style: !!parseStyleToken(existing.StyleEx,'schema_view_id')})); }
|
|
572
593
|
if (changed && !o.dryRun) {
|
|
573
|
-
db.prepare('UPDATE t_diagram SET Name=?, StyleEx=? WHERE Diagram_ID=?')
|
|
574
|
-
.run(intended.Name, anchoredStyleEx, Number(existing.Diagram_ID));
|
|
594
|
+
db.prepare('UPDATE t_diagram SET Name=?, StyleEx=?, PDATA=? WHERE Diagram_ID=?')
|
|
595
|
+
.run(intended.Name, anchoredStyleEx, anchoredPdata, Number(existing.Diagram_ID));
|
|
575
596
|
}
|
|
576
597
|
stats[changed ? 'updated' : 'skipped'].diagrams++;
|
|
577
598
|
} else {
|
|
@@ -582,7 +603,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
582
603
|
const diagAliasToId = new Map();
|
|
583
604
|
if (!o.dryRun) {
|
|
584
605
|
if (newDiags.length > 0) {
|
|
585
|
-
insertMany(db, 't_diagram', ['Name', 'Diagram_Type', 'Package_ID', 'ParentID', 'StyleEx', 'ea_guid'], newDiags);
|
|
606
|
+
insertMany(db, 't_diagram', ['Name', 'Diagram_Type', 'Package_ID', 'ParentID', 'StyleEx', 'PDATA', 'ea_guid'], newDiags);
|
|
586
607
|
}
|
|
587
608
|
for (let i = 0; i < newDiags.length; i += 200) {
|
|
588
609
|
const part = newDiags.slice(i, i + 200);
|
|
@@ -608,18 +629,43 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
608
629
|
if (view && view.view_id !== undefined && view.view_id !== null) { upsertMeta(db, 'view', view.view_id, view); }
|
|
609
630
|
}
|
|
610
631
|
}
|
|
611
|
-
// memberships:
|
|
632
|
+
// memberships: insert missing members, and prune stale schema-anchored members
|
|
633
|
+
// whose element/relationship is no longer in the view (authoritative canonical
|
|
634
|
+
// membership reconcile). Still-in-view members keep their geometry untouched;
|
|
635
|
+
// human-drawn shapes (no schema anchor) are never pruned.
|
|
636
|
+
const canonicalElemObjIds = new Set(
|
|
637
|
+
[...elemIdByAliasAll.values()].filter((v) => v >= 0).map((v) => Number(v))
|
|
638
|
+
);
|
|
639
|
+
const canonicalRelCids = new Set(
|
|
640
|
+
[...relAliasToId.values()].filter((v) => v >= 0).map((v) => Number(v))
|
|
641
|
+
);
|
|
612
642
|
for (const view of graph.views || []) {
|
|
613
643
|
if (!view || view.view_id === undefined || view.view_id === null) { continue; }
|
|
614
644
|
const viewId = String(view.view_id);
|
|
615
645
|
const diagramId = diagIdForView(viewId);
|
|
616
646
|
if (diagramId === null) { continue; }
|
|
647
|
+
const incl = view.included_elements || [];
|
|
617
648
|
const placedObjs = new Set();
|
|
618
649
|
const objs = db.prepare('SELECT Object_ID FROM t_diagramobjects WHERE Diagram_ID=?').all(diagramId);
|
|
619
650
|
for (const r of objs) { placedObjs.add(Number(r.Object_ID)); }
|
|
651
|
+
|
|
652
|
+
if (pruneMembership) {
|
|
653
|
+
const wantedObjs = new Set();
|
|
654
|
+
for (const elId of incl) {
|
|
655
|
+
const oid = elemIdByAliasAll.get(String(elId));
|
|
656
|
+
if (oid !== undefined) { wantedObjs.add(Number(oid)); }
|
|
657
|
+
}
|
|
658
|
+
const stale = [...placedObjs].filter((oid) => !wantedObjs.has(oid) && canonicalElemObjIds.has(oid));
|
|
659
|
+
if (stale.length > 0 && !o.dryRun) {
|
|
660
|
+
const del = db.prepare('DELETE FROM t_diagramobjects WHERE Diagram_ID=? AND Object_ID=?');
|
|
661
|
+
for (const oid of stale) { del.run(diagramId, oid); }
|
|
662
|
+
}
|
|
663
|
+
stats.removed.diagramObjects += stale.length;
|
|
664
|
+
for (const oid of stale) { placedObjs.delete(oid); }
|
|
665
|
+
}
|
|
666
|
+
|
|
620
667
|
const nextSeq = objs.length;
|
|
621
668
|
const newObjs = [];
|
|
622
|
-
const incl = view.included_elements || [];
|
|
623
669
|
let seq = nextSeq;
|
|
624
670
|
for (const elId of incl) {
|
|
625
671
|
const oid = elemIdByAliasAll.get(String(elId));
|
|
@@ -640,11 +686,28 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
640
686
|
}
|
|
641
687
|
stats.added.diagramObjects += newObjs.length;
|
|
642
688
|
|
|
689
|
+
const relIncl = view.included_relationships || [];
|
|
643
690
|
const placedLinks = new Set();
|
|
644
691
|
const links = db.prepare('SELECT ConnectorID FROM t_diagramlinks WHERE DiagramID=?').all(diagramId);
|
|
645
692
|
for (const r of links) { placedLinks.add(Number(r.ConnectorID)); }
|
|
693
|
+
|
|
694
|
+
if (pruneMembership) {
|
|
695
|
+
const wantedRelCids = new Set();
|
|
696
|
+
for (const relId of relIncl) {
|
|
697
|
+
const cid = relAliasToId.get(String(relId));
|
|
698
|
+
if (cid !== undefined && cid >= 0) { wantedRelCids.add(Number(cid)); }
|
|
699
|
+
}
|
|
700
|
+
const staleLinks = [...placedLinks].filter((cid) => !wantedRelCids.has(cid) && canonicalRelCids.has(cid));
|
|
701
|
+
if (staleLinks.length > 0 && !o.dryRun) {
|
|
702
|
+
const del = db.prepare('DELETE FROM t_diagramlinks WHERE DiagramID=? AND ConnectorID=?');
|
|
703
|
+
for (const cid of staleLinks) { del.run(diagramId, cid); }
|
|
704
|
+
}
|
|
705
|
+
stats.removed.diagramLinks += staleLinks.length;
|
|
706
|
+
for (const cid of staleLinks) { placedLinks.delete(cid); }
|
|
707
|
+
}
|
|
708
|
+
|
|
646
709
|
const newLinks = [];
|
|
647
|
-
for (const relId of
|
|
710
|
+
for (const relId of relIncl) {
|
|
648
711
|
const cid = relAliasToId.get(String(relId));
|
|
649
712
|
if (cid === undefined || cid < 0) { continue; }
|
|
650
713
|
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