[Fix] Prevent fields name conflicts with composite subfields names (#6713)

At field creation we are checking the availability of the name by
comparing it to the other fields' names' on the object; but for
composite fields the fields' names' as indicated in the repository do
not exactly match the column names' on the tables (e.g "createdBy" field
is actually represented by columns createdByName, createdBySource etc.).

In this PR we prevent the conflict with the standard composite fields'
names.
There is still room for errors with the custom composite fields: for
example a custom composite field "address" of type address on a custom
object "listing" will introduce the columns addressAddressStreet1,
addressAddressStreet2 etc. while we won't prevent the user from later
creating a custom field named "addressAddressStreet1".
For now I decided not to tackle this as this seem extremely edgy + would
impact performance on creation of all fields while never actually useful
(I think).
This commit is contained in:
Marie
2024-08-23 13:24:10 +02:00
committed by GitHub
parent 981f311ed0
commit ee6180a76f
27 changed files with 279 additions and 170 deletions

View File

@ -0,0 +1,60 @@
import {
FIELD_ACTOR_MOCK_NAME,
FIELD_ADDRESS_MOCK_NAME,
FIELD_CURRENCY_MOCK_NAME,
FIELD_FULL_NAME_MOCK_NAME,
FIELD_LINKS_MOCK_NAME,
objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { NameNotAvailableException } from 'src/engine/metadata-modules/utils/exceptions/name-not-available.exception';
import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils';
describe('validateFieldNameAvailabilityOrThrow', () => {
const objectMetadata = objectMetadataItemMock;
it('does not throw if name is not reserved', () => {
const name = 'testName';
expect(() =>
validateFieldNameAvailabilityOrThrow(name, objectMetadata),
).not.toThrow();
});
describe('error cases', () => {
it('throws error with LINKS suffixes', () => {
const name = `${FIELD_LINKS_MOCK_NAME}PrimaryLinkLabel`;
expect(() =>
validateFieldNameAvailabilityOrThrow(name, objectMetadata),
).toThrow(NameNotAvailableException);
});
it('throws error with CURRENCY suffixes', () => {
const name = `${FIELD_CURRENCY_MOCK_NAME}AmountMicros`;
expect(() =>
validateFieldNameAvailabilityOrThrow(name, objectMetadata),
).toThrow(NameNotAvailableException);
});
it('throws error with FULL_NAME suffixes', () => {
const name = `${FIELD_FULL_NAME_MOCK_NAME}FirstName`;
expect(() =>
validateFieldNameAvailabilityOrThrow(name, objectMetadata),
).toThrow(NameNotAvailableException);
});
it('throws error with ACTOR suffixes', () => {
const name = `${FIELD_ACTOR_MOCK_NAME}Name`;
expect(() =>
validateFieldNameAvailabilityOrThrow(name, objectMetadata),
).toThrow(NameNotAvailableException);
});
it('throws error with ADDRESS suffixes', () => {
const name = `${FIELD_ADDRESS_MOCK_NAME}AddressStreet1`;
expect(() =>
validateFieldNameAvailabilityOrThrow(name, objectMetadata),
).toThrow(NameNotAvailableException);
});
});
});

View File

@ -1,26 +1,24 @@
import {
validateMetadataNameOrThrow,
InvalidStringException,
NameTooLongException,
} from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
import { InvalidStringException } from 'src/engine/metadata-modules/utils/exceptions/invalid-string.exception';
import { NameTooLongException } from 'src/engine/metadata-modules/utils/exceptions/name-too-long.exception';
import { validateMetadataNameValidityOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-validity.utils';
describe('validateMetadataNameOrThrow', () => {
describe('validateMetadataNameValidityOrThrow', () => {
it('does not throw if string is valid', () => {
const input = 'testName';
expect(validateMetadataNameOrThrow(input)).not.toThrow;
expect(validateMetadataNameValidityOrThrow(input)).not.toThrow;
});
it('throws error if string has spaces', () => {
const input = 'name with spaces';
expect(() => validateMetadataNameOrThrow(input)).toThrow(
expect(() => validateMetadataNameValidityOrThrow(input)).toThrow(
InvalidStringException,
);
});
it('throws error if string starts with capital letter', () => {
const input = 'StringStartingWithCapitalLetter';
expect(() => validateMetadataNameOrThrow(input)).toThrow(
expect(() => validateMetadataNameValidityOrThrow(input)).toThrow(
InvalidStringException,
);
});
@ -28,7 +26,7 @@ describe('validateMetadataNameOrThrow', () => {
it('throws error if string has non latin characters', () => {
const input = 'בְרִבְרִ';
expect(() => validateMetadataNameOrThrow(input)).toThrow(
expect(() => validateMetadataNameValidityOrThrow(input)).toThrow(
InvalidStringException,
);
});
@ -36,7 +34,7 @@ describe('validateMetadataNameOrThrow', () => {
it('throws error if starts with digits', () => {
const input = '123string';
expect(() => validateMetadataNameOrThrow(input)).toThrow(
expect(() => validateMetadataNameValidityOrThrow(input)).toThrow(
InvalidStringException,
);
});
@ -44,14 +42,15 @@ describe('validateMetadataNameOrThrow', () => {
const inputWith63Characters =
'thisIsAstringWithSixtyThreeCharacters11111111111111111111111111';
expect(validateMetadataNameOrThrow(inputWith63Characters)).not.toThrow;
expect(validateMetadataNameValidityOrThrow(inputWith63Characters)).not
.toThrow;
});
it('throws error if string is above 63 characters', () => {
const inputWith64Characters =
'thisIsAstringWithSixtyFourCharacters1111111111111111111111111111';
expect(() => validateMetadataNameOrThrow(inputWith64Characters)).toThrow(
NameTooLongException,
);
expect(() =>
validateMetadataNameValidityOrThrow(inputWith64Characters),
).toThrow(NameTooLongException);
});
});

View File

@ -0,0 +1,7 @@
export class InvalidStringException extends Error {
constructor(string: string) {
const message = `String "${string}" is not valid`;
super(message);
}
}

View File

@ -0,0 +1,7 @@
export class NameNotAvailableException extends Error {
constructor(name: string) {
const message = `Name "${name}" is not available`;
super(message);
}
}

View File

@ -0,0 +1,7 @@
export class NameTooLongException extends Error {
constructor(string: string) {
const message = `String "${string}" exceeds 63 characters limit`;
super(message);
}
}

View File

@ -0,0 +1,38 @@
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { NameNotAvailableException } from 'src/engine/metadata-modules/utils/exceptions/name-not-available.exception';
const getReservedCompositeFieldNames = (
objectMetadata: ObjectMetadataEntity,
) => {
const reservedCompositeFieldsNames: string[] = [];
for (const field of objectMetadata.fields) {
if (isCompositeFieldMetadataType(field.type)) {
const base = field.name;
const compositeType = compositeTypeDefinitions.get(field.type);
compositeType?.properties.map((property) =>
reservedCompositeFieldsNames.push(
computeCompositeColumnName(base, property),
),
);
}
}
return reservedCompositeFieldsNames;
};
export const validateFieldNameAvailabilityOrThrow = (
name: string,
objectMetadata: ObjectMetadataEntity,
) => {
const reservedCompositeFieldsNames =
getReservedCompositeFieldNames(objectMetadata);
if (reservedCompositeFieldsNames.includes(name)) {
throw new NameNotAvailableException(name);
}
};

View File

@ -0,0 +1,14 @@
import { InvalidStringException } from 'src/engine/metadata-modules/utils/exceptions/invalid-string.exception';
import { NameTooLongException } from 'src/engine/metadata-modules/utils/exceptions/name-too-long.exception';
import { exceedsDatabaseIdentifierMaximumLength } from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils';
const VALID_STRING_PATTERN = /^[a-z][a-zA-Z0-9]*$/;
export const validateMetadataNameValidityOrThrow = (name: string) => {
if (!name.match(VALID_STRING_PATTERN)) {
throw new InvalidStringException(name);
}
if (exceedsDatabaseIdentifierMaximumLength(name)) {
throw new NameTooLongException(name);
}
};

View File

@ -1,28 +0,0 @@
import { exceedsDatabaseIdentifierMaximumLength } from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils';
const VALID_STRING_PATTERN = /^[a-z][a-zA-Z0-9]*$/;
export const validateMetadataNameOrThrow = (name: string) => {
if (!name.match(VALID_STRING_PATTERN)) {
throw new InvalidStringException(name);
}
if (exceedsDatabaseIdentifierMaximumLength(name)) {
throw new NameTooLongException(name);
}
};
export class InvalidStringException extends Error {
constructor(string: string) {
const message = `String "${string}" is not valid`;
super(message);
}
}
export class NameTooLongException extends Error {
constructor(string: string) {
const message = `String "${string}" exceeds 63 characters limit`;
super(message);
}
}