@eventcatalog/linter
Version:
A linter for EventCatalog to validate frontmatter and resource references
187 lines • 8.04 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateReferences = exports.buildResourceIndex = void 0;
const semver_1 = __importDefault(require("semver"));
const buildResourceIndex = (parsedFiles) => {
const index = {};
for (const parsedFile of parsedFiles) {
const { file, frontmatter } = parsedFile;
const { resourceType } = file;
// For users, teams, and domains, use the frontmatter id field instead of filename/directory
// This handles cases where filename is "aSmith.mdx" but frontmatter has id: "asmith"
// And where directory is "e-commerce" but frontmatter has id: "E-Commerce"
let resourceId = file.resourceId;
if ((resourceType === 'user' || resourceType === 'team' || resourceType === 'domain') &&
frontmatter.id &&
typeof frontmatter.id === 'string') {
resourceId = frontmatter.id;
}
if (!index[resourceType]) {
index[resourceType] = {};
}
if (!index[resourceType][resourceId]) {
index[resourceType][resourceId] = new Set();
}
if (frontmatter.version && typeof frontmatter.version === 'string') {
index[resourceType][resourceId].add(frontmatter.version);
}
else {
index[resourceType][resourceId].add('latest');
}
}
return index;
};
exports.buildResourceIndex = buildResourceIndex;
const checkResourceExists = (ref, resourceType, index) => {
const resourceVersions = index[resourceType]?.[ref.id];
if (!resourceVersions || resourceVersions.size === 0) {
return false;
}
if (!ref.version) {
return true;
}
const refVersion = ref.version === 'latest' ? ref.version : ref.version;
const availableVersions = Array.from(resourceVersions);
// Handle 'latest' specifically
if (refVersion === 'latest') {
return availableVersions.includes('latest') || availableVersions.length > 0;
}
// Check for exact match first
if (availableVersions.includes(refVersion)) {
return true;
}
// Handle semver patterns like '0.0.x', '^1.0.0', '~1.2.0', etc.
try {
// Filter out 'latest' from available versions for semver matching
const semverVersions = availableVersions.filter((v) => v !== 'latest' && semver_1.default.valid(v));
// Check if any available version satisfies the requested version pattern
for (const availableVersion of semverVersions) {
if (semver_1.default.satisfies(availableVersion, refVersion)) {
return true;
}
}
// Special handling for patterns like '0.0.x' which aren't standard semver ranges
if (refVersion.includes('.x')) {
const pattern = refVersion.replace(/\.x/g, '');
for (const availableVersion of semverVersions) {
if (availableVersion.startsWith(pattern)) {
return true;
}
}
}
return false;
}
catch (error) {
// If semver parsing fails, fall back to exact string match
return availableVersions.includes(refVersion);
}
};
const extractReferences = (parsedFile) => {
const { file, frontmatter } = parsedFile;
const references = [];
if (file.resourceType === 'domain') {
if (frontmatter.services && Array.isArray(frontmatter.services)) {
frontmatter.services.forEach((ref) => {
references.push({ ref, possibleTypes: ['service'], field: 'services' });
});
}
if (frontmatter.domains && Array.isArray(frontmatter.domains)) {
frontmatter.domains.forEach((ref) => {
references.push({ ref, possibleTypes: ['domain'], field: 'domains' });
});
}
if (frontmatter.entities && Array.isArray(frontmatter.entities)) {
frontmatter.entities.forEach((ref) => {
references.push({ ref, possibleTypes: ['entity'], field: 'entities' });
});
}
}
if (file.resourceType === 'service') {
if (frontmatter.sends && Array.isArray(frontmatter.sends)) {
frontmatter.sends.forEach((ref) => {
references.push({ ref, possibleTypes: ['event', 'command', 'query'], field: 'sends' });
});
}
if (frontmatter.receives && Array.isArray(frontmatter.receives)) {
frontmatter.receives.forEach((ref) => {
references.push({ ref, possibleTypes: ['event', 'command', 'query'], field: 'receives' });
});
}
if (frontmatter.entities && Array.isArray(frontmatter.entities)) {
frontmatter.entities.forEach((ref) => {
references.push({ ref, possibleTypes: ['entity'], field: 'entities' });
});
}
}
if (file.resourceType === 'flow' && frontmatter.steps && Array.isArray(frontmatter.steps)) {
frontmatter.steps.forEach((step, index) => {
if (step.message) {
references.push({
ref: step.message,
possibleTypes: ['event', 'command', 'query'],
field: `steps[${index}].message`,
});
}
if (step.service) {
references.push({ ref: step.service, possibleTypes: ['service'], field: `steps[${index}].service` });
}
});
}
if (file.resourceType === 'entity' && frontmatter.properties && Array.isArray(frontmatter.properties)) {
frontmatter.properties.forEach((prop, index) => {
if (prop.references) {
references.push({
ref: { id: prop.references },
possibleTypes: ['entity'],
field: `properties[${index}].references`,
});
}
});
}
if (frontmatter.owners && Array.isArray(frontmatter.owners)) {
frontmatter.owners.forEach((owner) => {
references.push({ ref: { id: owner }, possibleTypes: ['user', 'team'], field: 'owners' });
});
}
if (file.resourceType === 'team' && frontmatter.members && Array.isArray(frontmatter.members)) {
frontmatter.members.forEach((member) => {
references.push({ ref: { id: member }, possibleTypes: ['user'], field: 'members' });
});
}
return references;
};
const validateReferences = (parsedFiles) => {
const index = (0, exports.buildResourceIndex)(parsedFiles);
const errors = [];
for (const parsedFile of parsedFiles) {
const references = extractReferences(parsedFile);
for (const { ref, possibleTypes, field } of references) {
const found = possibleTypes.some((type) => checkResourceExists(ref, type, index));
if (!found) {
const versionStr = ref.version ? ` (version: ${ref.version})` : '';
const typeStr = possibleTypes.length === 1 ? possibleTypes[0] : possibleTypes.join('/');
let rule = 'refs/resource-exists';
if (field === 'owners') {
rule = 'refs/owner-exists';
}
else if (ref.version) {
rule = 'refs/valid-version-range';
}
errors.push({
type: 'reference',
resource: `${parsedFile.file.resourceType}/${parsedFile.file.resourceId}`,
field,
message: `Referenced ${typeStr} "${ref.id}"${versionStr} does not exist`,
file: parsedFile.file.relativePath,
rule,
});
}
}
}
return errors;
};
exports.validateReferences = validateReferences;
//# sourceMappingURL=reference-validator.js.map