Connect - Import Relation (#13419)

re-opened https://github.com/twentyhq/twenty/pull/13213
This commit is contained in:
Etienne
2025-07-25 09:48:17 +02:00
committed by GitHub
parent ca27995ca6
commit 741924751b
92 changed files with 1612 additions and 1153 deletions

View File

@ -8,6 +8,7 @@ import {
GraphQLString,
} from 'graphql';
import { RELATION_NESTED_QUERY_KEYWORDS } from 'twenty-shared/constants';
import { getUniqueConstraintsFields } from 'twenty-shared/utils';
import {
InputTypeDefinition,
@ -17,7 +18,6 @@ import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-build
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { getUniqueConstraintsFields } from 'src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { pascalCase } from 'src/utils/pascal-case';

View File

@ -10,14 +10,14 @@ export const fullNameCompositeType: CompositeType = {
type: FieldMetadataType.TEXT,
hidden: false,
isRequired: false,
isIncludedInUniqueConstraint: true,
isIncludedInUniqueConstraint: false,
},
{
name: 'lastName',
type: FieldMetadataType.TEXT,
hidden: false,
isRequired: false,
isIncludedInUniqueConstraint: true,
isIncludedInUniqueConstraint: false,
},
],
};

View File

@ -1,140 +0,0 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { getUniqueConstraintsFields } from 'src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util';
describe('getUniqueConstraintsFields', () => {
const mockIdField = {
id: 'field-id-1',
name: 'id',
label: 'ID',
type: FieldMetadataType.UUID,
objectMetadataId: 'object-id-1',
isNullable: false,
isUnique: false,
isCustom: false,
isSystem: true,
isActive: true,
isLabelSyncedWithName: false,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
};
const mockEmailField = {
id: 'field-id-2',
name: 'email',
label: 'Email',
type: FieldMetadataType.EMAILS,
objectMetadataId: 'object-id-1',
isNullable: true,
isUnique: true,
isCustom: false,
isSystem: false,
isActive: true,
isLabelSyncedWithName: false,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
};
const mockNameField = {
id: 'field-id-3',
name: 'name',
label: 'Name',
type: FieldMetadataType.TEXT,
objectMetadataId: 'object-id-1',
isNullable: true,
isUnique: false,
isCustom: false,
isSystem: false,
isActive: true,
isLabelSyncedWithName: false,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
};
const createMockIndexFieldMetadata = (
fieldMetadataId: string,
indexMetadataId: string,
order = 0,
) => ({
id: `index-field-${fieldMetadataId}-${indexMetadataId}`,
indexMetadataId,
fieldMetadataId,
order,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
});
const createMockIndexMetadata = (
id: string,
name: string,
isUnique: boolean,
indexFieldMetadatas: any,
) => ({
id,
name,
isUnique,
indexFieldMetadatas,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
indexWhereClause: null,
});
const createMockObjectMetadata = (fields: any, indexMetadatas: any) => ({
id: 'object-id-1',
workspaceId: 'workspace-id-1',
nameSingular: 'person',
namePlural: 'people',
labelSingular: 'Person',
labelPlural: 'People',
description: 'A person object',
icon: 'IconUser',
targetTableName: 'person',
fields,
indexMetadatas,
isSystem: false,
isCustom: false,
isActive: true,
isRemote: false,
isAuditLogged: true,
isSearchable: true,
});
it('should return the primary key constraint field if no unique indexes are present', () => {
const objectMetadata = createMockObjectMetadata(
[mockIdField, mockNameField],
[],
);
const result = getUniqueConstraintsFields(objectMetadata);
expect(result).toHaveLength(1);
expect(result[0]).toHaveLength(1);
expect(result[0][0]).toEqual(mockIdField);
});
it('should return the primary key constraint field and the unique indexes fields if unique indexes are present', () => {
const emailIndexFieldMetadata = createMockIndexFieldMetadata(
'field-id-2',
'index-id-1',
);
const emailIndex = createMockIndexMetadata(
'index-id-1',
'unique_email_index',
true,
[emailIndexFieldMetadata],
);
const objectMetadata = createMockObjectMetadata(
[mockIdField, mockEmailField, mockNameField],
[emailIndex],
);
const result = getUniqueConstraintsFields(objectMetadata);
expect(result).toHaveLength(2);
expect(result[0]).toHaveLength(1);
expect(result[0][0]).toEqual(mockIdField);
expect(result[1]).toHaveLength(1);
expect(result[1][0]).toEqual(mockEmailField);
});
});

