object-diagram-js-differ 1.0.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.
package/.eslintignore ADDED
@@ -0,0 +1 @@
1
+ dist
package/.eslintrc ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "plugin:bpmn-io/recommended"
3
+ }
@@ -0,0 +1,32 @@
1
+ name: CI
2
+ on: [ push, pull_request ]
3
+ jobs:
4
+ Build:
5
+
6
+ strategy:
7
+ matrix:
8
+ os: [ ubuntu-latest ]
9
+ node-version: [ 20 ]
10
+
11
+ runs-on: ${{ matrix.os }}
12
+
13
+ steps:
14
+ - name: Checkout
15
+ uses: actions/checkout@v4
16
+ - name: Use Node.js
17
+ uses: actions/setup-node@v4
18
+ with:
19
+ node-version: ${{ matrix.node-version }}
20
+ - name: Cache Node.js modules
21
+ uses: actions/cache@v3
22
+ with:
23
+ # npm cache files are stored in `~/.npm` on Linux/macOS
24
+ path: ~/.npm
25
+ key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
26
+ restore-keys: |
27
+ ${{ runner.OS }}-node-
28
+ ${{ runner.OS }}-
29
+ - name: Install dependencies
30
+ run: npm ci
31
+ - name: Build
32
+ run: npm run all
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ Diffing for object diagrams adapted from bpmn-js-differ.
2
+
3
+ ## License
4
+
5
+ MIT
@@ -0,0 +1,87 @@
1
+ /* eslint no-cond-assign: 0 */
2
+
3
+ function is(element, type) {
4
+ return element.$instanceOf(type);
5
+ }
6
+
7
+ function isAny(element, types) {
8
+ return types.some(function(t) {
9
+ return is(element, t);
10
+ });
11
+ }
12
+
13
+ function isTracked(element) {
14
+
15
+ const track = isAny(element, [
16
+ 'od:OdBoard',
17
+ 'od:Object',
18
+ 'od:Link',
19
+ ]);
20
+
21
+ if (track) {
22
+ return {
23
+ element: element,
24
+ property: ''
25
+ };
26
+ }
27
+ }
28
+
29
+ export default function ChangeHandler() {
30
+ this._layoutChanged = {};
31
+ this._changed = {};
32
+ this._removed = {};
33
+ this._added = {};
34
+ }
35
+
36
+
37
+ ChangeHandler.prototype.removed = function(model, property, element, idx) {
38
+
39
+ let tracked;
40
+
41
+ if (tracked = isTracked(element)) {
42
+ if (!this._removed[tracked.element.id]) {
43
+ this._removed[tracked.element.id] = element;
44
+ }
45
+ } else
46
+
47
+ if (tracked = isTracked(model)) {
48
+ this.changed(tracked.element, tracked.property + property + '[' + idx + ']', null, element);
49
+ }
50
+ };
51
+
52
+ ChangeHandler.prototype.changed = function(model, property, newValue, oldValue) {
53
+
54
+ let tracked;
55
+
56
+ if (tracked = isTracked(model)) {
57
+ let changed = this._changed[tracked.element.id];
58
+
59
+ if (!changed) {
60
+ changed = this._changed[tracked.element.id] = { model: model, attrs: { } };
61
+ }
62
+
63
+ if (oldValue !== undefined || newValue !== undefined) {
64
+ changed.attrs[property] = { oldValue: oldValue, newValue: newValue };
65
+ }
66
+ }
67
+ };
68
+
69
+ ChangeHandler.prototype.added = function(model, property, element, idx) {
70
+
71
+ let tracked;
72
+
73
+ if (tracked = isTracked(element)) {
74
+ if (!this._added[tracked.element.id]) {
75
+ this._added[tracked.element.id] = element;
76
+ }
77
+ } else
78
+
79
+ if (tracked = isTracked(model)) {
80
+ this.changed(tracked.element, tracked.property + property + '[' + idx + ']', element, null);
81
+ }
82
+ };
83
+
84
+ ChangeHandler.prototype.moved = function(model, property, oldIndex, newIndex) {
85
+
86
+ // noop
87
+ };
package/lib/diff.js ADDED
@@ -0,0 +1,5 @@
1
+ import Differ from './differ.js';
2
+
3
+ export default function diff(a, b, handler) {
4
+ return new Differ().diff(a, b, handler);
5
+ }
package/lib/differ.js ADDED
@@ -0,0 +1,102 @@
1
+ import {
2
+ forEach,
3
+ reduce,
4
+ isArray
5
+ } from 'min-dash';
6
+
7
+ import {
8
+ DiffPatcher
9
+ } from 'jsondiffpatch';
10
+
11
+ import ChangeHandler from './change-handler.js';
12
+
13
+
14
+ export default function Differ() { }
15
+
16
+
17
+ Differ.prototype.createDiff = function(a, b) {
18
+
19
+ // create a configured instance, match objects by name
20
+ const diffpatcher = new DiffPatcher({
21
+ objectHash: function(obj) {
22
+ return obj.id || JSON.stringify(obj);
23
+ },
24
+ propertyFilter: function(name, context) {
25
+ return name !== '$instanceOf';
26
+ }
27
+ });
28
+
29
+ return diffpatcher.diff(a, b);
30
+ };
31
+
32
+
33
+ Differ.prototype.diff = function(a, b, handler) {
34
+
35
+ handler = handler || new ChangeHandler();
36
+
37
+ function walk(diff, model) {
38
+
39
+ forEach(diff, function(d, key) {
40
+
41
+ if (d._t !== 'a' && isArray(d)) {
42
+
43
+ // take into account that collection properties are lazily
44
+ // initialized; this means that adding to an empty collection
45
+ // looks like setting an undefined variable to []
46
+ //
47
+ // ensure we detect this case and change it to an array diff
48
+ if (isArray(d[0])) {
49
+
50
+ d = reduce(d[0], function(newDelta, element, idx) {
51
+ const prefix = d.length === 3 ? '_' : '';
52
+
53
+ newDelta[prefix + idx] = [ element ];
54
+
55
+ return newDelta;
56
+ }, { _t: 'a' });
57
+ }
58
+
59
+ }
60
+
61
+
62
+ // is array
63
+ if (d._t === 'a') {
64
+
65
+ forEach(d, function(val, idx) {
66
+
67
+ if (idx === '_t') {
68
+ return;
69
+ }
70
+
71
+ const removed = /^_/.test(idx),
72
+ added = !removed && isArray(val),
73
+ moved = removed && val[0] === '';
74
+
75
+ idx = parseInt(removed ? idx.slice(1) : idx, 10);
76
+
77
+ if (added || (removed && !moved)) {
78
+ handler[removed ? 'removed' : 'added'](model, key, val[0], idx);
79
+ } else
80
+ if (moved) {
81
+ handler.moved(model, key, val[1], val[2]);
82
+ } else {
83
+ walk(val, model[key][idx]);
84
+ }
85
+ });
86
+ } else {
87
+ if (isArray(d)) {
88
+ handler.changed(model, key, d[0], d[1]);
89
+ } else {
90
+ handler.changed(model, key);
91
+ walk(d, model[key]);
92
+ }
93
+ }
94
+ });
95
+ }
96
+
97
+ const diff = this.createDiff(a, b);
98
+
99
+ walk(diff, b, handler);
100
+
101
+ return handler;
102
+ };
package/lib/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export {
2
+ default as Differ
3
+ } from './differ.js';
4
+
5
+ export {
6
+ default as diff
7
+ } from './diff.js';
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "object-diagram-js-differ",
3
+ "version": "1.0.0",
4
+ "description": "A semantic diffing utility for object diagram files",
5
+ "scripts": {
6
+ "all": "run-s lint test",
7
+ "lint": "eslint .",
8
+ "lintFix": "eslint . --fix",
9
+ "test": "jasmine"
10
+ },
11
+ "type": "module",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/timKraeuter/object-diagram-js-differ"
15
+ },
16
+ "keywords": [
17
+ "object diagram",
18
+ "diff"
19
+ ],
20
+ "author": {
21
+ "name": "Tim Kräuter",
22
+ "url": "https://timkraeuter.com/"
23
+ },
24
+ "license": "MIT",
25
+ "devDependencies": {
26
+ "object-diagram-moddle": "^1.0.1",
27
+ "eslint": "^8.54.0",
28
+ "eslint-plugin-bpmn-io": "^1.0.0",
29
+ "npm-run-all": "^4.1.5",
30
+ "chai": "^4.3.10",
31
+ "jasmine": "^5.1.0"
32
+ },
33
+ "dependencies": {
34
+ "jsondiffpatch": "^0.5.0",
35
+ "min-dash": "^3.0.0"
36
+ }
37
+ }
package/spec/.eslintrc ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "env": {
3
+ "jasmine": true
4
+ },
5
+ "extends": "plugin:bpmn-io/recommended"
6
+ }
@@ -0,0 +1,88 @@
1
+ import {
2
+ expect
3
+ } from 'chai';
4
+
5
+ import {
6
+ readFileSync
7
+ } from 'fs';
8
+
9
+ import ODModdle from 'object-diagram-moddle';
10
+
11
+ import { Differ } from '../lib/index.js';
12
+
13
+ describe('diff', function() {
14
+
15
+ it('should discover add', async function() {
16
+
17
+ // given
18
+ const aDiagram = readFileSync('spec/fixtures/base.xml', 'utf-8');
19
+ const bDiagram = readFileSync('spec/fixtures/add.xml', 'utf-8');
20
+
21
+ // when
22
+ const results = await getDiff(aDiagram, bDiagram);
23
+
24
+ // then
25
+ expect(results._added).to.have.keys(
26
+ [ 'Object_2840', 'Link_Object_2669_to_Object_2840_type_components' ]);
27
+ expect(results._removed).to.eql({});
28
+ expect(results._layoutChanged).to.eql({});
29
+ expect(results._changed).to.eql({});
30
+
31
+ });
32
+
33
+ it('should discover remove', async function() {
34
+
35
+ // given
36
+ const aDiagram = readFileSync('spec/fixtures/base.xml', 'utf-8');
37
+ const bDiagram = readFileSync('spec/fixtures/remove.xml', 'utf-8');
38
+
39
+ // when
40
+ const results = await getDiff(aDiagram, bDiagram);
41
+
42
+ // then
43
+ expect(results._added).to.eql({});
44
+ expect(results._removed).to.have.keys(
45
+ [ 'Object_2839', 'Link_Object_2669_to_Object_2839_type_components' ]);
46
+ expect(results._layoutChanged).to.eql({});
47
+ expect(results._changed).to.eql({});
48
+
49
+ });
50
+
51
+ it('should discover change', async function() {
52
+
53
+ // given
54
+ const aDiagram = readFileSync('spec/fixtures/base.xml', 'utf-8');
55
+ const bDiagram = readFileSync('spec/fixtures/change.xml', 'utf-8');
56
+
57
+ // when
58
+ const results = await getDiff(aDiagram, bDiagram);
59
+
60
+ // then
61
+ expect(results._added).to.eql({});
62
+ expect(results._removed).to.eql({});
63
+ expect(results._layoutChanged).to.eql({});
64
+ expect(results._changed).to.have.keys([ 'Object_2839' ]);
65
+
66
+ expect(results._changed['Object_2839'].attrs).to.deep.eq({
67
+ name: { oldValue: '2:Component', newValue: '2:QuantifiedComponent' },
68
+ attributeValues: { oldValue: 'quantity=77', newValue: 'quantity=1' }
69
+
70
+ });
71
+ });
72
+ });
73
+
74
+ // helpers //////////////////
75
+
76
+ async function importDiagram(diagram) {
77
+ const moddle = new ODModdle();
78
+ const defs = await moddle.fromXML(diagram);
79
+ return defs.rootElement;
80
+ }
81
+
82
+ async function getDiff(a, b) {
83
+ const aDefs = await importDiagram(a);
84
+ const bDefs = await importDiagram(b);
85
+
86
+ return new Differ().diff(aDefs, bDefs);
87
+ }
88
+
@@ -0,0 +1,44 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <od:definitions xmlns:od="http://tk/schema/od" xmlns:odDi="http://tk/schema/odDi" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC">
3
+ <od:odBoard id="Board_debug">
4
+ <od:link name="components" id="Link_Object_2669_to_Object_2839_type_components" type="components" sourceRef="Object_2669" targetRef="Object_2839" />
5
+ <od:object name="folding_wall_table:Product" id="Object_2669" attributeValues="cost=5&#10;name=&#34;Folding wall table&#34;">
6
+ <od:links>Link_Object_2669_to_Object_2839_type_components</od:links>
7
+ <od:links>Link_Object_2669_to_Object_2840_type_components</od:links>
8
+ </od:object>
9
+ <od:object name="2:QuantifiedComponent" id="Object_2839" attributeValues="quantity=1" />
10
+ <od:object name="3:QuantifiedComponent" id="Object_2840" attributeValues="quantity=26" />
11
+ <od:link name="components" id="Link_Object_2669_to_Object_2840_type_components" type="components" sourceRef="Object_2669" targetRef="Object_2840" />
12
+ </od:odBoard>
13
+ <odDi:odRootBoard id="RootBoard_debug">
14
+ <odDi:odPlane id="Plane_debug" boardElement="Board_debug">
15
+ <odDi:link id="Link_Object_2669_to_Object_2840_type_components_di" boardElement="Link_Object_2669_to_Object_2840_type_components">
16
+ <odDi:waypoint x="521.9" y="198.60000000000002" />
17
+ <odDi:waypoint x="521.9" y="233.60000000000002" />
18
+ <odDi:waypoint x="435.5" y="233.60000000000002" />
19
+ <odDi:waypoint x="435.5" y="296.6" />
20
+ <odDi:odLabel>
21
+ <dc:Bounds x="450" y="240" width="82" height="18" />
22
+ </odDi:odLabel>
23
+ </odDi:link>
24
+ <odDi:link id="Link_Object_2669_to_Object_2839_type_components_di" boardElement="Link_Object_2669_to_Object_2839_type_components">
25
+ <odDi:waypoint x="477.7" y="198.60000000000002" />
26
+ <odDi:waypoint x="477.7" y="223.60000000000002" />
27
+ <odDi:waypoint x="218.5" y="223.60000000000002" />
28
+ <odDi:waypoint x="218.5" y="296.6" />
29
+ <odDi:odLabel>
30
+ <dc:Bounds x="307" y="199" width="82" height="18" />
31
+ </odDi:odLabel>
32
+ </odDi:link>
33
+ <odDi:odShape id="Object_2669_di" boardElement="Object_2669">
34
+ <dc:Bounds x="434" y="120" width="221" height="78" />
35
+ </odDi:odShape>
36
+ <odDi:odShape id="Object_2839_di" boardElement="Object_2839">
37
+ <dc:Bounds x="120" y="297" width="197" height="59" />
38
+ </odDi:odShape>
39
+ <odDi:odShape id="Object_2840_di" boardElement="Object_2840">
40
+ <dc:Bounds x="337" y="297" width="197" height="59" />
41
+ </odDi:odShape>
42
+ </odDi:odPlane>
43
+ </odDi:odRootBoard>
44
+ </od:definitions>
@@ -0,0 +1,29 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <od:definitions xmlns:od="http://tk/schema/od" xmlns:odDi="http://tk/schema/odDi" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC">
3
+ <od:odBoard id="Board_debug">
4
+ <od:link name="components" id="Link_Object_2669_to_Object_2839_type_components" type="components" sourceRef="Object_2669" targetRef="Object_2839" />
5
+ <od:object name="folding_wall_table:Product" id="Object_2669" attributeValues="cost=5&#10;name=&#34;Folding wall table&#34;">
6
+ <od:links>Link_Object_2669_to_Object_2839_type_components</od:links>
7
+ </od:object>
8
+ <od:object name="2:QuantifiedComponent" id="Object_2839" attributeValues="quantity=1" />
9
+ </od:odBoard>
10
+ <odDi:odRootBoard id="RootBoard_debug">
11
+ <odDi:odPlane id="Plane_debug" boardElement="Board_debug">
12
+ <odDi:link id="Link_Object_2669_to_Object_2839_type_components_di" boardElement="Link_Object_2669_to_Object_2839_type_components">
13
+ <odDi:waypoint x="477.7" y="198.60000000000002" />
14
+ <odDi:waypoint x="477.7" y="223.60000000000002" />
15
+ <odDi:waypoint x="218.5" y="223.60000000000002" />
16
+ <odDi:waypoint x="218.5" y="296.6" />
17
+ <odDi:odLabel>
18
+ <dc:Bounds x="307" y="199" width="82" height="18" />
19
+ </odDi:odLabel>
20
+ </odDi:link>
21
+ <odDi:odShape id="Object_2669_di" boardElement="Object_2669">
22
+ <dc:Bounds x="434" y="120" width="221" height="78" />
23
+ </odDi:odShape>
24
+ <odDi:odShape id="Object_2839_di" boardElement="Object_2839">
25
+ <dc:Bounds x="120" y="297" width="197" height="59" />
26
+ </odDi:odShape>
27
+ </odDi:odPlane>
28
+ </odDi:odRootBoard>
29
+ </od:definitions>
@@ -0,0 +1,29 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <od:definitions xmlns:od="http://tk/schema/od" xmlns:odDi="http://tk/schema/odDi" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC">
3
+ <od:odBoard id="Board_debug">
4
+ <od:object name="folding_wall_table:Product" id="Object_2669" attributeValues="cost=5&#10;name=&#34;Folding wall table&#34;">
5
+ <od:links>Link_Object_2669_to_Object_2839_type_components</od:links>
6
+ </od:object>
7
+ <od:object name="2:Component" id="Object_2839" attributeValues="quantity=77" />
8
+ <od:link name="components" id="Link_Object_2669_to_Object_2839_type_components" type="components" sourceRef="Object_2669" targetRef="Object_2839" />
9
+ </od:odBoard>
10
+ <odDi:odRootBoard id="RootBoard_debug">
11
+ <odDi:odPlane id="Plane_debug" boardElement="Board_debug">
12
+ <odDi:link id="Link_Object_2669_to_Object_2839_type_components_di" boardElement="Link_Object_2669_to_Object_2839_type_components">
13
+ <odDi:waypoint x="477.7" y="198.60000000000002" />
14
+ <odDi:waypoint x="477.7" y="223.60000000000002" />
15
+ <odDi:waypoint x="218.5" y="223.60000000000002" />
16
+ <odDi:waypoint x="218.5" y="296.6" />
17
+ <odDi:odLabel>
18
+ <dc:Bounds x="307" y="199" width="82" height="18" />
19
+ </odDi:odLabel>
20
+ </odDi:link>
21
+ <odDi:odShape id="Object_2669_di" boardElement="Object_2669">
22
+ <dc:Bounds x="434" y="120" width="221" height="78" />
23
+ </odDi:odShape>
24
+ <odDi:odShape id="Object_2839_di" boardElement="Object_2839">
25
+ <dc:Bounds x="120" y="297" width="197" height="59" />
26
+ </odDi:odShape>
27
+ </odDi:odPlane>
28
+ </odDi:odRootBoard>
29
+ </od:definitions>
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <od:definitions xmlns:od="http://tk/schema/od" xmlns:odDi="http://tk/schema/odDi" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC">
3
+ <od:odBoard id="Board_debug">
4
+ <od:object name="folding_wall_table:Product" id="Object_2669" attributeValues="cost=5&#10;name=&#34;Folding wall table&#34;" />
5
+ </od:odBoard>
6
+ <odDi:odRootBoard id="RootBoard_debug">
7
+ <odDi:odPlane id="Plane_debug" boardElement="Board_debug">
8
+ <odDi:odShape id="Object_2669_di" boardElement="Object_2669">
9
+ <dc:Bounds x="434" y="120" width="221" height="78" />
10
+ </odDi:odShape>
11
+ </odDi:odPlane>
12
+ </odDi:odRootBoard>
13
+ </od:definitions>
@@ -0,0 +1,13 @@
1
+ {
2
+ "spec_dir": "spec",
3
+ "spec_files": [
4
+ "**/*[sS]pec.?(m)js"
5
+ ],
6
+ "helpers": [
7
+ "helpers/**/*.?(m)js"
8
+ ],
9
+ "env": {
10
+ "stopSpecOnExpectationFailure": false,
11
+ "random": true
12
+ }
13
+ }