Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 11 commits into from
Aug 23, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ import {
NameTooLongException,
validateMetadataNameOrThrow,
} from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
import {
NameNotAvailableException,
validateNameAvailabilityOrThrow,
} from 'src/engine/metadata-modules/utils/validate-name-availability.utils';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import {
Expand Down Expand Up @@ -140,7 +144,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
);
}

this.validateFieldMetadataInput<CreateFieldInput>(fieldMetadataInput);
this.validateFieldMetadataInput<CreateFieldInput>(
fieldMetadataInput,
objectMetadata.standardId,
);

const fieldAlreadyExists = await fieldMetadataRepository.findOne({
where: {
Expand Down Expand Up @@ -355,7 +362,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
}
}

this.validateFieldMetadataInput<UpdateFieldInput>(fieldMetadataInput);
this.validateFieldMetadataInput<UpdateFieldInput>(
fieldMetadataInput,
objectMetadata.standardId,
);

const updatableFieldInput =
existingFieldMetadata.isCustom === false
Expand Down Expand Up @@ -673,10 +683,14 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit

private validateFieldMetadataInput<
T extends UpdateFieldInput | CreateFieldInput,
>(fieldMetadataInput: T): T {
>(fieldMetadataInput: T, objectMetadataStandardId: string | null): T {
if (fieldMetadataInput.name) {
try {
validateMetadataNameOrThrow(fieldMetadataInput.name);
validateNameAvailabilityOrThrow(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure to completely understand this validateMetadataNameOrThrow vs validateNameAvailabilityOrThrow from the outside
I feel they should be wrapped together
maybe validateMetadataNameOrThrow should call validateNameAvailabilityOrThrow internally?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the functions names to add clarity, but one cannot call another as validateMetadataNameOrThrow (renamed to validateMetadataNameValidityOrThrow) checks for the correctness of the string (only contains non-latin char, does not start with digit etc) and is used in other services to check all metadata names (for objects and relations too)

fieldMetadataInput.name,
objectMetadataStandardId,
);
} catch (error) {
if (error instanceof InvalidStringException) {
throw new FieldMetadataException(
Expand All @@ -688,6 +702,11 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
`Name "${fieldMetadataInput.name}" exceeds 63 characters`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
} else if (error instanceof NameNotAvailableException) {
throw new FieldMetadataException(
`Name "${fieldMetadataInput.name}" is not available`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
} else {
throw error;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
NameNotAvailableException,
validateNameAvailabilityOrThrow,
} from 'src/engine/metadata-modules/utils/validate-name-availability.utils';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';

describe('validateNameAvailabilityOrThrow', () => {
describe('on person', () => {
const objectMetadataStandardId = STANDARD_OBJECT_IDS.person;

it('does not throw if name is not reserved', () => {
const name = 'testName';

expect(() =>
validateNameAvailabilityOrThrow(name, objectMetadataStandardId),
).not.toThrow();
});
it('throws error if name is reserved', () => {
const name = 'nameFirstName';

expect(() =>
validateNameAvailabilityOrThrow(name, objectMetadataStandardId),
).toThrow(NameNotAvailableException);
});
});
describe('on company', () => {
const objectMetadataStandardId = STANDARD_OBJECT_IDS.company;

it('does not throw if name is not reserved', () => {
const name = 'nameFirstName';

expect(() =>
validateNameAvailabilityOrThrow(name, objectMetadataStandardId),
).not.toThrow();
});
it('throws error if name is reserved', () => {
const name = 'domainNamePrimaryLinkUrl';

expect(() =>
validateNameAvailabilityOrThrow(name, objectMetadataStandardId),
).toThrow(NameNotAvailableException);
});
});
describe('on opportunity', () => {
const objectMetadataStandardId = STANDARD_OBJECT_IDS.opportunity;

it('does not throw if name is not reserved', () => {
const name = 'nameFirstName';

expect(() =>
validateNameAvailabilityOrThrow(name, objectMetadataStandardId),
).not.toThrow();
});
it('throws error if name is reserved', () => {
const name = 'annualRecurringRevenueAmountMicros';

expect(() =>
validateNameAvailabilityOrThrow(name, objectMetadataStandardId),
).toThrow(NameNotAvailableException);
});
});
describe('on custom object', () => {
const objectMetadataStandardId = null;

it('does not throw if name is not reserved', () => {
const name = 'nameFirstName';

expect(() =>
validateNameAvailabilityOrThrow(name, objectMetadataStandardId),
).not.toThrow();
});
it('throws error if name is reserved', () => {
const name = 'createdByName';

expect(() =>
validateNameAvailabilityOrThrow(name, objectMetadataStandardId),
).toThrow(NameNotAvailableException);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';

const createdByFieldsNames = [
'createdByName',
'createdByWorkspaceMemberId',
'createdBySource',
];

const linkedinLinkFieldsNames = [
'linkedinLinkPrimaryLinkLabel',
'linkedinLinkPrimaryLinkUrl',
'linkedinLinkPrimaryLinkLabel',
];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Duplicate 'linkedinLinkPrimaryLinkLabel' in linkedinLinkFieldsNames array


const xLinkFieldsNames = [
'xLinkPrimaryLinkLabel',
'xLinkPrimaryLinkUrl',
'xLinkSecondaryLinks',
];

const amountFieldsNames = [
'annualRecurringRevenueAmountMicros',
'annualRecurringRevenueCurrencyCode',
];

const reservedCompositeFieldsNamesForPerson = [
...createdByFieldsNames,
...linkedinLinkFieldsNames,
...xLinkFieldsNames,
'nameFirstName',
'nameLastName',
];
const reservedCompositeFieldsNamesForCompany = [
...createdByFieldsNames,
...linkedinLinkFieldsNames,
...xLinkFieldsNames,
...amountFieldsNames,
'domainNamePrimaryLinkLabel',
'domainNamePrimaryLinkUrl',
'domainNameSecondaryLinks',
'addressAddressStreet1',
'addressAddressStreet2',
'addressAddressCity',
'addressAddressState',
'addressAddressPostcode',
'addressAddressCountry',
'addressAddressLat',
'addressAddressLng',
'introVideoPrimaryLinkLabel',
'introVideoPrimaryLinkUrl',
'introVideoSecondaryLinks',
];
const reservedCompositeFieldsNamesForOpportunity = [
...createdByFieldsNames,
...amountFieldsNames,
];

const getReservedCompositeFieldsNames = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getReservedCompositeFieldNames

objectMetadataStandardId: string | null,
) => {
switch (objectMetadataStandardId) {
case STANDARD_OBJECT_IDS.person:
return reservedCompositeFieldsNamesForPerson;
case STANDARD_OBJECT_IDS.company:
return reservedCompositeFieldsNamesForCompany;
case STANDARD_OBJECT_IDS.opportunity:
return reservedCompositeFieldsNamesForOpportunity;
default:
return createdByFieldsNames;
}
};

export const validateNameAvailabilityOrThrow = (
name: string,
objectMetadataStandardId: string | null,
) => {
const reservedCompositeFieldsNames = getReservedCompositeFieldsNames(
objectMetadataStandardId,
);

if (reservedCompositeFieldsNames.includes(name)) {
throw new NameNotAvailableException(name);
}
};

export class NameNotAvailableException extends Error {
ijreilly marked this conversation as resolved.
Show resolved Hide resolved
constructor(name: string) {
const message = `Name "${name}" is not available`;

super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { QueryRunner, TableColumn } from 'typeorm';
import { v4 } from 'uuid';

import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
import { unserializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/unserialize-default-value';
import {
WorkspaceMigrationColumnAlter,
WorkspaceMigrationRenamedEnum,
Expand Down Expand Up @@ -80,15 +79,30 @@ export class WorkspaceMigrationEnumService {
}),
);

await this.migrateEnumValues(
queryRunner,
schemaName,
migrationColumn,
tableName,
oldColumnName,
enumValues,
renamedEnumValues,
);
if (columnDefinition.isNullable) {
await this.migrateEnumValues(
queryRunner,
schemaName,
migrationColumn,
tableName,
oldColumnName,
enumValues,
renamedEnumValues,
);
} else {
const defaultValue = columnDefinition.defaultValue?.replaceAll("'", '');

await this.migrateEnumValues(
queryRunner,
schemaName,
migrationColumn,
tableName,
oldColumnName,
enumValues,
renamedEnumValues,
defaultValue,
);
}

// Drop old column
await queryRunner.query(`
Expand Down Expand Up @@ -146,6 +160,7 @@ export class WorkspaceMigrationEnumService {
oldColumnName: string,
enumValues: string[],
renamedEnumValues?: WorkspaceMigrationRenamedEnum[],
defaultValueFallback?: string,
) {
const columnDefinition = migrationColumn.alteredColumnDefinition;

Expand Down Expand Up @@ -176,9 +191,7 @@ export class WorkspaceMigrationEnumService {
value: val,
renamedEnumValues: renamedEnumValues,
allEnumValues: enumValues,
defaultValueFallback: columnDefinition.isNullable
? null
: unserializeDefaultValue(columnDefinition.defaultValue),
defaultValueFallback: defaultValueFallback,
});

val = isDefined(migratedValue) ? `'${migratedValue}'` : null;
Expand Down
Loading