UNPKG

@eventcatalog/notifier

Version:

CLI tool to detect EventCatalog changes and send notifications

207 lines 9.32 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SubscribedSchemaChangedEvent = void 0; const Event_1 = require("./Event"); const sdk_1 = __importDefault(require("@eventcatalog/sdk")); const path_1 = __importDefault(require("path")); const logger_1 = require("../utils/logger"); const git_1 = require("../utils/git"); const diff_1 = require("diff"); const logger = new logger_1.Logger(); function generateSchemaDiff(schema1, schema2) { const changes = (0, diff_1.diffLines)(schema1, schema2); let diffOutput = '```diff\n'; changes.forEach((part) => { if (part.added) { const lines = part.value.split('\n').filter((line) => line.trim() !== ''); lines.forEach((line) => { diffOutput += `+${line}\n`; }); } else if (part.removed) { const lines = part.value.split('\n').filter((line) => line.trim() !== ''); lines.forEach((line) => { diffOutput += `-${line}\n`; }); } }); diffOutput += '```'; return diffOutput; } // TODO: Move to SDK? const isSchema = (file) => { return file.endsWith('.json') || file.endsWith('.avro') || file.endsWith('.proto'); }; class SubscribedSchemaChangedEvent extends Event_1.Event { constructor({ catalogPath, changedFiles, commitRange, options, }) { super({ catalogPath, changedFiles, commitRange, options }); this.eventId = 'subscribed-schema-changed'; } async process() { const { getMessageBySchemaPath, getConsumersOfSchema } = (0, sdk_1.default)(this.catalogPath); const consumers = []; let message = null; let schemaBefore = null; let schemaAfter = null; // Get any service that has been changed for (const file of this.changedFiles) { // isSchema? const isChangedFileASchema = isSchema(file); if (isChangedFileASchema) { try { const [beforeRange, afterRange] = this.parseCommitRange(this.commitRange); schemaBefore = (0, git_1.getFileAtCommit)(this.catalogPath, file, beforeRange); schemaAfter = (0, git_1.getFileAtCommit)(this.catalogPath, file, afterRange); message = await getMessageBySchemaPath(path_1.default.relative(this.catalogPath, file)); const consumersOfMessage = await getConsumersOfSchema(path_1.default.relative(this.catalogPath, file)); consumers.push(...consumersOfMessage); } catch (error) { logger.warn(`Failed to find message for schema, skipping: ${file}`); } } } // no schemas to compare if (!schemaBefore || !schemaAfter) { return []; } const diff = generateSchemaDiff(schemaBefore, schemaAfter); const notifications = []; for (const consumer of consumers) { notifications.push({ id: this.eventId, version: '1.0.0', resource: { id: message.id, name: message.name, version: message.version, // TODO: Get type of resource, SDK function, getResourceType(resource)? type: 'event', owners: message.owners, }, consumer: { id: consumer.id, name: consumer.name, version: consumer.version, type: 'service', owners: consumer.owners, }, before: schemaBefore.trim(), after: schemaAfter.trim(), diff: diff, metadata: { timestamp: new Date().toISOString(), catalog_path: this.catalogPath, }, }); } return notifications; } static getSlackMessage(config, notification, lifecycle, actionUrl) { const eventOwners = notification.resource.owners .map((owner) => { const id = owner.id || owner; return `<${config.eventcatalog_url}/docs/teams/${id}|${id}>`; }) .join(', '); const consumerOwners = notification.consumer.owners .map((owner) => { const id = owner.id || owner; return `<${config.eventcatalog_url}/docs/teams/${id}|${id}>`; }) .join(', '); const timestamp = new Date(notification.metadata.timestamp).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', }); const isDraft = lifecycle === 'draft'; const text = isDraft ? `🔄 Proposed Schema Change: ${notification.resource.name}` : `⚠️ Schema Change Detected: ${notification.resource.name}`; const pretext = isDraft ? `The producer service ${notification.consumer.name} is proposing to modify the schema of ${notification.resource.name}. This change may impact downstream consumers. Please review the proposed update and validate compatibility before it goes live.` : `The producer service ${notification.consumer.name} has modified the schema of ${notification.resource.name}. This change is now live and may impact downstream consumers. Please review the update and validate compatibility.`; return { text, attachments: [ { color: 'warning', pretext, fields: [ { title: '📧 Event Affected', value: `<${config.eventcatalog_url}/docs/events/${notification.resource.id}/${notification.resource.version}|${notification.resource.name}> (v${notification.resource.version})`, short: true, }, { title: '🏭 Producer Service', value: `<${config.eventcatalog_url}/docs/services/${notification.consumer.id}/${notification.consumer.version}|${notification.consumer.name}> (v${notification.consumer.version})`, short: true, }, { title: '👥 Impacted Consumers', value: consumerOwners || 'No consumers found', short: true, }, { title: '👤 Event Owners', value: eventOwners || 'No owners assigned', short: true, }, { title: '📅 Changed At', value: timestamp, short: true, }, { title: '📋 Summary of Change', value: 'The schema for ' + notification.resource.name + (isDraft ? ' is proposed to be updated' : ' has been updated') + ' to support richer metadata. This may require updates in consumers that rely on strict typing or validation.', short: false, }, { title: '📄 Schema Diff', value: notification.diff || 'No changes detected', short: false, }, // only if actionUrl is provided actionUrl && { title: '🔗 View Schema Changes', value: `<${actionUrl}|View Schema Changes>`, short: true, }, ], footer: 'EventCatalog Notifier • eventcatalog.dev', ts: Math.floor(Date.now() / 1000), }, ], }; } parseCommitRange(commitRange) { // Handle both .. (two-dot) and ... (three-dot) git range syntax if (commitRange.includes('...')) { // Three-dot syntax: A...B const parts = commitRange.split('...'); return [parts[0], parts[1]]; } else { // Two-dot syntax: A..B const parts = commitRange.split('..'); return [parts[0], parts[1]]; } } findRemovedReceives(beforeReceives, afterReceives) { const afterIds = new Set(afterReceives.map((r) => r.id)); return beforeReceives.filter((r) => !afterIds.has(r.id)); } } exports.SubscribedSchemaChangedEvent = SubscribedSchemaChangedEvent; //# sourceMappingURL=SubscribedSchemaChangedEvent.js.map