Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 91b2a944eb Unique field validation prevents creating multiple records with empty values — empty strings stored for composite field sub-columns conflict on PostgreSQL UNIQUE index
https://sonarly.com/issue/26872?type=bug

When a composite field (PHONES, EMAILS, LINKS) is marked as Unique on the People object, creating multiple records with that field left empty fails with "This value is already in use." The root cause is a two-part defect: (1) composite field sub-values like `primaryPhoneNumber` are stored as empty strings `''` rather than NULL when left blank, and (2) the unique index has no WHERE clause to exclude null-equivalent values. PostgreSQL treats two `''` values as duplicates in a UNIQUE index (unlike NULLs, which are treated as distinct). Simple TEXT fields do not exhibit this bug because `transformTextField` converts `''` to `null` before storage.

Fix: The fix normalizes empty-string phone sub-field values to null/undefined in `validateAndInferPhoneInput`, matching the existing pattern in `transformEmailsValue`.

**What changed:** In `transform-phones-value.util.ts`, the else branch of `validateAndInferPhoneInput` (when no valid phone number is provided) now:
- Converts empty-string `number` to `null` (so PostgreSQL stores NULL instead of '', allowing multiple empty records under a UNIQUE index)
- Converts empty-string `callingCode`/`countryCode` to `undefined` (removed by `removeUndefinedFields`, not stored)
- Preserves `undefined` inputs as-is (field not included in update operations)

**Why this works:** PostgreSQL treats multiple NULLs as distinct in UNIQUE indexes, but treats multiple empty strings ('') as duplicates. By ensuring empty phone fields are stored as NULL rather than '', the unique constraint allows multiple records with empty phone fields.

**Test added:** `transform-phones-value.util.spec.ts` covers the empty-string normalization case, undefined passthrough, and valid phone number handling, following the exact test pattern from the existing `transform-emails-value.util.spec.ts`.
2026-04-16 01:19:48 +00:00
2 changed files with 69 additions and 3 deletions
@@ -0,0 +1,66 @@
import { transformPhonesValue } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util';
describe('transformPhonesValue', () => {
it('should return undefined when input is undefined', () => {
const result = transformPhonesValue({ input: undefined });
expect(result).toBeUndefined();
});
it('should return null when input is null', () => {
const result = transformPhonesValue({ input: null });
expect(result).toBeNull();
});
it('should return null for primaryPhoneNumber when it is empty string', () => {
const result = transformPhonesValue({
input: {
primaryPhoneNumber: '',
primaryPhoneCountryCode: '',
primaryPhoneCallingCode: '',
additionalPhones: null,
},
});
expect(result?.primaryPhoneNumber).toBeNull();
});
it('should return undefined for primaryPhoneNumber when it is not provided', () => {
const result = transformPhonesValue({
input: {
additionalPhones: null,
},
});
expect(result?.primaryPhoneNumber).toBeUndefined();
});
it('should validate and return a valid phone number', () => {
const result = transformPhonesValue({
input: {
primaryPhoneNumber: '612345678',
primaryPhoneCountryCode: 'FR',
primaryPhoneCallingCode: '+33',
additionalPhones: null,
},
});
expect(result?.primaryPhoneNumber).toBe('612345678');
expect(result?.primaryPhoneCountryCode).toBe('FR');
expect(result?.primaryPhoneCallingCode).toBe('+33');
});
it('should return null additionalPhones when empty array is provided', () => {
const result = transformPhonesValue({
input: {
primaryPhoneNumber: '',
primaryPhoneCountryCode: '',
primaryPhoneCallingCode: '',
additionalPhones: null,
},
});
expect(result?.additionalPhones).toBeNull();
});
});
@@ -169,9 +169,9 @@ const validateAndInferPhoneInput = ({
}
return {
callingCode,
countryCode,
number,
callingCode: isNonEmptyString(callingCode) ? callingCode : undefined,
countryCode: isNonEmptyString(countryCode) ? countryCode : undefined,
number: isDefined(number) ? null : undefined,
};
};