Fixed plain object in field value for workflow (#17470)

This PR fixes a bug that arises in `formatResult` following up the
recent refactor for DATE_TIME :
https://github.com/twentyhq/twenty/pull/17407

In the case of workflows, we pass a plain object to `formatResult` : 

```ts
{
  before: null, 
  after: '2026-01-27T10:59:15.525Z'
}
```

So a case has been added to handle this. 

@Weiko are we ok with this more restrictive else-if part in this util ?
We could also just put a `continue` for unknown shapes.
This commit is contained in:
Lucas Bordeau
2026-01-27 14:21:56 +01:00
committed by GitHub
parent c5953c9d50
commit 326585157c
5 changed files with 127 additions and 5 deletions
@@ -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<T>(
| string
| Date
| null
| undefined;
| undefined
| Record<string, unknown>;
if (!isDefined(rawUpdatedDateTime)) {
continue;
@@ -208,9 +209,16 @@ export function formatResult<T>(
// @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}).`,
);
}
}
@@ -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\\""]"`;
@@ -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,
@@ -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<string, unknown> = { 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()');
});
});
@@ -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);
}
};