-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Changes from 3 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
4c65460
Fallback to default value when migrating value from enum
ijreilly 1cf33ba
Forbid naming custom fields like composite fields subfields
ijreilly fd26878
Revert "Fallback to default value when migrating value from enum"
ijreilly ac0b465
Dynamically compute reserved composite fields subfields names
ijreilly 1cd1db6
Fix typo
ijreilly 4c4e9f9
Remove unnecessary || []
ijreilly caefbee
rename functions
ijreilly 770defc
Check name availability for relations
ijreilly 0c9053f
fix tests
ijreilly 8f135dc
fix test
ijreilly d113d56
fix test
ijreilly File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
...nty-server/src/engine/metadata-modules/utils/__tests__/validate-name-availability.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
92 changes: 92 additions & 0 deletions
92
packages/twenty-server/src/engine/metadata-modules/utils/validate-name-availability.utils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)