relation-integration-tests (#13113)

This commit is contained in:
Guillim
2025-07-10 16:55:36 +02:00
committed by GitHub
parent 77b9217467
commit bed2c640c5
19 changed files with 1305 additions and 384 deletions

View File

@ -51,6 +51,7 @@ type FieldMetadataSettingsMapping = {
[FieldMetadataType.DATE_TIME]: FieldMetadataDateTimeSettings;
[FieldMetadataType.TEXT]: FieldMetadataTextSettings;
[FieldMetadataType.RELATION]: FieldMetadataRelationSettings;
[FieldMetadataType.MORPH_RELATION]: FieldMetadataRelationSettings;
};
export type FieldMetadataSettings<

View File

@ -5,6 +5,8 @@ import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto';
export interface FieldMetadataInterface<
T extends FieldMetadataType = FieldMetadataType,
> {
@ -25,6 +27,7 @@ export interface FieldMetadataInterface<
relationTargetFieldMetadata?: FieldMetadataInterface;
relationTargetObjectMetadataId?: string;
relationTargetObjectMetadata?: ObjectMetadataInterface;
relation?: RelationDTO;
isCustom?: boolean;
isSystem?: boolean;
isActive?: boolean;

View File

@ -584,7 +584,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
isRemoteCreation: fieldMetadataInput.isRemoteCreation ?? false,
});
migrationActions.push(...fieldMigrationActions);
if (fieldMetadataInput.type !== FieldMetadataType.MORPH_RELATION) {
migrationActions.push(...fieldMigrationActions);
}
}
}

View File

