twenty / packages /twenty-server /src /engine /twenty-orm /utils /format-result.util.ts
Jules
Initial clean CRM deployment with prebuilt assets
d9494a5
Raw
History Blame Contribute Delete
11.7 kB
import { isPlainObject } from '@nestjs/common/utils/shared.utils';
import { isNull } from '@sniptt/guards';
import {
FieldActorSource,
FieldMetadataType,
compositeTypeDefinitions,
} from 'twenty-shared/types';
import { isDefined, stringifySafely } from 'twenty-shared/utils';
import {
DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE,
DEFAULT_COMPOSITE_FIELDS_NULL_EQUIVALENT_VALUE,
DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
} from 'src/engine/api/common/common-args-processors/data-arg-processor/constants/null-equivalent-values.constant';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import {
buildFieldMapsFromFlatObjectMetadata,
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';
export function formatResult<T>(
// oxlint-disable-next-line typescript/no-explicit-any
data: any,
flatObjectMetadata: FlatObjectMetadata | undefined,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
fieldMapsForObject?: FieldMapsForObject,
): T {
if (!isDefined(data)) {
return data;
}
if (!isPlainObject(data)) {
if (Array.isArray(data)) {
return data.map((item) =>
formatResult(
item,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
fieldMapsForObject,
),
) as T;
}
return data;
}
if (!flatObjectMetadata) {
throw new Error('Object metadata is missing');
}
const fieldMaps =
fieldMapsForObject ??
buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
flatObjectMetadata,
);
const { fieldIdByName, fieldIdByJoinColumnName } = fieldMaps;
const compositeFieldMetadataMap = getCompositeFieldMetadataMap(
flatObjectMetadata,
flatFieldMetadataMaps,
);
const newData: object = {};
for (const [key, value] of Object.entries(data)) {
const compositePropertyArgs = compositeFieldMetadataMap.get(key);
const fieldMetadataId =
fieldIdByName[key] ||
fieldIdByJoinColumnName[key] ||
fieldIdByName[compositePropertyArgs?.parentField ?? ''];
const fieldMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: fieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
});
if (!isDefined(fieldMetadata)) {
continue;
}
const isRelation = fieldMetadata
? isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION)
: false;
if (isRelation) {
if (!isDefined(fieldMetadata?.relationTargetObjectMetadataId)) {
throw new Error(
`Relation target object metadata ID is missing for field "${key}"`,
);
}
const targetObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: fieldMetadata.relationTargetObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
if (!targetObjectMetadata) {
throw new Error(
`Object metadata for object metadataId "${fieldMetadata.relationTargetObjectMetadataId}" is missing`,
);
}
// @ts-expect-error legacy noImplicitAny
newData[key] = formatResult(
value,
targetObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
continue;
}
if (isDefined(compositePropertyArgs)) {
const { parentField, ...compositeProperty } = compositePropertyArgs;
// @ts-expect-error legacy noImplicitAny
if (!newData[parentField]) {
// @ts-expect-error legacy noImplicitAny
newData[parentField] = {};
}
// @ts-expect-error legacy noImplicitAny
newData[parentField][compositeProperty.name] = isNull(value)
? transformCompositeFieldNullValue(
value,
compositeProperty.name,
fieldMetadata,
)
: formatCompositeFieldValue(
value,
compositeProperty.name,
fieldMetadata,
);
continue;
}
const formattedFieldValue = formatFieldMetadataValue(
value,
fieldMetadata.type,
);
// @ts-expect-error legacy noImplicitAny
newData[key] = formattedFieldValue;
}
// After assembling composite fields, handle those with missing required subfields
handleEmptyCompositeFields(
newData,
flatObjectMetadata,
flatFieldMetadataMaps,
);
const fieldMetadataItemsOfTypeDateOnly = getFlatFieldsFromFlatObjectMetadata(
flatObjectMetadata,
flatFieldMetadataMaps,
).filter((field) => field.type === FieldMetadataType.DATE);
for (const dateField of fieldMetadataItemsOfTypeDateOnly) {
// @ts-expect-error legacy noImplicitAny
const rawUpdatedDate = newData[dateField.name] as string | null | undefined;
if (!isDefined(rawUpdatedDate)) {
continue;
}
// @ts-expect-error legacy noImplicitAny
newData[dateField.name] = rawUpdatedDate;
}
const fieldMetadataItemsOfTypeDateTimeOnly =
getFlatFieldsFromFlatObjectMetadata(
flatObjectMetadata,
flatFieldMetadataMaps,
).filter((field) => field.type === FieldMetadataType.DATE_TIME);
for (const dateTimeField of fieldMetadataItemsOfTypeDateTimeOnly) {
// @ts-expect-error legacy noImplicitAny
const rawUpdatedDateTime = newData[dateTimeField.name] as
| string
| Date
| null
| undefined
| Record<string, unknown>;
if (!isDefined(rawUpdatedDateTime)) {
continue;
}
if (
typeof rawUpdatedDateTime === 'string' ||
rawUpdatedDateTime instanceof Date ||
isPlainObject(rawUpdatedDateTime)
) {
// @ts-expect-error legacy noImplicitAny
newData[dateTimeField.name] = rawUpdatedDateTime;
} else {
const stringifiedUnknownValue = stringifySafely(rawUpdatedDateTime);
throw new Error(
`Invalid DATE_TIME field "${dateTimeField.name}", value: "${stringifiedUnknownValue}", it should be a string, Date instance or plain object, (current type : ${typeof rawUpdatedDateTime}).`,
);
}
}
return newData as T;
}
export function getCompositeFieldMetadataMap(
flatObjectMetadata: FlatObjectMetadata,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) {
const compositeFieldMetadataCollection = getCompositeFieldMetadataCollection(
flatObjectMetadata,
flatFieldMetadataMaps,
);
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(
// oxlint-disable-next-line typescript/no-explicit-any
value: any,
fieldMetadataType: FieldMetadataType,
) {
if (
typeof value === 'string' &&
(fieldMetadataType === FieldMetadataType.MULTI_SELECT ||
fieldMetadataType === FieldMetadataType.ARRAY)
) {
const cleanedValue = value.replace(/{|}/g, '').trim();
return cleanedValue ? cleanedValue.split(',') : [];
}
if (isNull(value)) {
if (
fieldMetadataType === FieldMetadataType.MULTI_SELECT ||
fieldMetadataType === FieldMetadataType.ARRAY
) {
return DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE;
}
if (fieldMetadataType === FieldMetadataType.TEXT) {
return DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE;
}
return value;
}
return value;
}
function transformCompositeFieldNullValue(
value: unknown,
compositePropertyName: string,
fieldMetadata: FlatFieldMetadata,
) {
if (!isNull(value)) return value;
return (
DEFAULT_COMPOSITE_FIELDS_NULL_EQUIVALENT_VALUE[fieldMetadata.type]?.[
compositePropertyName
] ?? value
);
}
/**
* Handles composite fields with missing required subfields.
* - For nullable fields: sets to null if all required subfields are null
* - For non-nullable fields: provides a default value to prevent GraphQL errors
*
* This handles existing records that were created before the field was added
* or records with incomplete data.
*/
function handleEmptyCompositeFields(
// oxlint-disable-next-line typescript/no-explicit-any
data: Record<string, any>,
flatObjectMetadata: FlatObjectMetadata,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) {
const compositeFieldMetadataCollection = getCompositeFieldMetadataCollection(
flatObjectMetadata,
flatFieldMetadataMaps,
);
for (const fieldMetadata of compositeFieldMetadataCollection) {
const fieldValue = data[fieldMetadata.name];
if (!isDefined(fieldValue) || !isPlainObject(fieldValue)) {
continue;
}
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
if (!compositeType) {
continue;
}
// oxlint-disable-next-line typescript/no-explicit-any
const typedFieldValue = fieldValue as Record<string, any>;
// Check if all required properties are null/undefined
const requiredProperties = compositeType.properties.filter(
(prop) => prop.isRequired,
);
const allRequiredPropertiesAreNull = requiredProperties.every(
(prop) =>
!isDefined(typedFieldValue[prop.name]) ||
isNull(typedFieldValue[prop.name]),
);
if (allRequiredPropertiesAreNull && requiredProperties.length > 0) {
if (fieldMetadata.isNullable) {
// Field is nullable, set to null
data[fieldMetadata.name] = null;
} else {
// Field is non-nullable, provide a default value
data[fieldMetadata.name] = getDefaultCompositeFieldValue(
fieldMetadata.type,
);
}
}
}
}
/**
* Returns a default value for non-nullable composite fields.
*/
function getDefaultCompositeFieldValue(
fieldType: FieldMetadataType,
// oxlint-disable-next-line typescript/no-explicit-any
): Record<string, any> | null {
switch (fieldType) {
case FieldMetadataType.ACTOR:
return {
source: FieldActorSource.MANUAL,
name: '',
workspaceMemberId: null,
context: {},
};
default:
// For other composite types, return null and let GraphQL handle the error
// This should be extended as needed for other non-nullable composite fields
return null;
}
}