@eventcatalog/notifier
Version:
CLI tool to detect EventCatalog changes and send notifications
207 lines • 9.32 kB
JavaScript
;
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