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 ed8949c2c5..e935f70ed0 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 @@ -6,7 +6,7 @@ import { FieldMetadataType, compositeTypeDefinitions, } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { isDefined, stringifySafely } from 'twenty-shared/utils'; import { DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE, @@ -194,7 +194,8 @@ export function formatResult( | string | Date | null - | undefined; + | undefined + | Record; if (!isDefined(rawUpdatedDateTime)) { continue; @@ -208,9 +209,16 @@ export function formatResult( // @ts-expect-error legacy noImplicitAny newData[dateTimeField.name] = dateIsoString; + } else if (isPlainObject(rawUpdatedDateTime)) { + const plainObjectValue = rawUpdatedDateTime; + + // @ts-expect-error legacy noImplicitAny + newData[dateTimeField.name] = plainObjectValue; } else { + const stringifiedUnknownValue = stringifySafely(rawUpdatedDateTime); + throw new Error( - `Invalid DATE_TIME field "${dateTimeField.name}", value: "${rawUpdatedDateTime}", it should be a string or Date instance, (current type : ${typeof rawUpdatedDateTime}).`, + `Invalid DATE_TIME field "${dateTimeField.name}", value: "${stringifiedUnknownValue}", it should be a string, Date instance or plain object, (current type : ${typeof rawUpdatedDateTime}).`, ); } } diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/date-time-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/date-time-field-create-input-validation.integration-spec.ts.snap index 230e817f8e..af4e350d7e 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/date-time-field-create-input-validation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/date-time-field-create-input-validation.integration-spec.ts.snap @@ -6,7 +6,7 @@ exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIM exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":{}} 1`] = `"Invalid value {} for date or date-time field "dateTimeField""`; -exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":1} 1`] = `"Invalid DATE_TIME field "dateTimeField", value: "1", it should be a string or Date instance, (current type : number)."`; +exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":1} 1`] = `"Invalid DATE_TIME field "dateTimeField", value: "1", it should be a string, Date instance or plain object, (current type : number)."`; exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":true} 1`] = `"Invalid value true for date or date-time field "dateTimeField""`; @@ -16,6 +16,6 @@ exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TI exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":{}} 1`] = `"["Invalid value {} for date or date-time field \\"dateTimeField\\""]"`; -exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":1} 1`] = `"["Invalid DATE_TIME field \\"dateTimeField\\", value: \\"1\\", it should be a string or Date instance, (current type : number)."]"`; +exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":1} 1`] = `"["Invalid DATE_TIME field \\"dateTimeField\\", value: \\"1\\", it should be a string, Date instance or plain object, (current type : number)."]"`; exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":true} 1`] = `"["Invalid value true for date or date-time field \\"dateTimeField\\""]"`; diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index 3278672d78..21b7c1e452 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -146,6 +146,7 @@ export { getHumanReadableNameFromCode } from './sentry/getHumanReadableNameFromC export { appendCopySuffix } from './strings/appendCopySuffix'; export { capitalize } from './strings/capitalize'; export { pascalCase } from './strings/pascalCase'; +export { stringifySafely } from './strings/stringifySafely'; export { uncapitalize } from './strings/uncapitalize'; export type { TipTapMarkType, diff --git a/packages/twenty-shared/src/utils/strings/__tests__/stringifySafely.test.ts b/packages/twenty-shared/src/utils/strings/__tests__/stringifySafely.test.ts new file mode 100644 index 0000000000..56b502cd96 --- /dev/null +++ b/packages/twenty-shared/src/utils/strings/__tests__/stringifySafely.test.ts @@ -0,0 +1,88 @@ +import { stringifySafely } from '../stringifySafely'; + +describe('stringifySafely', () => { + it('should stringify a simple object', () => { + expect(stringifySafely({ foo: 'bar' })).toBe('{"foo":"bar"}'); + }); + + it('should stringify an array', () => { + expect(stringifySafely([1, 2, 3])).toBe('[1,2,3]'); + }); + + it('should stringify a string', () => { + expect(stringifySafely('hello')).toBe('"hello"'); + }); + + it('should stringify a number', () => { + expect(stringifySafely(42)).toBe('42'); + }); + + it('should stringify null', () => { + expect(stringifySafely(null)).toBe('null'); + }); + + it('should stringify undefined', () => { + expect(stringifySafely(undefined)).toBe('undefined'); + }); + + it('should stringify a boolean', () => { + expect(stringifySafely(true)).toBe('true'); + expect(stringifySafely(false)).toBe('false'); + }); + + it('should stringify +Infinity', () => { + expect(stringifySafely(+Infinity)).toBe('Infinity'); + }); + + it('should stringify Infinity', () => { + expect(stringifySafely(Infinity)).toBe('Infinity'); + }); + + it('should stringify -Infinity', () => { + expect(stringifySafely(-Infinity)).toBe('-Infinity'); + }); + + it('should stringify NaN', () => { + expect(stringifySafely(NaN)).toBe('NaN'); + }); + + it('should fall back to String() for circular references', () => { + const circularObj: Record = { foo: 'bar' }; + circularObj.self = circularObj; + + expect(stringifySafely(circularObj)).toBe('[object Object]'); + }); + + it('should fall back to String() for BigInt values', () => { + const bigIntValue = BigInt(9007199254740991); + + expect(stringifySafely(bigIntValue)).toBe('9007199254740991'); + }); + + it('should fall back to String() for functions', () => { + // eslint-disable-next-line func-style, prefer-arrow/prefer-arrow-functions + const namedFunction = function myFunction() { + return 'test'; + }; + + expect(stringifySafely(namedFunction)).toBe(namedFunction.toString()); + }); + + it('should fall back to String() for arrow functions', () => { + const arrowFunction = () => 'test'; + + expect(stringifySafely(arrowFunction)).toBe(arrowFunction.toString()); + }); + + it('should fall back to String() for symbols', () => { + const symbol = Symbol('testSymbol'); + + expect(stringifySafely(symbol)).toBe('Symbol(testSymbol)'); + }); + + it('should fall back to String() for symbols without description', () => { + const symbol = Symbol(); + + expect(stringifySafely(symbol)).toBe('Symbol()'); + }); +}); diff --git a/packages/twenty-shared/src/utils/strings/stringifySafely.ts b/packages/twenty-shared/src/utils/strings/stringifySafely.ts new file mode 100644 index 0000000000..9097cdaa80 --- /dev/null +++ b/packages/twenty-shared/src/utils/strings/stringifySafely.ts @@ -0,0 +1,25 @@ +export const stringifySafely = (value: unknown): string => { + try { + if (value === undefined) { + return 'undefined'; + } else if (value === null) { + return 'null'; + } else if (value === Infinity) { + return 'Infinity'; + } else if (value === -Infinity) { + return '-Infinity'; + } else if (typeof value === 'number' && isNaN(value)) { + return 'NaN'; + } + + const stringifiedValue = JSON.stringify(value); + + if (stringifiedValue === undefined) { + return String(value); + } + + return stringifiedValue; + } catch { + return String(value); + } +};