212 lines
6.4 KiB
TypeScript
212 lines
6.4 KiB
TypeScript
import { isPlainObject } from '@nestjs/common/utils/shared.utils';
|
|
|
|
import { isNonEmptyString } from '@sniptt/guards';
|
|
import { FieldMetadataType } from 'twenty-shared/types';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
|
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
|
|
|
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
|
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
|
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
|
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
|
import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection';
|
|
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
|
import { isDate } from 'src/utils/date/isDate';
|
|
import { isValidDate } from 'src/utils/date/isValidDate';
|
|
|
|
export function formatResult<T>(
|
|
data: any,
|
|
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps | undefined,
|
|
objectMetadataMaps: ObjectMetadataMaps,
|
|
): T {
|
|
if (!data) {
|
|
return data;
|
|
}
|
|
|
|
if (Array.isArray(data)) {
|
|
return data.map((item) =>
|
|
formatResult(item, objectMetadataItemWithFieldMaps, objectMetadataMaps),
|
|
) as T;
|
|
}
|
|
|
|
if (!isPlainObject(data)) {
|
|
return data;
|
|
}
|
|
|
|
if (!objectMetadataItemWithFieldMaps) {
|
|
throw new Error('Object metadata is missing');
|
|
}
|
|
|
|
const compositeFieldMetadataMap = getCompositeFieldMetadataMap(
|
|
objectMetadataItemWithFieldMaps,
|
|
);
|
|
|
|
const newData: object = {};
|
|
const objectMetadaItemFieldsByName =
|
|
objectMetadataMaps.byId[objectMetadataItemWithFieldMaps.id]?.fieldsByName;
|
|
|
|
for (const [key, value] of Object.entries(data)) {
|
|
const compositePropertyArgs = compositeFieldMetadataMap.get(key);
|
|
|
|
const fieldMetadata = objectMetadataItemWithFieldMaps.fieldsByName[key] as
|
|
| FieldMetadataInterface<FieldMetadataType>
|
|
| undefined;
|
|
|
|
const isRelation = fieldMetadata
|
|
? isFieldMetadataInterfaceOfType(
|
|
fieldMetadata,
|
|
FieldMetadataType.RELATION,
|
|
)
|
|
: false;
|
|
|
|
if (!compositePropertyArgs && !isRelation) {
|
|
if (isPlainObject(value)) {
|
|
newData[key] = formatResult(
|
|
value,
|
|
objectMetadataItemWithFieldMaps,
|
|
objectMetadataMaps,
|
|
);
|
|
} else if (objectMetadaItemFieldsByName[key]) {
|
|
newData[key] = formatFieldMetadataValue(
|
|
value,
|
|
objectMetadaItemFieldsByName[key],
|
|
);
|
|
} else {
|
|
newData[key] = value;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (isRelation) {
|
|
if (!isDefined(fieldMetadata?.relationTargetObjectMetadataId)) {
|
|
throw new Error(
|
|
`Relation target object metadata ID is missing for field "${key}"`,
|
|
);
|
|
}
|
|
|
|
const targetObjectMetadata =
|
|
objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId];
|
|
|
|
if (!targetObjectMetadata) {
|
|
throw new Error(
|
|
`Object metadata for object metadataId "${fieldMetadata.relationTargetObjectMetadataId}" is missing`,
|
|
);
|
|
}
|
|
|
|
newData[key] = formatResult(
|
|
value,
|
|
targetObjectMetadata,
|
|
objectMetadataMaps,
|
|
);
|
|
}
|
|
|
|
if (!compositePropertyArgs) {
|
|
continue;
|
|
}
|
|
|
|
const { parentField, ...compositeProperty } = compositePropertyArgs;
|
|
|
|
if (!newData[parentField]) {
|
|
newData[parentField] = {};
|
|
}
|
|
|
|
newData[parentField][compositeProperty.name] = value;
|
|
}
|
|
|
|
const dateFieldMetadataCollection =
|
|
objectMetadataItemWithFieldMaps.fields.filter(
|
|
(field) => field.type === FieldMetadataType.DATE,
|
|
);
|
|
|
|
// This is a temporary fix to handle a bug in the frontend where the date gets returned in the wrong timezone,
|
|
// thus returning the wrong date.
|
|
//
|
|
// In short, for example :
|
|
// - DB stores `2025-01-01`
|
|
// - TypeORM .returning() returns `2024-12-31T23:00:00.000Z`
|
|
// - we shift +1h (or whatever the timezone offset is on the server)
|
|
// - we return `2025-01-01T00:00:00.000Z`
|
|
//
|
|
// See this PR for more details: https://github.com/twentyhq/twenty/pull/9700
|
|
const serverOffsetInMillisecondsToCounterActTypeORMAutomaticTimezoneShift =
|
|
new Date().getTimezoneOffset() * 60 * 1000;
|
|
|
|
for (const dateFieldMetadata of dateFieldMetadataCollection) {
|
|
const rawUpdatedDate = newData[dateFieldMetadata.name] as
|
|
| string
|
|
| null
|
|
| undefined
|
|
| Date;
|
|
|
|
if (!isDefined(rawUpdatedDate)) {
|
|
continue;
|
|
}
|
|
|
|
if (isDate(rawUpdatedDate)) {
|
|
if (isValidDate(rawUpdatedDate)) {
|
|
const shiftedDate = new Date(
|
|
rawUpdatedDate.getTime() -
|
|
serverOffsetInMillisecondsToCounterActTypeORMAutomaticTimezoneShift,
|
|
);
|
|
|
|
newData[dateFieldMetadata.name] = shiftedDate;
|
|
}
|
|
} else if (isNonEmptyString(rawUpdatedDate)) {
|
|
const currentDate = new Date(newData[dateFieldMetadata.name]);
|
|
|
|
const shiftedDate = new Date(
|
|
new Date(currentDate).getTime() -
|
|
serverOffsetInMillisecondsToCounterActTypeORMAutomaticTimezoneShift,
|
|
);
|
|
|
|
newData[dateFieldMetadata.name] = shiftedDate;
|
|
}
|
|
}
|
|
|
|
return newData as T;
|
|
}
|
|
|
|
export function getCompositeFieldMetadataMap(
|
|
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
|
) {
|
|
const compositeFieldMetadataCollection = getCompositeFieldMetadataCollection(
|
|
objectMetadataItemWithFieldMaps,
|
|
);
|
|
|
|
return new Map(
|
|
compositeFieldMetadataCollection.flatMap((fieldMetadata) => {
|
|
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
|
|
|
|
if (!compositeType) return [];
|
|
|
|
// Map each composite property to a [key, value] pair
|
|
return compositeType.properties.map((compositeProperty) => [
|
|
computeCompositeColumnName(fieldMetadata.name, compositeProperty),
|
|
{
|
|
parentField: fieldMetadata.name,
|
|
...compositeProperty,
|
|
},
|
|
]);
|
|
}),
|
|
);
|
|
}
|
|
|
|
function formatFieldMetadataValue(
|
|
value: any,
|
|
fieldMetadata: FieldMetadataInterface,
|
|
) {
|
|
if (
|
|
typeof value === 'string' &&
|
|
(fieldMetadata.type === FieldMetadataType.MULTI_SELECT ||
|
|
fieldMetadata.type === FieldMetadataType.ARRAY)
|
|
) {
|
|
const cleanedValue = value.replace(/{|}/g, '').trim();
|
|
|
|
return cleanedValue ? cleanedValue.split(',') : [];
|
|
}
|
|
|
|
return value;
|
|
}
|