relation-integration-tests (#13113)
This commit is contained in:
@ -51,6 +51,7 @@ type FieldMetadataSettingsMapping = {
|
|||||||
[FieldMetadataType.DATE_TIME]: FieldMetadataDateTimeSettings;
|
[FieldMetadataType.DATE_TIME]: FieldMetadataDateTimeSettings;
|
||||||
[FieldMetadataType.TEXT]: FieldMetadataTextSettings;
|
[FieldMetadataType.TEXT]: FieldMetadataTextSettings;
|
||||||
[FieldMetadataType.RELATION]: FieldMetadataRelationSettings;
|
[FieldMetadataType.RELATION]: FieldMetadataRelationSettings;
|
||||||
|
[FieldMetadataType.MORPH_RELATION]: FieldMetadataRelationSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldMetadataSettings<
|
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 { 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 { 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<
|
export interface FieldMetadataInterface<
|
||||||
T extends FieldMetadataType = FieldMetadataType,
|
T extends FieldMetadataType = FieldMetadataType,
|
||||||
> {
|
> {
|
||||||
@ -25,6 +27,7 @@ export interface FieldMetadataInterface<
|
|||||||
relationTargetFieldMetadata?: FieldMetadataInterface;
|
relationTargetFieldMetadata?: FieldMetadataInterface;
|
||||||
relationTargetObjectMetadataId?: string;
|
relationTargetObjectMetadataId?: string;
|
||||||
relationTargetObjectMetadata?: ObjectMetadataInterface;
|
relationTargetObjectMetadata?: ObjectMetadataInterface;
|
||||||
|
relation?: RelationDTO;
|
||||||
isCustom?: boolean;
|
isCustom?: boolean;
|
||||||
isSystem?: boolean;
|
isSystem?: boolean;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
|||||||
@ -584,7 +584,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
isRemoteCreation: fieldMetadataInput.isRemoteCreation ?? false,
|
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,
|
workspaceId: string,
|
||||||
queryRunner?: QueryRunner,
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
const relationFields = objectMetadata.fields.filter((field) =>
|
const relationFields = objectMetadata.fields.filter(
|
||||||
isFieldMetadataInterfaceOfType(field, FieldMetadataType.RELATION),
|
(field) =>
|
||||||
) as FieldMetadataEntity<FieldMetadataType.RELATION>[];
|
isFieldMetadataInterfaceOfType(field, FieldMetadataType.RELATION) ||
|
||||||
|
isFieldMetadataInterfaceOfType(field, FieldMetadataType.MORPH_RELATION),
|
||||||
|
) as FieldMetadataEntity<
|
||||||
|
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
|
||||||
|
>[];
|
||||||
|
|
||||||
const relationFieldsToDelete = [
|
const relationFieldsToDelete = [
|
||||||
...relationFields,
|
...relationFields,
|
||||||
...(relationFields.map(
|
...(relationFields.map(
|
||||||
(relation) => relation.relationTargetFieldMetadata,
|
(relation) => relation.relationTargetFieldMetadata,
|
||||||
) as FieldMetadataEntity<FieldMetadataType.RELATION>[]),
|
) as FieldMetadataEntity<
|
||||||
|
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
|
||||||
|
>[]),
|
||||||
];
|
];
|
||||||
|
|
||||||
await this.fieldMetadataRepository.delete(
|
if (relationFieldsToDelete.length !== 0) {
|
||||||
relationFieldsToDelete.map((relation) => relation.id),
|
await this.fieldMetadataRepository.delete(
|
||||||
);
|
relationFieldsToDelete.map((relation) => relation.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for (const relationToDelete of relationFieldsToDelete) {
|
for (const relationToDelete of relationFieldsToDelete) {
|
||||||
if (
|
if (
|
||||||
@ -272,28 +280,30 @@ export class ObjectMetadataMigrationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.workspaceMigrationService.createCustomMigration(
|
if (relationToDelete.type !== FieldMetadataType.MORPH_RELATION) {
|
||||||
generateMigrationName(
|
await this.workspaceMigrationService.createCustomMigration(
|
||||||
`delete-${RELATION_MIGRATION_PRIORITY_PREFIX}-${relationToDelete.name}`,
|
generateMigrationName(
|
||||||
),
|
`delete-${RELATION_MIGRATION_PRIORITY_PREFIX}-${relationToDelete.name}`,
|
||||||
workspaceId,
|
),
|
||||||
[
|
workspaceId,
|
||||||
{
|
[
|
||||||
name: computeTableName(
|
{
|
||||||
relationToDelete.object.nameSingular,
|
name: computeTableName(
|
||||||
relationToDelete.object.isCustom,
|
relationToDelete.object.nameSingular,
|
||||||
),
|
relationToDelete.object.isCustom,
|
||||||
action: WorkspaceMigrationTableActionType.ALTER,
|
),
|
||||||
columns: [
|
action: WorkspaceMigrationTableActionType.ALTER,
|
||||||
{
|
columns: [
|
||||||
action: WorkspaceMigrationColumnActionType.DROP,
|
{
|
||||||
columnName: joinColumnName,
|
action: WorkspaceMigrationColumnActionType.DROP,
|
||||||
} satisfies WorkspaceMigrationColumnDrop,
|
columnName: joinColumnName,
|
||||||
],
|
} satisfies WorkspaceMigrationColumnDrop,
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
queryRunner,
|
],
|
||||||
);
|
queryRunner,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.workspaceMigrationService.createCustomMigration(
|
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 { 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 { 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 { 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 { 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', () => {
|
beforeEach(async () => {
|
||||||
describe('FieldMetadataService name/label sync', () => {
|
const {
|
||||||
let createdObjectMetadataId = '';
|
data: {
|
||||||
|
createOneObject: { id: objectMetadataId },
|
||||||
beforeEach(async () => {
|
},
|
||||||
const {
|
} = await createOneObjectMetadata({
|
||||||
data: {
|
input: {
|
||||||
createOneObject: { id: objectMetadataId },
|
nameSingular: 'myTestObject',
|
||||||
},
|
namePlural: 'myTestObjects',
|
||||||
} = await createOneObjectMetadata({
|
labelSingular: 'My Test Object',
|
||||||
input: {
|
labelPlural: 'My Test Objects',
|
||||||
nameSingular: 'myTestObject',
|
icon: 'Icon123',
|
||||||
namePlural: 'myTestObjects',
|
},
|
||||||
labelSingular: 'My Test Object',
|
|
||||||
labelPlural: 'My Test Objects',
|
|
||||||
icon: 'Icon123',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
createdObjectMetadataId = objectMetadataId;
|
||||||
const { data } = await createOneFieldMetadata({
|
});
|
||||||
input: createFieldInput,
|
afterEach(async () => {
|
||||||
gqlFields: `
|
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
|
id
|
||||||
name
|
name
|
||||||
label
|
label
|
||||||
isLabelSyncedWithName
|
isLabelSyncedWithName
|
||||||
`,
|
`,
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(data.createOneField.name).toBe(FIELD_NAME);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set isLabelSyncWithName to false if not in input', async () => {
|
// Assert
|
||||||
// Arrange
|
expect(data.createOneField.name).toBe(FIELD_NAME);
|
||||||
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',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
describe('FieldMetadataService relation fields', () => {
|
|
||||||
let createdObjectMetadataPersonId = '';
|
|
||||||
let createdObjectMetadataOpportunityId = '';
|
|
||||||
let createdObjectMetadataCompanyId = '';
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
it('should set isLabelSyncWithName to false if not in input', async () => {
|
||||||
const {
|
// Arrange
|
||||||
data: {
|
const createFieldInput = {
|
||||||
createOneObject: { id: objectMetadataPersonId },
|
name: 'testField',
|
||||||
},
|
label: 'Test Field',
|
||||||
} = await createOneObjectMetadata({
|
type: FieldMetadataType.TEXT,
|
||||||
input: {
|
objectMetadataId: createdObjectMetadataId,
|
||||||
nameSingular: 'personForRelation',
|
};
|
||||||
namePlural: 'peopleForRelation',
|
|
||||||
labelSingular: 'Person For Relation',
|
|
||||||
labelPlural: 'People For Relation',
|
|
||||||
icon: 'IconPerson',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
createdObjectMetadataPersonId = objectMetadataPersonId;
|
// Act
|
||||||
|
const { data } = await createOneFieldMetadata({
|
||||||
const {
|
input: createFieldInput,
|
||||||
data: {
|
gqlFields: `
|
||||||
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 {
|
|
||||||
id
|
id
|
||||||
nameSingular
|
name
|
||||||
}
|
label
|
||||||
relation {
|
isLabelSyncedWithName
|
||||||
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 },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: replace xit by it once the Morph works
|
// Assert
|
||||||
xit('should create a MORPH_RELATION field type', async () => {
|
expect(data.createOneField.isLabelSyncedWithName).toBe(false);
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data: createdFieldOwner } = await createOneFieldMetadata({
|
it('should return an error when name and label are not synced but isLabelSyncedWithName is true', async () => {
|
||||||
input: createFieldInput,
|
// Arrange
|
||||||
gqlFields: `
|
const createFieldInput = {
|
||||||
id
|
name: 'testField',
|
||||||
name
|
label: 'Different Label',
|
||||||
label
|
type: FieldMetadataType.TEXT,
|
||||||
isLabelSyncedWithName
|
objectMetadataId: createdObjectMetadataId,
|
||||||
`,
|
isLabelSyncedWithName: true,
|
||||||
expectToFail: false,
|
};
|
||||||
});
|
|
||||||
|
|
||||||
// expect(createdFieldOwner.createOneField.name).toBe('owner');
|
// Act
|
||||||
|
const { errors } = await createOneFieldMetadata({
|
||||||
await deleteOneFieldMetadata({
|
input: createFieldInput,
|
||||||
input: { idToDelete: createdFieldOwner.createOneField.id },
|
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 { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||||
import {
|
import {
|
||||||
LISTING_NAME_PLURAL,
|
LISTING_NAME_PLURAL,
|
||||||
@ -5,13 +6,8 @@ import {
|
|||||||
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
|
} 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 { 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 { 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 { 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';
|
import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||||
|
|
||||||
describe.each(fieldMetadataEnumTypes)(
|
describe.each(fieldMetadataEnumTypes)(
|
||||||
@ -51,7 +47,9 @@ describe.each(fieldMetadataEnumTypes)(
|
|||||||
test.each(successfulTestCases)(
|
test.each(successfulTestCases)(
|
||||||
'Create $title',
|
'Create $title',
|
||||||
async ({ context: { input, expectedOptions } }) => {
|
async ({ context: { input, expectedOptions } }) => {
|
||||||
const { data, errors } = await createOneFieldMetadata({
|
const { data, errors } = await createOneFieldMetadata<
|
||||||
|
typeof testedFieldMetadataType
|
||||||
|
>({
|
||||||
input: {
|
input: {
|
||||||
objectMetadataId: createdObjectMetadataId,
|
objectMetadataId: createdObjectMetadataId,
|
||||||
type: testedFieldMetadataType,
|
type: testedFieldMetadataType,
|
||||||
@ -71,15 +69,13 @@ describe.each(fieldMetadataEnumTypes)(
|
|||||||
expect(data).not.toBeNull();
|
expect(data).not.toBeNull();
|
||||||
expect(data.createOneField).toBeDefined();
|
expect(data.createOneField).toBeDefined();
|
||||||
expect(data.createOneField.type).toEqual(testedFieldMetadataType);
|
expect(data.createOneField.type).toEqual(testedFieldMetadataType);
|
||||||
const createdOptions:
|
|
||||||
| FieldMetadataDefaultOption[]
|
|
||||||
| FieldMetadataComplexOption[] = data.createOneField.options;
|
|
||||||
|
|
||||||
|
const createdOptions = data.createOneField.options;
|
||||||
const optionsToCompare = expectedOptions ?? input.options;
|
const optionsToCompare = expectedOptions ?? input.options;
|
||||||
|
|
||||||
expect(errors).toBeUndefined();
|
expect(errors).toBeUndefined();
|
||||||
expect(createdOptions.length).toBe(optionsToCompare.length);
|
expect(createdOptions?.length).toBe(optionsToCompare.length);
|
||||||
createdOptions.forEach((option) => expect(option.id).toBeDefined());
|
createdOptions?.forEach((option) => expect(option.id).toBeDefined());
|
||||||
expect(createdOptions).toMatchObject(optionsToCompare);
|
expect(createdOptions).toMatchObject(optionsToCompare);
|
||||||
|
|
||||||
if (isDefined(input.defaultValue)) {
|
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,
|
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 () => {
|
beforeAll(async () => {
|
||||||
@ -118,7 +126,7 @@ describe('Field metadata relation creation should fail', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.each(failingLabelsCreationTestsUseCase)(
|
it.each(failingLabelsCreationTestsUseCase)(
|
||||||
'relation $title',
|
'relation ONE_TO_MANY $title',
|
||||||
async ({ context }) => {
|
async ({ context }) => {
|
||||||
const computedContext =
|
const computedContext =
|
||||||
typeof context === 'function' ? context(globalTestContext) : context;
|
typeof context === 'function' ? context(globalTestContext) : context;
|
||||||
@ -146,4 +154,34 @@ describe('Field metadata relation creation should fail', () => {
|
|||||||
expect(errors).toMatchSnapshot();
|
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 { faker } from '@faker-js/faker';
|
||||||
import { isDefined } from 'class-validator';
|
|
||||||
import { createOneOperation } from 'test/integration/graphql/utils/create-one-operation.util';
|
import { createOneOperation } from 'test/integration/graphql/utils/create-one-operation.util';
|
||||||
import { findOneOperation } from 'test/integration/graphql/utils/find-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';
|
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 { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input';
|
||||||
import { EachTestingContext } from 'twenty-shared/testing';
|
import { EachTestingContext } from 'twenty-shared/testing';
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
import { parseJson } from 'twenty-shared/utils';
|
import { isDefined, parseJson } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FieldMetadataComplexOption,
|
FieldMetadataComplexOption,
|
||||||
@ -85,7 +84,7 @@ describe('update-one-field-metadata-related-record', () => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
data: { createOneField },
|
data: { createOneField },
|
||||||
} = await createOneFieldMetadata({
|
} = await createOneFieldMetadata<typeof fieldMetadataType>({
|
||||||
input: {
|
input: {
|
||||||
objectMetadataId: createOneObject.id,
|
objectMetadataId: createOneObject.id,
|
||||||
type: fieldMetadataType,
|
type: fieldMetadataType,
|
||||||
@ -258,6 +257,10 @@ describe('update-one-field-metadata-related-record', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const optionsWithIds = createOneField.options;
|
const optionsWithIds = createOneField.options;
|
||||||
|
|
||||||
|
if (!isDefined(optionsWithIds)) {
|
||||||
|
throw new Error('optionsWithIds is not defined');
|
||||||
|
}
|
||||||
const updatedOptions = updateOptions(optionsWithIds);
|
const updatedOptions = updateOptions(optionsWithIds);
|
||||||
|
|
||||||
await updateOneFieldMetadata({
|
await updateOneFieldMetadata({
|
||||||
@ -358,6 +361,10 @@ describe('update-one-field-metadata-related-record', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const optionsWithIds = createOneField.options;
|
const optionsWithIds = createOneField.options;
|
||||||
|
|
||||||
|
if (!isDefined(optionsWithIds)) {
|
||||||
|
throw new Error('optionsWithIds is not defined');
|
||||||
|
}
|
||||||
const updatePayload = {
|
const updatePayload = {
|
||||||
options: optionsWithIds.map((option) => updateOption(option)),
|
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 { CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||||
import { PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.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 { 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,
|
input,
|
||||||
gqlFields,
|
gqlFields,
|
||||||
expectToFail = false,
|
expectToFail = false,
|
||||||
}: PerformMetadataQueryParams<CreateOneFieldFactoryInput>): CommonResponseBody<{
|
}: PerformMetadataQueryParams<CreateOneFieldFactoryInput>): CommonResponseBody<{
|
||||||
createOneField: FieldMetadataEntity;
|
createOneField: FieldMetadataInterface<T>;
|
||||||
}> => {
|
}> => {
|
||||||
const graphqlOperation = createOneFieldMetadataQueryFactory({
|
const graphqlOperation = createOneFieldMetadataQueryFactory({
|
||||||
input,
|
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