fix IndexFieldMetadata availability in IndexMetadata/ObjectMetadata in front (#12886)
Context : - IndexFieldMetadata was no longer available on 'objects' gql query ([since this PR](https://github.com/twentyhq/twenty/pull/12785)). Then, unicity checks on import do not work anymore. Fix : - Add a dataloader logic in indexFieldMetadata - Add extra check in unicity hook on import
This commit is contained in:
@ -30,7 +30,7 @@ const documents = {
|
|||||||
"\n mutation UpdateOneObjectMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateObjectPayload!\n ) {\n updateOneObject(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n isSearchable\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n isLabelSyncedWithName\n }\n }\n": types.UpdateOneObjectMetadataItemDocument,
|
"\n mutation UpdateOneObjectMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateObjectPayload!\n ) {\n updateOneObject(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n isSearchable\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n isLabelSyncedWithName\n }\n }\n": types.UpdateOneObjectMetadataItemDocument,
|
||||||
"\n mutation DeleteOneObjectMetadataItem($idToDelete: UUID!) {\n deleteOneObject(input: { id: $idToDelete }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n isSearchable\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n isLabelSyncedWithName\n }\n }\n": types.DeleteOneObjectMetadataItemDocument,
|
"\n mutation DeleteOneObjectMetadataItem($idToDelete: UUID!) {\n deleteOneObject(input: { id: $idToDelete }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n isSearchable\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n isLabelSyncedWithName\n }\n }\n": types.DeleteOneObjectMetadataItemDocument,
|
||||||
"\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.DeleteOneFieldMetadataItemDocument,
|
"\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.DeleteOneFieldMetadataItemDocument,
|
||||||
"\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadataList {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
|
"\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadataList {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadataList {\n id\n fieldMetadataId\n createdAt\n updatedAt\n order\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
|
||||||
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
||||||
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
||||||
"\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
"\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
||||||
@ -128,7 +128,7 @@ export function graphql(source: "\n mutation DeleteOneFieldMetadataItem($idToDe
|
|||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadataList {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadataList {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"];
|
export function graphql(source: "\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadataList {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadataList {\n id\n fieldMetadataId\n createdAt\n updatedAt\n order\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadataList {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadataList {\n id\n fieldMetadataId\n createdAt\n updatedAt\n order\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -778,6 +778,7 @@ export type Index = {
|
|||||||
__typename?: 'Index';
|
__typename?: 'Index';
|
||||||
createdAt: Scalars['DateTime'];
|
createdAt: Scalars['DateTime'];
|
||||||
id: Scalars['UUID'];
|
id: Scalars['UUID'];
|
||||||
|
indexFieldMetadataList: Array<IndexField>;
|
||||||
indexFieldMetadatas: IndexIndexFieldMetadatasConnection;
|
indexFieldMetadatas: IndexIndexFieldMetadatasConnection;
|
||||||
indexType: IndexType;
|
indexType: IndexType;
|
||||||
indexWhereClause?: Maybe<Scalars['String']>;
|
indexWhereClause?: Maybe<Scalars['String']>;
|
||||||
|
|||||||
@ -33,6 +33,13 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
|
|||||||
indexWhereClause
|
indexWhereClause
|
||||||
indexType
|
indexType
|
||||||
isUnique
|
isUnique
|
||||||
|
indexFieldMetadataList {
|
||||||
|
id
|
||||||
|
fieldMetadataId
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
order
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fieldsList {
|
fieldsList {
|
||||||
id
|
id
|
||||||
|
|||||||
@ -3,7 +3,10 @@ import { Index as GeneratedIndex } from '~/generated-metadata/graphql';
|
|||||||
|
|
||||||
export type IndexMetadataItem = Omit<
|
export type IndexMetadataItem = Omit<
|
||||||
GeneratedIndex,
|
GeneratedIndex,
|
||||||
'__typename' | 'indexFieldMetadatas' | 'objectMetadata'
|
| '__typename'
|
||||||
|
| 'indexFieldMetadatas'
|
||||||
|
| 'objectMetadata'
|
||||||
|
| 'indexFieldMetadataList'
|
||||||
> & {
|
> & {
|
||||||
__typename?: string;
|
__typename?: string;
|
||||||
indexFieldMetadatas: IndexFieldMetadataItem[];
|
indexFieldMetadatas: IndexFieldMetadataItem[];
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { IndexFieldMetadataItem } from '@/object-metadata/types/IndexFieldMetadataItem';
|
||||||
import { IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem';
|
import { IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem';
|
||||||
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
|
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
|
||||||
import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
|
import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
|
||||||
@ -26,7 +27,12 @@ export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({
|
|||||||
(index) =>
|
(index) =>
|
||||||
({
|
({
|
||||||
...index,
|
...index,
|
||||||
indexFieldMetadatas: [],
|
indexFieldMetadatas: index.indexFieldMetadataList.map(
|
||||||
|
(indexFieldMetadata) =>
|
||||||
|
({
|
||||||
|
...indexFieldMetadata,
|
||||||
|
}) satisfies IndexFieldMetadataItem,
|
||||||
|
),
|
||||||
}) satisfies IndexMetadataItem,
|
}) satisfies IndexMetadataItem,
|
||||||
),
|
),
|
||||||
} satisfies ObjectMetadataItem;
|
} satisfies ObjectMetadataItem;
|
||||||
|
|||||||
@ -81,9 +81,9 @@ describe('spreadsheetImportGetUnicityRowHook', () => {
|
|||||||
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
|
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
|
||||||
|
|
||||||
const testData: ImportedStructuredRow<string>[] = [
|
const testData: ImportedStructuredRow<string>[] = [
|
||||||
{ 'Link URL (domainName)': 'duplicaTe.com', id: '1' },
|
{ 'Link URL (domainName)': 'duplicaTe.com' },
|
||||||
{ 'Link URL (domainName)': 'duplicate.com ', id: '2' },
|
{ 'Link URL (domainName)': 'duplicate.com ' },
|
||||||
{ 'Link URL (domainName)': 'other.com', id: '3' },
|
{ 'Link URL (domainName)': 'other.com' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const addErrorMock = jest.fn();
|
const addErrorMock = jest.fn();
|
||||||
|
|||||||
@ -1,12 +1,24 @@
|
|||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||||
import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey';
|
import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey';
|
||||||
|
import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel';
|
||||||
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||||
import {
|
import {
|
||||||
ImportedStructuredRow,
|
ImportedStructuredRow,
|
||||||
SpreadsheetImportRowHook,
|
SpreadsheetImportRowHook,
|
||||||
} from '@/spreadsheet-import/types';
|
} from '@/spreadsheet-import/types';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
import {
|
||||||
|
isDefined,
|
||||||
|
lowercaseUrlAndRemoveTrailingSlash,
|
||||||
|
} from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
type Column = {
|
||||||
|
columnName: string;
|
||||||
|
fieldType: FieldMetadataType;
|
||||||
|
};
|
||||||
|
|
||||||
export const spreadsheetImportGetUnicityRowHook = (
|
export const spreadsheetImportGetUnicityRowHook = (
|
||||||
objectMetadataItem: ObjectMetadataItem,
|
objectMetadataItem: ObjectMetadataItem,
|
||||||
@ -15,8 +27,8 @@ export const spreadsheetImportGetUnicityRowHook = (
|
|||||||
(indexMetadata) => indexMetadata.isUnique,
|
(indexMetadata) => indexMetadata.isUnique,
|
||||||
);
|
);
|
||||||
|
|
||||||
const uniqueConstraintFields = [
|
const uniqueConstraintsWithColumnNames: Column[][] = [
|
||||||
['id'],
|
[{ columnName: 'id', fieldType: FieldMetadataType.UUID }],
|
||||||
...uniqueConstraints.map((indexMetadata) =>
|
...uniqueConstraints.map((indexMetadata) =>
|
||||||
indexMetadata.indexFieldMetadatas.flatMap((indexField) => {
|
indexMetadata.indexFieldMetadatas.flatMap((indexField) => {
|
||||||
const field = objectMetadataItem.fields.find(
|
const field = objectMetadataItem.fields.find(
|
||||||
@ -35,12 +47,13 @@ export const spreadsheetImportGetUnicityRowHook = (
|
|||||||
(subField) => subField.isIncludedInUniqueConstraint,
|
(subField) => subField.isIncludedInUniqueConstraint,
|
||||||
);
|
);
|
||||||
|
|
||||||
return uniqueSubFields.map((subField) =>
|
return uniqueSubFields.map((subField) => ({
|
||||||
getSubFieldOptionKey(field, subField.subFieldName),
|
columnName: getSubFieldOptionKey(field, subField.subFieldName),
|
||||||
);
|
fieldType: field.type,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return [field.name];
|
return [{ columnName: field.name, fieldType: field.type }];
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@ -50,9 +63,13 @@ export const spreadsheetImportGetUnicityRowHook = (
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
uniqueConstraintFields.forEach((uniqueConstraint) => {
|
uniqueConstraintsWithColumnNames.forEach((uniqueConstraint) => {
|
||||||
const rowUniqueValues = getUniqueValues(row, uniqueConstraint);
|
const rowUniqueValues = getUniqueValues(row, uniqueConstraint);
|
||||||
|
|
||||||
|
if (!isNonEmptyString(rowUniqueValues)) {
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
const duplicateRows = table.filter(
|
const duplicateRows = table.filter(
|
||||||
(r) => getUniqueValues(r, uniqueConstraint) === rowUniqueValues,
|
(r) => getUniqueValues(r, uniqueConstraint) === rowUniqueValues,
|
||||||
);
|
);
|
||||||
@ -61,10 +78,10 @@ export const spreadsheetImportGetUnicityRowHook = (
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
uniqueConstraint.forEach((field) => {
|
uniqueConstraint.forEach(({ columnName }) => {
|
||||||
if (isDefined(row[field])) {
|
if (isDefined(row[columnName])) {
|
||||||
addError(field, {
|
addError(columnName, {
|
||||||
message: `This ${field} value already exists in your import data`,
|
message: t`This ${columnName} value already exists in your import data`,
|
||||||
level: 'error',
|
level: 'error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -79,9 +96,24 @@ export const spreadsheetImportGetUnicityRowHook = (
|
|||||||
|
|
||||||
const getUniqueValues = (
|
const getUniqueValues = (
|
||||||
row: ImportedStructuredRow<string>,
|
row: ImportedStructuredRow<string>,
|
||||||
uniqueConstraint: string[],
|
uniqueConstraint: Column[],
|
||||||
) => {
|
) => {
|
||||||
return uniqueConstraint
|
return uniqueConstraint
|
||||||
.map((field) => row?.[field]?.toString().trim().toLowerCase())
|
.map(({ columnName, fieldType }) => {
|
||||||
|
// need to ensure the primary link url is processed before import as on server side
|
||||||
|
if (
|
||||||
|
fieldType === FieldMetadataType.LINKS &&
|
||||||
|
columnName.includes(
|
||||||
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.LINKS]
|
||||||
|
.primaryLinkUrl,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return lowercaseUrlAndRemoveTrailingSlash(
|
||||||
|
row?.[columnName]?.toString().trim() || '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return row?.[columnName]?.toString().trim().toLowerCase();
|
||||||
|
})
|
||||||
.join('');
|
.join('');
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,31 +0,0 @@
|
|||||||
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 = 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 = 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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
export const lowercaseDomainAndRemoveTrailingSlash = (url: string) => {
|
|
||||||
try {
|
|
||||||
return new URL(url).toString().replace(/\/$/, '');
|
|
||||||
} catch {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,7 +1,9 @@
|
|||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import {
|
||||||
|
isDefined,
|
||||||
|
lowercaseUrlAndRemoveTrailingSlash,
|
||||||
|
} 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 { 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';
|
import { LinkMetadataNullable } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
|
||||||
|
|
||||||
@ -46,14 +48,14 @@ export const transformLinksValue = (
|
|||||||
return {
|
return {
|
||||||
...value,
|
...value,
|
||||||
primaryLinkUrl: isDefined(primaryLinkUrl)
|
primaryLinkUrl: isDefined(primaryLinkUrl)
|
||||||
? lowercaseDomainAndRemoveTrailingSlash(primaryLinkUrl)
|
? lowercaseUrlAndRemoveTrailingSlash(primaryLinkUrl)
|
||||||
: primaryLinkUrl,
|
: primaryLinkUrl,
|
||||||
primaryLinkLabel,
|
primaryLinkLabel,
|
||||||
secondaryLinks: JSON.stringify(
|
secondaryLinks: JSON.stringify(
|
||||||
secondaryLinks?.map((link) => ({
|
secondaryLinks?.map((link) => ({
|
||||||
...link,
|
...link,
|
||||||
url: isDefined(link.url)
|
url: isDefined(link.url)
|
||||||
? lowercaseDomainAndRemoveTrailingSlash(link.url)
|
? lowercaseUrlAndRemoveTrailingSlash(link.url)
|
||||||
: link.url,
|
: link.url,
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -2,11 +2,13 @@ import DataLoader from 'dataloader';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
FieldMetadataLoaderPayload,
|
FieldMetadataLoaderPayload,
|
||||||
|
IndexFieldMetadataLoaderPayload,
|
||||||
IndexMetadataLoaderPayload,
|
IndexMetadataLoaderPayload,
|
||||||
RelationLoaderPayload,
|
RelationLoaderPayload,
|
||||||
} from 'src/engine/dataloaders/dataloader.service';
|
} from 'src/engine/dataloaders/dataloader.service';
|
||||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto';
|
||||||
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
|
|
||||||
@ -30,4 +32,9 @@ export interface IDataloaders {
|
|||||||
IndexMetadataLoaderPayload,
|
IndexMetadataLoaderPayload,
|
||||||
IndexMetadataDTO[]
|
IndexMetadataDTO[]
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
indexFieldMetadataLoader: DataLoader<
|
||||||
|
IndexFieldMetadataLoaderPayload,
|
||||||
|
IndexFieldMetadataDTO[]
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,15 +2,18 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import DataLoader from 'dataloader';
|
import DataLoader from 'dataloader';
|
||||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
import { IndexMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface';
|
||||||
|
|
||||||
import { IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
|
import { IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
|
||||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
|
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
|
||||||
import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/relation/field-metadata-relation.service';
|
import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/relation/field-metadata-relation.service';
|
||||||
|
import { IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto';
|
||||||
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||||
@ -46,6 +49,12 @@ export type IndexMetadataLoaderPayload = {
|
|||||||
objectMetadata: Pick<ObjectMetadataInterface, 'id'>;
|
objectMetadata: Pick<ObjectMetadataInterface, 'id'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type IndexFieldMetadataLoaderPayload = {
|
||||||
|
workspaceId: string;
|
||||||
|
objectMetadata: Pick<ObjectMetadataInterface, 'id'>;
|
||||||
|
indexMetadata: Pick<IndexMetadataInterface, 'id'>;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DataloaderService {
|
export class DataloaderService {
|
||||||
constructor(
|
constructor(
|
||||||
@ -58,11 +67,13 @@ export class DataloaderService {
|
|||||||
const relationLoader = this.createRelationLoader();
|
const relationLoader = this.createRelationLoader();
|
||||||
const fieldMetadataLoader = this.createFieldMetadataLoader();
|
const fieldMetadataLoader = this.createFieldMetadataLoader();
|
||||||
const indexMetadataLoader = this.createIndexMetadataLoader();
|
const indexMetadataLoader = this.createIndexMetadataLoader();
|
||||||
|
const indexFieldMetadataLoader = this.createIndexFieldMetadataLoader();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
relationLoader,
|
relationLoader,
|
||||||
fieldMetadataLoader,
|
fieldMetadataLoader,
|
||||||
indexMetadataLoader,
|
indexMetadataLoader,
|
||||||
|
indexFieldMetadataLoader,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,4 +190,49 @@ export class DataloaderService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createIndexFieldMetadataLoader() {
|
||||||
|
return new DataLoader<
|
||||||
|
IndexFieldMetadataLoaderPayload,
|
||||||
|
IndexFieldMetadataDTO[]
|
||||||
|
>(async (dataLoaderParams: IndexFieldMetadataLoaderPayload[]) => {
|
||||||
|
const workspaceId = dataLoaderParams[0].workspaceId;
|
||||||
|
|
||||||
|
const { objectMetadataMaps } =
|
||||||
|
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||||
|
{ workspaceId },
|
||||||
|
);
|
||||||
|
|
||||||
|
return dataLoaderParams.map(
|
||||||
|
({
|
||||||
|
objectMetadata: { id: objectMetadataId },
|
||||||
|
indexMetadata: { id: indexMetadataId },
|
||||||
|
}) => {
|
||||||
|
const indexMetadataEntity = objectMetadataMaps.byId[
|
||||||
|
objectMetadataId
|
||||||
|
].indexMetadatas.find(
|
||||||
|
(indexMetadata) => indexMetadata.id === indexMetadataId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isDefined(indexMetadataEntity)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexMetadataEntity.indexFieldMetadatas.map(
|
||||||
|
(indexFieldMetadata) => {
|
||||||
|
return {
|
||||||
|
id: indexFieldMetadata.id,
|
||||||
|
fieldMetadataId: indexFieldMetadata.fieldMetadataId,
|
||||||
|
order: indexFieldMetadata.order,
|
||||||
|
createdAt: new Date(indexFieldMetadata.createdAt),
|
||||||
|
updatedAt: new Date(indexFieldMetadata.updatedAt),
|
||||||
|
indexMetadataId,
|
||||||
|
workspaceId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
} from '@ptc-org/nestjs-query-graphql';
|
} from '@ptc-org/nestjs-query-graphql';
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsDateString,
|
IsDate,
|
||||||
IsEnum,
|
IsEnum,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
@ -81,11 +81,11 @@ export class IndexMetadataDTO {
|
|||||||
|
|
||||||
objectMetadataId: string;
|
objectMetadataId: string;
|
||||||
|
|
||||||
@IsDateString()
|
@IsDate()
|
||||||
@Field()
|
@Field()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
@IsDateString()
|
@IsDate()
|
||||||
@Field()
|
@Field()
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
|||||||
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||||
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
|
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
|
||||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||||
|
import { IndexMetadataResolver } from 'src/engine/metadata-modules/index-metadata/index-metadata.resolver';
|
||||||
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
|
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
|
||||||
import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor';
|
import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor';
|
||||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
||||||
@ -46,7 +47,7 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [IndexMetadataService],
|
providers: [IndexMetadataService, IndexMetadataResolver],
|
||||||
exports: [IndexMetadataService],
|
exports: [IndexMetadataService],
|
||||||
})
|
})
|
||||||
export class IndexMetadataModule {}
|
export class IndexMetadataModule {}
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||||
|
import { Context, Parent, ResolveField, Resolver } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||||
|
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||||
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
|
||||||
|
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||||
|
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||||
|
import { IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto';
|
||||||
|
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||||
|
import { objectMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util';
|
||||||
|
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||||
|
|
||||||
|
@UseGuards(WorkspaceAuthGuard)
|
||||||
|
@Resolver(() => IndexMetadataDTO)
|
||||||
|
@UsePipes(ResolverValidationPipe)
|
||||||
|
@UseFilters(
|
||||||
|
PreventNestToAutoLogGraphqlErrorsFilter,
|
||||||
|
PermissionsGraphqlApiExceptionFilter,
|
||||||
|
)
|
||||||
|
export class IndexMetadataResolver {
|
||||||
|
@ResolveField(() => [IndexFieldMetadataDTO], { nullable: false })
|
||||||
|
async indexFieldMetadataList(
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
|
@Parent() indexMetadata: IndexMetadataDTO,
|
||||||
|
@Context() context: { loaders: IDataloaders },
|
||||||
|
): Promise<IndexFieldMetadataDTO[]> {
|
||||||
|
try {
|
||||||
|
const indexFieldMetadataItems =
|
||||||
|
await context.loaders.indexFieldMetadataLoader.load({
|
||||||
|
objectMetadata: { id: indexMetadata.objectMetadataId },
|
||||||
|
indexMetadata,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return indexFieldMetadataItems;
|
||||||
|
} catch (error) {
|
||||||
|
objectMetadataGraphqlApiExceptionHandler(error);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,10 +5,10 @@ import axios, { AxiosInstance } from 'axios';
|
|||||||
import uniqBy from 'lodash.uniqby';
|
import uniqBy from 'lodash.uniqby';
|
||||||
import { TWENTY_COMPANIES_BASE_URL } from 'twenty-shared/constants';
|
import { TWENTY_COMPANIES_BASE_URL } from 'twenty-shared/constants';
|
||||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||||
|
import { lowercaseUrlAndRemoveTrailingSlash } from 'twenty-shared/utils';
|
||||||
import { DeepPartial, ILike, Repository } from 'typeorm';
|
import { DeepPartial, ILike, Repository } from 'typeorm';
|
||||||
|
|
||||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
import { lowercaseDomainAndRemoveTrailingSlash } from 'src/engine/api/graphql/workspace-query-runner/utils/query-runner-links.util';
|
|
||||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||||
@ -79,7 +79,7 @@ export class CreateCompanyService {
|
|||||||
const companiesWithoutTrailingSlash = companies.map((company) => ({
|
const companiesWithoutTrailingSlash = companies.map((company) => ({
|
||||||
...company,
|
...company,
|
||||||
domainName: company.domainName
|
domainName: company.domainName
|
||||||
? lowercaseDomainAndRemoveTrailingSlash(company.domainName)
|
? lowercaseUrlAndRemoveTrailingSlash(company.domainName)
|
||||||
: undefined,
|
: undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export { getAbsoluteUrlOrThrow } from './url/getAbsoluteUrlOrThrow';
|
|||||||
export { getUrlHostnameOrThrow } from './url/getUrlHostnameOrThrow';
|
export { getUrlHostnameOrThrow } from './url/getUrlHostnameOrThrow';
|
||||||
export { isValidHostname } from './url/isValidHostname';
|
export { isValidHostname } from './url/isValidHostname';
|
||||||
export { isValidUrl } from './url/isValidUrl';
|
export { isValidUrl } from './url/isValidUrl';
|
||||||
|
export { lowercaseUrlAndRemoveTrailingSlash } from './url/lowercaseUrlAndRemoveTrailingSlash';
|
||||||
export { isDefined } from './validation/isDefined';
|
export { isDefined } from './validation/isDefined';
|
||||||
export { isLabelIdentifierFieldMetadataTypes } from './validation/isLabelIdentifierFieldMetadataTypes';
|
export { isLabelIdentifierFieldMetadataTypes } from './validation/isLabelIdentifierFieldMetadataTypes';
|
||||||
export { isValidLocale } from './validation/isValidLocale';
|
export { isValidLocale } from './validation/isValidLocale';
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { lowercaseUrlAndRemoveTrailingSlash } from '@/utils/url/lowercaseUrlAndRemoveTrailingSlash';
|
||||||
|
|
||||||
|
describe('lowercaseUrlAndRemoveTrailingSlash', () => {
|
||||||
|
it('should leave lowcased domain unchanged', () => {
|
||||||
|
const primaryLinkUrl = 'https://www.example.com/test';
|
||||||
|
const result = lowercaseUrlAndRemoveTrailingSlash(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 = lowercaseUrlAndRemoveTrailingSlash(primaryLinkUrl);
|
||||||
|
|
||||||
|
expect(result).toBe('https://www.example.com/TEST');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add a trailing slash', () => {
|
||||||
|
const primaryLinkUrl = 'https://www.example.com';
|
||||||
|
const result = lowercaseUrlAndRemoveTrailingSlash(primaryLinkUrl);
|
||||||
|
|
||||||
|
expect(result).toBe('https://www.example.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
export const lowercaseUrlAndRemoveTrailingSlash = (url: string) => {
|
||||||
|
try {
|
||||||
|
return new URL(url).toString().toLowerCase().replace(/\/$/, '');
|
||||||
|
} catch {
|
||||||
|
return url.toLowerCase();
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user