@ -242,20 +242,28 @@ export class ObjectMetadataMigrationService {
workspaceId: string,
queryRunner?: QueryRunner,
) {
const relationFields = objectMetadata.fields.filter((field) =>
isFieldMetadataInterfaceOfType(field, FieldMetadataType.RELATION),
) as FieldMetadataEntity<FieldMetadataType.RELATION>[];
const relationFields = objectMetadata.fields.filter(
(field) =>
isFieldMetadataInterfaceOfType(field, FieldMetadataType.RELATION) ||
isFieldMetadataInterfaceOfType(field, FieldMetadataType.MORPH_RELATION),
) as FieldMetadataEntity<
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
>[];
const relationFieldsToDelete = [
...relationFields,
...(relationFields.map(
(relation) => relation.relationTargetFieldMetadata,
) as FieldMetadataEntity<FieldMetadataType.RELATION>[]),
) as FieldMetadataEntity<
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
>[]),
];
await this.fieldMetadataRepository.delete(
relationFieldsToDelete.map((relation) => relation.id),
);
if (relationFieldsToDelete.length !== 0) {
await this.fieldMetadataRepository.delete(
relationFieldsToDelete.map((relation) => relation.id),
);
}
for (const relationToDelete of relationFieldsToDelete) {
if (
@ -272,28 +280,30 @@ export class ObjectMetadataMigrationService {
);
}
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`delete-${RELATION_MIGRATION_PRIORITY_PREFIX}-${relationToDelete.name}`,
),
workspaceId,
[
{
name: computeTableName(
relationToDelete.object.nameSingular,
relationToDelete.object.isCustom,
),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName: joinColumnName,
} satisfies WorkspaceMigrationColumnDrop,
],
},
],
queryRunner,
);
if (relationToDelete.type !== FieldMetadataType.MORPH_RELATION) {
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`delete-${RELATION_MIGRATION_PRIORITY_PREFIX}-${relationToDelete.name}`,
),
workspaceId,
[
{
name: computeTableName(
relationToDelete.object.nameSingular,
relationToDelete.object.isCustom,
),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName: joinColumnName,
} satisfies WorkspaceMigrationColumnDrop,
],
},
],
queryRunner,
);
}
}
await this.workspaceMigrationService.createCustomMigration(

View File

@ -1,66 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Field metadata relation creation should fail relation when targetFieldLabel conflicts with an existing field on target object metadata id 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Name "collisionfieldlabel" is not available",
"name": "UserInputError",
},
]
`;
exports[`Field metadata relation creation should fail relation when targetFieldLabel contains only whitespace 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Invalid label: " "",
"name": "UserInputError",
},
]
`;
exports[`Field metadata relation creation should fail relation when targetFieldLabel exceeds maximum length 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Name is too long: it exceeds the 63 characters limit.",
"name": "UserInputError",
},
]
`;
exports[`Field metadata relation creation should fail relation when targetFieldLabel is empty 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Input is too short: """,
"name": "UserInputError",
},
]
`;
exports[`Field metadata relation creation should fail relation when targetObjectMetadataId is unknown 1`] = `
[
{
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exceptionEventId": "mocked-exception-id",
"userFriendlyMessage": "An error occurred.",
},
"message": "Object metadata relation target not found for relation creation payload",
},
]
`;

View File

@ -1,296 +1,108 @@
import { CreateOneFieldFactoryInput } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-query-factory.util';
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
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 { 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 { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
describe('createOne FieldMetadataService name/label sync', () => {
let createdObjectMetadataId = '';
describe('createOne', () => {
describe('FieldMetadataService name/label sync', () => {
let createdObjectMetadataId = '';
beforeEach(async () => {
const {
data: {
createOneObject: { id: objectMetadataId },
},
} = await createOneObjectMetadata({
input: {
nameSingular: 'myTestObject',
namePlural: 'myTestObjects',
labelSingular: 'My Test Object',
labelPlural: 'My Test Objects',
icon: 'Icon123',
},
});
createdObjectMetadataId = objectMetadataId;
beforeEach(async () => {
const {
data: {
createOneObject: { id: objectMetadataId },
},
} = await createOneObjectMetadata({
input: {
nameSingular: 'myTestObject',
namePlural: 'myTestObjects',
labelSingular: 'My Test Object',
labelPlural: 'My Test Objects',
icon: 'Icon123',
},
});
afterEach(async () => {
await deleteOneObjectMetadata({
input: { idToDelete: createdObjectMetadataId },
});
});
it('should create a field when name and label are synced correctly', async () => {
// Arrange
const FIELD_NAME = 'testField';
const createFieldInput = {
name: FIELD_NAME,
label: 'Test Field',
type: FieldMetadataType.TEXT,
objectMetadataId: createdObjectMetadataId,
isLabelSyncedWithName: true,
};
// Act
const { data } = await createOneFieldMetadata({
input: createFieldInput,
gqlFields: `
createdObjectMetadataId = objectMetadataId;
});
afterEach(async () => {
await deleteOneObjectMetadata({
input: { idToDelete: createdObjectMetadataId },
});
});
it('should create a field when name and label are synced correctly', async () => {
// Arrange
const FIELD_NAME = 'testField';
const createFieldInput = {
name: FIELD_NAME,
label: 'Test Field',
type: FieldMetadataType.TEXT,
objectMetadataId: createdObjectMetadataId,
isLabelSyncedWithName: true,
};
// Act
const { data } = await createOneFieldMetadata({
input: createFieldInput,
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
});
// Assert
expect(data.createOneField.name).toBe(FIELD_NAME);
});
it('should set isLabelSyncWithName to false if not in input', async () => {
// Arrange
const createFieldInput = {
name: 'testField',
label: 'Test Field',
type: FieldMetadataType.TEXT,
objectMetadataId: createdObjectMetadataId,
};
// Act
const { data } = await createOneFieldMetadata({
input: createFieldInput,
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
});
// Assert
expect(data.createOneField.isLabelSyncedWithName).toBe(false);
});
it('should return an error when name and label are not synced but isLabelSyncedWithName is true', async () => {
// Arrange
const createFieldInput = {
name: 'testField',
label: 'Different Label',
type: FieldMetadataType.TEXT,
objectMetadataId: createdObjectMetadataId,
isLabelSyncedWithName: true,
};
// Act
const { errors } = await createOneFieldMetadata({
input: createFieldInput,
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
expectToFail: true,
});
// Assert
expect(errors[0].message).toBe(
'Name is not synced with label. Expected name: "differentLabel", got testField',
);
});
// Assert
expect(data.createOneField.name).toBe(FIELD_NAME);
});
describe('FieldMetadataService relation fields', () => {
let createdObjectMetadataPersonId = '';
let createdObjectMetadataOpportunityId = '';
let createdObjectMetadataCompanyId = '';
beforeEach(async () => {
const {
data: {
createOneObject: { id: objectMetadataPersonId },
},
} = await createOneObjectMetadata({
input: {
nameSingular: 'personForRelation',
namePlural: 'peopleForRelation',
labelSingular: 'Person For Relation',
labelPlural: 'People For Relation',
icon: 'IconPerson',
},
});
it('should set isLabelSyncWithName to false if not in input', async () => {
// Arrange
const createFieldInput = {
name: 'testField',
label: 'Test Field',
type: FieldMetadataType.TEXT,
objectMetadataId: createdObjectMetadataId,
};
createdObjectMetadataPersonId = objectMetadataPersonId;
const {
data: {
createOneObject: { id: objectMetadataCompanyId },
},
} = await createOneObjectMetadata({
input: {
nameSingular: 'companyForRelation',
namePlural: 'companiesForRelation',
labelSingular: 'Company For Relation',
labelPlural: 'Companies For Relation',
icon: 'IconCompany',
},
});
createdObjectMetadataCompanyId = objectMetadataCompanyId;
const {
data: {
createOneObject: { id: objectMetadataOpportunityId },
},
} = await createOneObjectMetadata({
input: {
nameSingular: 'opportunityForRelation',
namePlural: 'opportunitiesForRelation',
labelSingular: 'Opportunity For Relation',
labelPlural: 'Opportunities For Relation',
icon: 'IconOpportunity',
},
});
createdObjectMetadataOpportunityId = objectMetadataOpportunityId;
});
afterEach(async () => {
await deleteOneObjectMetadata({
input: { idToDelete: createdObjectMetadataPersonId },
});
await deleteOneObjectMetadata({
input: { idToDelete: createdObjectMetadataOpportunityId },
});
await deleteOneObjectMetadata({
input: { idToDelete: createdObjectMetadataCompanyId },
});
});
it('should create a RELATION field type', async () => {
const createFieldInput: CreateOneFieldFactoryInput = {
name: 'person',
label: 'person field',
type: FieldMetadataType.RELATION,
objectMetadataId: createdObjectMetadataOpportunityId,
isLabelSyncedWithName: false,
relationCreationPayload: {
targetObjectMetadataId: createdObjectMetadataPersonId,
targetFieldLabel: 'opportunity',
targetFieldIcon: 'IconListOpportunity',
type: RelationType.MANY_TO_ONE,
},
};
const { data: createdFieldPerson } = await createOneFieldMetadata({
input: createFieldInput,
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
expectToFail: false,
});
expect(createdFieldPerson.createOneField.name).toBe('person');
// TODO : find a way to filter by objectmetadataid toavoid loading all fieldMetadata objects
const findOpportunityOperation = findManyFieldsMetadataQueryFactory({
gqlFields: `
id
name
object {
// Act
const { data } = await createOneFieldMetadata({
input: createFieldInput,
gqlFields: `
id
nameSingular
}
relation {
type
}
settings
`,
input: {
filter: {},
paging: { first: 10000 },
},
});
const opportunityFieldsResponse = await makeMetadataAPIRequest(
findOpportunityOperation,
);
const allFields = opportunityFieldsResponse.body.data.fields.edges;
const opportunityFieldOnPerson = allFields.find(
(field: any) =>
field.node?.object?.id === createdObjectMetadataPersonId &&
field.node?.name ===
createFieldInput.relationCreationPayload?.targetFieldLabel,
).node;
expect(opportunityFieldOnPerson.object.nameSingular).toBe(
'personForRelation',
);
expect(opportunityFieldOnPerson.relation.type).toBe(
RelationType.ONE_TO_MANY,
);
await deleteOneFieldMetadata({
input: { idToDelete: createdFieldPerson.createOneField.id },
});
name
label
isLabelSyncedWithName
`,
});
// TODO: replace xit by it once the Morph works
xit('should create a MORPH_RELATION field type', async () => {
const createFieldInput: CreateOneFieldFactoryInput = {
name: 'owner',
label: 'owner field',
type: FieldMetadataType.MORPH_RELATION,
objectMetadataId: createdObjectMetadataOpportunityId,
isLabelSyncedWithName: false,
morphRelationsCreationPayload: [
{
targetObjectMetadataId: createdObjectMetadataPersonId,
targetFieldLabel: 'opportunity',
targetFieldIcon: 'IconListOpportunity',
type: RelationType.MANY_TO_ONE,
},
{
targetObjectMetadataId: createdObjectMetadataCompanyId,
targetFieldLabel: 'opportunity',
targetFieldIcon: 'IconListOpportunity',
type: RelationType.MANY_TO_ONE,
},
],
};
// Assert
expect(data.createOneField.isLabelSyncedWithName).toBe(false);
});
const { data: createdFieldOwner } = await createOneFieldMetadata({
input: createFieldInput,
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
expectToFail: false,
});
it('should return an error when name and label are not synced but isLabelSyncedWithName is true', async () => {
// Arrange
const createFieldInput = {
name: 'testField',
label: 'Different Label',
type: FieldMetadataType.TEXT,
objectMetadataId: createdObjectMetadataId,
isLabelSyncedWithName: true,
};
// expect(createdFieldOwner.createOneField.name).toBe('owner');
await deleteOneFieldMetadata({
input: { idToDelete: createdFieldOwner.createOneField.id },
});
// Act
const { errors } = await createOneFieldMetadata({
input: createFieldInput,
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
expectToFail: true,
});
// Assert
expect(errors[0].message).toBe(
'Name is not synced with label. Expected name: "differentLabel", got testField',
);
});
});

View File

@ -1,3 +1,4 @@
import { CREATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/create-enum-field-metadata-test-cases';
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import {
LISTING_NAME_PLURAL,
@ -5,13 +6,8 @@ import {
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
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 { CREATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/create-enum-field-metadata-test-cases';
import { isDefined } from 'twenty-shared/utils';
import {
FieldMetadataComplexOption,
FieldMetadataDefaultOption,
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
describe.each(fieldMetadataEnumTypes)(
@ -51,7 +47,9 @@ describe.each(fieldMetadataEnumTypes)(
test.each(successfulTestCases)(
'Create $title',
async ({ context: { input, expectedOptions } }) => {
const { data, errors } = await createOneFieldMetadata({
const { data, errors } = await createOneFieldMetadata<
typeof testedFieldMetadataType
>({
input: {
objectMetadataId: createdObjectMetadataId,
type: testedFieldMetadataType,
@ -71,15 +69,13 @@ describe.each(fieldMetadataEnumTypes)(
expect(data).not.toBeNull();
expect(data.createOneField).toBeDefined();
expect(data.createOneField.type).toEqual(testedFieldMetadataType);
const createdOptions:
| FieldMetadataDefaultOption[]
| FieldMetadataComplexOption[] = data.createOneField.options;
const createdOptions = data.createOneField.options;
const optionsToCompare = expectedOptions ?? input.options;
expect(errors).toBeUndefined();
expect(createdOptions.length).toBe(optionsToCompare.length);
createdOptions.forEach((option) => expect(option.id).toBeDefined());
expect(createdOptions?.length).toBe(optionsToCompare.length);
createdOptions?.forEach((option) => expect(option.id).toBeDefined());
expect(createdOptions).toMatchObject(optionsToCompare);
if (isDefined(input.defaultValue)) {

View File

@ -0,0 +1,187 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when targetFieldLabel conflicts with an existing field on target object metadata id 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Name "collisionfieldlabel" is not available",
"name": "UserInputError",
},
]
`;
exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when targetFieldLabel contains only whitespace 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Invalid label: " "",
"name": "UserInputError",
},
]
`;
exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when targetFieldLabel exceeds maximum length 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Name is too long: it exceeds the 63 characters limit.",
"name": "UserInputError",
},
]
`;
exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when targetFieldLabel is empty 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Input is too short: """,
"name": "UserInputError",
},
]
`;
exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when targetObjectMetadataId is unknown 1`] = `
[
{
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exceptionEventId": "mocked-exception-id",
"userFriendlyMessage": "An error occurred.",
},
"message": "Object metadata relation target not found for relation creation payload",
},
]
`;
exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when type is a wrong value 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"subCode": "INVALID_FIELD_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Relation creation payload is invalid: type must be one of the following values: ONE_TO_MANY, MANY_TO_ONE",
"name": "UserInputError",
},
]
`;
exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when type is not provided 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"subCode": "INVALID_FIELD_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Relation creation payload is invalid: type must be one of the following values: ONE_TO_MANY, MANY_TO_ONE",
"name": "UserInputError",
},
]
`;
exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when targetFieldLabel conflicts with an existing field on target object metadata id 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Name "collisionfieldlabel" is not available",
"name": "UserInputError",
},
]
`;
exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when targetFieldLabel contains only whitespace 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Invalid label: " "",
"name": "UserInputError",
},
]
`;
exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when targetFieldLabel exceeds maximum length 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Name is too long: it exceeds the 63 characters limit.",
"name": "UserInputError",
},
]
`;
exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when targetFieldLabel is empty 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Input is too short: """,
"name": "UserInputError",
},
]
`;
exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when targetObjectMetadataId is unknown 1`] = `
[
{
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exceptionEventId": "mocked-exception-id",
"userFriendlyMessage": "An error occurred.",
},
"message": "Object metadata relation target not found for relation creation payload",
},
]
`;
exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when type is a wrong value 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"subCode": "INVALID_FIELD_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Relation creation payload is invalid: type must be one of the following values: ONE_TO_MANY, MANY_TO_ONE",
"name": "UserInputError",
},
]
`;
exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when type is not provided 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"subCode": "INVALID_FIELD_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Relation creation payload is invalid: type must be one of the following values: ONE_TO_MANY, MANY_TO_ONE",
"name": "UserInputError",
},
]
`;

View File

@ -0,0 +1,171 @@
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 { EachTestingContext } from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
describe('createOne FieldMetadataService morph relation fields', () => {
let createdObjectMetadataPersonId = '';
let createdObjectMetadataOpportunityId = '';
let createdObjectMetadataCompanyId = '';
beforeEach(async () => {
const {
data: {
createOneObject: { id: objectMetadataPersonId },
},
} = await createOneObjectMetadata({
input: {
nameSingular: 'personForMorphRelation',
namePlural: 'peopleForMorphRelation',
labelSingular: 'Person For Morph Relation',
labelPlural: 'People For Morph Relation',
icon: 'IconPerson',
},
});
createdObjectMetadataPersonId = objectMetadataPersonId;
const {
data: {
createOneObject: { id: objectMetadataCompanyId },
},
} = await createOneObjectMetadata({
input: {
nameSingular: 'companyForMorphRelation',
namePlural: 'companiesForMorphRelation',
labelSingular: 'Company For Morph Relation',
labelPlural: 'Companies For Morph Relation',
icon: 'IconCompany',
},
});
createdObjectMetadataCompanyId = objectMetadataCompanyId;
const {
data: {
createOneObject: { id: objectMetadataOpportunityId },
},
} = await createOneObjectMetadata({
input: {
nameSingular: 'opportunityForMorphRelation',
namePlural: 'opportunitiesForMorphRelation',
labelSingular: 'Opportunity For Morph Relation',
labelPlural: 'Opportunities For Morph Relation',
icon: 'IconOpportunity',
},
});
createdObjectMetadataOpportunityId = objectMetadataOpportunityId;
});
afterEach(async () => {
await deleteOneObjectMetadata({
input: { idToDelete: createdObjectMetadataPersonId },
});
await deleteOneObjectMetadata({
input: { idToDelete: createdObjectMetadataOpportunityId },
});
await deleteOneObjectMetadata({
input: { idToDelete: createdObjectMetadataCompanyId },
});
});
type EachTestingContextArray = EachTestingContext<
| {
relationType: RelationType;
objectMetadataId: string;
firstTargetObjectMetadataId: string;
secondTargetObjectMetadataId: string;
type: FieldMetadataType;
}
| ((args: {
objectMetadataId: string;
firstTargetObjectMetadataId: string;
secondTargetObjectMetadataId: string;
}) => {
relationType: RelationType;
objectMetadataId: string;
firstTargetObjectMetadataId: string;
secondTargetObjectMetadataId: string;
type: FieldMetadataType;
})
>[];
const eachTestingContextArray: EachTestingContextArray = [
{
title: 'should create a MORPH_RELATION field type MANY_TO_ONE',
context: ({
objectMetadataId,
firstTargetObjectMetadataId,
secondTargetObjectMetadataId,
}) => ({
relationType: RelationType.MANY_TO_ONE,
objectMetadataId,
firstTargetObjectMetadataId,
secondTargetObjectMetadataId,
type: FieldMetadataType.MORPH_RELATION,
}),
},
{
title: 'should create a MORPH_RELATION field type ONE_TO_MANY',
context: ({
objectMetadataId,
firstTargetObjectMetadataId,
secondTargetObjectMetadataId,
}) => ({
relationType: RelationType.ONE_TO_MANY,
objectMetadataId,
firstTargetObjectMetadataId,
secondTargetObjectMetadataId,
type: FieldMetadataType.MORPH_RELATION,
}),
},
];
it.each(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,
});
expect(createdField.id).toBeDefined();
expect(createdField.name).toBe('owner');
// expect(createdField.relation).toBeUndefined();
// expect(createdField.morphRelations[0].type).toBe(
// contextPayload.relationType,
// );
// expect(createdField.morphRelations[0].targetFieldMetadata.id).toBeDefined();
const isManyToOne =
contextPayload.relationType === RelationType.MANY_TO_ONE;
if (isManyToOne) {
expect(createdField.settings?.joinColumnName).toBe(
'ownerOpportunityForMorphRelationId',
);
} else {
expect(createdField.settings?.joinColumnName).toBeUndefined();
}
// TODO: check the morphrelation targets are created correctly (wait for Query Morph Relations)
await deleteOneFieldMetadata({
input: { idToDelete: createdField.id },
}).catch();
});
});

View File

@ -0,0 +1,229 @@
import { faker } from '@faker-js/faker/.';
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.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 { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input';
import { EachTestingContext } from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
type GlobalTestContext = {
objectMetadataIds: {
firstTargetObjectId: string;
secondTargetObjectId: string;
sourceObjectId: string;
};
targetFieldLabel: string;
type: FieldMetadataType;
targetFieldIcon: string;
collisionFieldLabel: string;
};
const globalTestContext: GlobalTestContext = {
objectMetadataIds: {
firstTargetObjectId: '',
secondTargetObjectId: '',
sourceObjectId: '',
},
targetFieldLabel: 'defaultTargetFieldLabel',
type: FieldMetadataType.MORPH_RELATION,
targetFieldIcon: 'IconBuildingSkyscraper',
collisionFieldLabel: 'collisionfieldlabel',
};
type TestedRelationCreationPayload = Partial<
NonNullable<CreateFieldInput['relationCreationPayload']>
>;
type CreateOneObjectMetadataItemTestingContext = EachTestingContext<
| TestedRelationCreationPayload
| ((context: GlobalTestContext) => TestedRelationCreationPayload)
>[];
describe('Field metadata morph relation creation should fail', () => {
const failingLabelsCreationTestsUseCase: CreateOneObjectMetadataItemTestingContext =
[
{
title: 'when targetFieldLabel is empty',
context: { targetFieldLabel: '' },
},
{
title: 'when targetFieldLabel exceeds maximum length',
context: {
targetFieldLabel: 'A'.repeat(64),
},
},
{
// Not handled gracefully should be refactored
title: 'when targetObjectMetadataId is unknown',
context: {
targetObjectMetadataId: faker.string.uuid(),
},
},
{
title: 'when targetFieldLabel contains only whitespace',
context: { targetFieldLabel: ' ' },
},
{
title:
'when targetFieldLabel conflicts with an existing field on target object metadata id',
context: ({ collisionFieldLabel }) => ({
targetFieldLabel: collisionFieldLabel,
}),
},
{
title: 'when type is not provided',
context: { type: undefined },
},
{
title: 'when type is a wrong value',
context: { type: 'wrong' as RelationType },
},
];
beforeAll(async () => {
const {
data: {
createOneObject: { id: sourceObjectId },
},
} = await createOneObjectMetadata({
input: getMockCreateObjectInput({
namePlural: 'sourceRelations',
nameSingular: 'sourceRelation',
}),
});
const {
data: {
createOneObject: { id: firstTargetObjectId },
},
} = await createOneObjectMetadata({
input: getMockCreateObjectInput({
namePlural: 'firstTargetRelations',
nameSingular: 'firstTargetRelation',
}),
});
const {
data: {
createOneObject: { id: secondTargetObjectId },
},
} = await createOneObjectMetadata({
input: getMockCreateObjectInput({
namePlural: 'secondTargetRelations',
nameSingular: 'secondTargetRelation',
}),
});
globalTestContext.objectMetadataIds = {
sourceObjectId,
firstTargetObjectId,
secondTargetObjectId,
};
const { data } = await createOneFieldMetadata({
input: {
objectMetadataId: firstTargetObjectId,
name: globalTestContext.collisionFieldLabel,
label: 'LabelThatCouldBeAnything',
isLabelSyncedWithName: false,
type: FieldMetadataType.TEXT,
},
});
expect(data).toBeDefined();
});
afterAll(async () => {
for (const objectMetadataId of Object.values(
globalTestContext.objectMetadataIds,
)) {
await deleteOneObjectMetadata({
input: {
idToDelete: objectMetadataId,
},
});
}
});
it.each(failingLabelsCreationTestsUseCase)(
'relation ONE_TO_MANY $title',
async ({ context }) => {
const computedContext =
typeof context === 'function' ? context(globalTestContext) : context;
const { errors } = await createOneFieldMetadata({
expectToFail: true,
input: {
objectMetadataId: globalTestContext.objectMetadataIds.sourceObjectId,
name: 'owner',
label: 'owner field',
isLabelSyncedWithName: false,
type: FieldMetadataType.MORPH_RELATION,
morphRelationsCreationPayload: [
{
targetFieldLabel: 'defaultFirstTargetFieldLabel',
type: RelationType.ONE_TO_MANY,
targetObjectMetadataId:
globalTestContext.objectMetadataIds.firstTargetObjectId,
targetFieldIcon: 'IconBuildingSkyscraper',
...computedContext,
},
{
targetFieldLabel: 'defaultSecondTargetFieldLabel',
type: RelationType.ONE_TO_MANY,
targetObjectMetadataId:
globalTestContext.objectMetadataIds.secondTargetObjectId,
targetFieldIcon: 'IconBuildingSkyscraper',
...computedContext,
},
],
},
});
expect(errors).toBeDefined();
expect(errors).toMatchSnapshot();
},
);
it.each(failingLabelsCreationTestsUseCase)(
'relation MANY_TO_ONE $title',
async ({ context }) => {
const computedContext =
typeof context === 'function' ? context(globalTestContext) : context;
const { errors } = await createOneFieldMetadata({
expectToFail: true,
input: {
objectMetadataId: globalTestContext.objectMetadataIds.sourceObjectId,
name: 'owner',
label: 'owner field',
isLabelSyncedWithName: false,
type: FieldMetadataType.MORPH_RELATION,
morphRelationsCreationPayload: [
{
targetFieldLabel: 'defaultFirstTargetFieldLabel',
type: RelationType.MANY_TO_ONE,
targetObjectMetadataId:
globalTestContext.objectMetadataIds.firstTargetObjectId,
targetFieldIcon: 'IconBuildingSkyscraper',
...computedContext,
},
{
targetFieldLabel: 'defaultSecondTargetFieldLabel',
type: RelationType.MANY_TO_ONE,
targetObjectMetadataId:
globalTestContext.objectMetadataIds.secondTargetObjectId,
targetFieldIcon: 'IconBuildingSkyscraper',
...computedContext,
},
],
},
});
expect(errors).toBeDefined();
expect(errors).toMatchSnapshot();
},
);
});

View File

@ -0,0 +1,187 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Field metadata relation creation should fail relation MANY_TO_ONE when targetFieldLabel conflicts with an existing field on target object metadata id 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Name "collisionfieldlabel" is not available",
"name": "UserInputError",
},
]
`;
exports[`Field metadata relation creation should fail relation MANY_TO_ONE when targetFieldLabel contains only whitespace 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Invalid label: " "",
"name": "UserInputError",
},
]
`;
exports[`Field metadata relation creation should fail relation MANY_TO_ONE when targetFieldLabel exceeds maximum length 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Name is too long: it exceeds the 63 characters limit.",
"name": "UserInputError",
},
]
`;
exports[`Field metadata relation creation should fail relation MANY_TO_ONE when targetFieldLabel is empty 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Input is too short: """,
"name": "UserInputError",
},
]
`;
exports[`Field metadata relation creation should fail relation MANY_TO_ONE when targetObjectMetadataId is unknown 1`] = `
[
{
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exceptionEventId": "mocked-exception-id",
"userFriendlyMessage": "An error occurred.",
},
"message": "Object metadata relation target not found for relation creation payload",
},
]
`;
exports[`Field metadata relation creation should fail relation MANY_TO_ONE when type is a wrong value 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"subCode": "INVALID_FIELD_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Relation creation payload is invalid: type must be one of the following values: ONE_TO_MANY, MANY_TO_ONE",
"name": "UserInputError",
},
]
`;
exports[`Field metadata relation creation should fail relation MANY_TO_ONE when type is not provided 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"subCode": "INVALID_FIELD_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Relation creation payload is invalid: type must be one of the following values: ONE_TO_MANY, MANY_TO_ONE",
"name": "UserInputError",
},
]
`;
exports[`Field metadata relation creation should fail relation ONE_TO_MANY when targetFieldLabel conflicts with an existing field on target object metadata id 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Name "collisionfieldlabel" is not available",
"name": "UserInputError",
},
]
`;
exports[`Field metadata relation creation should fail relation ONE_TO_MANY when targetFieldLabel contains only whitespace 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Invalid label: " "",
"name": "UserInputError",
},
]
`;
exports[`Field metadata relation creation should fail relation ONE_TO_MANY when targetFieldLabel exceeds maximum length 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Name is too long: it exceeds the 63 characters limit.",
"name": "UserInputError",
},
]
`;
exports[`Field metadata relation creation should fail relation ONE_TO_MANY when targetFieldLabel is empty 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Input is too short: """,
"name": "UserInputError",
},
]
`;
exports[`Field metadata relation creation should fail relation ONE_TO_MANY when targetObjectMetadataId is unknown 1`] = `
[
{
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exceptionEventId": "mocked-exception-id",
"userFriendlyMessage": "An error occurred.",
},
"message": "Object metadata relation target not found for relation creation payload",
},
]
`;
exports[`Field metadata relation creation should fail relation ONE_TO_MANY when type is a wrong value 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"subCode": "INVALID_FIELD_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Relation creation payload is invalid: type must be one of the following values: ONE_TO_MANY, MANY_TO_ONE",
"name": "UserInputError",
},
]
`;
exports[`Field metadata relation creation should fail relation ONE_TO_MANY when type is not provided 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"subCode": "INVALID_FIELD_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Relation creation payload is invalid: type must be one of the following values: ONE_TO_MANY, MANY_TO_ONE",
"name": "UserInputError",
},
]
`;

View File

@ -0,0 +1,196 @@
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 { EachTestingContext } from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
describe('createOne FieldMetadataService relation fields', () => {
let createdObjectMetadataPersonId = '';
let createdObjectMetadataOpportunityId = '';
beforeEach(async () => {
const {
data: {
createOneObject: { id: objectMetadataPersonId },
},
} = await createOneObjectMetadata({
input: {
nameSingular: 'personForRelation',
namePlural: 'peopleForRelation',
labelSingular: 'Person For Relation',
labelPlural: 'People For Relation',
icon: 'IconPerson',
},
});
createdObjectMetadataPersonId = objectMetadataPersonId;
const {
data: {
createOneObject: { id: objectMetadataOpportunityId },
},
} = await createOneObjectMetadata({
input: {
nameSingular: 'opportunityForRelation',
namePlural: 'opportunitiesForRelation',
labelSingular: 'Opportunity For Relation',
labelPlural: 'Opportunities For Relation',
icon: 'IconOpportunity',
},
});
createdObjectMetadataOpportunityId = objectMetadataOpportunityId;
});
afterEach(async () => {
await deleteOneObjectMetadata({
input: { idToDelete: createdObjectMetadataPersonId },
});
await deleteOneObjectMetadata({
input: { idToDelete: createdObjectMetadataOpportunityId },
});
});
type EachTestingContextArray = EachTestingContext<
| {
relationType: RelationType;
objectMetadataId: string;
targetObjectMetadataId: string;
type: FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION;
}
| ((args: { objectMetadataId: string; targetObjectMetadataId: string }) => {
relationType: RelationType;
objectMetadataId: string;
targetObjectMetadataId: string;
type: FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION;
})
>[];
const eachTestingContextArray: EachTestingContextArray = [
{
title: 'should create a RELATION field type MANY_TO_ONE',
context: ({ objectMetadataId, targetObjectMetadataId }) => ({
relationType: RelationType.MANY_TO_ONE,
objectMetadataId,
targetObjectMetadataId,
type: FieldMetadataType.RELATION,
}),
},
{
title: 'should create a RELATION field type ONE_TO_MANY',
context: ({ objectMetadataId, targetObjectMetadataId }) => ({
relationType: RelationType.ONE_TO_MANY,
objectMetadataId,
targetObjectMetadataId,
type: FieldMetadataType.RELATION,
}),
},
];
it.each(eachTestingContextArray)('$title', async ({ context }) => {
const contextPayload =
typeof context === 'function'
? context({
objectMetadataId: createdObjectMetadataOpportunityId,
targetObjectMetadataId: createdObjectMetadataPersonId,
})
: context;
const createdField = await createRelationBetweenObjects({
objectMetadataId: contextPayload.objectMetadataId,
targetObjectMetadataId: contextPayload.targetObjectMetadataId,
type: contextPayload.type,
relationType: contextPayload.relationType,
});
expect(createdField.id).toBeDefined();
expect(createdField.name).toBe('person');
expect(createdField.relation?.type).toBe(contextPayload.relationType);
expect(createdField.relation?.targetFieldMetadata.id).toBeDefined();
// TODO: expect(createdField.morphRelations).toBeUndefined();
const isManyToOne =
contextPayload.relationType === RelationType.MANY_TO_ONE;
if (isManyToOne) {
expect(createdField.settings?.joinColumnName).toBe('personId');
} else {
expect(createdField.settings?.joinColumnName).toBeUndefined();
}
if (!isDefined(createdField.relation?.targetFieldMetadata?.id)) {
throw new Error('targetFieldMetadata.id is not defined');
}
const opportunityFieldOnPerson = await findFieldMetadata({
fieldMetadataId: createdField.relation.targetFieldMetadata.id,
});
expect(opportunityFieldOnPerson.object.nameSingular).toBe(
'personForRelation',
);
expect(opportunityFieldOnPerson.relation.type).toBe(
isManyToOne ? RelationType.ONE_TO_MANY : RelationType.MANY_TO_ONE,
);
expect(
opportunityFieldOnPerson.relation.targetFieldMetadata.id,
).toBeDefined();
expect(
opportunityFieldOnPerson.relation.targetObjectMetadata.id,
).toBeDefined();
if (!isManyToOne) {
expect(opportunityFieldOnPerson.settings?.joinColumnName).toBe(
'opportunityId',
);
} else {
expect(opportunityFieldOnPerson.settings?.joinColumnName).toBeUndefined();
}
await deleteOneFieldMetadata({
input: { idToDelete: createdField.id },
}).catch();
});
});
const findFieldMetadata = async ({
fieldMetadataId,
}: {
fieldMetadataId: string;
}) => {
const operation = findManyFieldsMetadataQueryFactory({
gqlFields: `
id
name
object {
id
nameSingular
}
relation {
type
targetFieldMetadata {
id
}
targetObjectMetadata {
id
}
}
settings
`,
input: {
filter: {
id: { eq: fieldMetadataId },
},
paging: { first: 10 },
},
});
const fields = await makeMetadataAPIRequest(operation);
const field = fields.body.data.fields.edges?.[0]?.node;
return field;
};

View File

@ -62,6 +62,14 @@ describe('Field metadata relation creation should fail', () => {
targetFieldLabel: collisionFieldLabel,
}),
},
{
title: 'when type is not provided',
context: { type: undefined },
},
{
title: 'when type is a wrong value',
context: { type: 'wrong' as RelationType },
},
];
beforeAll(async () => {
@ -118,7 +126,7 @@ describe('Field metadata relation creation should fail', () => {
});
it.each(failingLabelsCreationTestsUseCase)(
'relation $title',
'relation ONE_TO_MANY $title',
async ({ context }) => {
const computedContext =
typeof context === 'function' ? context(globalTestContext) : context;
@ -146,4 +154,34 @@ describe('Field metadata relation creation should fail', () => {
expect(errors).toMatchSnapshot();
},
);
it.each(failingLabelsCreationTestsUseCase)(
'relation MANY_TO_ONE $title',
async ({ context }) => {
const computedContext =
typeof context === 'function' ? context(globalTestContext) : context;
const { errors } = await createOneFieldMetadata({
expectToFail: true,
input: {
objectMetadataId: globalTestContext.objectMetadataIds.sourceObjectId,
name: 'fieldname',
label: 'Relation field',
isLabelSyncedWithName: false,
type: FieldMetadataType.RELATION,
relationCreationPayload: {
targetFieldLabel: 'defaultTargetFieldLabel',
type: RelationType.MANY_TO_ONE,
targetObjectMetadataId:
globalTestContext.objectMetadataIds.targetObjectId,
targetFieldIcon: 'IconBuildingSkyscraper',
...computedContext,
},
},
});
expect(errors).toBeDefined();
expect(errors).toMatchSnapshot();
},
);
});

View File

@ -1,5 +1,4 @@
import { faker } from '@faker-js/faker';
import { isDefined } from 'class-validator';
import { createOneOperation } from 'test/integration/graphql/utils/create-one-operation.util';
import { findOneOperation } from 'test/integration/graphql/utils/find-one-operation.util';
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
@ -9,7 +8,7 @@ import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object
import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input';
import { EachTestingContext } from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { parseJson } from 'twenty-shared/utils';
import { isDefined, parseJson } from 'twenty-shared/utils';
import {
FieldMetadataComplexOption,
@ -85,7 +84,7 @@ describe('update-one-field-metadata-related-record', () => {
const {
data: { createOneField },
} = await createOneFieldMetadata({
} = await createOneFieldMetadata<typeof fieldMetadataType>({
input: {
objectMetadataId: createOneObject.id,
type: fieldMetadataType,
@ -258,6 +257,10 @@ describe('update-one-field-metadata-related-record', () => {
});
const optionsWithIds = createOneField.options;
if (!isDefined(optionsWithIds)) {
throw new Error('optionsWithIds is not defined');
}
const updatedOptions = updateOptions(optionsWithIds);
await updateOneFieldMetadata({
@ -358,6 +361,10 @@ describe('update-one-field-metadata-related-record', () => {
});
const optionsWithIds = createOneField.options;
if (!isDefined(optionsWithIds)) {
throw new Error('optionsWithIds is not defined');
}
const updatePayload = {
options: optionsWithIds.map((option) => updateOption(option)),
};

View File

@ -6,15 +6,16 @@ import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/m
import { CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
export const createOneFieldMetadata = async ({
export const createOneFieldMetadata = async <T extends FieldMetadataType>({
input,
gqlFields,
expectToFail = false,
}: PerformMetadataQueryParams<CreateOneFieldFactoryInput>): CommonResponseBody<{
createOneField: FieldMetadataEntity;
createOneField: FieldMetadataInterface<T>;
}> => {
const graphqlOperation = createOneFieldMetadataQueryFactory({
input,

View File

@ -0,0 +1,80 @@
import { CreateOneFieldFactoryInput } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-query-factory.util';
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import { FieldMetadataType } from 'twenty-shared/types';
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';
export const createMorphRelationBetweenObjects = async ({
objectMetadataId,
firstTargetObjectMetadataId,
secondTargetObjectMetadataId,
type,
relationType,
name,
label,
isLabelSyncedWithName,
targetFieldLabel,
targetFieldIcon,
}: {
objectMetadataId: string;
firstTargetObjectMetadataId: string;
secondTargetObjectMetadataId: string;
type: FieldMetadataType;
relationType: RelationType;
name?: string;
label?: string;
isLabelSyncedWithName?: boolean;
targetFieldLabel?: string;
targetFieldIcon?: string;
}) => {
const createFieldInput: CreateOneFieldFactoryInput = {
name: name || 'owner',
label: label || 'owner field',
type,
objectMetadataId,
isLabelSyncedWithName: isLabelSyncedWithName || false,
morphRelationsCreationPayload: [
{
targetObjectMetadataId: firstTargetObjectMetadataId,
targetFieldLabel: targetFieldLabel || 'opportunity',
targetFieldIcon: targetFieldIcon || 'IconListOpportunity',
type: relationType,
},
{
targetObjectMetadataId: secondTargetObjectMetadataId,
targetFieldLabel: targetFieldLabel || 'opportunity',
targetFieldIcon: targetFieldIcon || 'IconListOpportunity',
type: relationType,
},
],
};
// TODO: add morphRelations to the query once available
// morphRelations {
// type
// targetFieldMetadata {
// id
// }
// }
const {
data: { createOneField: createdFieldPerson },
} = await createOneFieldMetadata({
input: createFieldInput,
gqlFields: `
id
name
label
isLabelSyncedWithName
settings
object {
id
nameSingular
}
`,
expectToFail: false,
});
return createdFieldPerson as FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>;
};

View File

@ -0,0 +1,67 @@
import { CreateOneFieldFactoryInput } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-query-factory.util';
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
export const createRelationBetweenObjects = async ({
objectMetadataId,
targetObjectMetadataId,
type,
relationType,
name,
label,
isLabelSyncedWithName,
targetFieldLabel,
targetFieldIcon,
}: {
objectMetadataId: string;
targetObjectMetadataId: string;
type: FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION;
relationType: RelationType;
name?: string;
label?: string;
isLabelSyncedWithName?: boolean;
targetFieldLabel?: string;
targetFieldIcon?: string;
}) => {
const createFieldInput: CreateOneFieldFactoryInput = {
name: name || 'person',
label: label || 'person field',
type: type,
objectMetadataId: objectMetadataId,
isLabelSyncedWithName: isLabelSyncedWithName || false,
relationCreationPayload: {
targetObjectMetadataId: targetObjectMetadataId,
targetFieldLabel: targetFieldLabel || 'opportunity',
targetFieldIcon: targetFieldIcon || 'IconListOpportunity',
type: relationType,
},
};
const {
data: { createOneField: createdFieldPerson },
} = await createOneFieldMetadata<typeof type>({
input: createFieldInput,
gqlFields: `
id
name
label
isLabelSyncedWithName
relation {
type
targetFieldMetadata {
id
}
}
settings
object {
id
nameSingular
}
`,
expectToFail: false,
});
return createdFieldPerson;
};