Refactor morph field name and morph data loader (#14299)
# Introduction Storing morph relation field names directly in database, using morphId to aggregate them Removing dynamic morph field metadata computation in schemas and data loader Will add integration tests on morph data loader entry closes https://github.com/twentyhq/core-team-issues/issues/1425 closes https://github.com/twentyhq/core-team-issues/issues/1424 closes https://github.com/twentyhq/core-team-issues/issues/1423
This commit is contained in:
+5
-17
@@ -6,11 +6,7 @@ import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObject
|
||||
import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||
import { isNonCompositeField } from '@/object-record/object-filter-dropdown/utils/isNonCompositeField';
|
||||
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||
import {
|
||||
computeMorphRelationFieldJoinColumnName,
|
||||
computeMorphRelationFieldName,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
|
||||
type MapFieldMetadataToGraphQLQueryArgs = {
|
||||
@@ -44,6 +40,8 @@ export const mapFieldMetadataToGraphQLQuery = ({
|
||||
return gqlField;
|
||||
}
|
||||
|
||||
// We could factorize morph relation fields mapping to be passing through the RELATION handler too as now they share
|
||||
// the same name and join column name logic
|
||||
if (
|
||||
fieldType === FieldMetadataType.MORPH_RELATION &&
|
||||
(fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY ||
|
||||
@@ -51,12 +49,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
|
||||
) {
|
||||
let gqlMorphField = '';
|
||||
for (const morphRelation of fieldMetadata.morphRelations ?? []) {
|
||||
const relationFieldName = computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadata.name,
|
||||
relationDirection: fieldMetadata.settings?.relationType,
|
||||
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
|
||||
namePlural: morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
const relationFieldName = morphRelation.sourceFieldMetadata.name;
|
||||
const relationMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.id === morphRelation.targetObjectMetadata.id,
|
||||
@@ -103,12 +96,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
|
||||
}
|
||||
|
||||
if (fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE) {
|
||||
const joinColumnName = computeMorphRelationFieldJoinColumnName({
|
||||
name: fieldMetadata.name,
|
||||
targetObjectMetadataNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
});
|
||||
if (gqlField === joinColumnName) {
|
||||
if (gqlField === fieldMetadata.settings?.joinColumnName) {
|
||||
gqlMorphField += `${gqlField}
|
||||
`;
|
||||
continue;
|
||||
|
||||
+12
-21
@@ -5,7 +5,7 @@ import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueri
|
||||
import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||
import { isRecordGqlFieldsNode } from '@/object-record/graphql/utils/isRecordGraphlFieldsNode';
|
||||
import { FieldMetadataType, type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type MapObjectMetadataToGraphQLQueryArgs = {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
@@ -54,29 +54,25 @@ export const mapObjectMetadataToGraphQLQuery = ({
|
||||
.filter((field) => isDefined(field.settings?.joinColumnName));
|
||||
|
||||
const manyToOneRelationGqlFieldWithFieldMetadata =
|
||||
manyToOneRelationFields.flatMap((field) => {
|
||||
const isMorphRelation = field.type === FieldMetadataType.MORPH_RELATION;
|
||||
manyToOneRelationFields.flatMap((fieldMetadata) => {
|
||||
const isMorphRelation =
|
||||
fieldMetadata.type === FieldMetadataType.MORPH_RELATION;
|
||||
if (!isMorphRelation) {
|
||||
return {
|
||||
gqlField: field.settings?.joinColumnName,
|
||||
fieldMetadata: field,
|
||||
gqlField: fieldMetadata.settings?.joinColumnName,
|
||||
fieldMetadata: fieldMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
if (!isDefined(field.morphRelations)) {
|
||||
if (!isDefined(fieldMetadata.morphRelations)) {
|
||||
throw new Error(
|
||||
`Field ${field.name} is missing, please refresh the page. If the problem persists, please contact support.`,
|
||||
`Field ${fieldMetadata.name} is missing, please refresh the page. If the problem persists, please contact support.`,
|
||||
);
|
||||
}
|
||||
|
||||
return field.morphRelations.map((morphRelation) => ({
|
||||
gqlField: computeMorphRelationFieldName({
|
||||
fieldName: field.name,
|
||||
relationDirection: morphRelation.type,
|
||||
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
|
||||
namePlural: morphRelation.targetObjectMetadata.namePlural,
|
||||
}),
|
||||
fieldMetadata: field,
|
||||
return fieldMetadata.morphRelations.map((morphRelation) => ({
|
||||
gqlField: morphRelation.sourceFieldMetadata.name,
|
||||
fieldMetadata: fieldMetadata,
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -103,12 +99,7 @@ export const mapObjectMetadataToGraphQLQuery = ({
|
||||
}
|
||||
|
||||
return fieldMetadata.morphRelations.map((morphRelation) => ({
|
||||
gqlField: computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadata.name,
|
||||
relationDirection: morphRelation.type,
|
||||
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
|
||||
namePlural: morphRelation.targetObjectMetadata.namePlural,
|
||||
}),
|
||||
gqlField: morphRelation.sourceFieldMetadata.name,
|
||||
fieldMetadata,
|
||||
}));
|
||||
});
|
||||
|
||||
+4
-10
@@ -12,7 +12,7 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useAttachMorphRelatedRecordFromRecord = () => {
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
@@ -61,14 +61,8 @@ export const useAttachMorphRelatedRecordFromRecord = () => {
|
||||
|
||||
if (isDefined(parentRecord)) {
|
||||
relatedObjectMetadataItems.forEach((relatedObjectMetadataItem) => {
|
||||
const computedFieldName = computeMorphRelationFieldName({
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
relationDirection: fieldDefinition.metadata.relationType,
|
||||
nameSingular: relatedObjectMetadataItem.nameSingular,
|
||||
namePlural: relatedObjectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
const currentMorphFieldValue = parentRecord[computedFieldName];
|
||||
const currentMorphFieldValue =
|
||||
parentRecord[fieldDefinition.metadata.fieldName];
|
||||
|
||||
const objectRecordFromCache = getRecordFromCache({
|
||||
objectMetadataItem: relatedObjectMetadataItem,
|
||||
@@ -84,7 +78,7 @@ export const useAttachMorphRelatedRecordFromRecord = () => {
|
||||
|
||||
set(recordStoreFamilyState(recordId), {
|
||||
...parentRecord,
|
||||
[computedFieldName]: [
|
||||
[fieldDefinition.metadata.fieldName]: [
|
||||
...currentMorphFieldValue,
|
||||
objectRecordFromCache,
|
||||
],
|
||||
|
||||
+6
-12
@@ -13,7 +13,7 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useDetachMorphRelatedRecordFromRecord = () => {
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
@@ -61,15 +61,8 @@ export const useDetachMorphRelatedRecordFromRecord = () => {
|
||||
|
||||
if (isDefined(parentRecord)) {
|
||||
relatedObjectMetadataItems.forEach((relatedObjectMetadataItem) => {
|
||||
const computedFieldName = computeMorphRelationFieldName({
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
relationDirection: fieldDefinition.metadata.relationType,
|
||||
nameSingular: relatedObjectMetadataItem.nameSingular,
|
||||
namePlural: relatedObjectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
const currentMorphFieldValue = parentRecord[
|
||||
computedFieldName
|
||||
fieldDefinition.metadata.fieldName
|
||||
] as ObjectRecord[];
|
||||
|
||||
const objectRecordFromCache = getRecordFromCache({
|
||||
@@ -87,9 +80,10 @@ export const useDetachMorphRelatedRecordFromRecord = () => {
|
||||
if (Array.isArray(currentMorphFieldValue)) {
|
||||
set(recordStoreFamilyState(recordId), {
|
||||
...parentRecord,
|
||||
[computedFieldName]: currentMorphFieldValue.filter(
|
||||
(record) => record.id !== relatedRecordId,
|
||||
),
|
||||
[fieldDefinition.metadata.fieldName]:
|
||||
currentMorphFieldValue.filter(
|
||||
(record) => record.id !== relatedRecordId,
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
+2
-7
@@ -7,7 +7,7 @@ import { visibleRecordFieldsComponentSelector } from '@/object-record/record-fie
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useRecordTableRecordGqlFields = ({
|
||||
objectMetadataItem,
|
||||
@@ -58,12 +58,7 @@ export const useRecordTableRecordGqlFields = ({
|
||||
}
|
||||
|
||||
return fieldMetadataItem.morphRelations.map((morphRelation) => [
|
||||
computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadataItem.name,
|
||||
relationDirection: morphRelation.type,
|
||||
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
|
||||
namePlural: morphRelation.targetObjectMetadata.namePlural,
|
||||
}),
|
||||
morphRelation.sourceFieldMetadata.name,
|
||||
true,
|
||||
]);
|
||||
}),
|
||||
|
||||
+2
-9
@@ -3,8 +3,7 @@ import { selectorFamily } from 'recoil';
|
||||
import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { RelationType } from 'twenty-shared/types';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const recordStoreMorphManyToOneValueWithObjectNameFamilySelector =
|
||||
selectorFamily({
|
||||
@@ -22,17 +21,11 @@ export const recordStoreMorphManyToOneValueWithObjectNameFamilySelector =
|
||||
({ get }) => {
|
||||
const morphValuesWithObjectName = morphRelations.map(
|
||||
(morphRelation) => {
|
||||
const computedFieldName = computeMorphRelationFieldName({
|
||||
fieldName,
|
||||
relationDirection: RelationType.MANY_TO_ONE,
|
||||
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
|
||||
namePlural: morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
return {
|
||||
objectNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
value: get(recordStoreFamilyState(recordId))?.[
|
||||
computedFieldName
|
||||
fieldName
|
||||
] as ObjectRecord,
|
||||
};
|
||||
},
|
||||
|
||||
+2
-11
@@ -3,8 +3,6 @@ import { selectorFamily } from 'recoil';
|
||||
import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { RelationType } from 'twenty-shared/types';
|
||||
import { computeMorphRelationFieldName } from 'twenty-shared/utils';
|
||||
|
||||
export const recordStoreMorphOneToManyValueWithObjectNameFamilySelector =
|
||||
selectorFamily({
|
||||
@@ -22,18 +20,11 @@ export const recordStoreMorphOneToManyValueWithObjectNameFamilySelector =
|
||||
({ get }) => {
|
||||
const morphValuesWithObjectName = morphRelations.map(
|
||||
(morphRelation) => {
|
||||
const computedFieldName = computeMorphRelationFieldName({
|
||||
fieldName,
|
||||
relationDirection: RelationType.ONE_TO_MANY,
|
||||
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
|
||||
namePlural: morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
return {
|
||||
objectNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
value: (get(recordStoreFamilyState(recordId))?.[
|
||||
computedFieldName
|
||||
] || []) as ObjectRecord[],
|
||||
value: (get(recordStoreFamilyState(recordId))?.[fieldName] ||
|
||||
[]) as ObjectRecord[],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
+1
-11
@@ -9,7 +9,6 @@ import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldCont
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { SettingsDataModelSetFieldValueEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect';
|
||||
import { useFieldPreviewValue } from '@/settings/data-model/fields/preview/hooks/useFieldPreviewValue';
|
||||
import { computeMorphRelationFieldName } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
@@ -118,16 +117,7 @@ export const SettingsDataModelRelationFieldPreview = ({
|
||||
>
|
||||
<SettingsDataModelSetFieldValueEffect
|
||||
recordId={recordId}
|
||||
gqlFieldName={
|
||||
fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION
|
||||
? computeMorphRelationFieldName({
|
||||
fieldName: fieldName ?? '',
|
||||
relationDirection: fieldMetadataItem.settings?.relationType,
|
||||
nameSingular: relationTargetObjectNameSingular,
|
||||
namePlural: relationTargetObjectMetadataItem.namePlural,
|
||||
})
|
||||
: fieldName
|
||||
}
|
||||
gqlFieldName={fieldName}
|
||||
value={fieldPreviewValue}
|
||||
/>
|
||||
<StyledFieldPreview shrink={shrink}>
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class UniqueFieldMetadataNameForWorkspaceObjectMetadata1756976545860
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'UniqueFieldMetadataNameForWorkspaceObjectMetadata1756976545860';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."fieldMetadata" ADD CONSTRAINT "IDX_FIELD_METADATA_NAME_OBJECT_METADATA_ID_WORKSPACE_ID_UNIQUE" UNIQUE ("name", "objectMetadataId", "workspaceId")`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."fieldMetadata" DROP CONSTRAINT "IDX_FIELD_METADATA_NAME_OBJECT_METADATA_ID_WORKSPACE_ID_UNIQUE"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-9
@@ -5,7 +5,6 @@ import { GraphqlQuerySelectedFieldsAggregateParser } from 'src/engine/api/graphq
|
||||
import { GraphqlQuerySelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser';
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
@@ -134,14 +133,8 @@ export class GraphqlQuerySelectedFieldsParser {
|
||||
accumulator.select[joinColumnName] = true;
|
||||
}
|
||||
|
||||
const morphRelationFieldName = computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadata.name,
|
||||
relationDirection: fieldMetadata.settings.relationType,
|
||||
targetObjectMetadata,
|
||||
});
|
||||
|
||||
const graphqlSelectedFieldValue =
|
||||
graphqlSelectedFields[morphRelationFieldName];
|
||||
graphqlSelectedFields[fieldMetadata.name];
|
||||
|
||||
if (!isDefined(graphqlSelectedFieldValue)) {
|
||||
continue;
|
||||
@@ -149,7 +142,7 @@ export class GraphqlQuerySelectedFieldsParser {
|
||||
|
||||
this.graphqlQuerySelectedFieldsRelationParser.parseRelationField(
|
||||
fieldMetadata,
|
||||
morphRelationFieldName,
|
||||
fieldMetadata.name,
|
||||
graphqlSelectedFieldValue,
|
||||
accumulator,
|
||||
);
|
||||
|
||||
+20
-29
@@ -17,7 +17,6 @@ import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-q
|
||||
import { type AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util';
|
||||
import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util';
|
||||
@@ -255,52 +254,44 @@ export class ObjectRecordsToGraphqlConnectionHelper {
|
||||
continue;
|
||||
}
|
||||
|
||||
const morphRelationFieldName = computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadata.name,
|
||||
relationDirection: fieldMetadata.settings.relationType,
|
||||
targetObjectMetadata,
|
||||
});
|
||||
|
||||
const fieldMetadataNameWithId = `${morphRelationFieldName}Id`;
|
||||
const fieldMetadataNameWithId = `${fieldMetadata.name}Id`;
|
||||
|
||||
if (isDefined(objectRecord[fieldMetadataNameWithId])) {
|
||||
processedObjectRecord[fieldMetadataNameWithId] =
|
||||
objectRecord[fieldMetadataNameWithId];
|
||||
}
|
||||
|
||||
const objectValue = objectRecord[morphRelationFieldName];
|
||||
const objectValue = objectRecord[fieldMetadata.name];
|
||||
|
||||
if (!isDefined(objectValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(objectValue)) {
|
||||
processedObjectRecord[morphRelationFieldName] = this.createConnection(
|
||||
{
|
||||
objectRecords: objectValue,
|
||||
parentObjectRecord: objectRecord,
|
||||
objectRecordsAggregatedValues:
|
||||
objectRecordsAggregatedValues[fieldMetadata.name],
|
||||
selectedAggregatedFields:
|
||||
selectedAggregatedFields[fieldMetadata.name],
|
||||
objectName: targetObjectMetadata.nameSingular,
|
||||
take,
|
||||
totalCount:
|
||||
objectRecordsAggregatedValues[fieldMetadata.name]?.totalCount ??
|
||||
objectValue.length,
|
||||
order,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
depth: depth + 1,
|
||||
},
|
||||
);
|
||||
processedObjectRecord[fieldMetadata.name] = this.createConnection({
|
||||
objectRecords: objectValue,
|
||||
parentObjectRecord: objectRecord,
|
||||
objectRecordsAggregatedValues:
|
||||
objectRecordsAggregatedValues[fieldMetadata.name],
|
||||
selectedAggregatedFields:
|
||||
selectedAggregatedFields[fieldMetadata.name],
|
||||
objectName: targetObjectMetadata.nameSingular,
|
||||
take,
|
||||
totalCount:
|
||||
objectRecordsAggregatedValues[fieldMetadata.name]?.totalCount ??
|
||||
objectValue.length,
|
||||
order,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
depth: depth + 1,
|
||||
});
|
||||
} else if (isPlainObject(objectValue)) {
|
||||
const targetObjectMetadata = getTargetObjectMetadataOrThrow(
|
||||
fieldMetadata,
|
||||
this.objectMetadataMaps,
|
||||
);
|
||||
|
||||
processedObjectRecord[morphRelationFieldName] = this.processRecord({
|
||||
processedObjectRecord[fieldMetadata.name] = this.processRecord({
|
||||
objectRecord: objectValue,
|
||||
objectRecordsAggregatedValues:
|
||||
objectRecordsAggregatedValues[fieldMetadata.name],
|
||||
|
||||
+1
-9
@@ -2,11 +2,9 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util';
|
||||
import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util';
|
||||
import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
export const buildColumnsToSelect = ({
|
||||
select,
|
||||
relations,
|
||||
@@ -84,13 +82,7 @@ const getRequiredRelationColumns = (
|
||||
continue;
|
||||
}
|
||||
|
||||
const morphRelationFieldName = computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadata.name,
|
||||
relationDirection: fieldMetadata.settings.relationType,
|
||||
targetObjectMetadata,
|
||||
});
|
||||
|
||||
const relationValue = relations[morphRelationFieldName];
|
||||
const relationValue = relations[fieldMetadata.name];
|
||||
|
||||
if (
|
||||
!isDefined(relationValue) ||
|
||||
|
||||
+1
-8
@@ -3,7 +3,6 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util';
|
||||
import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
@@ -58,14 +57,8 @@ export function getFieldMetadataFromGraphQLField({
|
||||
return;
|
||||
}
|
||||
|
||||
const name = computeMorphRelationFieldName({
|
||||
fieldName: morphRelation.fieldMetadata.name,
|
||||
relationDirection: morphRelation.fieldMetadata.settings.relationType,
|
||||
targetObjectMetadata: morphRelation.targetObjectMetadata,
|
||||
});
|
||||
|
||||
possibleGraphQLFieldNames.push({
|
||||
graphQLField: name,
|
||||
graphQLField: morphRelation.fieldMetadata.name,
|
||||
fieldMetadata: morphRelation.fieldMetadata,
|
||||
targetObjectMetadata: morphRelation.targetObjectMetadata,
|
||||
});
|
||||
|
||||
+1
-11
@@ -14,7 +14,6 @@ import { RelationTypeV2Factory } from 'src/engine/api/graphql/workspace-schema-b
|
||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
|
||||
import { objectContainsRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field';
|
||||
import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
|
||||
@@ -169,16 +168,7 @@ export class ExtendObjectTypeDefinitionV2Factory {
|
||||
);
|
||||
}
|
||||
|
||||
const fieldName =
|
||||
fieldMetadata.type === FieldMetadataType.MORPH_RELATION
|
||||
? computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadata.name,
|
||||
relationDirection: fieldMetadata.settings.relationType,
|
||||
targetObjectMetadata: objectMetadataTarget,
|
||||
})
|
||||
: fieldMetadata.name;
|
||||
|
||||
fields[fieldName] = {
|
||||
fields[fieldMetadata.name] = {
|
||||
type: relationType,
|
||||
args: argsType,
|
||||
description: fieldMetadata.description,
|
||||
|
||||
@@ -9,31 +9,17 @@ import {
|
||||
type RelationLoaderPayload,
|
||||
} from 'src/engine/dataloaders/dataloader.service';
|
||||
import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { type RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto';
|
||||
import { type IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto';
|
||||
import { type IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
export interface IDataloaders {
|
||||
relationLoader: DataLoader<
|
||||
RelationLoaderPayload,
|
||||
{
|
||||
sourceObjectMetadata: ObjectMetadataEntity;
|
||||
targetObjectMetadata: ObjectMetadataEntity;
|
||||
sourceFieldMetadata: FieldMetadataEntity;
|
||||
targetFieldMetadata: FieldMetadataEntity;
|
||||
}
|
||||
>;
|
||||
relationLoader: DataLoader<RelationLoaderPayload, RelationDTO | null>;
|
||||
|
||||
morphRelationLoader: DataLoader<
|
||||
MorphRelationLoaderPayload,
|
||||
{
|
||||
sourceObjectMetadata: ObjectMetadataEntity;
|
||||
targetObjectMetadata: ObjectMetadataEntity;
|
||||
sourceFieldMetadata: FieldMetadataEntity;
|
||||
targetFieldMetadata: FieldMetadataEntity;
|
||||
}[]
|
||||
RelationDTO[] | null
|
||||
>;
|
||||
|
||||
fieldMetadataLoader: DataLoader<
|
||||
|
||||
@@ -2,19 +2,24 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import DataLoader from 'dataloader';
|
||||
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type IndexMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface';
|
||||
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
|
||||
import { filterMorphRelationDuplicateFieldsDTO } from 'src/engine/dataloaders/utils/filter-morph-relation-duplicate-fields.util';
|
||||
import { filterMorphRelationDuplicateFields } from 'src/engine/dataloaders/utils/filter-morph-relation-duplicate-fields.util';
|
||||
import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataMorphRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service';
|
||||
import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service';
|
||||
import { fromFieldMetadataEntityToFieldMetadataDto } from 'src/engine/metadata-modules/field-metadata/utils/from-field-metadata-entity-to-field-metadata-dto.util';
|
||||
import { resolveFieldMetadataStandardOverride } from 'src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util';
|
||||
import { findAllOthersMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util';
|
||||
import { fromMorphOrRelationFlatFieldMetadataToRelationDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-morph-or-relation-flat-field-metadata-to-relation-dto.util';
|
||||
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
import { findFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util';
|
||||
import { findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-in-flat-object-metadata-maps-or-throw.util';
|
||||
import { type IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto';
|
||||
import { type IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@@ -28,27 +33,14 @@ export type RelationMetadataLoaderPayload = {
|
||||
|
||||
export type RelationLoaderPayload = {
|
||||
workspaceId: string;
|
||||
fieldMetadata: Pick<
|
||||
FieldMetadataEntity,
|
||||
| 'type'
|
||||
| 'id'
|
||||
| 'objectMetadataId'
|
||||
| 'relationTargetFieldMetadataId'
|
||||
| 'relationTargetObjectMetadataId'
|
||||
>;
|
||||
fieldMetadataId: string;
|
||||
objectMetadataId: string;
|
||||
};
|
||||
|
||||
export type MorphRelationLoaderPayload = {
|
||||
workspaceId: string;
|
||||
fieldMetadata: Pick<
|
||||
FieldMetadataEntity,
|
||||
| 'type'
|
||||
| 'id'
|
||||
| 'objectMetadataId'
|
||||
| 'relationTargetFieldMetadataId'
|
||||
| 'relationTargetObjectMetadataId'
|
||||
| 'name'
|
||||
>;
|
||||
fieldMetadataId: string;
|
||||
objectMetadataId: string;
|
||||
};
|
||||
|
||||
export type FieldMetadataLoaderPayload = {
|
||||
@@ -77,8 +69,6 @@ export type ObjectMetadataLoaderPayload = {
|
||||
export class DataloaderService {
|
||||
constructor(
|
||||
private readonly i18nService: I18nService,
|
||||
private readonly fieldMetadataRelationService: FieldMetadataRelationService,
|
||||
private readonly fieldMetadataMorphRelationService: FieldMetadataMorphRelationService,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
) {}
|
||||
|
||||
@@ -101,60 +91,139 @@ export class DataloaderService {
|
||||
}
|
||||
|
||||
private createRelationLoader() {
|
||||
return new DataLoader<
|
||||
RelationLoaderPayload,
|
||||
{
|
||||
sourceObjectMetadata: ObjectMetadataEntity;
|
||||
targetObjectMetadata: ObjectMetadataEntity;
|
||||
sourceFieldMetadata: FieldMetadataEntity;
|
||||
targetFieldMetadata: FieldMetadataEntity;
|
||||
}
|
||||
>(async (dataLoaderParams: RelationLoaderPayload[]) => {
|
||||
const workspaceId = dataLoaderParams[0].workspaceId;
|
||||
const fieldMetadataItems = dataLoaderParams.map(
|
||||
(dataLoaderParam) => dataLoaderParam.fieldMetadata,
|
||||
);
|
||||
return new DataLoader<RelationLoaderPayload, RelationDTO | null>(
|
||||
async (dataLoaderParams: RelationLoaderPayload[]) => {
|
||||
const relationDtos: Array<RelationDTO | null> = [];
|
||||
const workspaceId = dataLoaderParams[0].workspaceId;
|
||||
const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } =
|
||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
const fieldMetadataRelationCollection =
|
||||
await this.fieldMetadataRelationService.findCachedFieldMetadataRelation(
|
||||
fieldMetadataItems,
|
||||
workspaceId,
|
||||
);
|
||||
for (const { fieldMetadataId, objectMetadataId } of dataLoaderParams) {
|
||||
const sourceFlatFieldMetadata =
|
||||
findFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({
|
||||
fieldMetadataId,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
objectMetadataId,
|
||||
});
|
||||
|
||||
return fieldMetadataRelationCollection;
|
||||
});
|
||||
if (
|
||||
!isFlatFieldMetadataOfType(
|
||||
sourceFlatFieldMetadata,
|
||||
FieldMetadataType.RELATION,
|
||||
)
|
||||
) {
|
||||
relationDtos.push(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourceFlatObjectMetadata =
|
||||
findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
objectMetadataId: sourceFlatFieldMetadata.objectMetadataId,
|
||||
});
|
||||
|
||||
if (
|
||||
isFlatFieldMetadataOfType(
|
||||
sourceFlatFieldMetadata.flatRelationTargetFieldMetadata,
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
)
|
||||
) {
|
||||
const allMorphFlatFieldMetadatas =
|
||||
findAllOthersMorphRelationFlatFieldMetadatasOrThrow({
|
||||
flatFieldMetadata:
|
||||
sourceFlatFieldMetadata.flatRelationTargetFieldMetadata,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
}).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
||||
|
||||
relationDtos.push(
|
||||
fromMorphOrRelationFlatFieldMetadataToRelationDto({
|
||||
flatFieldMetadata: {
|
||||
...sourceFlatFieldMetadata,
|
||||
flatRelationTargetFieldMetadata:
|
||||
allMorphFlatFieldMetadatas[0],
|
||||
},
|
||||
sourceFlatObjectMetadata,
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
relationDtos.push(
|
||||
fromMorphOrRelationFlatFieldMetadataToRelationDto({
|
||||
flatFieldMetadata: sourceFlatFieldMetadata,
|
||||
sourceFlatObjectMetadata,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return relationDtos;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private createMorphRelationLoader() {
|
||||
return new DataLoader<
|
||||
MorphRelationLoaderPayload,
|
||||
{
|
||||
sourceObjectMetadata: ObjectMetadataEntity;
|
||||
targetObjectMetadata: ObjectMetadataEntity;
|
||||
sourceFieldMetadata: FieldMetadataEntity;
|
||||
targetFieldMetadata: FieldMetadataEntity;
|
||||
}[]
|
||||
>(async (dataLoaderParams: MorphRelationLoaderPayload[]) => {
|
||||
const workspaceId = dataLoaderParams[0].workspaceId;
|
||||
return new DataLoader<MorphRelationLoaderPayload, RelationDTO[] | null>(
|
||||
async (dataLoaderParams: MorphRelationLoaderPayload[]) => {
|
||||
const workspaceId = dataLoaderParams[0].workspaceId;
|
||||
const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } =
|
||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
const relationDtos: Array<RelationDTO[] | null> = [];
|
||||
|
||||
const fieldMetadataItems = dataLoaderParams.map(
|
||||
(dataLoaderParam) => dataLoaderParam.fieldMetadata,
|
||||
);
|
||||
for (const { fieldMetadataId, objectMetadataId } of dataLoaderParams) {
|
||||
const morphFlatFieldMetadata =
|
||||
findFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({
|
||||
fieldMetadataId,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
objectMetadataId,
|
||||
});
|
||||
|
||||
const fieldMetadataMorphRelationCollection =
|
||||
await this.fieldMetadataMorphRelationService.findCachedFieldMetadataMorphRelation(
|
||||
fieldMetadataItems,
|
||||
workspaceId,
|
||||
);
|
||||
if (
|
||||
!isFlatFieldMetadataOfType(
|
||||
morphFlatFieldMetadata,
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
)
|
||||
) {
|
||||
relationDtos.push(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
return fieldMetadataItems.map((fieldMetadataItem) => {
|
||||
return fieldMetadataMorphRelationCollection.filter(
|
||||
(fieldMetadataMorphRelation) =>
|
||||
fieldMetadataItem.name ===
|
||||
fieldMetadataMorphRelation.sourceFieldMetadata.name,
|
||||
);
|
||||
});
|
||||
});
|
||||
const sourceFlatObjectMetadata =
|
||||
findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
objectMetadataId: morphFlatFieldMetadata.objectMetadataId,
|
||||
});
|
||||
|
||||
const relatedMorphFlatFieldMetadatas =
|
||||
findAllOthersMorphRelationFlatFieldMetadatasOrThrow({
|
||||
flatFieldMetadata: morphFlatFieldMetadata,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
});
|
||||
const allMorphFlatFieldMetadatas = [
|
||||
morphFlatFieldMetadata,
|
||||
...relatedMorphFlatFieldMetadatas,
|
||||
];
|
||||
|
||||
relationDtos.push(
|
||||
allMorphFlatFieldMetadatas.map((flatFieldMetadata) =>
|
||||
fromMorphOrRelationFlatFieldMetadataToRelationDto({
|
||||
flatFieldMetadata,
|
||||
sourceFlatObjectMetadata,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return relationDtos;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private createIndexMetadataLoader() {
|
||||
@@ -221,9 +290,9 @@ export class DataloaderService {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fields = Object.values(
|
||||
const overriddenFieldMetadataEntities = Object.values(
|
||||
objectMetadata.fieldsById,
|
||||
).map<FieldMetadataDTO>((fieldMetadata) => {
|
||||
).map<FieldMetadataEntity>((fieldMetadata) => {
|
||||
const overridesFieldToCompute = [
|
||||
'icon',
|
||||
'label',
|
||||
@@ -252,14 +321,19 @@ export class DataloaderService {
|
||||
{},
|
||||
);
|
||||
|
||||
return fromFieldMetadataEntityToFieldMetadataDto({
|
||||
return {
|
||||
...fieldMetadata,
|
||||
...overrides,
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
return filterMorphRelationDuplicateFieldsDTO<FieldMetadataDTO>(
|
||||
fields,
|
||||
const filteredFieldMetadataEntities =
|
||||
filterMorphRelationDuplicateFields<FieldMetadataEntity>(
|
||||
overriddenFieldMetadataEntities,
|
||||
);
|
||||
|
||||
return filteredFieldMetadataEntities.map(
|
||||
fromFieldMetadataEntityToFieldMetadataDto,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
+4
-4
@@ -1,13 +1,13 @@
|
||||
export const filterMorphRelationDuplicateFieldsDTO = <
|
||||
T extends { name: string; id: string },
|
||||
export const filterMorphRelationDuplicateFields = <
|
||||
T extends { createdAt: Date; morphId: string | null },
|
||||
>(
|
||||
fields: T[],
|
||||
) => {
|
||||
return fields.filter((currentField) => {
|
||||
return !fields.some(
|
||||
(otherField) =>
|
||||
otherField.name === currentField.name &&
|
||||
otherField.id > currentField.id,
|
||||
otherField.morphId === currentField.morphId &&
|
||||
otherField.createdAt.getTime() > currentField.createdAt.getTime(),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
+4
-1
@@ -148,7 +148,10 @@ export class FieldMetadataDTO<T extends FieldMetadataType = FieldMetadataType> {
|
||||
@Field({ nullable: true })
|
||||
isLabelSyncedWithName?: boolean;
|
||||
|
||||
@IsDateString()
|
||||
@IsDateString(undefined, {
|
||||
message: ({ value }) =>
|
||||
`Field metadata created at is invalid got ${JSON.stringify(value)} isDate: ${value instanceof Date}`,
|
||||
})
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
+6
@@ -11,6 +11,7 @@ import {
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Relation,
|
||||
Unique,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@@ -36,6 +37,11 @@ import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permis
|
||||
@Index('IDX_FIELD_METADATA_RELATION_TARGET_OBJECT_METADATA_ID', [
|
||||
'relationTargetObjectMetadataId',
|
||||
])
|
||||
@Unique('IDX_FIELD_METADATA_NAME_OBJECT_METADATA_ID_WORKSPACE_ID_UNIQUE', [
|
||||
'name',
|
||||
'objectMetadataId',
|
||||
'workspaceId',
|
||||
])
|
||||
@Index('IDX_FIELD_METADATA_OBJECT_METADATA_ID_WORKSPACE_ID', [
|
||||
'objectMetadataId',
|
||||
'workspaceId',
|
||||
|
||||
+21
-79
@@ -8,7 +8,6 @@ import {
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
@@ -33,21 +32,12 @@ import {
|
||||
UpdateOneFieldMetadataInput,
|
||||
type UpdateFieldInput,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook';
|
||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
|
||||
import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2';
|
||||
import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util';
|
||||
import { fromFieldMetadataEntityToFieldMetadataDto } from 'src/engine/metadata-modules/field-metadata/utils/from-field-metadata-entity-to-field-metadata-dto.util';
|
||||
import { fromObjectMetadataEntityToObjectMetadataDto } from 'src/engine/metadata-modules/field-metadata/utils/from-object-metadata-entity-to-object-metadata-dto.util';
|
||||
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util';
|
||||
import { isMorphRelationFieldMetadataType } from 'src/engine/utils/is-morph-relation-field-metadata-type.util';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
@@ -76,7 +66,7 @@ export class FieldMetadataResolver {
|
||||
workspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
fieldMetadataGraphqlApiExceptionHandler(error);
|
||||
return fieldMetadataGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,44 +161,22 @@ export class FieldMetadataResolver {
|
||||
@ResolveField(() => RelationDTO, { nullable: true })
|
||||
async relation(
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Parent() fieldMetadata: FieldMetadataEntity<FieldMetadataType.RELATION>,
|
||||
@Context() context: { loaders: IDataloaders },
|
||||
): Promise<RelationDTO | null | undefined> {
|
||||
if (!isMorphOrRelationFieldMetadataType(fieldMetadata.type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Parent()
|
||||
{
|
||||
id: fieldMetadataId,
|
||||
objectMetadataId,
|
||||
}: Pick<FieldMetadataDTO, 'id' | 'objectMetadataId'>,
|
||||
@Context() context: { loaders: IDataloaders },
|
||||
): Promise<RelationDTO | null> {
|
||||
try {
|
||||
const {
|
||||
sourceObjectMetadata,
|
||||
targetObjectMetadata,
|
||||
sourceFieldMetadata,
|
||||
targetFieldMetadata,
|
||||
} = await context.loaders.relationLoader.load({
|
||||
fieldMetadata,
|
||||
return await context.loaders.relationLoader.load({
|
||||
fieldMetadataId,
|
||||
objectMetadataId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
if (!isDefined(fieldMetadata.settings)) {
|
||||
throw new FieldMetadataException(
|
||||
'Relation settings are required',
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
type: fieldMetadata.settings.relationType,
|
||||
sourceObjectMetadata:
|
||||
fromObjectMetadataEntityToObjectMetadataDto(sourceObjectMetadata),
|
||||
targetObjectMetadata:
|
||||
fromObjectMetadataEntityToObjectMetadataDto(targetObjectMetadata),
|
||||
sourceFieldMetadata:
|
||||
fromFieldMetadataEntityToFieldMetadataDto(sourceFieldMetadata),
|
||||
targetFieldMetadata:
|
||||
fromFieldMetadataEntityToFieldMetadataDto(targetFieldMetadata),
|
||||
};
|
||||
} catch (error) {
|
||||
fieldMetadataGraphqlApiExceptionHandler(error);
|
||||
return fieldMetadataGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,46 +184,20 @@ export class FieldMetadataResolver {
|
||||
async morphRelations(
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Parent()
|
||||
fieldMetadata: FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>,
|
||||
{
|
||||
id: fieldMetadataId,
|
||||
objectMetadataId,
|
||||
}: Pick<FieldMetadataDTO, 'id' | 'objectMetadataId'>,
|
||||
@Context() context: { loaders: IDataloaders },
|
||||
): Promise<RelationDTO[] | null | undefined> {
|
||||
if (!isMorphRelationFieldMetadataType(fieldMetadata.type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
): Promise<RelationDTO[] | null> {
|
||||
try {
|
||||
const morphRelations = await context.loaders.morphRelationLoader.load({
|
||||
fieldMetadata,
|
||||
return await context.loaders.morphRelationLoader.load({
|
||||
fieldMetadataId,
|
||||
objectMetadataId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
// typescript issue, it's not possible to use the fieldMetadata.settings directly in morphRelations.map
|
||||
const settings = fieldMetadata.settings;
|
||||
|
||||
if (!isDefined(settings) || !isDefined(settings.relationType)) {
|
||||
throw new FieldMetadataException(
|
||||
`Morph relation settings ${isDefined(settings) && 'relationType'} are required`,
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
);
|
||||
}
|
||||
|
||||
return morphRelations.map<RelationDTO>((morphRelation) => ({
|
||||
type: settings.relationType,
|
||||
sourceObjectMetadata: fromObjectMetadataEntityToObjectMetadataDto(
|
||||
morphRelation.sourceObjectMetadata,
|
||||
),
|
||||
targetObjectMetadata: fromObjectMetadataEntityToObjectMetadataDto(
|
||||
morphRelation.targetObjectMetadata,
|
||||
),
|
||||
sourceFieldMetadata: fromFieldMetadataEntityToFieldMetadataDto(
|
||||
morphRelation.sourceFieldMetadata,
|
||||
),
|
||||
targetFieldMetadata: fromFieldMetadataEntityToFieldMetadataDto(
|
||||
morphRelation.targetFieldMetadata,
|
||||
),
|
||||
}));
|
||||
} catch (error) {
|
||||
fieldMetadataGraphqlApiExceptionHandler(error);
|
||||
return fieldMetadataGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-136
@@ -2,10 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import omit from 'lodash.omit';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
computeMorphRelationFieldJoinColumnName,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@@ -18,20 +15,17 @@ import {
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service';
|
||||
import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util';
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util';
|
||||
import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
@Injectable()
|
||||
export class FieldMetadataMorphRelationService {
|
||||
constructor(
|
||||
private readonly fieldMetadataRelationService: FieldMetadataRelationService,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
) {}
|
||||
|
||||
async createMorphRelationFieldMetadataItems({
|
||||
@@ -67,9 +61,9 @@ export class FieldMetadataMorphRelationService {
|
||||
const fieldsCreated: FieldMetadataEntity[] = [];
|
||||
const morphId = v4();
|
||||
|
||||
for (const relation of morphRelationsCreationPayload) {
|
||||
for (const relationCreationPayload of morphRelationsCreationPayload) {
|
||||
const targetObjectMetadata =
|
||||
objectMetadataMaps.byId[relation.targetObjectMetadataId];
|
||||
objectMetadataMaps.byId[relationCreationPayload.targetObjectMetadataId];
|
||||
|
||||
if (!isDefined(targetObjectMetadata)) {
|
||||
throw new FieldMetadataException(
|
||||
@@ -78,15 +72,21 @@ export class FieldMetadataMorphRelationService {
|
||||
);
|
||||
}
|
||||
|
||||
const currentMorphRelationFieldName = computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadataForCreate.name,
|
||||
relationType: relationCreationPayload.type,
|
||||
targetObjectMetadata,
|
||||
});
|
||||
const relationFieldMetadataForCreate =
|
||||
this.fieldMetadataRelationService.computeCustomRelationFieldMetadataForCreation(
|
||||
{
|
||||
fieldMetadataInput: fieldMetadataForCreate,
|
||||
relationCreationPayload: relation,
|
||||
joinColumnName: computeMorphRelationFieldJoinColumnName({
|
||||
name: fieldMetadataForCreate.name,
|
||||
targetObjectMetadataNameSingular:
|
||||
targetObjectMetadata.nameSingular,
|
||||
fieldMetadataInput: {
|
||||
...fieldMetadataForCreate,
|
||||
name: currentMorphRelationFieldName,
|
||||
},
|
||||
relationCreationPayload: relationCreationPayload,
|
||||
joinColumnName: computeMorphOrRelationFieldJoinColumnName({
|
||||
name: currentMorphRelationFieldName,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -106,16 +106,16 @@ export class FieldMetadataMorphRelationService {
|
||||
);
|
||||
|
||||
const targetFieldMetadataName = computeMetadataNameFromLabel(
|
||||
relation.targetFieldLabel,
|
||||
relationCreationPayload.targetFieldLabel,
|
||||
);
|
||||
|
||||
const targetFieldMetadataToCreate = prepareCustomFieldMetadataForCreation(
|
||||
{
|
||||
objectMetadataId: relation.targetObjectMetadataId,
|
||||
objectMetadataId: relationCreationPayload.targetObjectMetadataId,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: targetFieldMetadataName,
|
||||
label: relation.targetFieldLabel,
|
||||
icon: relation.targetFieldIcon,
|
||||
label: relationCreationPayload.targetFieldLabel,
|
||||
icon: relationCreationPayload.targetFieldIcon,
|
||||
workspaceId: fieldMetadataForCreate.workspaceId,
|
||||
settings: fieldMetadataForCreate.settings,
|
||||
},
|
||||
@@ -130,11 +130,11 @@ export class FieldMetadataMorphRelationService {
|
||||
targetFieldLabel: fieldMetadataForCreate.label,
|
||||
targetFieldIcon: fieldMetadataForCreate.icon ?? 'Icon123',
|
||||
type:
|
||||
relation.type === RelationType.ONE_TO_MANY
|
||||
relationCreationPayload.type === RelationType.ONE_TO_MANY
|
||||
? RelationType.MANY_TO_ONE
|
||||
: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
joinColumnName: computeRelationFieldJoinColumnName({
|
||||
joinColumnName: computeMorphOrRelationFieldJoinColumnName({
|
||||
name: targetFieldMetadataToCreate.name,
|
||||
}),
|
||||
},
|
||||
@@ -163,117 +163,4 @@ export class FieldMetadataMorphRelationService {
|
||||
|
||||
return fieldsCreated;
|
||||
}
|
||||
|
||||
async findCachedFieldMetadataMorphRelation(
|
||||
fieldMetadataItems: Array<
|
||||
Pick<
|
||||
FieldMetadataEntity,
|
||||
| 'id'
|
||||
| 'type'
|
||||
| 'objectMetadataId'
|
||||
| 'relationTargetFieldMetadataId'
|
||||
| 'relationTargetObjectMetadataId'
|
||||
| 'name'
|
||||
>
|
||||
>,
|
||||
workspaceId: string,
|
||||
): Promise<
|
||||
Array<{
|
||||
sourceObjectMetadata: ObjectMetadataEntity;
|
||||
sourceFieldMetadata: FieldMetadataEntity;
|
||||
targetObjectMetadata: ObjectMetadataEntity;
|
||||
targetFieldMetadata: FieldMetadataEntity;
|
||||
}>
|
||||
> {
|
||||
const objectMetadataMaps =
|
||||
await this.workspaceCacheStorageService.getObjectMetadataMapsOrThrow(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const fieldMetadataItemsAndMorphSiblings: Pick<
|
||||
FieldMetadataEntity,
|
||||
| 'id'
|
||||
| 'type'
|
||||
| 'objectMetadataId'
|
||||
| 'relationTargetFieldMetadataId'
|
||||
| 'relationTargetObjectMetadataId'
|
||||
| 'name'
|
||||
>[] = fieldMetadataItems.flatMap((fieldMetadataItem) => {
|
||||
const fieldsById =
|
||||
objectMetadataMaps.byId[fieldMetadataItem.objectMetadataId]?.fieldsById;
|
||||
|
||||
if (!isDefined(fieldsById)) {
|
||||
throw new FieldMetadataException(
|
||||
`Fields by id not found for object metadata ${fieldMetadataItem.objectMetadataId}`,
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
);
|
||||
}
|
||||
|
||||
return Object.values(fieldsById)
|
||||
.filter(
|
||||
(fieldMetadataById) =>
|
||||
fieldMetadataItem.name === fieldMetadataById.name,
|
||||
)
|
||||
.map((fieldMetadataById) => {
|
||||
return {
|
||||
id: fieldMetadataById.id,
|
||||
type: fieldMetadataById.type,
|
||||
objectMetadataId: fieldMetadataById.objectMetadataId,
|
||||
relationTargetFieldMetadataId:
|
||||
fieldMetadataById.relationTargetFieldMetadataId,
|
||||
relationTargetObjectMetadataId:
|
||||
fieldMetadataById.relationTargetObjectMetadataId,
|
||||
name: fieldMetadataById.name,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return fieldMetadataItemsAndMorphSiblings.map((fieldMetadataItem) => {
|
||||
const {
|
||||
id,
|
||||
objectMetadataId,
|
||||
relationTargetFieldMetadataId,
|
||||
relationTargetObjectMetadataId,
|
||||
} = fieldMetadataItem;
|
||||
|
||||
if (!relationTargetObjectMetadataId || !relationTargetFieldMetadataId) {
|
||||
throw new FieldMetadataException(
|
||||
`Relation target object metadata id or relation target field metadata id not found for field metadata ${id}`,
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
);
|
||||
}
|
||||
|
||||
const sourceObjectMetadata = objectMetadataMaps.byId[objectMetadataId];
|
||||
const targetObjectMetadata =
|
||||
objectMetadataMaps.byId[relationTargetObjectMetadataId];
|
||||
const sourceFieldMetadata = sourceObjectMetadata?.fieldsById[id];
|
||||
const targetFieldMetadata =
|
||||
targetObjectMetadata?.fieldsById[relationTargetFieldMetadataId];
|
||||
|
||||
if (
|
||||
!sourceObjectMetadata ||
|
||||
!targetObjectMetadata ||
|
||||
!sourceFieldMetadata ||
|
||||
!targetFieldMetadata
|
||||
) {
|
||||
throw new FieldMetadataException(
|
||||
`Field relation metadata not found for field metadata ${id}`,
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
sourceObjectMetadata:
|
||||
getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
sourceObjectMetadata,
|
||||
) as ObjectMetadataEntity,
|
||||
sourceFieldMetadata: sourceFieldMetadata as FieldMetadataEntity,
|
||||
targetObjectMetadata:
|
||||
getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
targetObjectMetadata,
|
||||
) as ObjectMetadataEntity,
|
||||
targetFieldMetadata: targetFieldMetadata as FieldMetadataEntity,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+2
-111
@@ -10,7 +10,6 @@ import { v4 } from 'uuid';
|
||||
import { type FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { filterMorphRelationDuplicateFieldsDTO } from 'src/engine/dataloaders/utils/filter-morph-relation-duplicate-fields.util';
|
||||
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
@@ -18,15 +17,12 @@ import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util';
|
||||
import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util';
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util';
|
||||
import { validateRelationCreationPayloadOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/validate-relation-creation-payload-or-throw.util';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils';
|
||||
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-or-throw.utils';
|
||||
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
||||
@@ -108,7 +104,7 @@ export class FieldMetadataRelationService {
|
||||
? RelationType.MANY_TO_ONE
|
||||
: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
joinColumnName: computeRelationFieldJoinColumnName({
|
||||
joinColumnName: computeMorphOrRelationFieldJoinColumnName({
|
||||
name: targetFieldMetadataToCreate.name,
|
||||
}),
|
||||
});
|
||||
@@ -215,111 +211,6 @@ export class FieldMetadataRelationService {
|
||||
return fieldMetadataInput;
|
||||
}
|
||||
|
||||
async findCachedFieldMetadataRelation(
|
||||
fieldMetadataItems: Array<
|
||||
Pick<
|
||||
FieldMetadataEntity,
|
||||
| 'id'
|
||||
| 'type'
|
||||
| 'objectMetadataId'
|
||||
| 'relationTargetFieldMetadataId'
|
||||
| 'relationTargetObjectMetadataId'
|
||||
>
|
||||
>,
|
||||
workspaceId: string,
|
||||
): Promise<
|
||||
Array<{
|
||||
sourceObjectMetadata: ObjectMetadataEntity;
|
||||
sourceFieldMetadata: FieldMetadataEntity;
|
||||
targetObjectMetadata: ObjectMetadataEntity;
|
||||
targetFieldMetadata: FieldMetadataEntity;
|
||||
}>
|
||||
> {
|
||||
const objectMetadataMaps =
|
||||
await this.workspaceCacheStorageService.getObjectMetadataMapsOrThrow(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return fieldMetadataItems.map((fieldMetadataItem) => {
|
||||
const {
|
||||
id,
|
||||
objectMetadataId,
|
||||
relationTargetFieldMetadataId,
|
||||
relationTargetObjectMetadataId,
|
||||
} = fieldMetadataItem;
|
||||
|
||||
if (!relationTargetObjectMetadataId || !relationTargetFieldMetadataId) {
|
||||
throw new FieldMetadataException(
|
||||
`Relation target object metadata id or relation target field metadata id not found for field metadata ${id}`,
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
);
|
||||
}
|
||||
|
||||
const sourceObjectMetadata = objectMetadataMaps.byId[objectMetadataId];
|
||||
const targetObjectMetadata =
|
||||
objectMetadataMaps.byId[relationTargetObjectMetadataId];
|
||||
const sourceFieldMetadata = sourceObjectMetadata?.fieldsById[id];
|
||||
const targetFieldMetadata =
|
||||
targetObjectMetadata?.fieldsById[relationTargetFieldMetadataId];
|
||||
|
||||
if (
|
||||
!sourceObjectMetadata ||
|
||||
!targetObjectMetadata ||
|
||||
!sourceFieldMetadata ||
|
||||
!targetFieldMetadata
|
||||
) {
|
||||
throw new FieldMetadataException(
|
||||
`Field relation metadata not found for field metadata ${id}`,
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
);
|
||||
}
|
||||
|
||||
// Morph relation specific:
|
||||
// to keep the fieldMetadata API simple
|
||||
// we decided to override the targetFieldMetadata with the existing one
|
||||
// in case the relation target is a morph relation
|
||||
const isRelationTargetMorphRelation =
|
||||
isFieldMetadataTypeMorphRelation(targetFieldMetadata);
|
||||
|
||||
const targetObjectMetadataFields = Object.values(
|
||||
targetObjectMetadata.fieldsById,
|
||||
);
|
||||
|
||||
const targetObjectMetadataWithoutDuplicates =
|
||||
filterMorphRelationDuplicateFieldsDTO<FieldMetadataEntity>(
|
||||
targetObjectMetadataFields,
|
||||
);
|
||||
const targetFieldMetadataSelected =
|
||||
targetObjectMetadataWithoutDuplicates.find(
|
||||
(fieldMetadata) => fieldMetadata.name === targetFieldMetadata.name,
|
||||
);
|
||||
|
||||
if (!isDefined(targetFieldMetadataSelected)) {
|
||||
throw new FieldMetadataException(
|
||||
`Target field metadata not found for field metadata ${id}`,
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
|
||||
);
|
||||
}
|
||||
|
||||
const targetFieldMetadataOverride = isRelationTargetMorphRelation
|
||||
? targetFieldMetadataSelected
|
||||
: targetFieldMetadata;
|
||||
|
||||
return {
|
||||
sourceObjectMetadata:
|
||||
getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
sourceObjectMetadata,
|
||||
) as ObjectMetadataEntity,
|
||||
sourceFieldMetadata: sourceFieldMetadata as FieldMetadataEntity,
|
||||
targetObjectMetadata:
|
||||
getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
targetObjectMetadata,
|
||||
) as ObjectMetadataEntity,
|
||||
targetFieldMetadata: targetFieldMetadataOverride as FieldMetadataEntity,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// TODO refactor and strictly type
|
||||
computeCustomRelationFieldMetadataForCreation({
|
||||
fieldMetadataInput,
|
||||
|
||||
+23
-28
@@ -1,10 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import {
|
||||
isDefined,
|
||||
trimAndRemoveDuplicatedWhitespacesFromString,
|
||||
} from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
@@ -39,15 +36,15 @@ export class FieldMetadataServiceV2 {
|
||||
) {}
|
||||
|
||||
async createOne({
|
||||
fieldMetadataInput,
|
||||
createFieldInput,
|
||||
workspaceId,
|
||||
}: {
|
||||
fieldMetadataInput: Omit<CreateFieldInput, 'workspaceId'>;
|
||||
createFieldInput: Omit<CreateFieldInput, 'workspaceId'>;
|
||||
workspaceId: string;
|
||||
}): Promise<FieldMetadataEntity> {
|
||||
const [createdFieldMetadata] = await this.createMany({
|
||||
fieldMetadataInputs: [fieldMetadataInput],
|
||||
workspaceId,
|
||||
createFieldInputs: [createFieldInput],
|
||||
});
|
||||
|
||||
if (!isDefined(createdFieldMetadata)) {
|
||||
@@ -206,13 +203,13 @@ export class FieldMetadataServiceV2 {
|
||||
}
|
||||
|
||||
async createMany({
|
||||
fieldMetadataInputs,
|
||||
createFieldInputs,
|
||||
workspaceId,
|
||||
}: {
|
||||
fieldMetadataInputs: Omit<CreateFieldInput, 'workspaceId'>[];
|
||||
createFieldInputs: Omit<CreateFieldInput, 'workspaceId'>[];
|
||||
workspaceId: string;
|
||||
}): Promise<FieldMetadataEntity[]> {
|
||||
if (!fieldMetadataInputs.length) {
|
||||
if (createFieldInputs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -221,24 +218,24 @@ export class FieldMetadataServiceV2 {
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const allInputTranspilationsResults = (
|
||||
await Promise.all(
|
||||
fieldMetadataInputs.map(
|
||||
async (fieldMetadataInput) =>
|
||||
await fromCreateFieldInputToFlatFieldMetadatasToCreate({
|
||||
existingFlatObjectMetadataMaps,
|
||||
workspaceId,
|
||||
rawCreateFieldInput: fieldMetadataInput,
|
||||
}),
|
||||
),
|
||||
)
|
||||
).flat();
|
||||
const allTranspiledTranspilationInputs = [];
|
||||
|
||||
for (const createInput of createFieldInputs) {
|
||||
allTranspiledTranspilationInputs.push(
|
||||
await fromCreateFieldInputToFlatFieldMetadatasToCreate({
|
||||
existingFlatObjectMetadataMaps,
|
||||
workspaceId,
|
||||
rawCreateFieldInput: createInput,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
throwOnFieldInputTranspilationsError(
|
||||
allInputTranspilationsResults,
|
||||
allTranspiledTranspilationInputs,
|
||||
'Multiple validation errors occurred while creating field',
|
||||
);
|
||||
const flatFieldMetadatasToCreate = allInputTranspilationsResults.flatMap(
|
||||
|
||||
const flatFieldMetadatasToCreate = allTranspiledTranspilationInputs.flatMap(
|
||||
({ result }) => result,
|
||||
);
|
||||
|
||||
@@ -289,10 +286,8 @@ export class FieldMetadataServiceV2 {
|
||||
return this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
name: In(
|
||||
fieldMetadataInputs.map((flatFieldMetadata) =>
|
||||
trimAndRemoveDuplicatedWhitespacesFromString(
|
||||
flatFieldMetadata.name,
|
||||
),
|
||||
allTranspiledTranspilationInputs.map(
|
||||
({ result: flatFieldMetadatas }) => flatFieldMetadatas[0].name,
|
||||
),
|
||||
),
|
||||
workspaceId,
|
||||
|
||||
+34
-69
@@ -31,7 +31,7 @@ import { FieldMetadataRelatedRecordsService } from 'src/engine/metadata-modules/
|
||||
import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service';
|
||||
import { FieldMetadataValidationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service';
|
||||
import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2';
|
||||
import { areFieldMetadatasTypeRelationOrMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-type-relation-or-morph-relation.util';
|
||||
import { areFieldMetadatasOfType } from 'src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-of-type.util';
|
||||
import { assertDoesNotNullifyDefaultValueForNonNullableField } from 'src/engine/metadata-modules/field-metadata/utils/assert-does-not-nullify-default-value-for-non-nullable-field.util';
|
||||
import { buildUpdatableStandardFieldInput } from 'src/engine/metadata-modules/field-metadata/utils/build-updatable-standard-field-input.util';
|
||||
import { checkCanDeactivateFieldOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/check-can-deactivate-field-or-throw';
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
computeColumnName,
|
||||
computeCompositeColumnName,
|
||||
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util';
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
import { createMigrationActions } from 'src/engine/metadata-modules/field-metadata/utils/create-migration-actions.util';
|
||||
import { generateRatingOptions } from 'src/engine/metadata-modules/field-metadata/utils/generate-rating-optionts.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
@@ -74,9 +74,9 @@ import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
||||
|
||||
type GenerateMigrationArgs = {
|
||||
fieldMetadata: FieldMetadataEntity<
|
||||
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
|
||||
>;
|
||||
fieldMetadata:
|
||||
| FieldMetadataEntity<FieldMetadataType.RELATION>
|
||||
| FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>;
|
||||
workspaceId: string;
|
||||
queryRunner: QueryRunner;
|
||||
};
|
||||
@@ -116,7 +116,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
|
||||
if (isWorkspaceMigrationV2Enabled) {
|
||||
return this.fieldMetadataServiceV2.createOne({
|
||||
fieldMetadataInput,
|
||||
createFieldInput: fieldMetadataInput,
|
||||
workspaceId: fieldMetadataInput.workspaceId,
|
||||
});
|
||||
}
|
||||
@@ -472,66 +472,33 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
|
||||
if (isFieldMetadataTypeRelation(fieldMetadata)) {
|
||||
const fieldMetadataIdsToDelete: string[] = [];
|
||||
const isRelationTargetMorphRelation = isFieldMetadataTypeMorphRelation(
|
||||
fieldMetadata.relationTargetFieldMetadata,
|
||||
|
||||
fieldMetadataIdsToDelete.push(
|
||||
fieldMetadata.id,
|
||||
fieldMetadata.relationTargetFieldMetadataId,
|
||||
);
|
||||
|
||||
if (isRelationTargetMorphRelation) {
|
||||
const morphRelationsWithSameName =
|
||||
await this.getMorphRelationsWithSameName({
|
||||
fieldMetadataName: fieldMetadata.relationTargetFieldMetadata.name,
|
||||
objectMetadataId:
|
||||
fieldMetadata.relationTargetFieldMetadata.objectMetadataId,
|
||||
workspaceId,
|
||||
fieldMetadataRepository,
|
||||
});
|
||||
await fieldMetadataRepository.delete({
|
||||
id: In(fieldMetadataIdsToDelete),
|
||||
});
|
||||
|
||||
morphRelationsWithSameName.forEach((morphRelation) => {
|
||||
fieldMetadataIdsToDelete.push(
|
||||
morphRelation.id,
|
||||
morphRelation.relationTargetFieldMetadataId,
|
||||
);
|
||||
});
|
||||
|
||||
await fieldMetadataRepository.delete({
|
||||
id: In(fieldMetadataIdsToDelete),
|
||||
});
|
||||
|
||||
for (const morphRelation of morphRelationsWithSameName) {
|
||||
await this.generateDeleteRelationMigration({
|
||||
fieldMetadata: morphRelation,
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
fieldMetadataIdsToDelete.push(
|
||||
fieldMetadata.id,
|
||||
fieldMetadata.relationTargetFieldMetadataId,
|
||||
);
|
||||
|
||||
await fieldMetadataRepository.delete({
|
||||
id: In(fieldMetadataIdsToDelete),
|
||||
});
|
||||
|
||||
await this.generateDeleteRelationMigration({
|
||||
fieldMetadata,
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
});
|
||||
}
|
||||
await this.generateDeleteRelationMigration({
|
||||
fieldMetadata,
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
});
|
||||
} else if (isFieldMetadataTypeMorphRelation(fieldMetadata)) {
|
||||
const fieldMetadataIdsToDelete: string[] = [];
|
||||
|
||||
const morphRelationsWithSameName =
|
||||
await this.getMorphRelationsWithSameName({
|
||||
fieldMetadataName: fieldMetadata.name,
|
||||
const allMophFieldMetadatas =
|
||||
await this.getAllMorphFieldMetadatasByMorphId({
|
||||
morphId: fieldMetadata.morphId,
|
||||
objectMetadataId: fieldMetadata.objectMetadataId,
|
||||
workspaceId,
|
||||
fieldMetadataRepository,
|
||||
});
|
||||
|
||||
morphRelationsWithSameName.forEach((morphRelation) => {
|
||||
allMophFieldMetadatas.forEach((morphRelation) => {
|
||||
fieldMetadataIdsToDelete.push(
|
||||
morphRelation.id,
|
||||
morphRelation.relationTargetFieldMetadataId,
|
||||
@@ -542,7 +509,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
id: In(fieldMetadataIdsToDelete),
|
||||
});
|
||||
|
||||
for (const morphRelation of morphRelationsWithSameName) {
|
||||
for (const morphRelation of allMophFieldMetadatas) {
|
||||
await this.generateDeleteRelationMigration({
|
||||
fieldMetadata: morphRelation,
|
||||
workspaceId,
|
||||
@@ -675,7 +642,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
|
||||
if (isWorkspaceMigrationV2Enabled) {
|
||||
return this.fieldMetadataServiceV2.createMany({
|
||||
fieldMetadataInputs,
|
||||
createFieldInputs: fieldMetadataInputs,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
@@ -1036,7 +1003,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
{
|
||||
fieldMetadataInput: fieldMetadataForCreate,
|
||||
relationCreationPayload: fieldMetadataInput.relationCreationPayload,
|
||||
joinColumnName: computeRelationFieldJoinColumnName({
|
||||
joinColumnName: computeMorphOrRelationFieldJoinColumnName({
|
||||
name: fieldMetadataForCreate.name,
|
||||
}),
|
||||
},
|
||||
@@ -1072,24 +1039,20 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
);
|
||||
}
|
||||
|
||||
private async getMorphRelationsWithSameName({
|
||||
fieldMetadataName,
|
||||
private async getAllMorphFieldMetadatasByMorphId({
|
||||
morphId,
|
||||
objectMetadataId,
|
||||
workspaceId,
|
||||
fieldMetadataRepository,
|
||||
}: {
|
||||
fieldMetadataName: string;
|
||||
morphId: string;
|
||||
objectMetadataId: string;
|
||||
workspaceId: string;
|
||||
fieldMetadataRepository: Repository<FieldMetadataEntity>;
|
||||
}): Promise<
|
||||
FieldMetadataEntity<
|
||||
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
|
||||
>[]
|
||||
> {
|
||||
}): Promise<FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>[]> {
|
||||
const fieldMetadatas = await fieldMetadataRepository.find({
|
||||
where: {
|
||||
name: fieldMetadataName,
|
||||
morphId,
|
||||
objectMetadataId,
|
||||
workspaceId,
|
||||
},
|
||||
@@ -1100,9 +1063,11 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
],
|
||||
});
|
||||
|
||||
if (!areFieldMetadatasTypeRelationOrMorphRelation(fieldMetadatas)) {
|
||||
if (
|
||||
!areFieldMetadatasOfType(fieldMetadatas, FieldMetadataType.MORPH_RELATION)
|
||||
) {
|
||||
throw new FieldMetadataException(
|
||||
'At least one field metadata is not a relation or morph relation',
|
||||
`Encountered a non morph relation morphId related flat field metadata type`,
|
||||
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
|
||||
export const areFieldMetadatasOfType = <T extends FieldMetadataType>(
|
||||
fieldMetadatas: FieldMetadataEntity[],
|
||||
fieldMetadataType: T,
|
||||
): fieldMetadatas is Array<FieldMetadataEntity & FieldMetadataEntity<T>> =>
|
||||
fieldMetadatas.every((fieldMetadata) =>
|
||||
isFlatFieldMetadataOfType(fieldMetadata, fieldMetadataType),
|
||||
);
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util';
|
||||
import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util';
|
||||
|
||||
export const areFieldMetadatasTypeRelationOrMorphRelation = (
|
||||
fieldMetadatas: FieldMetadataEntity[],
|
||||
): fieldMetadatas is Array<
|
||||
FieldMetadataEntity &
|
||||
FieldMetadataEntity<
|
||||
FieldMetadataType.MORPH_RELATION | FieldMetadataType.RELATION
|
||||
>
|
||||
> => {
|
||||
return fieldMetadatas.every(
|
||||
(fieldMetadata) =>
|
||||
isFieldMetadataTypeRelation(fieldMetadata) ||
|
||||
isFieldMetadataTypeMorphRelation(fieldMetadata),
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -2,7 +2,7 @@ type ComputeRelationFieldJoinColumnNameArgs = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const computeRelationFieldJoinColumnName = ({
|
||||
export const computeMorphOrRelationFieldJoinColumnName = ({
|
||||
name,
|
||||
}: ComputeRelationFieldJoinColumnNameArgs) => {
|
||||
return `${name}Id`;
|
||||
+4
-10
@@ -6,7 +6,7 @@ import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-me
|
||||
|
||||
type ComputeMorphRelationFieldNameArgs = {
|
||||
fieldName: string;
|
||||
relationDirection: RelationType;
|
||||
relationType: RelationType;
|
||||
targetObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'namePlural'
|
||||
@@ -15,18 +15,12 @@ type ComputeMorphRelationFieldNameArgs = {
|
||||
|
||||
export const computeMorphRelationFieldName = ({
|
||||
fieldName,
|
||||
relationDirection,
|
||||
relationType,
|
||||
targetObjectMetadata,
|
||||
}: ComputeMorphRelationFieldNameArgs): string => {
|
||||
if (relationDirection === RelationType.MANY_TO_ONE) {
|
||||
if (relationType === RelationType.MANY_TO_ONE) {
|
||||
return `${fieldName}${capitalize(targetObjectMetadata.nameSingular)}`;
|
||||
}
|
||||
|
||||
if (relationDirection === RelationType.ONE_TO_MANY) {
|
||||
return `${fieldName}${capitalize(targetObjectMetadata.namePlural)}`;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Invalid relation direction: ${relationDirection} for field ${fieldName}`,
|
||||
);
|
||||
return `${fieldName}${capitalize(targetObjectMetadata.namePlural)}`;
|
||||
};
|
||||
|
||||
+1
@@ -6,6 +6,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
|
||||
import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
|
||||
// TODO simplify
|
||||
export const resolveFieldMetadataStandardOverride = (
|
||||
fieldMetadata: Pick<
|
||||
FieldMetadataDTO,
|
||||
|
||||
+6
-6
@@ -138,7 +138,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"isUnique": null,
|
||||
"label": "newFieldLabel",
|
||||
"morphId": Any<String>,
|
||||
"name": "newField",
|
||||
"name": "newFieldPets",
|
||||
"objectMetadataId": Any<String>,
|
||||
"options": null,
|
||||
"relationTargetFieldMetadataId": Any<String>,
|
||||
@@ -252,7 +252,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"isUnique": null,
|
||||
"label": "newFieldLabel",
|
||||
"morphId": Any<String>,
|
||||
"name": "newField",
|
||||
"name": "newFieldPets",
|
||||
"objectMetadataId": Any<String>,
|
||||
"options": null,
|
||||
"relationTargetFieldMetadataId": Any<String>,
|
||||
@@ -317,7 +317,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"isUnique": null,
|
||||
"label": "newFieldLabel",
|
||||
"morphId": Any<String>,
|
||||
"name": "newField",
|
||||
"name": "newFieldPets",
|
||||
"objectMetadataId": Any<String>,
|
||||
"options": null,
|
||||
"relationTargetFieldMetadataId": Any<String>,
|
||||
@@ -444,7 +444,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"isUnique": null,
|
||||
"label": "newFieldLabel",
|
||||
"morphId": Any<String>,
|
||||
"name": "newField",
|
||||
"name": "newFieldCompanies",
|
||||
"objectMetadataId": Any<String>,
|
||||
"options": null,
|
||||
"relationTargetFieldMetadataId": Any<String>,
|
||||
@@ -558,7 +558,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"isUnique": null,
|
||||
"label": "newFieldLabel",
|
||||
"morphId": Any<String>,
|
||||
"name": "newField",
|
||||
"name": "newFieldCompanies",
|
||||
"objectMetadataId": Any<String>,
|
||||
"options": null,
|
||||
"relationTargetFieldMetadataId": Any<String>,
|
||||
@@ -623,7 +623,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"isUnique": null,
|
||||
"label": "newFieldLabel",
|
||||
"morphId": Any<String>,
|
||||
"name": "newField",
|
||||
"name": "newFieldCompanies",
|
||||
"objectMetadataId": Any<String>,
|
||||
"options": null,
|
||||
"relationTargetFieldMetadataId": Any<String>,
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
import {
|
||||
FlatObjectMetadataMapsException,
|
||||
FlatObjectMetadataMapsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/flat-object-metadata-maps/flat-object-metadata-maps.exception';
|
||||
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
|
||||
|
||||
export type FindAllMorphRelationFlatFieldMetadatasOrThrowArgs = {
|
||||
flatObjectMetadataMaps: FlatObjectMetadataMaps;
|
||||
flatFieldMetadata: FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>;
|
||||
};
|
||||
export const findAllOthersMorphRelationFlatFieldMetadatasOrThrow = ({
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadata: morphRelationFlatFieldMetadata,
|
||||
}: FindAllMorphRelationFlatFieldMetadatasOrThrowArgs): FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>[] => {
|
||||
const flatObjectMetadataWithFlatFieldMaps =
|
||||
flatObjectMetadataMaps.byId[
|
||||
morphRelationFlatFieldMetadata.objectMetadataId
|
||||
];
|
||||
|
||||
if (!isDefined(flatObjectMetadataWithFlatFieldMaps)) {
|
||||
throw new FlatObjectMetadataMapsException(
|
||||
'Morph field relation object metadata not found',
|
||||
FlatObjectMetadataMapsExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!isDefined(
|
||||
flatObjectMetadataWithFlatFieldMaps.fieldsById[
|
||||
morphRelationFlatFieldMetadata.id
|
||||
],
|
||||
)
|
||||
) {
|
||||
throw new FlatObjectMetadataMapsException(
|
||||
'Morph relation field not found in related object metadata',
|
||||
FlatObjectMetadataMapsExceptionCode.FIELD_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return flatObjectMetadataWithFlatFieldMaps.flatFieldMetadatas.filter(
|
||||
(
|
||||
flatFieldMetadata,
|
||||
): flatFieldMetadata is FlatFieldMetadata<FieldMetadataType.MORPH_RELATION> =>
|
||||
isFlatFieldMetadataOfType(
|
||||
flatFieldMetadata,
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
) &&
|
||||
flatFieldMetadata.morphId === morphRelationFlatFieldMetadata.morphId &&
|
||||
flatFieldMetadata.id !== morphRelationFlatFieldMetadata.id,
|
||||
);
|
||||
};
|
||||
+21
-54
@@ -1,14 +1,9 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { findAllOthersMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util';
|
||||
import { findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-relation-flat-field-metadatas-target-flat-field-metadata-or-throw.util';
|
||||
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
import {
|
||||
FlatObjectMetadataMapsException,
|
||||
FlatObjectMetadataMapsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/flat-object-metadata-maps/flat-object-metadata-maps.exception';
|
||||
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
|
||||
|
||||
export type FindFlatFieldMetadatasRelatedToMorphRelationOrThrowArgs = {
|
||||
@@ -19,54 +14,26 @@ export const findFlatFieldMetadatasRelatedToMorphRelationOrThrow = ({
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadata: morphRelationFlatFieldMetadata,
|
||||
}: FindFlatFieldMetadatasRelatedToMorphRelationOrThrowArgs): FlatFieldMetadata<MorphOrRelationFieldMetadataType>[] => {
|
||||
const flatObjectMetadataWithFlatFieldMaps =
|
||||
flatObjectMetadataMaps.byId[
|
||||
morphRelationFlatFieldMetadata.objectMetadataId
|
||||
];
|
||||
const allMorphFlatFieldMetadatas =
|
||||
findAllOthersMorphRelationFlatFieldMetadatasOrThrow({
|
||||
flatFieldMetadata: morphRelationFlatFieldMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatObjectMetadataWithFlatFieldMaps)) {
|
||||
throw new FlatObjectMetadataMapsException(
|
||||
'Morph field relation object metadata not found',
|
||||
FlatObjectMetadataMapsExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
return [
|
||||
morphRelationFlatFieldMetadata,
|
||||
...allMorphFlatFieldMetadatas,
|
||||
].flatMap((flatFieldMetadata) => {
|
||||
const relationTargetFlatFieldMetadata =
|
||||
findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow({
|
||||
flatFieldMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(
|
||||
flatObjectMetadataWithFlatFieldMaps.fieldsById[
|
||||
morphRelationFlatFieldMetadata.id
|
||||
],
|
||||
)
|
||||
) {
|
||||
throw new FlatObjectMetadataMapsException(
|
||||
'Morph relation field not found in related object metadata',
|
||||
FlatObjectMetadataMapsExceptionCode.FIELD_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
if (flatFieldMetadata.id === morphRelationFlatFieldMetadata.id) {
|
||||
return [relationTargetFlatFieldMetadata];
|
||||
}
|
||||
|
||||
return flatObjectMetadataWithFlatFieldMaps.flatFieldMetadatas.flatMap(
|
||||
(flatFieldMetadata) => {
|
||||
if (
|
||||
!isFlatFieldMetadataOfType(
|
||||
flatFieldMetadata,
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
) ||
|
||||
flatFieldMetadata.name !== morphRelationFlatFieldMetadata.name
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const relationTargetFlatFieldMetadata =
|
||||
findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow({
|
||||
flatFieldMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (flatFieldMetadata.id === morphRelationFlatFieldMetadata.id) {
|
||||
return [relationTargetFlatFieldMetadata];
|
||||
}
|
||||
|
||||
return [flatFieldMetadata, relationTargetFlatFieldMetadata];
|
||||
},
|
||||
);
|
||||
return [flatFieldMetadata, relationTargetFlatFieldMetadata];
|
||||
});
|
||||
};
|
||||
|
||||
+28
-3
@@ -1,8 +1,9 @@
|
||||
import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatRelationTargetFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-relation-target-field-metadata.type';
|
||||
|
||||
export const fromFlatFieldMetadataToFieldMetadataDto = (
|
||||
flatFieldMetadata: FlatFieldMetadata,
|
||||
flatFieldMetadata: FlatFieldMetadata | FlatRelationTargetFieldMetadata,
|
||||
): FieldMetadataDTO => {
|
||||
const {
|
||||
createdAt,
|
||||
@@ -13,11 +14,35 @@ export const fromFlatFieldMetadataToFieldMetadataDto = (
|
||||
isNullable,
|
||||
isUnique,
|
||||
settings,
|
||||
...rest
|
||||
id,
|
||||
label,
|
||||
name,
|
||||
objectMetadataId,
|
||||
type,
|
||||
workspaceId,
|
||||
defaultValue,
|
||||
isActive,
|
||||
isCustom,
|
||||
isLabelSyncedWithName,
|
||||
isSystem,
|
||||
isUIReadOnly,
|
||||
options,
|
||||
} = flatFieldMetadata;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
id,
|
||||
label,
|
||||
name,
|
||||
objectMetadataId,
|
||||
type,
|
||||
workspaceId,
|
||||
defaultValue,
|
||||
isActive,
|
||||
isCustom,
|
||||
isLabelSyncedWithName,
|
||||
isSystem,
|
||||
isUIReadOnly,
|
||||
options,
|
||||
createdAt: new Date(createdAt),
|
||||
updatedAt: new Date(updatedAt),
|
||||
description: description ?? undefined,
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util';
|
||||
|
||||
export const fromMorphOrRelationFlatFieldMetadataToRelationDto = ({
|
||||
flatFieldMetadata,
|
||||
sourceFlatObjectMetadata,
|
||||
}: {
|
||||
sourceFlatObjectMetadata: FlatObjectMetadata;
|
||||
flatFieldMetadata: FlatFieldMetadata<MorphOrRelationFieldMetadataType>;
|
||||
}) => ({
|
||||
type: flatFieldMetadata.settings.relationType,
|
||||
sourceObjectMetadata: fromFlatObjectMetadataToObjectMetadataDto(
|
||||
sourceFlatObjectMetadata,
|
||||
),
|
||||
sourceFieldMetadata:
|
||||
fromFlatFieldMetadataToFieldMetadataDto(flatFieldMetadata),
|
||||
targetObjectMetadata: fromFlatObjectMetadataToObjectMetadataDto(
|
||||
flatFieldMetadata.flatRelationTargetObjectMetadata,
|
||||
),
|
||||
targetFieldMetadata: fromFlatFieldMetadataToFieldMetadataDto(
|
||||
flatFieldMetadata.flatRelationTargetFieldMetadata,
|
||||
),
|
||||
});
|
||||
+11
-8
@@ -1,14 +1,13 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
computeMorphRelationFieldJoinColumnName,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type';
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util';
|
||||
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { generateMorphOrRelationFlatFieldMetadataPair } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util';
|
||||
@@ -68,17 +67,21 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({
|
||||
const morphId = v4();
|
||||
const flatFieldMetadatas = morphRelationCreationPayload.flatMap(
|
||||
({ relationCreationPayload, targetFlatObjectMetadata }) => {
|
||||
const currentMorphRelationFieldName = computeMorphRelationFieldName({
|
||||
fieldName: createFieldInput.name,
|
||||
relationType: relationCreationPayload.type,
|
||||
targetObjectMetadata: targetFlatObjectMetadata,
|
||||
});
|
||||
const sourceFlatObjectMetadataJoinColumnName =
|
||||
computeMorphRelationFieldJoinColumnName({
|
||||
name: createFieldInput.name,
|
||||
targetObjectMetadataNameSingular:
|
||||
targetFlatObjectMetadata.nameSingular,
|
||||
computeMorphOrRelationFieldJoinColumnName({
|
||||
name: currentMorphRelationFieldName,
|
||||
});
|
||||
|
||||
return generateMorphOrRelationFlatFieldMetadataPair({
|
||||
createFieldInput: {
|
||||
...createFieldInput,
|
||||
relationCreationPayload,
|
||||
name: currentMorphRelationFieldName,
|
||||
},
|
||||
sourceFlatObjectMetadataJoinColumnName,
|
||||
sourceFlatObjectMetadata,
|
||||
|
||||
+5
-4
@@ -4,7 +4,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util';
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { generateMorphOrRelationFlatFieldMetadataPair } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util';
|
||||
@@ -58,9 +58,10 @@ export const fromRelationCreateFieldInputToFlatFieldMetadatas = async ({
|
||||
...createFieldInput,
|
||||
relationCreationPayload,
|
||||
},
|
||||
sourceFlatObjectMetadataJoinColumnName: computeRelationFieldJoinColumnName({
|
||||
name: createFieldInput.name,
|
||||
}),
|
||||
sourceFlatObjectMetadataJoinColumnName:
|
||||
computeMorphOrRelationFieldJoinColumnName({
|
||||
name: createFieldInput.name,
|
||||
}),
|
||||
sourceFlatObjectMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
workspaceId,
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type';
|
||||
import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util';
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { getDefaultFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
@@ -92,7 +92,7 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
|
||||
};
|
||||
const targetFlatFieldMetadataSettings =
|
||||
computeFieldMetadataRelationSettingsForRelationType({
|
||||
joinColumnName: computeRelationFieldJoinColumnName({
|
||||
joinColumnName: computeMorphOrRelationFieldJoinColumnName({
|
||||
name: targetCreateFieldInput.name,
|
||||
}),
|
||||
relationType:
|
||||
|
||||
+35
-4
@@ -1,8 +1,11 @@
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import {
|
||||
type FlatObjectMetadataWithoutFields,
|
||||
type FlatObjectMetadata,
|
||||
} from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
|
||||
export const fromFlatObjectMetadataToObjectMetadataDto = (
|
||||
flatObjectMetadata: FlatObjectMetadata,
|
||||
flatObjectMetadata: FlatObjectMetadata | FlatObjectMetadataWithoutFields,
|
||||
): ObjectMetadataDTO => {
|
||||
const {
|
||||
createdAt,
|
||||
@@ -12,11 +15,39 @@ export const fromFlatObjectMetadataToObjectMetadataDto = (
|
||||
standardOverrides,
|
||||
shortcut,
|
||||
duplicateCriteria,
|
||||
...rest
|
||||
id,
|
||||
isActive,
|
||||
isCustom,
|
||||
isLabelSyncedWithName,
|
||||
isRemote,
|
||||
isSearchable,
|
||||
isSystem,
|
||||
isUIReadOnly,
|
||||
labelPlural,
|
||||
labelSingular,
|
||||
namePlural,
|
||||
nameSingular,
|
||||
workspaceId,
|
||||
imageIdentifierFieldMetadataId,
|
||||
labelIdentifierFieldMetadataId,
|
||||
} = flatObjectMetadata;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
id,
|
||||
isActive,
|
||||
isCustom,
|
||||
isLabelSyncedWithName,
|
||||
isRemote,
|
||||
isSearchable,
|
||||
isSystem,
|
||||
isUIReadOnly,
|
||||
labelPlural,
|
||||
labelSingular,
|
||||
namePlural,
|
||||
nameSingular,
|
||||
workspaceId,
|
||||
imageIdentifierFieldMetadataId,
|
||||
labelIdentifierFieldMetadataId,
|
||||
createdAt: new Date(createdAt),
|
||||
updatedAt: new Date(updatedAt),
|
||||
description: description ?? undefined,
|
||||
|
||||
+7
-9
@@ -10,7 +10,7 @@ import { findFlatObjectMetadataInFlatObjectMetadataMaps } from 'src/engine/metad
|
||||
import { FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-properties-to-compare.constant';
|
||||
import { type FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { renameFlatObjectMetadataManyToOneMorphRelationTargetFlatFieldMetadatasSettingsJoinColumnName } from 'src/engine/metadata-modules/flat-object-metadata/utils/rename-flat-object-metadata-morph-relation-flat-field-metadatas-settings-join-column-name.util';
|
||||
import { renameRelatedMorphFieldOnObjectNamesUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util';
|
||||
import { OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata-standard-overrides-properties.constant';
|
||||
import { type UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
||||
import {
|
||||
@@ -130,14 +130,12 @@ export const fromUpdateObjectInputToFlatObjectMetadata = ({
|
||||
[property]: updatedPropertyValue,
|
||||
};
|
||||
const newUpdatedOtherObjectFlatFieldMetadatas =
|
||||
property === 'nameSingular'
|
||||
? renameFlatObjectMetadataManyToOneMorphRelationTargetFlatFieldMetadatasSettingsJoinColumnName(
|
||||
{
|
||||
existingFlatObjectMetadataMaps,
|
||||
fromFlatObjectMetadata: updatedFlatObjectMetadata,
|
||||
toFlatObjectMetadata: updatedFlatObjectMetadata,
|
||||
},
|
||||
)
|
||||
property === 'nameSingular' || property === 'namePlural'
|
||||
? renameRelatedMorphFieldOnObjectNamesUpdate({
|
||||
existingFlatObjectMetadataMaps,
|
||||
fromFlatObjectMetadata: updatedFlatObjectMetadata,
|
||||
toFlatObjectMetadata: updatedFlatObjectMetadata,
|
||||
})
|
||||
: [];
|
||||
|
||||
return {
|
||||
|
||||
+3
-7
@@ -1,22 +1,20 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
|
||||
import { findFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
type GetFlatObjectMetadataMorphRelationFlatFieldMetadatasArgs = {
|
||||
type GetFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrowArgs = {
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
existingFlatObjectMetadataMaps: FlatObjectMetadataMaps;
|
||||
};
|
||||
export const getFlatObjectMetadataManyToOneTargetMorphRelationFlatFieldMetadatasOrThrow =
|
||||
export const getFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrow =
|
||||
({
|
||||
flatObjectMetadata,
|
||||
existingFlatObjectMetadataMaps,
|
||||
}: GetFlatObjectMetadataMorphRelationFlatFieldMetadatasArgs) => {
|
||||
}: GetFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrowArgs): FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>[] => {
|
||||
const manyToOneMorphRelationTargetFlatFieldMetadataIds =
|
||||
flatObjectMetadata.flatFieldMetadatas
|
||||
.filter(
|
||||
@@ -27,8 +25,6 @@ export const getFlatObjectMetadataManyToOneTargetMorphRelationFlatFieldMetadatas
|
||||
flatFieldMetadata,
|
||||
FieldMetadataType.RELATION,
|
||||
) &&
|
||||
flatFieldMetadata.settings.relationType ===
|
||||
RelationType.MANY_TO_ONE &&
|
||||
isFlatFieldMetadataOfType(
|
||||
flatFieldMetadata.flatRelationTargetFieldMetadata,
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { type FieldMetadataType, type FromTo } from 'twenty-shared/types';
|
||||
import { computeMorphRelationFieldJoinColumnName } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { getFlatObjectMetadataManyToOneTargetMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-flat-object-metadata-many-to-one-target-morph-relation-flat-field-metadatas-or-throw.util';
|
||||
|
||||
type RenameFlatObjectMetadataManyToOneMorphRelationTargetFlatFieldMetadatasSettingsJoinColumnNameArgs =
|
||||
FromTo<FlatObjectMetadata, 'flatObjectMetadata'> & {
|
||||
existingFlatObjectMetadataMaps: FlatObjectMetadataMaps;
|
||||
};
|
||||
export const renameFlatObjectMetadataManyToOneMorphRelationTargetFlatFieldMetadatasSettingsJoinColumnName =
|
||||
({
|
||||
fromFlatObjectMetadata,
|
||||
toFlatObjectMetadata,
|
||||
existingFlatObjectMetadataMaps,
|
||||
}: RenameFlatObjectMetadataManyToOneMorphRelationTargetFlatFieldMetadatasSettingsJoinColumnNameArgs): FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>[] => {
|
||||
const manyToOneMorphRelationFlatFieldMetadatas =
|
||||
getFlatObjectMetadataManyToOneTargetMorphRelationFlatFieldMetadatasOrThrow(
|
||||
{
|
||||
flatObjectMetadata: fromFlatObjectMetadata,
|
||||
existingFlatObjectMetadataMaps,
|
||||
},
|
||||
);
|
||||
|
||||
const updatedFlatFieldMetadatas =
|
||||
manyToOneMorphRelationFlatFieldMetadatas.map(
|
||||
(morphRelationFlatFieldMetadata) => {
|
||||
const newJoinColumnName = computeMorphRelationFieldJoinColumnName({
|
||||
name: morphRelationFlatFieldMetadata.name,
|
||||
targetObjectMetadataNameSingular: toFlatObjectMetadata.nameSingular,
|
||||
});
|
||||
|
||||
return {
|
||||
...morphRelationFlatFieldMetadata,
|
||||
settings: {
|
||||
...morphRelationFlatFieldMetadata.settings,
|
||||
joinColumnName: newJoinColumnName,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return updatedFlatFieldMetadatas;
|
||||
};
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
RelationType,
|
||||
type FieldMetadataType,
|
||||
type FromTo,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { getFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-flat-object-metadata-many-to-one-target-morph-relation-flat-field-metadatas-or-throw.util';
|
||||
|
||||
const searchAndReplaceLast = ({
|
||||
replace,
|
||||
search,
|
||||
source,
|
||||
}: {
|
||||
source: string;
|
||||
search: string;
|
||||
replace: string;
|
||||
}) => {
|
||||
const lastIndex = source.lastIndexOf(search);
|
||||
|
||||
if (lastIndex === -1) return source;
|
||||
|
||||
return (
|
||||
source.slice(0, lastIndex) +
|
||||
replace +
|
||||
source.slice(lastIndex + search.length)
|
||||
);
|
||||
};
|
||||
|
||||
type RenameRelatedMorphFieldOnObjectNamesUpdateArgs = FromTo<
|
||||
FlatObjectMetadata,
|
||||
'flatObjectMetadata'
|
||||
> & {
|
||||
existingFlatObjectMetadataMaps: FlatObjectMetadataMaps;
|
||||
};
|
||||
export const renameRelatedMorphFieldOnObjectNamesUpdate = ({
|
||||
fromFlatObjectMetadata,
|
||||
existingFlatObjectMetadataMaps,
|
||||
toFlatObjectMetadata,
|
||||
}: RenameRelatedMorphFieldOnObjectNamesUpdateArgs): FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>[] => {
|
||||
const manyToOneMorphRelationFlatFieldMetadatas =
|
||||
getFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrow({
|
||||
flatObjectMetadata: fromFlatObjectMetadata,
|
||||
existingFlatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
const updatedFlatFieldMetadatas =
|
||||
manyToOneMorphRelationFlatFieldMetadatas.map(
|
||||
(morphRelationFlatFieldMetadata) => {
|
||||
const isManyToOneRelationType =
|
||||
morphRelationFlatFieldMetadata.settings.relationType ===
|
||||
RelationType.MANY_TO_ONE;
|
||||
const initialMorphRelationFieldName = searchAndReplaceLast({
|
||||
source: morphRelationFlatFieldMetadata.name,
|
||||
replace: '',
|
||||
search: isManyToOneRelationType
|
||||
? fromFlatObjectMetadata.nameSingular
|
||||
: fromFlatObjectMetadata.namePlural,
|
||||
});
|
||||
const newMorphFieldName = computeMorphRelationFieldName({
|
||||
fieldName: initialMorphRelationFieldName,
|
||||
relationType: morphRelationFlatFieldMetadata.settings.relationType,
|
||||
targetObjectMetadata: toFlatObjectMetadata,
|
||||
});
|
||||
const newJoinColumnName = isManyToOneRelationType
|
||||
? computeMorphOrRelationFieldJoinColumnName({
|
||||
name: newMorphFieldName,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...morphRelationFlatFieldMetadata,
|
||||
name: newMorphFieldName,
|
||||
settings: {
|
||||
...morphRelationFlatFieldMetadata.settings,
|
||||
joinColumnName: newJoinColumnName,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return updatedFlatFieldMetadatas;
|
||||
};
|
||||
+7
-9
@@ -2,17 +2,14 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
capitalize,
|
||||
computeMorphRelationFieldJoinColumnName,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { type QueryRunner, Repository } from 'typeorm';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import {
|
||||
ObjectMetadataException,
|
||||
@@ -522,9 +519,12 @@ export class ObjectMetadataFieldRelationService {
|
||||
return fieldMetadatas;
|
||||
}
|
||||
|
||||
// Not maintained on v1, this side effect is broken and will duplicated field name references on
|
||||
// object update
|
||||
// It's functional on v2
|
||||
public async updateMorphRelationsJoinColumnName({
|
||||
existingObjectMetadata,
|
||||
objectMetadataForUpdate,
|
||||
objectMetadataForUpdate: _,
|
||||
queryRunner,
|
||||
}: {
|
||||
existingObjectMetadata: Pick<
|
||||
@@ -567,10 +567,8 @@ export class ObjectMetadataFieldRelationService {
|
||||
|
||||
if (morphRelationFieldMetadataToUpdate.length > 0) {
|
||||
for (const morphRelationFieldMetadata of morphRelationFieldMetadataToUpdate) {
|
||||
const newJoinColumnName = computeMorphRelationFieldJoinColumnName({
|
||||
const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({
|
||||
name: morphRelationFieldMetadata.name,
|
||||
targetObjectMetadataNameSingular:
|
||||
objectMetadataForUpdate.nameSingular,
|
||||
});
|
||||
|
||||
await fieldMetadataRepository.save({
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
|
||||
+1
-11
@@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { type EntitySchemaRelationOptions } from 'typeorm';
|
||||
|
||||
import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { determineSchemaRelationDetails } from 'src/engine/twenty-orm/utils/determine-schema-relation-details.util';
|
||||
@@ -62,16 +61,7 @@ export class EntitySchemaRelationFactory {
|
||||
);
|
||||
}
|
||||
|
||||
const fieldName =
|
||||
fieldMetadata.type === FieldMetadataType.MORPH_RELATION
|
||||
? computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadata.name,
|
||||
relationDirection: fieldMetadata.settings.relationType,
|
||||
targetObjectMetadata,
|
||||
})
|
||||
: fieldMetadata.name;
|
||||
|
||||
entitySchemaRelationMap[fieldName] = {
|
||||
entitySchemaRelationMap[fieldMetadata.name] = {
|
||||
type: schemaRelationDetails.relationType,
|
||||
target: schemaRelationDetails.target,
|
||||
inverseSide: schemaRelationDetails.inverseSide,
|
||||
|
||||
+15
@@ -20,4 +20,19 @@ export const PET_CUSTOM_RELATION_FIELD_SEEDS: (FieldMetadataSeed & {
|
||||
],
|
||||
targetObjectMetadataNames: ['surveyResult', 'rocket'],
|
||||
},
|
||||
{
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
label: 'Helper of the Pet (rocket or survey)',
|
||||
name: 'helper',
|
||||
icon: 'IconRelationOneToMany',
|
||||
morphRelationsCreationPayload: [
|
||||
{
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
targetObjectMetadataId: 'to-be-resolved-later',
|
||||
targetFieldLabel: 'Helped by',
|
||||
targetFieldIcon: 'IconRelationOneToMany',
|
||||
},
|
||||
],
|
||||
targetObjectMetadataNames: ['surveyResult', 'rocket'],
|
||||
},
|
||||
];
|
||||
|
||||
+53
-44
@@ -1,8 +1,10 @@
|
||||
import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util';
|
||||
import { createMorphRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
import {
|
||||
eachTestingContextFilter,
|
||||
type EachTestingContext,
|
||||
} from 'twenty-shared/testing';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
@@ -18,6 +20,7 @@ describe('createOne FieldMetadataService morph relation fields', () => {
|
||||
createOneObject: { id: objectMetadataPersonId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
nameSingular: 'personForMorphRelation',
|
||||
namePlural: 'peopleForMorphRelation',
|
||||
@@ -34,6 +37,7 @@ describe('createOne FieldMetadataService morph relation fields', () => {
|
||||
createOneObject: { id: objectMetadataCompanyId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
nameSingular: 'companyForMorphRelation',
|
||||
namePlural: 'companiesForMorphRelation',
|
||||
@@ -50,6 +54,7 @@ describe('createOne FieldMetadataService morph relation fields', () => {
|
||||
createOneObject: { id: objectMetadataOpportunityId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
nameSingular: 'opportunityForMorphRelation',
|
||||
namePlural: 'opportunitiesForMorphRelation',
|
||||
@@ -63,12 +68,15 @@ describe('createOne FieldMetadataService morph relation fields', () => {
|
||||
});
|
||||
afterEach(async () => {
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: createdObjectMetadataPersonId },
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: createdObjectMetadataOpportunityId },
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: createdObjectMetadataCompanyId },
|
||||
});
|
||||
});
|
||||
@@ -118,51 +126,52 @@ describe('createOne FieldMetadataService morph relation fields', () => {
|
||||
},
|
||||
];
|
||||
|
||||
it.each(eachTestingContextArray)('$title', async ({ context }) => {
|
||||
const contextPayload =
|
||||
typeof context === 'function'
|
||||
? context({
|
||||
objectMetadataId: createdObjectMetadataOpportunityId,
|
||||
firstTargetObjectMetadataId: createdObjectMetadataPersonId,
|
||||
secondTargetObjectMetadataId: createdObjectMetadataCompanyId,
|
||||
})
|
||||
: context;
|
||||
it.each(eachTestingContextFilter(eachTestingContextArray))(
|
||||
'$title',
|
||||
async ({ context }) => {
|
||||
const contextPayload =
|
||||
typeof context === 'function'
|
||||
? context({
|
||||
objectMetadataId: createdObjectMetadataOpportunityId,
|
||||
firstTargetObjectMetadataId: createdObjectMetadataPersonId,
|
||||
secondTargetObjectMetadataId: createdObjectMetadataCompanyId,
|
||||
})
|
||||
: context;
|
||||
|
||||
const createdField = await createMorphRelationBetweenObjects({
|
||||
objectMetadataId: contextPayload.objectMetadataId,
|
||||
firstTargetObjectMetadataId: contextPayload.firstTargetObjectMetadataId,
|
||||
secondTargetObjectMetadataId: contextPayload.secondTargetObjectMetadataId,
|
||||
type: contextPayload.type,
|
||||
relationType: contextPayload.relationType,
|
||||
});
|
||||
const createdField = await createMorphRelationBetweenObjects({
|
||||
objectMetadataId: contextPayload.objectMetadataId,
|
||||
firstTargetObjectMetadataId: contextPayload.firstTargetObjectMetadataId,
|
||||
secondTargetObjectMetadataId:
|
||||
contextPayload.secondTargetObjectMetadataId,
|
||||
type: contextPayload.type,
|
||||
relationType: contextPayload.relationType,
|
||||
});
|
||||
|
||||
expect(createdField.id).toBeDefined();
|
||||
expect(createdField.name).toBe('owner');
|
||||
expect(createdField.id).toBeDefined();
|
||||
|
||||
const morphRelationTargetIds = createdField.morphRelations.map(
|
||||
(relation) => relation.targetObjectMetadata.id,
|
||||
);
|
||||
|
||||
expect(morphRelationTargetIds).toContain(
|
||||
contextPayload.firstTargetObjectMetadataId,
|
||||
);
|
||||
expect(morphRelationTargetIds).toContain(
|
||||
contextPayload.secondTargetObjectMetadataId,
|
||||
);
|
||||
|
||||
const isManyToOne =
|
||||
contextPayload.relationType === RelationType.MANY_TO_ONE;
|
||||
|
||||
if (isManyToOne) {
|
||||
expect(createdField.settings?.joinColumnName).toBe(
|
||||
'ownerPersonForMorphRelationId',
|
||||
const morphRelationTargetIds = createdField.morphRelations.map(
|
||||
(relation) => relation.targetObjectMetadata.id,
|
||||
);
|
||||
} else {
|
||||
expect(createdField.settings?.joinColumnName).toBeUndefined();
|
||||
}
|
||||
|
||||
await deleteOneFieldMetadata({
|
||||
input: { idToDelete: createdField.id },
|
||||
}).catch();
|
||||
});
|
||||
expect(morphRelationTargetIds).toContain(
|
||||
contextPayload.firstTargetObjectMetadataId,
|
||||
);
|
||||
expect(morphRelationTargetIds).toContain(
|
||||
contextPayload.secondTargetObjectMetadataId,
|
||||
);
|
||||
|
||||
const isManyToOne =
|
||||
contextPayload.relationType === RelationType.MANY_TO_ONE;
|
||||
|
||||
if (isManyToOne) {
|
||||
expect(createdField.settings?.joinColumnName).toBe(
|
||||
'ownerPersonForMorphRelationId',
|
||||
);
|
||||
expect(createdField.name).toBe('ownerPersonForMorphRelation');
|
||||
} else {
|
||||
expect(createdField.settings?.joinColumnName).toBeUndefined();
|
||||
expect(createdField.name).toBe('ownerPeopleForMorphRelation');
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
+10
-12
@@ -1,12 +1,11 @@
|
||||
import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util';
|
||||
import { findManyFieldsMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { createRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-relation-between-objects.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { findManyFieldsMetadata } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata.util';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
@@ -155,7 +154,14 @@ const findFieldMetadata = async ({
|
||||
}: {
|
||||
fieldMetadataId: string;
|
||||
}) => {
|
||||
const operation = findManyFieldsMetadataQueryFactory({
|
||||
const { fields } = await findManyFieldsMetadata({
|
||||
input: {
|
||||
filter: {
|
||||
id: { eq: fieldMetadataId },
|
||||
},
|
||||
paging: { first: 10 },
|
||||
},
|
||||
expectToFail: false,
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
@@ -174,16 +180,8 @@ const findFieldMetadata = async ({
|
||||
}
|
||||
settings
|
||||
`,
|
||||
input: {
|
||||
filter: {
|
||||
id: { eq: fieldMetadataId },
|
||||
},
|
||||
paging: { first: 10 },
|
||||
},
|
||||
});
|
||||
|
||||
const fields = await makeMetadataAPIRequest(operation);
|
||||
const field = fields.body.data.fields.edges?.[0]?.node;
|
||||
const field = fields[0]?.node;
|
||||
|
||||
return field;
|
||||
};
|
||||
|
||||
+67
-35
@@ -1,88 +1,119 @@
|
||||
import { findManyFieldsMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util';
|
||||
import { createMorphRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { forceCreateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { type RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
|
||||
describe('Rename an object metadata with morph relation should succeed', () => {
|
||||
let opportunityId = '';
|
||||
let personId = '';
|
||||
let companyId = '';
|
||||
let morphRelationField: FieldMetadataEntity<FieldMetadataType.MORPH_RELATION> & {
|
||||
morphRelations: RelationDTO[];
|
||||
};
|
||||
let createdObjectMetadataPersonId: string;
|
||||
let createdObjectMetadataOpportunityId: string;
|
||||
let createdObjectMetadataCompanyId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: aId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
} = await forceCreateOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'opportunityForRename',
|
||||
namePlural: 'opportunitiesForRename',
|
||||
nameSingular: 'opportunityForRenameSecond',
|
||||
namePlural: 'opportunitiesForRenameSecond',
|
||||
labelSingular: 'Opportunity For Rename',
|
||||
labelPlural: 'Opportunities For Rename',
|
||||
icon: 'IconOpportunity',
|
||||
},
|
||||
});
|
||||
|
||||
opportunityId = aId;
|
||||
createdObjectMetadataOpportunityId = aId;
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: bId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
} = await forceCreateOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'personForRename',
|
||||
namePlural: 'peopleForRename',
|
||||
nameSingular: 'personForRenameSecond',
|
||||
namePlural: 'peopleForRenameSecond',
|
||||
labelSingular: 'Person For Rename',
|
||||
labelPlural: 'People For Rename',
|
||||
icon: 'IconPerson',
|
||||
},
|
||||
});
|
||||
|
||||
personId = bId;
|
||||
createdObjectMetadataPersonId = bId;
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: cId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
} = await forceCreateOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'companyForRename',
|
||||
namePlural: 'companiesForRename',
|
||||
nameSingular: 'companyForRenameSecond',
|
||||
namePlural: 'companiesForRenameSecond',
|
||||
labelSingular: 'Company For Rename',
|
||||
labelPlural: 'Companies For Rename',
|
||||
icon: 'IconCompany',
|
||||
},
|
||||
});
|
||||
|
||||
companyId = cId;
|
||||
createdObjectMetadataCompanyId = cId;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteOneObjectMetadata({ input: { idToDelete: opportunityId } });
|
||||
await deleteOneObjectMetadata({ input: { idToDelete: personId } });
|
||||
await deleteOneObjectMetadata({ input: { idToDelete: companyId } });
|
||||
const createdObjectMetadataIds = [
|
||||
createdObjectMetadataPersonId,
|
||||
createdObjectMetadataOpportunityId,
|
||||
createdObjectMetadataCompanyId,
|
||||
];
|
||||
|
||||
for (const objectMetadataId of createdObjectMetadataIds) {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: objectMetadataId,
|
||||
updatePayload: { isActive: false },
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: objectMetadataId },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should rename custom object, and update the join column name of the morph relation that contains the object name', async () => {
|
||||
morphRelationField = await createMorphRelationBetweenObjects({
|
||||
it('should rename custom object, and update both the field name and join column name of the morph relation that contains the object name', async () => {
|
||||
const morphRelationField = await createMorphRelationBetweenObjects({
|
||||
name: 'owner',
|
||||
objectMetadataId: opportunityId,
|
||||
firstTargetObjectMetadataId: personId,
|
||||
secondTargetObjectMetadataId: companyId,
|
||||
objectMetadataId: createdObjectMetadataOpportunityId,
|
||||
firstTargetObjectMetadataId: createdObjectMetadataPersonId,
|
||||
secondTargetObjectMetadataId: createdObjectMetadataCompanyId,
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
});
|
||||
|
||||
expect(morphRelationField.morphRelations.length).toBe(2);
|
||||
|
||||
const { data } = await updateOneObjectMetadata({
|
||||
gqlFields: `
|
||||
nameSingular
|
||||
@@ -91,20 +122,21 @@ describe('Rename an object metadata with morph relation should succeed', () => {
|
||||
labelPlural
|
||||
`,
|
||||
input: {
|
||||
idToUpdate: personId,
|
||||
idToUpdate: createdObjectMetadataPersonId,
|
||||
updatePayload: {
|
||||
nameSingular: 'personForRename2',
|
||||
namePlural: 'peopleForRename2',
|
||||
nameSingular: 'personForRenameSecond2',
|
||||
namePlural: 'peopleForRenameSecond2',
|
||||
labelSingular: 'Person For Rename2',
|
||||
labelPlural: 'People For Rename2',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(data.updateOneObject.nameSingular).toBe('personForRename2');
|
||||
expect(data.updateOneObject.nameSingular).toBe('personForRenameSecond2');
|
||||
|
||||
const ownerFieldMetadataOnPersonId = morphRelationField.morphRelations.find(
|
||||
(morphRelation) => morphRelation.targetObjectMetadata.id === personId,
|
||||
(morphRelation) =>
|
||||
morphRelation.targetObjectMetadata.id === createdObjectMetadataPersonId,
|
||||
)?.sourceFieldMetadata.id;
|
||||
|
||||
if (!ownerFieldMetadataOnPersonId) {
|
||||
@@ -118,7 +150,7 @@ describe('Rename an object metadata with morph relation should succeed', () => {
|
||||
});
|
||||
|
||||
expect(fieldAfterRenaming.settings.joinColumnName).toBe(
|
||||
'ownerPersonForRename2Id',
|
||||
'ownerPersonForRenameSecondId',
|
||||
);
|
||||
});
|
||||
});
|
||||
+1
@@ -58,6 +58,7 @@ export const createMorphRelationBetweenObjects = async ({
|
||||
input: createFieldInput,
|
||||
gqlFields: `
|
||||
id
|
||||
type
|
||||
name
|
||||
label
|
||||
isLabelSyncedWithName
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ export const forceCreateOneObjectMetadata = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(result.errors)) {
|
||||
if (!isDefined(result.errors) || result.errors.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
import { capitalize } from '@/utils/strings';
|
||||
|
||||
type ComputeMorphRelationFieldJoinColumnNameArgs = {
|
||||
name: string;
|
||||
targetObjectMetadataNameSingular: string;
|
||||
};
|
||||
|
||||
export const computeMorphRelationFieldJoinColumnName = ({
|
||||
name,
|
||||
targetObjectMetadataNameSingular,
|
||||
}: ComputeMorphRelationFieldJoinColumnNameArgs) => {
|
||||
return `${name}${capitalize(targetObjectMetadataNameSingular)}Id`;
|
||||
};
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
|
||||
import { capitalize } from '@/utils/strings';
|
||||
|
||||
enum RelationType {
|
||||
MANY_TO_ONE = 'MANY_TO_ONE',
|
||||
ONE_TO_MANY = 'ONE_TO_MANY'
|
||||
}
|
||||
|
||||
|
||||
type ComputeMorphRelationFieldNameArgs = {
|
||||
fieldName: string;
|
||||
relationDirection: RelationType;
|
||||
nameSingular: string;
|
||||
namePlural: string;
|
||||
};
|
||||
|
||||
export const computeMorphRelationFieldName = ({
|
||||
fieldName,
|
||||
relationDirection,
|
||||
nameSingular,
|
||||
namePlural,
|
||||
}: ComputeMorphRelationFieldNameArgs): string => {
|
||||
if (relationDirection === RelationType.MANY_TO_ONE) {
|
||||
return `${fieldName}${capitalize(nameSingular)}`;
|
||||
}
|
||||
|
||||
if (relationDirection === RelationType.ONE_TO_MANY) {
|
||||
return `${fieldName}${capitalize(namePlural)}`;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Invalid relation direction: ${relationDirection} for field ${fieldName}`,
|
||||
);
|
||||
};
|
||||
@@ -14,8 +14,6 @@ export { sumByProperty } from './array/sumByProperty';
|
||||
export { assertUnreachable } from './assertUnreachable';
|
||||
export { deepMerge } from './deepMerge';
|
||||
export { extractAndSanitizeObjectStringFields } from './extractAndSanitizeObjectStringFields';
|
||||
export { computeMorphRelationFieldJoinColumnName } from './fieldMetadata/compute-morph-relation-field-join-column-name.util';
|
||||
export { computeMorphRelationFieldName } from './fieldMetadata/compute-morph-relation-field-name.util';
|
||||
export { isFieldMetadataDateKind } from './fieldMetadata/isFieldMetadataDateKind';
|
||||
export { fromArrayToUniqueKeyRecord } from './from-array-to-unique-key-record.util';
|
||||
export { fromArrayToValuesByKeyRecord } from './fromArrayToValuesByKeyRecord.util';
|
||||
|
||||
Reference in New Issue
Block a user