View File

@ -1,53 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
export const getUniqueConstraintsFields = <
K extends {
id: string;
name: string;
},
T extends {
id: string;
indexMetadatas: {
id: string;
isUnique: boolean;
indexFieldMetadatas: { fieldMetadataId: string }[];
}[];
fields: K[];
},
>(
objectMetadata: T,
): K[][] => {
const uniqueIndexes = objectMetadata.indexMetadatas.filter(
(index) => index.isUnique,
);
const fieldsMapById = new Map(
objectMetadata.fields.map((field) => [field.id, field]),
);
const primaryKeyConstraintField = objectMetadata.fields.find(
(field) => field.name === 'id',
);
if (!isDefined(primaryKeyConstraintField)) {
throw new Error(
`Primary key constraint field not found for object metadata ${objectMetadata.id}`,
);
}
const otherUniqueConstraintsFields = uniqueIndexes.map((index) =>
index.indexFieldMetadatas.map((field) => {
const indexField = fieldsMapById.get(field.fieldMetadataId);
if (!isDefined(indexField)) {
throw new Error(
`Index field not found for field id ${field.fieldMetadataId} in index metadata ${index.id}`,
);
}
return indexField;
}),
);
return [[primaryKeyConstraintField], ...otherUniqueConstraintsFields];
};

View File

@ -15,6 +15,7 @@ import {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { formatConnectRecordNotFoundErrorMessage } from 'src/engine/twenty-orm/relation-nested-queries/utils/formatConnectRecordNotFoundErrorMessage.util';
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
import { computeRelationConnectQueryConfigs } from 'src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util';
import { createSqlWhereTupleInClause } from 'src/engine/twenty-orm/utils/create-sql-where-tuple-in-clause.utils';
@ -196,12 +197,19 @@ export class RelationNestedQueries {
);
if (recordToConnect.length !== 1) {
const recordToConnectTotal = recordToConnect.length;
const connectFieldName = connectQueryConfig.connectFieldName;
const { errorMessage, userFriendlyMessage } =
formatConnectRecordNotFoundErrorMessage(
connectQueryConfig.connectFieldName,
recordToConnect.length,
connectQueryConfig.recordToConnectConditionByEntityIndex[index],
);
throw new TwentyORMException(
`Expected 1 record to connect to ${connectFieldName}, but found ${recordToConnectTotal}.`,
errorMessage,
TwentyORMExceptionCode.CONNECT_RECORD_NOT_FOUND,
{
userFriendlyMessage,
},
);
}

View File

@ -0,0 +1,21 @@
import { formatConnectRecordNotFoundErrorMessage } from 'src/engine/twenty-orm/relation-nested-queries/utils/formatConnectRecordNotFoundErrorMessage.util';
describe('formatConnectRecordNotFoundErrorMessage', () => {
it('should format the error message correctly', () => {
const result = formatConnectRecordNotFoundErrorMessage(
'connectFieldName',
0,
[
['field1', 'value1'],
['field2', 'value2'],
],
);
expect(result).toEqual({
errorMessage:
'Expected 1 record to connect to connectFieldName, but found 0 for field1 = value1 and field2 = value2',
userFriendlyMessage:
'Expected 1 record to connect to connectFieldName, but found 0 for field1 = value1 and field2 = value2',
});
});
});

View File

@ -0,0 +1,18 @@
import { t } from '@lingui/core/macro';
import { UniqueConstraintCondition } from 'src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type';
export const formatConnectRecordNotFoundErrorMessage = (
connectFieldName: string,
recordToConnectTotal: number,
uniqueConstraint: UniqueConstraintCondition,
) => {
const formattedConnectCondition = uniqueConstraint
.map(([field, value]) => `${field} = ${value}`)
.join(' and ');
return {
errorMessage: `Expected 1 record to connect to ${connectFieldName}, but found ${recordToConnectTotal} for ${formattedConnectCondition}`,
userFriendlyMessage: t`Expected 1 record to connect to ${connectFieldName}, but found ${recordToConnectTotal} for ${formattedConnectCondition}`,
};
};

View File

@ -1,13 +1,12 @@
import { t } from '@lingui/core/macro';
import deepEqual from 'deep-equal';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { getUniqueConstraintsFields, isDefined } from 'twenty-shared/utils';
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 { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { getUniqueConstraintsFields } from 'src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';