@eventcatalog/cli 0.5.3 → 0.5.4

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/dist/cli/index.js CHANGED
@@ -1251,7 +1251,10 @@ var snapshotList = async (opts) => {
1251
1251
  // src/cli/governance/rules.ts
1252
1252
  var import_node_fs5 = __toESM(require("fs"));
1253
1253
  var import_node_path4 = __toESM(require("path"));
1254
+ var import_node_crypto2 = require("crypto");
1254
1255
  var import_js_yaml = __toESM(require("js-yaml"));
1256
+ var import_semver = require("semver");
1257
+ var import_sdk6 = __toESM(require("@eventcatalog/sdk"));
1255
1258
  var loadGovernanceConfig = (catalogDir) => {
1256
1259
  const yamlPath = import_node_path4.default.join(catalogDir, "governance.yaml");
1257
1260
  const ymlPath = import_node_path4.default.join(catalogDir, "governance.yml");
@@ -1309,15 +1312,16 @@ var buildMessageMap = (snapshot2) => {
1309
1312
  for (const msg of snapshot2.resources.messages.queries) map.set(msg.id, msg);
1310
1313
  return map;
1311
1314
  };
1312
- var buildProducerIndex = (snapshot2) => {
1315
+ var buildServiceIndex = (snapshot2, direction) => {
1313
1316
  const index = /* @__PURE__ */ new Map();
1314
1317
  for (const service of snapshot2.resources.services) {
1315
- if (!service.sends) continue;
1316
- for (const s of service.sends) {
1317
- let producers = index.get(s.id);
1318
- if (!producers) {
1319
- producers = [];
1320
- index.set(s.id, producers);
1318
+ const pointers = service[direction];
1319
+ if (!pointers) continue;
1320
+ for (const pointer of pointers) {
1321
+ let entries = index.get(pointer.id);
1322
+ if (!entries) {
1323
+ entries = [];
1324
+ index.set(pointer.id, entries);
1321
1325
  }
1322
1326
  const entry = {
1323
1327
  id: service.id,
@@ -1326,17 +1330,85 @@ var buildProducerIndex = (snapshot2) => {
1326
1330
  if (service.owners && Array.isArray(service.owners) && service.owners.length > 0) {
1327
1331
  entry.owners = service.owners;
1328
1332
  }
1329
- producers.push(entry);
1333
+ entries.push(entry);
1330
1334
  }
1331
1335
  }
1332
1336
  return index;
1333
1337
  };
1338
+ var getMessageTypeKey = (resourceId, type) => `${type}:${resourceId}`;
1339
+ var buildLatestMessageVersionMap = (snapshot2) => {
1340
+ const versions = /* @__PURE__ */ new Map();
1341
+ for (const event of snapshot2.resources.messages.events) {
1342
+ versions.set(getMessageTypeKey(event.id, "event"), event.version);
1343
+ }
1344
+ for (const command of snapshot2.resources.messages.commands) {
1345
+ versions.set(getMessageTypeKey(command.id, "command"), command.version);
1346
+ }
1347
+ for (const query of snapshot2.resources.messages.queries) {
1348
+ versions.set(getMessageTypeKey(query.id, "query"), query.version);
1349
+ }
1350
+ return versions;
1351
+ };
1352
+ var getTargetMessageVersion = (resourceChange) => {
1353
+ if (resourceChange.changeType === "versioned") {
1354
+ return resourceChange.newVersion || resourceChange.version;
1355
+ }
1356
+ return resourceChange.version;
1357
+ };
1358
+ var pointerTargetsChangedVersion = (pointer, resourceChange, latestMessageVersions) => {
1359
+ if (pointer.id !== resourceChange.resourceId) return false;
1360
+ const targetVersion = getTargetMessageVersion(resourceChange);
1361
+ const pointerVersion = pointer.version;
1362
+ if (!pointerVersion || pointerVersion === "latest") {
1363
+ const latestVersion = latestMessageVersions.get(getMessageTypeKey(resourceChange.resourceId, resourceChange.type));
1364
+ if (!latestVersion) return true;
1365
+ return latestVersion === targetVersion;
1366
+ }
1367
+ if ((0, import_semver.validRange)(pointerVersion)) {
1368
+ try {
1369
+ return (0, import_semver.satisfies)(targetVersion, pointerVersion);
1370
+ } catch {
1371
+ return false;
1372
+ }
1373
+ }
1374
+ return pointerVersion === targetVersion;
1375
+ };
1376
+ var getServicesForSchemaChange = (snapshot2, direction, resourceChange, latestMessageVersions) => {
1377
+ const matches = [];
1378
+ for (const service of snapshot2.resources.services) {
1379
+ const pointers = service[direction];
1380
+ if (!pointers) continue;
1381
+ const hasMatch = pointers.some((pointer) => pointerTargetsChangedVersion(pointer, resourceChange, latestMessageVersions));
1382
+ if (!hasMatch) continue;
1383
+ const entry = {
1384
+ id: service.id,
1385
+ version: service.version
1386
+ };
1387
+ if (service.owners && Array.isArray(service.owners) && service.owners.length > 0) {
1388
+ entry.owners = service.owners;
1389
+ }
1390
+ matches.push(entry);
1391
+ }
1392
+ return matches;
1393
+ };
1394
+ var matchesSchemaChangeResource = (schemaChange, resources) => {
1395
+ return resources.some((resource) => {
1396
+ if (resource === "*") return true;
1397
+ if (resource.startsWith("message:")) return schemaChange.resourceChange.resourceId === resource.slice(8);
1398
+ if (resource.startsWith("consumes:"))
1399
+ return schemaChange.consumerServices.some((service) => service.id === resource.slice(9));
1400
+ if (resource.startsWith("produces:"))
1401
+ return schemaChange.producerServices.some((service) => service.id === resource.slice(9));
1402
+ if (resource.startsWith("service:")) return schemaChange.producerServices.some((service) => service.id === resource.slice(8));
1403
+ return false;
1404
+ });
1405
+ };
1334
1406
  var evaluateDeprecationRules = (diff, config, targetSnapshot, targetMessageSets, baseSnapshot) => {
1335
1407
  const deprecationRules = config.rules.filter((rule) => rule.when.includes("message_deprecated"));
1336
1408
  if (deprecationRules.length === 0) return [];
1337
1409
  const targetMessages = buildMessageMap(targetSnapshot);
1338
1410
  const baseMessages = baseSnapshot ? buildMessageMap(baseSnapshot) : void 0;
1339
- const producerIndex = buildProducerIndex(targetSnapshot);
1411
+ const producerIndex = buildServiceIndex(targetSnapshot, "sends");
1340
1412
  const deprecatedResources = diff.resources.filter((rc) => {
1341
1413
  if (!MESSAGE_RESOURCE_TYPES.has(rc.type)) return false;
1342
1414
  if (!rc.changedFields?.includes("deprecated")) return false;
@@ -1363,6 +1435,29 @@ var evaluateDeprecationRules = (diff, config, targetSnapshot, targetMessageSets,
1363
1435
  }
1364
1436
  return results;
1365
1437
  };
1438
+ var evaluateSchemaChangeRules = (diff, config, targetSnapshot) => {
1439
+ const schemaRules = config.rules.filter((rule) => rule.when.includes("schema_changed"));
1440
+ if (schemaRules.length === 0) return [];
1441
+ const schemaChangedResources = diff.resources.filter((rc) => {
1442
+ if (!MESSAGE_RESOURCE_TYPES.has(rc.type)) return false;
1443
+ return rc.changedFields?.includes("schemaHash");
1444
+ });
1445
+ if (schemaChangedResources.length === 0) return [];
1446
+ const latestMessageVersions = buildLatestMessageVersionMap(targetSnapshot);
1447
+ const schemaChanges = schemaChangedResources.map((resourceChange) => ({
1448
+ resourceChange,
1449
+ producerServices: getServicesForSchemaChange(targetSnapshot, "sends", resourceChange, latestMessageVersions),
1450
+ consumerServices: getServicesForSchemaChange(targetSnapshot, "receives", resourceChange, latestMessageVersions)
1451
+ }));
1452
+ const results = [];
1453
+ for (const rule of schemaRules) {
1454
+ const matched = schemaChanges.filter((schemaChange) => matchesSchemaChangeResource(schemaChange, rule.resources));
1455
+ if (matched.length > 0) {
1456
+ results.push({ rule, trigger: "schema_changed", matchedChanges: [], schemaChanges: matched });
1457
+ }
1458
+ }
1459
+ return results;
1460
+ };
1366
1461
  var evaluateGovernanceRules = (diff, config, targetSnapshot, baseSnapshot) => {
1367
1462
  const results = [];
1368
1463
  const targetMessageSets = targetSnapshot ? buildServiceMessageSets(targetSnapshot) : void 0;
@@ -1382,6 +1477,7 @@ var evaluateGovernanceRules = (diff, config, targetSnapshot, baseSnapshot) => {
1382
1477
  }
1383
1478
  if (targetSnapshot && targetMessageSets) {
1384
1479
  results.push(...evaluateDeprecationRules(diff, config, targetSnapshot, targetMessageSets, baseSnapshot));
1480
+ results.push(...evaluateSchemaChangeRules(diff, config, targetSnapshot));
1385
1481
  }
1386
1482
  return results;
1387
1483
  };
@@ -1400,9 +1496,51 @@ var resolveEnvVars = (value) => {
1400
1496
  return envValue;
1401
1497
  });
1402
1498
  };
1499
+ var readSchemaDetails = async (sdk, resourceId, version2, type) => {
1500
+ if (!MESSAGE_RESOURCE_TYPES.has(type)) return {};
1501
+ try {
1502
+ const schema = await sdk.getSchemaForMessage(resourceId, version2);
1503
+ if (!schema) return {};
1504
+ return {
1505
+ content: schema.schema,
1506
+ schemaPath: schema.fileName,
1507
+ schemaHash: (0, import_node_crypto2.createHash)("sha256").update(schema.schema).digest("hex")
1508
+ };
1509
+ } catch {
1510
+ return {};
1511
+ }
1512
+ };
1513
+ var enrichSchemaContent = async (results, baseCatalogDir, targetCatalogDir) => {
1514
+ const baseSDK = (0, import_sdk6.default)(baseCatalogDir);
1515
+ const targetSDK = (0, import_sdk6.default)(targetCatalogDir);
1516
+ const promises = [];
1517
+ for (const result of results) {
1518
+ if (!result.schemaChanges) continue;
1519
+ for (const sc of result.schemaChanges) {
1520
+ const { resourceId, version: version2, type, changeType, previousVersion, newVersion } = sc.resourceChange;
1521
+ const baseVersion = changeType === "versioned" ? previousVersion || version2 : version2;
1522
+ const targetVersion = changeType === "versioned" ? newVersion || version2 : version2;
1523
+ promises.push(
1524
+ (async () => {
1525
+ const [before, after] = await Promise.all([
1526
+ readSchemaDetails(baseSDK, resourceId, baseVersion, type),
1527
+ readSchemaDetails(targetSDK, resourceId, targetVersion, type)
1528
+ ]);
1529
+ sc.before = before.content;
1530
+ sc.after = after.content;
1531
+ sc.beforeSchemaPath = before.schemaPath;
1532
+ sc.afterSchemaPath = after.schemaPath;
1533
+ sc.beforeSchemaHash = before.schemaHash;
1534
+ sc.afterSchemaHash = after.schemaHash;
1535
+ })()
1536
+ );
1537
+ }
1538
+ }
1539
+ await Promise.all(promises);
1540
+ };
1403
1541
 
1404
1542
  // src/cli/governance/actions.ts
1405
- var import_node_crypto2 = require("crypto");
1543
+ var import_node_crypto3 = require("crypto");
1406
1544
  var buildMessageTypeMap = (snapshot2) => {
1407
1545
  const map = /* @__PURE__ */ new Map();
1408
1546
  for (const event of snapshot2.resources.messages.events) {
@@ -1426,7 +1564,7 @@ var buildServiceOwnersMap = (snapshot2) => {
1426
1564
  return map;
1427
1565
  };
1428
1566
  var executeGovernanceActions = async (results, opts = {}) => {
1429
- const { messageTypes, status, serviceOwners } = opts;
1567
+ const { messageTypes, status, serviceOwners, baseRef, targetRef } = opts;
1430
1568
  const webhookCalls = [];
1431
1569
  const now = (/* @__PURE__ */ new Date()).toISOString();
1432
1570
  for (const result of results) {
@@ -1439,6 +1577,46 @@ var executeGovernanceActions = async (results, opts = {}) => {
1439
1577
  headers[key] = resolveEnvVars(value);
1440
1578
  }
1441
1579
  }
1580
+ if (result.schemaChanges && result.schemaChanges.length > 0) {
1581
+ for (const sc of result.schemaChanges) {
1582
+ const messageType = messageTypes?.get(sc.resourceChange.resourceId) || "message";
1583
+ const payload = {
1584
+ specversion: "1.0",
1585
+ type: "eventcatalog.governance.schema_changed",
1586
+ source: "eventcatalog/governance",
1587
+ id: (0, import_node_crypto3.randomUUID)(),
1588
+ time: now,
1589
+ datacontenttype: "application/json",
1590
+ data: {
1591
+ schemaVersion: 1,
1592
+ ...status && { status },
1593
+ summary: `Schema changed for ${messageType} ${sc.resourceChange.resourceId}`,
1594
+ message: {
1595
+ id: sc.resourceChange.resourceId,
1596
+ version: sc.resourceChange.version,
1597
+ type: messageType
1598
+ },
1599
+ schema: {
1600
+ beforeHash: sc.beforeSchemaHash ?? null,
1601
+ afterHash: sc.afterSchemaHash ?? null,
1602
+ beforePath: sc.beforeSchemaPath ?? null,
1603
+ afterPath: sc.afterSchemaPath ?? null
1604
+ },
1605
+ refs: {
1606
+ base: baseRef ?? null,
1607
+ target: targetRef ?? null
1608
+ },
1609
+ consumers: sc.consumerServices,
1610
+ producers: sc.producerServices
1611
+ }
1612
+ };
1613
+ webhookCalls.push({
1614
+ urlTemplate: action.url,
1615
+ request: fetch(url, { method: "POST", headers, body: JSON.stringify(payload) })
1616
+ });
1617
+ }
1618
+ continue;
1619
+ }
1442
1620
  if (result.deprecationChanges && result.deprecationChanges.length > 0) {
1443
1621
  for (const dc of result.deprecationChanges) {
1444
1622
  const messageType = messageTypes?.get(dc.resourceChange.resourceId) || "message";
@@ -1448,7 +1626,7 @@ var executeGovernanceActions = async (results, opts = {}) => {
1448
1626
  specversion: "1.0",
1449
1627
  type: `eventcatalog.governance.message_deprecated`,
1450
1628
  source: "eventcatalog/governance",
1451
- id: (0, import_node_crypto2.randomUUID)(),
1629
+ id: (0, import_node_crypto3.randomUUID)(),
1452
1630
  time: now,
1453
1631
  datacontenttype: "application/json",
1454
1632
  data: {
@@ -1483,7 +1661,7 @@ var executeGovernanceActions = async (results, opts = {}) => {
1483
1661
  specversion: "1.0",
1484
1662
  type: `eventcatalog.governance.${result.trigger}`,
1485
1663
  source: "eventcatalog/governance",
1486
- id: (0, import_node_crypto2.randomUUID)(),
1664
+ id: (0, import_node_crypto3.randomUUID)(),
1487
1665
  time: now,
1488
1666
  datacontenttype: "application/json",
1489
1667
  data: {
@@ -1531,7 +1709,14 @@ var formatGovernanceOutput = (results) => {
1531
1709
  const lines = ["Governance:", ""];
1532
1710
  for (const result of results) {
1533
1711
  lines.push(` Rule "${result.rule.name}" triggered (${result.trigger}):`);
1534
- if (result.deprecationChanges && result.deprecationChanges.length > 0) {
1712
+ if (result.schemaChanges && result.schemaChanges.length > 0) {
1713
+ for (const sc of result.schemaChanges) {
1714
+ const consumers = sc.consumerServices.length > 0 ? sc.consumerServices.map((c2) => c2.id).join(", ") : "no known consumers";
1715
+ lines.push(
1716
+ ` ! Schema changed for ${sc.resourceChange.resourceId} (${sc.resourceChange.type}) \u2014 consumers: ${consumers}`
1717
+ );
1718
+ }
1719
+ } else if (result.deprecationChanges && result.deprecationChanges.length > 0) {
1535
1720
  for (const dc of result.deprecationChanges) {
1536
1721
  const producers = dc.producerServices.length > 0 ? dc.producerServices.map((p) => p.id).join(", ") : "unknown producer";
1537
1722
  lines.push(` ! ${dc.resourceChange.resourceId} (${dc.resourceChange.type}) deprecated by ${producers}`);
@@ -1554,7 +1739,7 @@ var import_node_child_process = require("child_process");
1554
1739
  var import_node_fs6 = require("fs");
1555
1740
  var import_node_os = require("os");
1556
1741
  var import_dotenv = __toESM(require("dotenv"));
1557
- var import_sdk6 = __toESM(require("@eventcatalog/sdk"));
1742
+ var import_sdk7 = __toESM(require("@eventcatalog/sdk"));
1558
1743
  var import_license = require("@eventcatalog/license");
1559
1744
  var BRANCH_NAME_RE = /^[a-zA-Z0-9._\-/]+$/;
1560
1745
  var extractBranchToTempDir = (branch, catalogDir, tempDirs) => {
@@ -1588,15 +1773,17 @@ var governanceCheck = async (opts) => {
1588
1773
  const baseTmpDir = extractBranchToTempDir(baseBranch, dir, tempDirs);
1589
1774
  const baseSnapshotDir = trackTempDir("ec-snap-base-");
1590
1775
  const targetSnapshotDir = trackTempDir("ec-snap-target-");
1591
- const baseSDK = (0, import_sdk6.default)(baseTmpDir);
1776
+ const baseSDK = (0, import_sdk7.default)(baseTmpDir);
1592
1777
  const baseResult = await baseSDK.createSnapshot({ label: `base-${baseBranch}`, outputDir: baseSnapshotDir });
1593
1778
  let targetResult;
1779
+ let targetCatalogDir;
1594
1780
  if (opts.target) {
1595
- const targetTmpDir = extractBranchToTempDir(opts.target, dir, tempDirs);
1596
- const targetSDK = (0, import_sdk6.default)(targetTmpDir);
1781
+ targetCatalogDir = extractBranchToTempDir(opts.target, dir, tempDirs);
1782
+ const targetSDK = (0, import_sdk7.default)(targetCatalogDir);
1597
1783
  targetResult = await targetSDK.createSnapshot({ label: `target-${opts.target}`, outputDir: targetSnapshotDir });
1598
1784
  } else {
1599
- const targetSDK = (0, import_sdk6.default)(dir);
1785
+ targetCatalogDir = dir;
1786
+ const targetSDK = (0, import_sdk7.default)(dir);
1600
1787
  targetResult = await targetSDK.createSnapshot({ label: "current", outputDir: targetSnapshotDir });
1601
1788
  }
1602
1789
  const diff = await baseSDK.diffSnapshots(baseResult.filePath, targetResult.filePath);
@@ -1605,12 +1792,15 @@ var governanceCheck = async (opts) => {
1605
1792
  return "No governance.yaml (or governance.yml) found or no rules defined.";
1606
1793
  }
1607
1794
  const results = evaluateGovernanceRules(diff, config, targetResult.snapshot, baseResult.snapshot);
1795
+ await enrichSchemaContent(results, baseTmpDir, targetCatalogDir);
1608
1796
  const messageTypes = buildMessageTypeMap(targetResult.snapshot);
1609
1797
  const serviceOwners = buildServiceOwnersMap(targetResult.snapshot);
1610
1798
  const actionOutput = await executeGovernanceActions(results, {
1611
1799
  messageTypes,
1612
1800
  status: opts.status,
1613
- serviceOwners
1801
+ serviceOwners,
1802
+ baseRef: baseBranch,
1803
+ targetRef: opts.target || "working-directory"
1614
1804
  });
1615
1805
  if (opts.format === "json") {
1616
1806
  return JSON.stringify({ baseBranch, target: opts.target || "working directory", results, diff: diff.summary }, null, 2);