relation-integration-tests (#13113)
This commit is contained in:
@ -51,6 +51,7 @@ type FieldMetadataSettingsMapping = {
|
||||
[FieldMetadataType.DATE_TIME]: FieldMetadataDateTimeSettings;
|
||||
[FieldMetadataType.TEXT]: FieldMetadataTextSettings;
|
||||
[FieldMetadataType.RELATION]: FieldMetadataRelationSettings;
|
||||
[FieldMetadataType.MORPH_RELATION]: FieldMetadataRelationSettings;
|
||||
};
|
||||
|
||||
export type FieldMetadataSettings<
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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",
|
||||
},
|
||||
]
|
||||
`;
|
||||
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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",
|
||||
},
|
||||
]
|
||||
`;
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
},
|
||||
);
|
||||
});
|
||||
@ -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",
|
||||
},
|
||||
]
|
||||
`;
|
||||
@ -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;
|
||||
};
|
||||
@ -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();
|
||||
},
|
||||
);
|
||||
});
|
||||
@ -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)),
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user