diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-composite-field-value.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-composite-field-value.util.spec.ts new file mode 100644 index 0000000000..787e29c6e5 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-composite-field-value.util.spec.ts @@ -0,0 +1,61 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock'; +import { formatCompositeFieldValue } from 'src/engine/twenty-orm/utils/format-composite-field-value.util'; + +describe('formatCompositeFieldValue', () => { + const addressFieldMetadata = getFlatFieldMetadataMock({ + universalIdentifier: 'address', + objectMetadataId: 'object-metadata-id', + type: FieldMetadataType.ADDRESS, + }); + + const currencyFieldMetadata = getFlatFieldMetadataMock({ + universalIdentifier: 'amount', + objectMetadataId: 'object-metadata-id', + type: FieldMetadataType.CURRENCY, + }); + + it('should parse addressLat/addressLng returned as strings into numbers', () => { + expect( + formatCompositeFieldValue( + '40.7532256', + 'addressLat', + addressFieldMetadata, + ), + ).toBe(40.7532256); + expect( + formatCompositeFieldValue( + '-73.99294600000002', + 'addressLng', + addressFieldMetadata, + ), + ).toBe(-73.99294600000002); + }); + + it('should keep coordinates that are already numbers unchanged', () => { + expect( + formatCompositeFieldValue(40.7532256, 'addressLat', addressFieldMetadata), + ).toBe(40.7532256); + }); + + it('should not coerce text address subfields that look numeric', () => { + expect( + formatCompositeFieldValue( + '10001', + 'addressPostcode', + addressFieldMetadata, + ), + ).toBe('10001'); + }); + + it('should still parse currency amountMicros returned as a string', () => { + expect( + formatCompositeFieldValue( + '5000000', + 'amountMicros', + currencyFieldMetadata, + ), + ).toBe(5000000); + }); +}); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-composite-field-value.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-composite-field-value.util.ts new file mode 100644 index 0000000000..86ab8c37df --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-composite-field-value.util.ts @@ -0,0 +1,38 @@ +import { isNonEmptyString } from '@sniptt/guards'; +import { FieldMetadataType } from 'twenty-shared/types'; + +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; + +export const formatCompositeFieldValue = ( + value: unknown, + compositePropertyName: string, + fieldMetadata: FlatFieldMetadata, +) => { + switch (fieldMetadata.type) { + case FieldMetadataType.CURRENCY: { + if (compositePropertyName === 'amountMicros') { + if (isNonEmptyString(value)) { + return parseInt(value); + } + + return value; + } + break; + } + case FieldMetadataType.ADDRESS: { + if ( + compositePropertyName === 'addressLat' || + compositePropertyName === 'addressLng' + ) { + if (isNonEmptyString(value)) { + return parseFloat(value); + } + + return value; + } + break; + } + } + + return value; +}; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts index 7f8814b864..68716ebe83 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts @@ -1,6 +1,6 @@ import { isPlainObject } from '@nestjs/common/utils/shared.utils'; -import { isNonEmptyString, isNull } from '@sniptt/guards'; +import { isNull } from '@sniptt/guards'; import { FieldActorSource, FieldMetadataType, @@ -23,6 +23,7 @@ import { type FieldMapsForObject, } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { formatCompositeFieldValue } from 'src/engine/twenty-orm/utils/format-composite-field-value.util'; import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; @@ -292,26 +293,6 @@ function transformCompositeFieldNullValue( ); } -function formatCompositeFieldValue( - value: unknown, - compositePropertyName: string, - fieldMetadata: FlatFieldMetadata, -) { - switch (fieldMetadata.type) { - case FieldMetadataType.CURRENCY: { - if (compositePropertyName === 'amountMicros') { - if (isNonEmptyString(value)) { - return parseInt(value); - } - - return value; - } - } - } - - return value; -} - /** * Handles composite fields with missing required subfields. * - For nullable fields: sets to null if all required subfields are null