@eventcatalog/diff 0.1.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/COMPATIBILITY.md +257 -0
- package/LICENSE +31 -0
- package/README.md +210 -0
- package/dist/index.d.mts +244 -0
- package/dist/index.d.ts +244 -0
- package/dist/index.js +851 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +820 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +60 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
// src/diff.ts
|
|
2
|
+
import { resolve } from "@eventcatalog/sdk";
|
|
3
|
+
|
|
4
|
+
// src/utils/entities.ts
|
|
5
|
+
var indexEntities = (graph) => {
|
|
6
|
+
const byIdAndVersion = /* @__PURE__ */ new Map();
|
|
7
|
+
const firstById = /* @__PURE__ */ new Map();
|
|
8
|
+
for (const entity of graph.entities) {
|
|
9
|
+
byIdAndVersion.set(`${entity.id}@${entity.version ?? ""}`, entity);
|
|
10
|
+
if (!firstById.has(entity.id)) firstById.set(entity.id, entity);
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
find: (id, version) => version === null ? firstById.get(id) : byIdAndVersion.get(`${id}@${version}`)
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/edges.ts
|
|
18
|
+
var diffEdges = (a, b) => {
|
|
19
|
+
const before = byIdentity(a);
|
|
20
|
+
const after = byIdentity(b);
|
|
21
|
+
const entitiesA = indexEntities(a);
|
|
22
|
+
const entitiesB = indexEntities(b);
|
|
23
|
+
const added = [];
|
|
24
|
+
const removed = [];
|
|
25
|
+
for (const key of /* @__PURE__ */ new Set([...before.keys(), ...after.keys()])) {
|
|
26
|
+
const edgeA = before.get(key);
|
|
27
|
+
const edgeB = after.get(key);
|
|
28
|
+
if (!edgeA) added.push(toDiffEdge(entitiesB, edgeB));
|
|
29
|
+
else if (!edgeB) removed.push(toDiffEdge(entitiesA, edgeA));
|
|
30
|
+
else if (isResolved(edgeA) && !isResolved(edgeB)) removed.push(toDiffEdge(entitiesA, edgeA));
|
|
31
|
+
else if (!isResolved(edgeA) && isResolved(edgeB)) added.push(toDiffEdge(entitiesB, edgeB));
|
|
32
|
+
}
|
|
33
|
+
return { added: sort(added), removed: sort(removed) };
|
|
34
|
+
};
|
|
35
|
+
var identity = (edge) => `${edge.direction}|${edge.via ?? ""}|${edge.from}|${edge.to}`;
|
|
36
|
+
var isResolved = (edge) => edge.status === "resolved";
|
|
37
|
+
var byIdentity = (graph) => {
|
|
38
|
+
const map = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const edge of graph.edges) {
|
|
40
|
+
const existing = map.get(identity(edge));
|
|
41
|
+
if (!existing || !isResolved(existing) && isResolved(edge)) map.set(identity(edge), edge);
|
|
42
|
+
}
|
|
43
|
+
return map;
|
|
44
|
+
};
|
|
45
|
+
var toDiffEdge = (entities, edge) => ({
|
|
46
|
+
direction: edge.direction,
|
|
47
|
+
from: end(entities, edge.from, edge.fromVersion),
|
|
48
|
+
to: end(entities, edge.to, edge.resolved),
|
|
49
|
+
...edge.via !== void 0 ? { via: edge.via } : {}
|
|
50
|
+
});
|
|
51
|
+
var end = (entities, id, version) => {
|
|
52
|
+
const entity = entities.find(id, version);
|
|
53
|
+
return {
|
|
54
|
+
...entity ? { type: entity.type } : {},
|
|
55
|
+
id,
|
|
56
|
+
...version !== null ? { version } : {}
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
var sort = (edges) => [...edges].sort(
|
|
60
|
+
(left, right) => left.direction.localeCompare(right.direction) || left.from.id.localeCompare(right.from.id) || left.to.id.localeCompare(right.to.id) || (left.via ?? "").localeCompare(right.via ?? "")
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// src/impact.ts
|
|
64
|
+
var MESSAGE_TYPES = /* @__PURE__ */ new Set(["event", "command", "query"]);
|
|
65
|
+
var isMessageType = (type) => type !== void 0 && MESSAGE_TYPES.has(type);
|
|
66
|
+
var impactOf = ({ baseline, schemaChanges, resourcesRemoved, edgesRemoved }) => {
|
|
67
|
+
const graph = { edges: baseline.edges, entities: indexEntities(baseline) };
|
|
68
|
+
return [
|
|
69
|
+
...impactOfSchemaChanges(graph, schemaChanges),
|
|
70
|
+
...impactOfRemovedMessages(graph, resourcesRemoved),
|
|
71
|
+
...impactOfRemovedEdges(graph, edgesRemoved, resourcesRemoved)
|
|
72
|
+
];
|
|
73
|
+
};
|
|
74
|
+
var impactOfSchemaChanges = (baseline, schemaChanges) => schemaChanges.filter((change) => change.breaking === true && change.direction !== null).map((change) => ({
|
|
75
|
+
message: { type: change.message.type, id: change.message.id, version: change.message.version.a },
|
|
76
|
+
reason: "schema_breaking_change",
|
|
77
|
+
direction: change.direction,
|
|
78
|
+
producers: partiesOf(baseline, change.message.id, change.message.version.a, "sends"),
|
|
79
|
+
consumers: partiesOf(baseline, change.message.id, change.message.version.a, "receives")
|
|
80
|
+
}));
|
|
81
|
+
var impactOfRemovedMessages = (baseline, resourcesRemoved) => resourcesRemoved.filter((resource) => isMessageType(resource.type)).map((resource) => ({
|
|
82
|
+
message: { type: resource.type, id: resource.id, version: resource.version },
|
|
83
|
+
reason: "message_removed",
|
|
84
|
+
producers: partiesOf(baseline, resource.id, resource.version, "sends"),
|
|
85
|
+
consumers: partiesOf(baseline, resource.id, resource.version, "receives")
|
|
86
|
+
})).filter((impact) => impact.producers.length > 0 || impact.consumers.length > 0);
|
|
87
|
+
var impactOfRemovedEdges = (baseline, edgesRemoved, resourcesRemoved) => {
|
|
88
|
+
const removedMessages = new Set(resourcesRemoved.filter((r) => isMessageType(r.type)).map((r) => r.id));
|
|
89
|
+
const byMessage = /* @__PURE__ */ new Map();
|
|
90
|
+
for (const edge of edgesRemoved) {
|
|
91
|
+
if (edge.via !== void 0 || !isMessageType(edge.to.type) || removedMessages.has(edge.to.id)) continue;
|
|
92
|
+
if (edge.direction !== "sends" && edge.direction !== "receives") continue;
|
|
93
|
+
const reason = edge.direction === "receives" ? "consumer_removed" : "producer_removed";
|
|
94
|
+
const key = `${reason}|${edge.to.type}:${edge.to.id}`;
|
|
95
|
+
const impact = byMessage.get(key) ?? {
|
|
96
|
+
message: { type: edge.to.type, id: edge.to.id },
|
|
97
|
+
reason,
|
|
98
|
+
producers: [],
|
|
99
|
+
consumers: []
|
|
100
|
+
};
|
|
101
|
+
const party = baseline.entities.find(edge.from.id, edge.from.version ?? null);
|
|
102
|
+
if (party) (edge.direction === "receives" ? impact.consumers : impact.producers).push(toImpactedResource(party));
|
|
103
|
+
byMessage.set(key, impact);
|
|
104
|
+
}
|
|
105
|
+
return [...byMessage.values()].map((impact) => ({
|
|
106
|
+
...impact,
|
|
107
|
+
producers: sortParties(impact.producers),
|
|
108
|
+
consumers: sortParties(impact.consumers)
|
|
109
|
+
}));
|
|
110
|
+
};
|
|
111
|
+
var partiesOf = (graph, messageId, messageVersion, direction) => sortParties(
|
|
112
|
+
graph.edges.filter(
|
|
113
|
+
(edge) => edge.direction === direction && edge.via === void 0 && edge.to === messageId && pointsAt(edge, messageVersion)
|
|
114
|
+
).map((edge) => graph.entities.find(edge.from, edge.fromVersion)).filter((entity) => entity !== void 0).map(toImpactedResource)
|
|
115
|
+
);
|
|
116
|
+
var pointsAt = (edge, version) => edge.resolved === null || version === void 0 || edge.resolved === version;
|
|
117
|
+
var toImpactedResource = (entity) => ({
|
|
118
|
+
type: entity.type,
|
|
119
|
+
id: entity.id,
|
|
120
|
+
...entity.version !== void 0 ? { version: entity.version } : {},
|
|
121
|
+
...entity.owners !== void 0 ? { owners: entity.owners } : {}
|
|
122
|
+
});
|
|
123
|
+
var sortParties = (parties) => [...parties].sort((left, right) => left.id.localeCompare(right.id) || (left.version ?? "").localeCompare(right.version ?? ""));
|
|
124
|
+
|
|
125
|
+
// src/utils/versions.ts
|
|
126
|
+
import { coerce, compare, valid } from "semver";
|
|
127
|
+
var toComparable = (version) => valid(version) ?? coerce(version)?.version ?? void 0;
|
|
128
|
+
var compareVersions = (left, right) => {
|
|
129
|
+
const a = left ? toComparable(left) : void 0;
|
|
130
|
+
const b = right ? toComparable(right) : void 0;
|
|
131
|
+
if (a && b) return compare(a, b);
|
|
132
|
+
return (left ?? "").localeCompare(right ?? "");
|
|
133
|
+
};
|
|
134
|
+
var latest = (items) => items.reduce(
|
|
135
|
+
(best, item) => best && compareVersions(best.version, item.version) >= 0 ? best : item,
|
|
136
|
+
void 0
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// src/resources.ts
|
|
140
|
+
var diffResources = (a, b) => {
|
|
141
|
+
const before = byKey(a);
|
|
142
|
+
const after = byKey(b);
|
|
143
|
+
const added = [];
|
|
144
|
+
const removed = [];
|
|
145
|
+
const changed = [];
|
|
146
|
+
for (const key of /* @__PURE__ */ new Set([...before.keys(), ...after.keys()])) {
|
|
147
|
+
const versionsA = before.get(key) ?? [];
|
|
148
|
+
const versionsB = after.get(key) ?? [];
|
|
149
|
+
if (versionsA.length === 0) {
|
|
150
|
+
added.push(...versionsB.map(ref));
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (versionsB.length === 0) {
|
|
154
|
+
removed.push(...versionsA.map(ref));
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const latestA = latest(versionsA);
|
|
158
|
+
const latestB = latest(versionsB);
|
|
159
|
+
const fields = changedFields(latestA, latestB);
|
|
160
|
+
if (fields.length > 0) {
|
|
161
|
+
changed.push({ type: latestA.type, id: latestA.id, version: { a: latestA.version, b: latestB.version }, fields });
|
|
162
|
+
}
|
|
163
|
+
const has = (list, version) => list.some((resource) => resource.version === version);
|
|
164
|
+
for (const resource of versionsB) {
|
|
165
|
+
if (resource !== latestB && !has(versionsA, resource.version)) added.push(ref(resource));
|
|
166
|
+
}
|
|
167
|
+
for (const resource of versionsA) {
|
|
168
|
+
if (resource !== latestA && !has(versionsB, resource.version)) removed.push(ref(resource));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { added: sort2(added), removed: sort2(removed), changed: sort2(changed) };
|
|
172
|
+
};
|
|
173
|
+
var byKey = (index) => {
|
|
174
|
+
const map = /* @__PURE__ */ new Map();
|
|
175
|
+
for (const resource of index.resources) {
|
|
176
|
+
const key = `${resource.type}:${resource.id}`;
|
|
177
|
+
map.set(key, [...map.get(key) ?? [], resource]);
|
|
178
|
+
}
|
|
179
|
+
return map;
|
|
180
|
+
};
|
|
181
|
+
var ref = (resource) => ({
|
|
182
|
+
type: resource.type,
|
|
183
|
+
id: resource.id,
|
|
184
|
+
...resource.version !== void 0 ? { version: resource.version } : {}
|
|
185
|
+
});
|
|
186
|
+
var changedFields = (before, after) => {
|
|
187
|
+
const fields = [];
|
|
188
|
+
if (before.version !== after.version) fields.push("version");
|
|
189
|
+
if (before.name !== after.name) fields.push("name");
|
|
190
|
+
if (stable(before.owners) !== stable(after.owners)) fields.push("owners");
|
|
191
|
+
if (stable(before.deprecated) !== stable(after.deprecated)) fields.push("deprecated");
|
|
192
|
+
return fields;
|
|
193
|
+
};
|
|
194
|
+
var stable = (value) => JSON.stringify(value ?? null);
|
|
195
|
+
var sort2 = (items) => [...items].sort(
|
|
196
|
+
(left, right) => left.type.localeCompare(right.type) || left.id.localeCompare(right.id) || compareVersions(versionOf(left.version), versionOf(right.version))
|
|
197
|
+
);
|
|
198
|
+
var versionOf = (version) => typeof version === "object" ? version.a : version;
|
|
199
|
+
|
|
200
|
+
// src/json-schema/compare.ts
|
|
201
|
+
var compareJsonSchemas = (before, after) => {
|
|
202
|
+
const context = { rootBefore: before, rootAfter: after, changes: [], visitedRefs: /* @__PURE__ */ new Set() };
|
|
203
|
+
walk(before, after, "", context);
|
|
204
|
+
return context.changes;
|
|
205
|
+
};
|
|
206
|
+
var walk = (beforeInput, afterInput, pointerInput, context) => {
|
|
207
|
+
const { before, after, pointer: pointer2, skip } = dereference(beforeInput, afterInput, pointerInput, context);
|
|
208
|
+
if (skip) return;
|
|
209
|
+
if (typeof before === "boolean" || typeof after === "boolean") {
|
|
210
|
+
compareBooleanSchemas(before, after, pointer2, context);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
compareDeprecated(before, after, pointer2, context);
|
|
214
|
+
compareType(before, after, pointer2, context);
|
|
215
|
+
compareEnum(before, after, pointer2, context);
|
|
216
|
+
compareConstraints(before, after, pointer2, context);
|
|
217
|
+
compareProperties(before, after, pointer2, context);
|
|
218
|
+
compareRequired(before, after, pointer2, context);
|
|
219
|
+
compareAdditionalProperties(before, after, pointer2, context);
|
|
220
|
+
compareItems(before, after, pointer2, context);
|
|
221
|
+
compareUnion(before, after, pointer2, "oneOf", context);
|
|
222
|
+
compareUnion(before, after, pointer2, "anyOf", context);
|
|
223
|
+
compareAllOf(before, after, pointer2, context);
|
|
224
|
+
compareUnsupportedKeywords(before, after, pointer2, context);
|
|
225
|
+
};
|
|
226
|
+
var emit = (context, change) => {
|
|
227
|
+
context.changes.push(change);
|
|
228
|
+
};
|
|
229
|
+
var dereference = (before, after, pointer2, context) => {
|
|
230
|
+
const refBefore = refOf(before);
|
|
231
|
+
const refAfter = refOf(after);
|
|
232
|
+
if (refBefore === void 0 && refAfter === void 0) return { before, after, pointer: pointer2, skip: false };
|
|
233
|
+
if (refBefore !== void 0 && refAfter !== void 0) {
|
|
234
|
+
const key = `${refBefore}|${refAfter}`;
|
|
235
|
+
if (context.visitedRefs.has(key)) return { before, after, pointer: pointer2, skip: true };
|
|
236
|
+
context.visitedRefs.add(key);
|
|
237
|
+
return {
|
|
238
|
+
before: resolveRef(context.rootBefore, refBefore),
|
|
239
|
+
after: resolveRef(context.rootAfter, refAfter),
|
|
240
|
+
pointer: refAfter.slice(1),
|
|
241
|
+
skip: false
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
before: refBefore !== void 0 ? resolveRef(context.rootBefore, refBefore) : before,
|
|
246
|
+
after: refAfter !== void 0 ? resolveRef(context.rootAfter, refAfter) : after,
|
|
247
|
+
pointer: pointer2,
|
|
248
|
+
skip: false
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
var refOf = (schema) => isObjectSchema(schema) && typeof schema.$ref === "string" && schema.$ref.startsWith("#/") ? schema.$ref : void 0;
|
|
252
|
+
var MAX_REF_HOPS = 32;
|
|
253
|
+
var resolveRef = (root, ref2) => {
|
|
254
|
+
let target = resolvePointer(root, ref2);
|
|
255
|
+
for (let hop = 0; hop < MAX_REF_HOPS; hop++) {
|
|
256
|
+
const next = refOf(target);
|
|
257
|
+
if (next === void 0) return target;
|
|
258
|
+
target = resolvePointer(root, next);
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
};
|
|
262
|
+
var resolvePointer = (root, ref2) => {
|
|
263
|
+
let current = root;
|
|
264
|
+
for (const segment of ref2.slice(2).split("/")) {
|
|
265
|
+
if (!isObjectSchema(current)) return true;
|
|
266
|
+
current = current[unescapePointer(segment)];
|
|
267
|
+
}
|
|
268
|
+
return current === void 0 ? true : current;
|
|
269
|
+
};
|
|
270
|
+
var compareBooleanSchemas = (before, after, pointer2, context) => {
|
|
271
|
+
if (before === after) return;
|
|
272
|
+
const relaxed = before === false || after === true;
|
|
273
|
+
emit(context, { kind: relaxed ? "schema.relaxed" : "schema.restricted", path: pointer2, before, after });
|
|
274
|
+
};
|
|
275
|
+
var compareType = (before, after, pointer2, context) => {
|
|
276
|
+
const typesBefore = typesOf(before);
|
|
277
|
+
const typesAfter = typesOf(after);
|
|
278
|
+
const path = `${pointer2}/type`;
|
|
279
|
+
if (typesBefore === void 0 && typesAfter === void 0) return;
|
|
280
|
+
if (typesBefore === void 0) {
|
|
281
|
+
emit(context, { kind: "type.narrowed", path, before: "any", after: after.type });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (typesAfter === void 0) {
|
|
285
|
+
emit(context, { kind: "type.widened", path, before: before.type, after: "any" });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const afterCoversBefore = covers(typesAfter, typesBefore);
|
|
289
|
+
const beforeCoversAfter = covers(typesBefore, typesAfter);
|
|
290
|
+
if (afterCoversBefore && beforeCoversAfter) return;
|
|
291
|
+
const kind = afterCoversBefore ? "type.widened" : beforeCoversAfter ? "type.narrowed" : "type.changed";
|
|
292
|
+
emit(context, { kind, path, before: before.type, after: after.type });
|
|
293
|
+
};
|
|
294
|
+
var typesOf = (schema) => {
|
|
295
|
+
if (schema.type === void 0) return void 0;
|
|
296
|
+
return new Set(Array.isArray(schema.type) ? schema.type : [schema.type]);
|
|
297
|
+
};
|
|
298
|
+
var covers = (outer, inner) => [...inner].every((type) => outer.has(type) || type === "integer" && outer.has("number"));
|
|
299
|
+
var compareEnum = (before, after, pointer2, context) => {
|
|
300
|
+
const valuesBefore = enumOf(before);
|
|
301
|
+
const valuesAfter = enumOf(after);
|
|
302
|
+
const path = `${pointer2}/enum`;
|
|
303
|
+
if (valuesBefore === void 0 && valuesAfter === void 0) return;
|
|
304
|
+
if (valuesBefore === void 0) {
|
|
305
|
+
emit(context, { kind: "enum.added", path, after: valuesAfter });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (valuesAfter === void 0) {
|
|
309
|
+
emit(context, { kind: "enum.removed", path, before: valuesBefore });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const keysBefore = new Set(valuesBefore.map(stableStringify));
|
|
313
|
+
const keysAfter = new Set(valuesAfter.map(stableStringify));
|
|
314
|
+
for (const value of valuesAfter) {
|
|
315
|
+
if (!keysBefore.has(stableStringify(value))) emit(context, { kind: "enum.value.added", path, after: value });
|
|
316
|
+
}
|
|
317
|
+
for (const value of valuesBefore) {
|
|
318
|
+
if (!keysAfter.has(stableStringify(value))) emit(context, { kind: "enum.value.removed", path, before: value });
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
var enumOf = (schema) => {
|
|
322
|
+
if (Array.isArray(schema.enum)) return schema.enum;
|
|
323
|
+
if ("const" in schema) return [schema.const];
|
|
324
|
+
return void 0;
|
|
325
|
+
};
|
|
326
|
+
var MIN_CONSTRAINTS = ["minLength", "minimum", "exclusiveMinimum", "minItems", "minProperties"];
|
|
327
|
+
var MAX_CONSTRAINTS = ["maxLength", "maximum", "exclusiveMaximum", "maxItems", "maxProperties"];
|
|
328
|
+
var PRESENCE_CONSTRAINTS = ["pattern", "format", "multipleOf"];
|
|
329
|
+
var FLAG_CONSTRAINTS = ["uniqueItems"];
|
|
330
|
+
var compareConstraints = (before, after, pointer2, context) => {
|
|
331
|
+
for (const keyword of MIN_CONSTRAINTS) compareBound(before, after, pointer2, keyword, "higher-is-tighter", context);
|
|
332
|
+
for (const keyword of MAX_CONSTRAINTS) compareBound(before, after, pointer2, keyword, "lower-is-tighter", context);
|
|
333
|
+
for (const keyword of PRESENCE_CONSTRAINTS) comparePresence(before, after, pointer2, keyword, context);
|
|
334
|
+
for (const keyword of FLAG_CONSTRAINTS) compareFlag(before, after, pointer2, keyword, context);
|
|
335
|
+
};
|
|
336
|
+
var compareBound = (before, after, pointer2, keyword, mode, context) => {
|
|
337
|
+
const valueBefore = numberOrUndefined(before[keyword]);
|
|
338
|
+
const valueAfter = numberOrUndefined(after[keyword]);
|
|
339
|
+
if (valueBefore === valueAfter) return;
|
|
340
|
+
const path = `${pointer2}/${keyword}`;
|
|
341
|
+
const change = { path, keyword, before: valueBefore, after: valueAfter };
|
|
342
|
+
if (valueBefore === void 0) return emit(context, { kind: "constraint.tightened", ...change });
|
|
343
|
+
if (valueAfter === void 0) return emit(context, { kind: "constraint.loosened", ...change });
|
|
344
|
+
const tighter = mode === "higher-is-tighter" ? valueAfter > valueBefore : valueAfter < valueBefore;
|
|
345
|
+
emit(context, { kind: tighter ? "constraint.tightened" : "constraint.loosened", ...change });
|
|
346
|
+
};
|
|
347
|
+
var comparePresence = (before, after, pointer2, keyword, context) => {
|
|
348
|
+
const valueBefore = before[keyword];
|
|
349
|
+
const valueAfter = after[keyword];
|
|
350
|
+
if (stableStringify(valueBefore) === stableStringify(valueAfter)) return;
|
|
351
|
+
const change = { path: `${pointer2}/${keyword}`, keyword, before: valueBefore, after: valueAfter };
|
|
352
|
+
if (valueBefore === void 0) return emit(context, { kind: "constraint.tightened", ...change });
|
|
353
|
+
if (valueAfter === void 0) return emit(context, { kind: "constraint.loosened", ...change });
|
|
354
|
+
emit(context, { kind: "constraint.changed", ...change });
|
|
355
|
+
};
|
|
356
|
+
var compareFlag = (before, after, pointer2, keyword, context) => {
|
|
357
|
+
const flagBefore = before[keyword] === true;
|
|
358
|
+
const flagAfter = after[keyword] === true;
|
|
359
|
+
if (flagBefore === flagAfter) return;
|
|
360
|
+
emit(context, {
|
|
361
|
+
kind: flagAfter ? "constraint.tightened" : "constraint.loosened",
|
|
362
|
+
path: `${pointer2}/${keyword}`,
|
|
363
|
+
keyword,
|
|
364
|
+
before: flagBefore,
|
|
365
|
+
after: flagAfter
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
var compareProperties = (before, after, pointer2, context) => {
|
|
369
|
+
const propsBefore = propertiesOf(before);
|
|
370
|
+
const propsAfter = propertiesOf(after);
|
|
371
|
+
const names = /* @__PURE__ */ new Set([...Object.keys(propsBefore), ...Object.keys(propsAfter)]);
|
|
372
|
+
for (const name of names) {
|
|
373
|
+
const path = `${pointer2}/properties/${escapePointer(name)}`;
|
|
374
|
+
const inBefore = name in propsBefore;
|
|
375
|
+
const inAfter = name in propsAfter;
|
|
376
|
+
if (!inBefore && inAfter) {
|
|
377
|
+
const kind = isClosed(before) ? "property.added-to-closed-object" : "property.added";
|
|
378
|
+
emit(context, { kind, path, after: propsAfter[name] });
|
|
379
|
+
} else if (inBefore && !inAfter) {
|
|
380
|
+
const kind = isClosed(after) ? "property.removed-from-closed-object" : "property.removed";
|
|
381
|
+
emit(context, { kind, path, before: propsBefore[name] });
|
|
382
|
+
} else {
|
|
383
|
+
walk(propsBefore[name], propsAfter[name], path, context);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
var isClosed = (schema) => schema.additionalProperties === false;
|
|
388
|
+
var compareDeprecated = (before, after, pointer2, context) => {
|
|
389
|
+
if (before.deprecated !== true && after.deprecated === true) {
|
|
390
|
+
emit(context, { kind: "schema.deprecated", path: pointer2, before: false, after: true });
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
var compareRequired = (before, after, pointer2, context) => {
|
|
394
|
+
const requiredBefore = requiredOf(before);
|
|
395
|
+
const requiredAfter = requiredOf(after);
|
|
396
|
+
const propsAfter = propertiesOf(after);
|
|
397
|
+
for (const name of requiredAfter) {
|
|
398
|
+
if (requiredBefore.has(name)) continue;
|
|
399
|
+
const property = propsAfter[name];
|
|
400
|
+
const hasDefault = isObjectSchema(property) && property.default !== void 0;
|
|
401
|
+
emit(context, {
|
|
402
|
+
kind: hasDefault ? "required.added-with-default" : "required.added",
|
|
403
|
+
path: `${pointer2}/properties/${escapePointer(name)}`,
|
|
404
|
+
before: false,
|
|
405
|
+
after: true
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
for (const name of requiredBefore) {
|
|
409
|
+
if (!requiredAfter.has(name)) {
|
|
410
|
+
emit(context, {
|
|
411
|
+
kind: "required.removed",
|
|
412
|
+
path: `${pointer2}/properties/${escapePointer(name)}`,
|
|
413
|
+
before: true,
|
|
414
|
+
after: false
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
var propertiesOf = (schema) => isObjectSchema(schema.properties) ? schema.properties : {};
|
|
420
|
+
var requiredOf = (schema) => new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
421
|
+
var compareAdditionalProperties = (before, after, pointer2, context) => {
|
|
422
|
+
const apBefore = before.additionalProperties ?? true;
|
|
423
|
+
const apAfter = after.additionalProperties ?? true;
|
|
424
|
+
const path = `${pointer2}/additionalProperties`;
|
|
425
|
+
if (apBefore !== false && apAfter === false) return emit(context, { kind: "additionalProperties.closed", path });
|
|
426
|
+
if (apBefore === false && apAfter !== false) return emit(context, { kind: "additionalProperties.opened", path });
|
|
427
|
+
walk(apBefore, apAfter, path, context);
|
|
428
|
+
};
|
|
429
|
+
var compareItems = (before, after, pointer2, context) => {
|
|
430
|
+
const tupleBefore = tupleOf(before);
|
|
431
|
+
const tupleAfter = tupleOf(after);
|
|
432
|
+
if (tupleBefore || tupleAfter) {
|
|
433
|
+
const keyword = Array.isArray(after.items) || Array.isArray(before.items) ? "items" : "prefixItems";
|
|
434
|
+
const itemsBefore = tupleBefore ?? [];
|
|
435
|
+
const itemsAfter = tupleAfter ?? [];
|
|
436
|
+
const shared = Math.min(itemsBefore.length, itemsAfter.length);
|
|
437
|
+
for (let i = 0; i < shared; i++) walk(itemsBefore[i], itemsAfter[i], `${pointer2}/${keyword}/${i}`, context);
|
|
438
|
+
for (let i = shared; i < itemsAfter.length; i++) {
|
|
439
|
+
emit(context, { kind: "tuple.item.added", path: `${pointer2}/${keyword}/${i}`, after: itemsAfter[i] });
|
|
440
|
+
}
|
|
441
|
+
for (let i = shared; i < itemsBefore.length; i++) {
|
|
442
|
+
emit(context, { kind: "tuple.item.removed", path: `${pointer2}/${keyword}/${i}`, before: itemsBefore[i] });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const schemaBefore = itemsSchemaOf(before);
|
|
446
|
+
const schemaAfter = itemsSchemaOf(after);
|
|
447
|
+
if (schemaBefore === void 0 && schemaAfter === void 0) return;
|
|
448
|
+
walk(schemaBefore ?? true, schemaAfter ?? true, `${pointer2}/items`, context);
|
|
449
|
+
};
|
|
450
|
+
var tupleOf = (schema) => {
|
|
451
|
+
if (Array.isArray(schema.items)) return schema.items;
|
|
452
|
+
if (Array.isArray(schema.prefixItems)) return schema.prefixItems;
|
|
453
|
+
return void 0;
|
|
454
|
+
};
|
|
455
|
+
var itemsSchemaOf = (schema) => schema.items !== void 0 && !Array.isArray(schema.items) ? schema.items : void 0;
|
|
456
|
+
var compareUnion = (before, after, pointer2, keyword, context) => {
|
|
457
|
+
compareBranches(before, after, pointer2, keyword, "union.branch.added", "union.branch.removed", context);
|
|
458
|
+
};
|
|
459
|
+
var compareAllOf = (before, after, pointer2, context) => {
|
|
460
|
+
compareBranches(before, after, pointer2, "allOf", "allOf.branch.added", "allOf.branch.removed", context);
|
|
461
|
+
};
|
|
462
|
+
var compareBranches = (before, after, pointer2, keyword, addedKind, removedKind, context) => {
|
|
463
|
+
const branchesBefore = Array.isArray(before[keyword]) ? before[keyword] : void 0;
|
|
464
|
+
const branchesAfter = Array.isArray(after[keyword]) ? after[keyword] : void 0;
|
|
465
|
+
const path = `${pointer2}/${keyword}`;
|
|
466
|
+
if (branchesBefore === void 0 && branchesAfter === void 0) return;
|
|
467
|
+
if (branchesBefore === void 0) {
|
|
468
|
+
return emit(context, { kind: "constraint.tightened", path, keyword, after: branchesAfter });
|
|
469
|
+
}
|
|
470
|
+
if (branchesAfter === void 0) {
|
|
471
|
+
return emit(context, { kind: "constraint.loosened", path, keyword, before: branchesBefore });
|
|
472
|
+
}
|
|
473
|
+
const keysBefore = branchesBefore.map(stableStringify);
|
|
474
|
+
const keysAfter = branchesAfter.map(stableStringify);
|
|
475
|
+
const matchedAfter = /* @__PURE__ */ new Set();
|
|
476
|
+
const unmatchedBefore = [];
|
|
477
|
+
branchesBefore.forEach((_branch, i) => {
|
|
478
|
+
const j = keysAfter.findIndex((key, index) => key === keysBefore[i] && !matchedAfter.has(index));
|
|
479
|
+
if (j >= 0) matchedAfter.add(j);
|
|
480
|
+
else unmatchedBefore.push(i);
|
|
481
|
+
});
|
|
482
|
+
const unmatchedAfter = branchesAfter.map((_branch, j) => j).filter((j) => !matchedAfter.has(j));
|
|
483
|
+
if (unmatchedBefore.length === unmatchedAfter.length) {
|
|
484
|
+
unmatchedBefore.forEach((i, n) => {
|
|
485
|
+
const j = unmatchedAfter[n];
|
|
486
|
+
walk(branchesBefore[i], branchesAfter[j], `${path}/${j}`, context);
|
|
487
|
+
});
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
for (const j of unmatchedAfter) emit(context, { kind: addedKind, path: `${path}/${j}`, keyword, after: branchesAfter[j] });
|
|
491
|
+
for (const i of unmatchedBefore) emit(context, { kind: removedKind, path: `${path}/${i}`, keyword, before: branchesBefore[i] });
|
|
492
|
+
};
|
|
493
|
+
var UNSUPPORTED_KEYWORDS = [
|
|
494
|
+
// Any $ref still present here is external (local ones were resolved). We cannot see its target.
|
|
495
|
+
"$ref",
|
|
496
|
+
"patternProperties",
|
|
497
|
+
"propertyNames",
|
|
498
|
+
"not",
|
|
499
|
+
"if",
|
|
500
|
+
"then",
|
|
501
|
+
"else",
|
|
502
|
+
"dependentRequired",
|
|
503
|
+
"dependentSchemas",
|
|
504
|
+
"dependencies",
|
|
505
|
+
"contains",
|
|
506
|
+
"minContains",
|
|
507
|
+
"maxContains",
|
|
508
|
+
"unevaluatedProperties",
|
|
509
|
+
"unevaluatedItems",
|
|
510
|
+
"additionalItems"
|
|
511
|
+
];
|
|
512
|
+
var compareUnsupportedKeywords = (before, after, pointer2, context) => {
|
|
513
|
+
for (const keyword of UNSUPPORTED_KEYWORDS) {
|
|
514
|
+
if (stableStringify(before[keyword]) === stableStringify(after[keyword])) continue;
|
|
515
|
+
emit(context, {
|
|
516
|
+
kind: "keyword.changed",
|
|
517
|
+
path: `${pointer2}/${keyword}`,
|
|
518
|
+
keyword,
|
|
519
|
+
before: before[keyword],
|
|
520
|
+
after: after[keyword]
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
var isObjectSchema = (schema) => typeof schema === "object" && schema !== null;
|
|
525
|
+
var numberOrUndefined = (value) => typeof value === "number" ? value : void 0;
|
|
526
|
+
var stableStringify = (value) => JSON.stringify(
|
|
527
|
+
value,
|
|
528
|
+
(_key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item
|
|
529
|
+
) ?? "undefined";
|
|
530
|
+
var escapePointer = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
531
|
+
var unescapePointer = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
532
|
+
|
|
533
|
+
// src/json-schema/rules.ts
|
|
534
|
+
var WIDENS = { backward: false, forward: true };
|
|
535
|
+
var NARROWS = { backward: true, forward: false };
|
|
536
|
+
var INCOMPATIBLE = { backward: true, forward: true };
|
|
537
|
+
var SAFE = { backward: false, forward: false };
|
|
538
|
+
var RULES = {
|
|
539
|
+
// --- properties -----------------------------------------------------------
|
|
540
|
+
// Old messages simply don't have the property; new messages carry an extra one.
|
|
541
|
+
"property.added": { ...SAFE, reason: () => "property added" },
|
|
542
|
+
// The old schema was closed (additionalProperties: false), so an old reader rejects the new property.
|
|
543
|
+
"property.added-to-closed-object": {
|
|
544
|
+
...WIDENS,
|
|
545
|
+
reason: () => "property added to a closed object (old readers reject unknown properties)"
|
|
546
|
+
},
|
|
547
|
+
// Old messages still carry the property but the new schema ignores it; new messages lack
|
|
548
|
+
// a property the old schema knew about but did not require.
|
|
549
|
+
"property.removed": { ...SAFE, reason: () => "property removed" },
|
|
550
|
+
// The new schema is closed, so a new reader rejects old messages that still carry the property.
|
|
551
|
+
"property.removed-from-closed-object": {
|
|
552
|
+
...NARROWS,
|
|
553
|
+
reason: () => "property removed from a closed object (new readers reject old messages that still carry it)"
|
|
554
|
+
},
|
|
555
|
+
// Old messages were written without the property, so a new consumer that requires it cannot read them.
|
|
556
|
+
"required.added": { ...NARROWS, reason: () => "property became required" },
|
|
557
|
+
// The new consumer fills the gap with the default, so old messages still read.
|
|
558
|
+
"required.added-with-default": { ...SAFE, reason: () => "property became required but has a default" },
|
|
559
|
+
// New messages may omit the property, so an old consumer that requires it cannot read them.
|
|
560
|
+
"required.removed": { ...WIDENS, reason: () => "property is no longer required" },
|
|
561
|
+
// --- types ----------------------------------------------------------------
|
|
562
|
+
"type.widened": { ...WIDENS, reason: (c) => `type widened from ${show(c.before)} to ${show(c.after)}` },
|
|
563
|
+
"type.narrowed": { ...NARROWS, reason: (c) => `type narrowed from ${show(c.before)} to ${show(c.after)}` },
|
|
564
|
+
"type.changed": { ...INCOMPATIBLE, reason: (c) => `type changed from ${show(c.before)} to ${show(c.after)}` },
|
|
565
|
+
// --- enum and const -------------------------------------------------------
|
|
566
|
+
"enum.added": { ...NARROWS, reason: (c) => `values restricted to ${show(c.after)}` },
|
|
567
|
+
"enum.removed": { ...WIDENS, reason: () => "enum restriction removed" },
|
|
568
|
+
"enum.value.added": { ...WIDENS, reason: (c) => `enum value ${show(c.after)} added` },
|
|
569
|
+
"enum.value.removed": { ...NARROWS, reason: (c) => `enum value ${show(c.before)} removed` },
|
|
570
|
+
// --- constraints ----------------------------------------------------------
|
|
571
|
+
"constraint.tightened": {
|
|
572
|
+
...NARROWS,
|
|
573
|
+
reason: (c) => c.before === void 0 ? `${c.keyword} added (${show(c.after)})` : `${c.keyword} tightened from ${show(c.before)} to ${show(c.after)}`
|
|
574
|
+
},
|
|
575
|
+
"constraint.loosened": {
|
|
576
|
+
...WIDENS,
|
|
577
|
+
reason: (c) => c.after === void 0 ? `${c.keyword} removed` : `${c.keyword} loosened from ${show(c.before)} to ${show(c.after)}`
|
|
578
|
+
},
|
|
579
|
+
"constraint.changed": { ...INCOMPATIBLE, reason: (c) => `${c.keyword} changed from ${show(c.before)} to ${show(c.after)}` },
|
|
580
|
+
// --- content model --------------------------------------------------------
|
|
581
|
+
"additionalProperties.closed": { ...NARROWS, reason: () => "additional properties are no longer allowed" },
|
|
582
|
+
"additionalProperties.opened": { ...WIDENS, reason: () => "additional properties are now allowed" },
|
|
583
|
+
// --- arrays ---------------------------------------------------------------
|
|
584
|
+
"tuple.item.added": { ...NARROWS, reason: () => "tuple position is now constrained" },
|
|
585
|
+
"tuple.item.removed": { ...WIDENS, reason: () => "tuple position is no longer constrained" },
|
|
586
|
+
// --- composition ----------------------------------------------------------
|
|
587
|
+
"union.branch.added": { ...WIDENS, reason: (c) => `${c.keyword} branch added` },
|
|
588
|
+
"union.branch.removed": { ...NARROWS, reason: (c) => `${c.keyword} branch removed` },
|
|
589
|
+
"allOf.branch.added": { ...NARROWS, reason: () => "allOf branch added" },
|
|
590
|
+
"allOf.branch.removed": { ...WIDENS, reason: () => "allOf branch removed" },
|
|
591
|
+
// --- boolean schemas ------------------------------------------------------
|
|
592
|
+
"schema.restricted": { ...NARROWS, reason: () => "schema now accepts fewer values" },
|
|
593
|
+
"schema.relaxed": { ...WIDENS, reason: () => "schema now accepts more values" },
|
|
594
|
+
// --- annotations ----------------------------------------------------------
|
|
595
|
+
// Changes nothing about accepted values, but consumers of the diff want to see it.
|
|
596
|
+
"schema.deprecated": { ...SAFE, reason: () => "marked as deprecated" },
|
|
597
|
+
// --- unknown --------------------------------------------------------------
|
|
598
|
+
// We cannot reason about it, so we refuse to call it safe.
|
|
599
|
+
"keyword.changed": { ...INCOMPATIBLE, reason: (c) => `${c.keyword} changed, compatibility cannot be determined` }
|
|
600
|
+
};
|
|
601
|
+
var isBreaking = (change, strategy) => {
|
|
602
|
+
const rule = RULES[change.kind];
|
|
603
|
+
switch (strategy) {
|
|
604
|
+
case "backward":
|
|
605
|
+
return rule.backward;
|
|
606
|
+
case "forward":
|
|
607
|
+
return rule.forward;
|
|
608
|
+
case "full":
|
|
609
|
+
return rule.backward || rule.forward;
|
|
610
|
+
case "none":
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
var reasonFor = (change) => RULES[change.kind].reason(change);
|
|
615
|
+
var show = (value) => typeof value === "string" ? value : JSON.stringify(value);
|
|
616
|
+
|
|
617
|
+
// src/json-schema/index.ts
|
|
618
|
+
var checkJsonSchemaCompatibility = (before, after, strategy) => {
|
|
619
|
+
const changes = compareJsonSchemas(before, after);
|
|
620
|
+
const ops = changes.map((change) => toOp(change, strategy));
|
|
621
|
+
const checksBackward = strategy === "backward" || strategy === "full";
|
|
622
|
+
const checksForward = strategy === "forward" || strategy === "full";
|
|
623
|
+
const breaksBackward = checksBackward && changes.some((change) => isBreaking(change, "backward"));
|
|
624
|
+
const breaksForward = checksForward && changes.some((change) => isBreaking(change, "forward"));
|
|
625
|
+
const direction = breaksBackward && breaksForward ? "both" : breaksBackward ? "backward" : breaksForward ? "forward" : null;
|
|
626
|
+
return { breaking: direction !== null, direction, ops };
|
|
627
|
+
};
|
|
628
|
+
var opFor = (change) => {
|
|
629
|
+
if (change.before === void 0 && change.after !== void 0) return "add";
|
|
630
|
+
if (change.before !== void 0 && change.after === void 0) return "remove";
|
|
631
|
+
return "replace";
|
|
632
|
+
};
|
|
633
|
+
var toOp = (change, strategy) => ({
|
|
634
|
+
op: opFor(change),
|
|
635
|
+
path: change.path,
|
|
636
|
+
kind: change.kind,
|
|
637
|
+
reason: reasonFor(change),
|
|
638
|
+
breaking: isBreaking(change, strategy)
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// src/schemas.ts
|
|
642
|
+
var MESSAGE_TYPES2 = /* @__PURE__ */ new Set(["event", "command", "query"]);
|
|
643
|
+
var isMessage = (resource) => MESSAGE_TYPES2.has(resource.type);
|
|
644
|
+
var diffSchemas = (a, b, options) => {
|
|
645
|
+
const before = messagesByKey(a);
|
|
646
|
+
const after = messagesByKey(b);
|
|
647
|
+
const changes = [];
|
|
648
|
+
for (const [key, versionsA] of before) {
|
|
649
|
+
const versionsB = after.get(key);
|
|
650
|
+
if (!versionsB) continue;
|
|
651
|
+
for (const [messageA, messageB] of pairVersions(versionsA, versionsB)) {
|
|
652
|
+
changes.push(...diffMessageSchemas(messageA, messageB, options));
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return changes;
|
|
656
|
+
};
|
|
657
|
+
var messagesByKey = (index) => {
|
|
658
|
+
const byKey2 = /* @__PURE__ */ new Map();
|
|
659
|
+
for (const resource of index.resources) {
|
|
660
|
+
if (!isMessage(resource)) continue;
|
|
661
|
+
const key = `${resource.type}:${resource.id}`;
|
|
662
|
+
byKey2.set(key, [...byKey2.get(key) ?? [], resource]);
|
|
663
|
+
}
|
|
664
|
+
return byKey2;
|
|
665
|
+
};
|
|
666
|
+
var pairVersions = (versionsA, versionsB) => {
|
|
667
|
+
const byVersionB = new Map(versionsB.map((message) => [message.version, message]));
|
|
668
|
+
const pairs = [];
|
|
669
|
+
for (const messageA of [...versionsA].sort((left, right) => compareVersions(left.version, right.version))) {
|
|
670
|
+
const messageB = byVersionB.get(messageA.version);
|
|
671
|
+
if (messageB) pairs.push([messageA, messageB]);
|
|
672
|
+
}
|
|
673
|
+
const latestA = latest(versionsA);
|
|
674
|
+
const latestB = latest(versionsB);
|
|
675
|
+
if (latestA && latestB && latestA.version !== latestB.version) pairs.push([latestA, latestB]);
|
|
676
|
+
return pairs;
|
|
677
|
+
};
|
|
678
|
+
var diffMessageSchemas = (messageA, messageB, options) => {
|
|
679
|
+
const { strategy, includeSchemaContent } = options;
|
|
680
|
+
const message = { type: messageA.type, id: messageA.id, version: { a: messageA.version, b: messageB.version } };
|
|
681
|
+
const changes = [];
|
|
682
|
+
for (const { before, after } of pairSchemas(messageA.schemas ?? [], messageB.schemas ?? [])) {
|
|
683
|
+
if (before && after) {
|
|
684
|
+
if (before.hash !== void 0 && before.hash === after.hash) continue;
|
|
685
|
+
changes.push({
|
|
686
|
+
message,
|
|
687
|
+
change: "modified",
|
|
688
|
+
before: pointer(before, includeSchemaContent),
|
|
689
|
+
after: pointer(after, includeSchemaContent),
|
|
690
|
+
strategy,
|
|
691
|
+
...judge(before, after, strategy)
|
|
692
|
+
});
|
|
693
|
+
} else if (after) {
|
|
694
|
+
changes.push({ message, change: "added", after: pointer(after, includeSchemaContent), strategy, ...UNKNOWN });
|
|
695
|
+
} else if (before) {
|
|
696
|
+
changes.push({ message, change: "removed", before: pointer(before, includeSchemaContent), strategy, ...UNKNOWN });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return changes;
|
|
700
|
+
};
|
|
701
|
+
var pairSchemas = (schemasA, schemasB) => {
|
|
702
|
+
const pairs = [];
|
|
703
|
+
let remainingA = [...schemasA];
|
|
704
|
+
let remainingB = [...schemasB];
|
|
705
|
+
const matchBy = (key) => {
|
|
706
|
+
for (const before of [...remainingA]) {
|
|
707
|
+
const keyA = key(before);
|
|
708
|
+
if (keyA === void 0) continue;
|
|
709
|
+
const after = remainingB.find((candidate) => key(candidate) === keyA);
|
|
710
|
+
if (!after) continue;
|
|
711
|
+
pairs.push({ before, after });
|
|
712
|
+
remainingA = remainingA.filter((schema) => schema !== before);
|
|
713
|
+
remainingB = remainingB.filter((schema) => schema !== after);
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
matchBy((schema) => schema.id);
|
|
717
|
+
matchBy((schema) => schema.path);
|
|
718
|
+
matchBy((schema) => schema.default ? "default" : void 0);
|
|
719
|
+
if (remainingA.length === 1 && remainingB.length === 1) {
|
|
720
|
+
pairs.push({ before: remainingA[0], after: remainingB[0] });
|
|
721
|
+
remainingA = [];
|
|
722
|
+
remainingB = [];
|
|
723
|
+
}
|
|
724
|
+
for (const before of remainingA) pairs.push({ before });
|
|
725
|
+
for (const after of remainingB) pairs.push({ after });
|
|
726
|
+
return pairs;
|
|
727
|
+
};
|
|
728
|
+
var pointer = (schema, includeContent) => ({
|
|
729
|
+
path: schema.path ?? "",
|
|
730
|
+
hash: schema.hash,
|
|
731
|
+
...includeContent && schema.content !== void 0 ? { content: schema.content } : {}
|
|
732
|
+
});
|
|
733
|
+
var UNKNOWN = { breaking: null, direction: null, ops: [] };
|
|
734
|
+
var judge = (before, after, strategy) => {
|
|
735
|
+
const beforeContent = asJsonSchema(before);
|
|
736
|
+
const afterContent = asJsonSchema(after);
|
|
737
|
+
if (beforeContent === void 0 || afterContent === void 0) return UNKNOWN;
|
|
738
|
+
return checkJsonSchemaCompatibility(beforeContent, afterContent, strategy);
|
|
739
|
+
};
|
|
740
|
+
var asJsonSchema = (schema) => {
|
|
741
|
+
if (schema.format !== void 0 && schema.format !== "json-schema") return void 0;
|
|
742
|
+
if (schema.format === void 0 && !(schema.path ?? "").endsWith(".json")) return void 0;
|
|
743
|
+
const parsed = parseJson(schema.content);
|
|
744
|
+
if (parsed === void 0) return void 0;
|
|
745
|
+
if (schema.format === void 0 && !looksLikeJsonSchema(parsed)) return void 0;
|
|
746
|
+
return parsed;
|
|
747
|
+
};
|
|
748
|
+
var SCHEMA_KEYWORDS = [
|
|
749
|
+
"$schema",
|
|
750
|
+
"type",
|
|
751
|
+
"properties",
|
|
752
|
+
"items",
|
|
753
|
+
"required",
|
|
754
|
+
"enum",
|
|
755
|
+
"oneOf",
|
|
756
|
+
"anyOf",
|
|
757
|
+
"allOf",
|
|
758
|
+
"$ref",
|
|
759
|
+
"definitions",
|
|
760
|
+
"$defs"
|
|
761
|
+
];
|
|
762
|
+
var looksLikeJsonSchema = (value) => typeof value === "boolean" || SCHEMA_KEYWORDS.some((keyword) => keyword in value);
|
|
763
|
+
var parseJson = (content) => {
|
|
764
|
+
if (content === void 0) return void 0;
|
|
765
|
+
try {
|
|
766
|
+
const parsed = JSON.parse(content);
|
|
767
|
+
return typeof parsed === "boolean" || typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
|
|
768
|
+
} catch {
|
|
769
|
+
return void 0;
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
// src/types.ts
|
|
774
|
+
var ARCHITECTURE_DIFF_SCHEMA_VERSION = 1;
|
|
775
|
+
|
|
776
|
+
// src/diff.ts
|
|
777
|
+
var DEFAULT_STRATEGY = "full";
|
|
778
|
+
var diff = (a, b, options = {}) => {
|
|
779
|
+
const strategy = options.strategy ?? DEFAULT_STRATEGY;
|
|
780
|
+
const graphA = resolve([a]);
|
|
781
|
+
const graphB = resolve([b]);
|
|
782
|
+
const resources = diffResources(a, b);
|
|
783
|
+
const edges = diffEdges(graphA, graphB);
|
|
784
|
+
const schemaChanges = diffSchemas(a, b, { strategy, includeSchemaContent: options.includeSchemaContent ?? false });
|
|
785
|
+
const impact = impactOf({ baseline: graphA, schemaChanges, resourcesRemoved: resources.removed, edgesRemoved: edges.removed });
|
|
786
|
+
const schemaBreaking = schemaChanges.filter((change) => change.breaking === true).length;
|
|
787
|
+
const schemaUnknown = schemaChanges.filter((change) => change.breaking === null).length;
|
|
788
|
+
const messageRemoved = impact.some((entry) => entry.reason === "message_removed");
|
|
789
|
+
return {
|
|
790
|
+
schemaVersion: ARCHITECTURE_DIFF_SCHEMA_VERSION,
|
|
791
|
+
refs: {
|
|
792
|
+
a: { source: a.source, commit: a.commit },
|
|
793
|
+
b: { source: b.source, commit: b.commit }
|
|
794
|
+
},
|
|
795
|
+
compatibility: { strategy },
|
|
796
|
+
summary: {
|
|
797
|
+
breaking: schemaBreaking > 0 || messageRemoved,
|
|
798
|
+
resourcesAdded: resources.added.length,
|
|
799
|
+
resourcesRemoved: resources.removed.length,
|
|
800
|
+
resourcesChanged: resources.changed.length,
|
|
801
|
+
edgesAdded: edges.added.length,
|
|
802
|
+
edgesRemoved: edges.removed.length,
|
|
803
|
+
schemaChanges: schemaChanges.length,
|
|
804
|
+
schemaBreaking,
|
|
805
|
+
schemaUnknown
|
|
806
|
+
},
|
|
807
|
+
resources,
|
|
808
|
+
edges,
|
|
809
|
+
schemaChanges,
|
|
810
|
+
impact
|
|
811
|
+
};
|
|
812
|
+
};
|
|
813
|
+
export {
|
|
814
|
+
ARCHITECTURE_DIFF_SCHEMA_VERSION,
|
|
815
|
+
DEFAULT_STRATEGY,
|
|
816
|
+
checkJsonSchemaCompatibility,
|
|
817
|
+
compareJsonSchemas,
|
|
818
|
+
diff
|
|
819
|
+
};
|
|
820
|
+
//# sourceMappingURL=index.mjs.map
|