/* eslint-disable no-useless-constructor */ import { CompassLinkType, Component, CreateLinkInput, } from '@atlassian/forge-graphql-types'; import { YamlLink } from '../../../../types'; import { AdditionalLinksValidationError } from './models/errors'; import { LinksInputValidator } from './models/links-input-validator-types'; import { MAX_LINKS_OF_TYPE, MAX_NAME_LENGTH, } from '../../../../helpers/constants'; enum SCMHosts { BITBUCKET = 'bitbucket.org', GITHUB = 'github.com', } function removeSubdirectories(url: string) { const parsedUrl = new URL(url); const { hostname, pathname, protocol } = parsedUrl; const pathToRepo = pathname.split('/').slice(1, 3).join('/'); if (hostname === SCMHosts.BITBUCKET || hostname === SCMHosts.GITHUB) { return `${protocol}//${hostname}/${pathToRepo}`; } return url; } function removeTrailingSlash(url: string) { return url.endsWith('/') ? url.slice(0, -1) : url; } function isSameLink(link1: CreateLinkInput, link2: CreateLinkInput): boolean { const isSameWithoutSubdirectories = removeSubdirectories(link1.url) === removeSubdirectories(link2.url) && link1.type === link2.type; const isSameWithoutTrailingSlash = removeTrailingSlash(link1.url) === removeTrailingSlash(link2.url) && link1.type === link2.type; return isSameWithoutSubdirectories || isSameWithoutTrailingSlash; } export class ConfigLinksTransformer { constructor( private additionalLinks: CreateLinkInput[], private configLinks: YamlLink[], private validator: LinksInputValidator, private currentComponent?: Component, ) {} private mapConfigLinksToLinkInput(): CreateLinkInput[] { return this.configLinks.map(({ url, type, name }) => ({ url, type: type as CompassLinkType, ...(name ? { name } : {}), })); } private getUniqueLinks(): CreateLinkInput[] { const configLinksInput = this.mapConfigLinksToLinkInput(); const allLinks = configLinksInput.concat( this.additionalLinks.filter( (additionalLink) => !configLinksInput.find((configLink) => isSameLink(configLink, additionalLink), ), ), ); const stringifiedLinks = allLinks.map((link) => { const linkWithSortedKeys = Object.fromEntries( Object.entries(link).sort(([a], [b]) => a.localeCompare(b)), ); return JSON.stringify(linkWithSortedKeys); }); return [...new Set(stringifiedLinks)].map((item) => JSON.parse(item)); } getLinks(): CreateLinkInput[] { if (!this.currentComponent) { throw new AdditionalLinksValidationError(['Component not found']); } if (this.additionalLinks.length === 0) { return [...this.mapConfigLinksToLinkInput()]; } this.additionalLinks.forEach((link) => { this.validator.validateLinkKeys( link, this.currentComponent.typeId ?? this.currentComponent.type, MAX_NAME_LENGTH, ); }); const uniqueLinks = this.getUniqueLinks(); this.validator.validateMaxLinksByType(uniqueLinks, MAX_LINKS_OF_TYPE); if (this.validator.errors.length > 0) { throw new AdditionalLinksValidationError(this.validator.errors); } return uniqueLinks; } }