In some cases, the person is undefined so we fallback on the workspacemember, and sometimes he is missing the firstname/lastname. Not sure how this can happen, but one user experiencd such a thing so better to catch this  fix https://discord.com/channels/1130383047699738754/1366145144838951022 close https://github.com/twentyhq/core-team-issues/issues/918
285 lines
8.6 KiB
TypeScript
285 lines
8.6 KiB
TypeScript
import { isPlainObject } from '@nestjs/common/utils/shared.utils';
|
|
|
|
import { isNonEmptyString } from '@sniptt/guards';
|
|
import { isDefined } from 'class-validator';
|
|
import { FieldMetadataType } from 'twenty-shared/types';
|
|
|
|
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 { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
|
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 { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util';
|
|
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,
|
|
objectMetadataMaps: ObjectMetadataMaps,
|
|
isNewRelationEnabled: boolean,
|
|
): T {
|
|
if (!data) {
|
|
return data;
|
|
}
|
|
|
|
if (Array.isArray(data)) {
|
|
return data.map((item) =>
|
|
formatResult(
|
|
item,
|
|
objectMetadataItemWithFieldMaps,
|
|
objectMetadataMaps,
|
|
isNewRelationEnabled,
|
|
),
|
|
) as T;
|
|
}
|
|
|
|
if (!isPlainObject(data)) {
|
|
return data;
|
|
}
|
|
|
|
if (!objectMetadataItemWithFieldMaps) {
|
|
throw new Error('Object metadata is missing');
|
|
}
|
|
|
|
const compositeFieldMetadataMap = getCompositeFieldMetadataMap(
|
|
objectMetadataItemWithFieldMaps,
|
|
);
|
|
|
|
const relationMetadataMap: Map<
|
|
string,
|
|
{
|
|
relationMetadata: RelationMetadataEntity | undefined;
|
|
relationType: string;
|
|
}
|
|
> = isNewRelationEnabled
|
|
? new Map()
|
|
: new Map(
|
|
Object.values(objectMetadataItemWithFieldMaps.fieldsById)
|
|
.filter((fieldMetadata) =>
|
|
isFieldMetadataInterfaceOfType(
|
|
fieldMetadata,
|
|
FieldMetadataType.RELATION,
|
|
),
|
|
)
|
|
.map((fieldMetadata) => [
|
|
fieldMetadata.name,
|
|
{
|
|
relationMetadata:
|
|
fieldMetadata.fromRelationMetadata ??
|
|
fieldMetadata.toRelationMetadata,
|
|
relationType: computeRelationType(
|
|
fieldMetadata,
|
|
fieldMetadata.fromRelationMetadata ??
|
|
(fieldMetadata.toRelationMetadata as RelationMetadataEntity),
|
|
),
|
|
},
|
|
]),
|
|
);
|
|
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];
|
|
|
|
const isRelation = fieldMetadata
|
|
? isFieldMetadataInterfaceOfType(
|
|
fieldMetadata,
|
|
FieldMetadataType.RELATION,
|
|
)
|
|
: false;
|
|
|
|
if (!compositePropertyArgs && !isRelation) {
|
|
if (isPlainObject(value)) {
|
|
newData[key] = formatResult(
|
|
value,
|
|
objectMetadataItemWithFieldMaps,
|
|
objectMetadataMaps,
|
|
isNewRelationEnabled,
|
|
);
|
|
} else if (objectMetadaItemFieldsByName[key]) {
|
|
newData[key] = formatFieldMetadataValue(
|
|
value,
|
|
objectMetadaItemFieldsByName[key],
|
|
);
|
|
} else {
|
|
newData[key] = value;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!isNewRelationEnabled) {
|
|
const { relationMetadata, relationType } =
|
|
relationMetadataMap.get(key) ?? {};
|
|
|
|
if (relationMetadata) {
|
|
const toObjectMetadata =
|
|
objectMetadataMaps.byId[relationMetadata.toObjectMetadataId];
|
|
|
|
const fromObjectMetadata =
|
|
objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId];
|
|
|
|
if (!toObjectMetadata) {
|
|
throw new Error(
|
|
`Object metadata for object metadataId "${relationMetadata.toObjectMetadataId}" is missing`,
|
|
);
|
|
}
|
|
|
|
if (!fromObjectMetadata) {
|
|
throw new Error(
|
|
`Object metadata for object metadataId "${relationMetadata.fromObjectMetadataId}" is missing`,
|
|
);
|
|
}
|
|
|
|
newData[key] = formatResult(
|
|
value,
|
|
relationType === 'one-to-many'
|
|
? toObjectMetadata
|
|
: fromObjectMetadata,
|
|
objectMetadataMaps,
|
|
isNewRelationEnabled,
|
|
);
|
|
continue;
|
|
}
|
|
} else {
|
|
if (isRelation) {
|
|
if (!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,
|
|
isNewRelationEnabled,
|
|
);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|