Removing trailing slashes (#12658)

Fix inconsistent domain URL formats : removing the last / that was
caused by URL method

Standardize URL formatting to ensure consistent links storage and
retrieval of domain URLs across the application. Will improve the
dedpulicates in the links

Note: there is another temporary issue from google that was solved on
the 13th of june https://groups.google.com/g/adwords-api/c/tRSQMRZrJYM
but we consider this out of this scope

Fixes #12621
This commit is contained in:
Guillim
2025-06-17 16:29:14 +02:00
committed by GitHub
parent cc7a37b0cc
commit 1cee587709
14 changed files with 414 additions and 70 deletions

View File

@ -1,17 +1,31 @@
import { lowercaseDomain } from 'src/engine/api/graphql/workspace-query-runner/utils/query-runner-links.util';
import { lowercaseDomainAndRemoveTrailingSlash } from 'src/engine/api/graphql/workspace-query-runner/utils/query-runner-links.util';
describe('queryRunner LINKS util', () => {
it('should leave lowcased domain unchanged', () => {
const primaryLinkUrl = 'https://www.example.com/test';
const result = lowercaseDomain(primaryLinkUrl);
const result = lowercaseDomainAndRemoveTrailingSlash(primaryLinkUrl);
expect(result).toBe('https://www.example.com/test');
});
it('should lowercase the domain of the primary link url', () => {
const primaryLinkUrl = 'htTps://wwW.exAmple.coM/TEST';
const result = lowercaseDomain(primaryLinkUrl);
const result = lowercaseDomainAndRemoveTrailingSlash(primaryLinkUrl);
expect(result).toBe('https://www.example.com/TEST');
});
it('should not add a trailing slash', () => {
const primaryLinkUrl = 'https://www.example.com';
const result = lowercaseDomainAndRemoveTrailingSlash(primaryLinkUrl);
expect(result).toBe('https://www.example.com');
});
it('should not add a trailing slash', () => {
const primaryLinkUrl = 'https://www.example.com/toto/';
const result = lowercaseDomainAndRemoveTrailingSlash(primaryLinkUrl);
expect(result).toBe('https://www.example.com/toto');
});
});

View File

@ -1,6 +1,6 @@
export const lowercaseDomain = (url: string) => {
export const lowercaseDomainAndRemoveTrailingSlash = (url: string) => {
try {
return new URL(url).toString();
return new URL(url).toString().replace(/\/$/, '');
} catch {
return url;
}

View File

@ -1,15 +1,15 @@
import { Injectable } from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { lowercaseDomain } from 'src/engine/api/graphql/workspace-query-runner/utils/query-runner-links.util';
import { removeEmptyLinks } from 'src/engine/core-modules/record-transformer/utils/remove-empty-links';
import {
LinksFieldGraphQLInput,
transformLinksValue,
} from 'src/engine/core-modules/record-transformer/utils/transform-links-value.util';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { LinkMetadataNullable } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
import {
RichTextV2Metadata,
richTextV2ValueSchema,
@ -86,7 +86,7 @@ export class RecordInputTransformerService {
case FieldMetadataType.RICH_TEXT_V2:
return this.transformRichTextV2Value(value);
case FieldMetadataType.LINKS:
return this.transformLinksValue(value);
return transformLinksValue(value as LinksFieldGraphQLInput);
case FieldMetadataType.EMAILS:
return this.transformEmailsValue(value);
default:
@ -132,48 +132,6 @@ export class RecordInputTransformerService {
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private transformLinksValue(value: any): any {
if (!value) {
return value;
}
const primaryLinkUrlRaw = value.primaryLinkUrl as string | null;
const primaryLinkLabelRaw = value.primaryLinkLabel as string | null;
const secondaryLinksRaw = value.secondaryLinks as string | null;
let secondaryLinksArray: LinkMetadataNullable[] | null = null;
if (isNonEmptyString(secondaryLinksRaw)) {
try {
secondaryLinksArray = JSON.parse(secondaryLinksRaw);
} catch {
/* empty */
}
}
const { primaryLinkLabel, primaryLinkUrl, secondaryLinks } =
removeEmptyLinks({
primaryLinkUrl: primaryLinkUrlRaw,
primaryLinkLabel: primaryLinkLabelRaw,
secondaryLinks: secondaryLinksArray,
});
return {
...value,
primaryLinkUrl: isDefined(primaryLinkUrl)
? lowercaseDomain(primaryLinkUrl)
: primaryLinkUrl,
primaryLinkLabel,
secondaryLinks: JSON.stringify(
secondaryLinks?.map((link) => ({
...link,
url: isDefined(link.url) ? lowercaseDomain(link.url) : link.url,
})),
),
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private transformEmailsValue(value: any): any {

View File

@ -0,0 +1,79 @@
import { transformLinksValue } from 'src/engine/core-modules/record-transformer/utils/transform-links-value.util';
describe('transformLinksValue', () => {
it('should handle null/undefined/empty object values', () => {
expect(transformLinksValue(null)).toBeNull();
expect(transformLinksValue(undefined)).toBeUndefined();
expect(transformLinksValue({})).toEqual({
primaryLinkLabel: null,
primaryLinkUrl: null,
secondaryLinks: '[]',
});
});
describe('primary link', () => {
it('should transform uppercase', () => {
const input = {
primaryLinkUrl: 'HTTPS://EXAMPLE.COM',
primaryLinkLabel: 'Example',
secondaryLinks: '[]',
};
const expected = {
primaryLinkUrl: 'https://example.com',
primaryLinkLabel: 'Example',
secondaryLinks: '[]',
};
expect(transformLinksValue(input)).toEqual(expected);
});
it('should remove trailing slash', () => {
const input = {
primaryLinkUrl: 'https://example.com/',
primaryLinkLabel: 'Example',
secondaryLinks: '[]',
};
const expected = {
primaryLinkUrl: 'https://example.com',
primaryLinkLabel: 'Example',
secondaryLinks: '[]',
};
expect(transformLinksValue(input)).toEqual(expected);
});
it('should work fine without protocol', () => {
const input = {
primaryLinkUrl: 'example.com',
primaryLinkLabel: 'Example',
secondaryLinks: '[]',
};
const expected = {
primaryLinkUrl: 'example.com',
primaryLinkLabel: 'Example',
secondaryLinks: '[]',
};
expect(transformLinksValue(input)).toEqual(expected);
});
it('should work fine with www', () => {
const input = {
primaryLinkUrl: 'www.example.com',
primaryLinkLabel: 'Example',
secondaryLinks: '[]',
};
const expected = {
primaryLinkUrl: 'www.example.com',
primaryLinkLabel: 'Example',
secondaryLinks: '[]',
};
expect(transformLinksValue(input)).toEqual(expected);
});
});
});

View File

@ -0,0 +1,61 @@
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { lowercaseDomainAndRemoveTrailingSlash } from 'src/engine/api/graphql/workspace-query-runner/utils/query-runner-links.util';
import { removeEmptyLinks } from 'src/engine/core-modules/record-transformer/utils/remove-empty-links';
import { LinkMetadataNullable } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
export type LinksFieldGraphQLInput =
| {
primaryLinkUrl?: string | null;
primaryLinkLabel?: string | null;
secondaryLinks?: string | null;
}
| null
| undefined;
export const transformLinksValue = (
value: LinksFieldGraphQLInput,
): LinksFieldGraphQLInput => {
if (!value) {
return value;
}
const primaryLinkUrlRaw = value.primaryLinkUrl as string | null;
const primaryLinkLabelRaw = value.primaryLinkLabel as string | null;
const secondaryLinksRaw = value.secondaryLinks as string | null;
let secondaryLinksArray: LinkMetadataNullable[] | null = null;
if (isNonEmptyString(secondaryLinksRaw)) {
try {
secondaryLinksArray = JSON.parse(secondaryLinksRaw);
} catch {
/* empty */
}
}
const { primaryLinkLabel, primaryLinkUrl, secondaryLinks } = removeEmptyLinks(
{
primaryLinkUrl: primaryLinkUrlRaw,
primaryLinkLabel: primaryLinkLabelRaw,
secondaryLinks: secondaryLinksArray,
},
);
return {
...value,
primaryLinkUrl: isDefined(primaryLinkUrl)
? lowercaseDomainAndRemoveTrailingSlash(primaryLinkUrl)
: primaryLinkUrl,
primaryLinkLabel,
secondaryLinks: JSON.stringify(
secondaryLinks?.map((link) => ({
...link,
url: isDefined(link.url)
? lowercaseDomainAndRemoveTrailingSlash(link.url)
: link.url,
})),
),
};
};