Fix composite field update v2 (#15290)

# Introduction
Fixing composite field update by computing field column type for each of
its properties instead of globally

## Coverage
Added integration tests for each composite field on both successful
`create` and `update`
```ts
Test Suites: 17 passed, 17 total
Tests:       104 passed, 104 total
Snapshots:   14 passed, 14 total
Time:        135.431 s, estimated 143 s
```

## Conlusion
Related to https://github.com/twentyhq/core-team-issues/issues/1753
This commit is contained in:
Paul Rastoin
2025-10-23 16:26:46 +02:00
committed by GitHub
parent 3f5efdbabc
commit 0780380fe5
21 changed files with 2846 additions and 165 deletions
@@ -301,14 +301,14 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
tableName,
update,
}: UpdateFieldPropertyUpdateHandlerArgs<'defaultValue'>) {
const columnType = fieldMetadataTypeToColumnType(
flatFieldMetadata.type,
) as ColumnType;
if (isCompositeFieldMetadataType(flatFieldMetadata.type)) {
const compositeType = getCompositeTypeOrThrow(flatFieldMetadata.type);
for (const property of compositeType.properties) {
const columnType = fieldMetadataTypeToColumnType(
property.type,
) as ColumnType;
if (isMorphOrRelationFieldMetadataType(property.type)) {
throw new WorkspaceMigrationRunnerException(
'Relation field metadata in composite type is not supported yet',
@@ -331,7 +331,7 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
defaultValue: compositeDefaultValue,
});
return await this.workspaceSchemaManagerService.columnManager.alterColumnDefault(
await this.workspaceSchemaManagerService.columnManager.alterColumnDefault(
{
queryRunner,
schemaName,
@@ -341,8 +341,14 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
},
);
}
return;
}
const columnType = fieldMetadataTypeToColumnType(
flatFieldMetadata.type,
) as ColumnType;
const serializedNewDefaultValue = serializeDefaultValueV2({
columnName: flatFieldMetadata.name,
schemaName,
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Phone field metadata tests suite It should fail to create primary phone field with conflicting country code and calling code 1`] = `
exports[`failing create phone field metadata test suite it should fail to create primary phone field with conflicting country code and calling code 1`] = `
[
{
"extensions": {
@@ -14,7 +14,7 @@ exports[`Phone field metadata tests suite It should fail to create primary phone
]
`;
exports[`Phone field metadata tests suite It should fail to create primary phone field with conflicting country code and calling code 2`] = `
exports[`failing create phone field metadata test suite it should fail to create primary phone field with conflicting country code and calling code 2`] = `
[
{
"extensions": {
@@ -28,7 +28,7 @@ exports[`Phone field metadata tests suite It should fail to create primary phone
]
`;
exports[`Phone field metadata tests suite It should fail to create primary phone field with conflicting phone number calling code 1`] = `
exports[`failing create phone field metadata test suite it should fail to create primary phone field with conflicting phone number calling code 1`] = `
[
{
"extensions": {
@@ -42,7 +42,7 @@ exports[`Phone field metadata tests suite It should fail to create primary phone
]
`;
exports[`Phone field metadata tests suite It should fail to create primary phone field with conflicting phone number calling code 2`] = `
exports[`failing create phone field metadata test suite it should fail to create primary phone field with conflicting phone number calling code 2`] = `
[
{
"extensions": {
@@ -56,7 +56,7 @@ exports[`Phone field metadata tests suite It should fail to create primary phone
]
`;
exports[`Phone field metadata tests suite It should fail to create primary phone field with conflicting phone number country code 1`] = `
exports[`failing create phone field metadata test suite it should fail to create primary phone field with conflicting phone number country code 1`] = `
[
{
"extensions": {
@@ -70,7 +70,7 @@ exports[`Phone field metadata tests suite It should fail to create primary phone
]
`;
exports[`Phone field metadata tests suite It should fail to create primary phone field with conflicting phone number country code 2`] = `
exports[`failing create phone field metadata test suite it should fail to create primary phone field with conflicting phone number country code 2`] = `
[
{
"extensions": {
@@ -84,7 +84,7 @@ exports[`Phone field metadata tests suite It should fail to create primary phone
]
`;
exports[`Phone field metadata tests suite It should fail to create primary phone field with invalid calling code 1`] = `
exports[`failing create phone field metadata test suite it should fail to create primary phone field with invalid calling code 1`] = `
[
{
"extensions": {
@@ -98,7 +98,7 @@ exports[`Phone field metadata tests suite It should fail to create primary phone
]
`;
exports[`Phone field metadata tests suite It should fail to create primary phone field with invalid calling code 2`] = `
exports[`failing create phone field metadata test suite it should fail to create primary phone field with invalid calling code 2`] = `
[
{
"extensions": {
@@ -112,7 +112,7 @@ exports[`Phone field metadata tests suite It should fail to create primary phone
]
`;
exports[`Phone field metadata tests suite It should fail to create primary phone field with invalid country code 1`] = `
exports[`failing create phone field metadata test suite it should fail to create primary phone field with invalid country code 1`] = `
[
{
"extensions": {
@@ -126,7 +126,7 @@ exports[`Phone field metadata tests suite It should fail to create primary phone
]
`;
exports[`Phone field metadata tests suite It should fail to create primary phone field with invalid country code 2`] = `
exports[`failing create phone field metadata test suite it should fail to create primary phone field with invalid country code 2`] = `
[
{
"extensions": {
@@ -140,7 +140,7 @@ exports[`Phone field metadata tests suite It should fail to create primary phone
]
`;
exports[`Phone field metadata tests suite It should fail to create primary phone field with invalid phone number format 1`] = `
exports[`failing create phone field metadata test suite it should fail to create primary phone field with invalid phone number format 1`] = `
[
{
"extensions": {
@@ -154,7 +154,7 @@ exports[`Phone field metadata tests suite It should fail to create primary phone
]
`;
exports[`Phone field metadata tests suite It should fail to create primary phone field with invalid phone number format 2`] = `
exports[`failing create phone field metadata test suite it should fail to create primary phone field with invalid phone number format 2`] = `
[
{
"extensions": {
@@ -168,7 +168,7 @@ exports[`Phone field metadata tests suite It should fail to create primary phone
]
`;
exports[`Phone field metadata tests suite It should fail to create primary phone field without country or calling code at all 1`] = `
exports[`failing create phone field metadata test suite it should fail to create primary phone field without country or calling code at all 1`] = `
[
{
"extensions": {
@@ -182,7 +182,7 @@ exports[`Phone field metadata tests suite It should fail to create primary phone
]
`;
exports[`Phone field metadata tests suite It should fail to create primary phone field without country or calling code at all 2`] = `
exports[`failing create phone field metadata test suite it should fail to create primary phone field without country or calling code at all 2`] = `
[
{
"extensions": {
@@ -0,0 +1,213 @@
import { faker } from '@faker-js/faker';
import { type CountryCode } from 'libphonenumber-js';
import { createOneOperation } from 'test/integration/graphql/utils/create-one-operation.util';
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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { type EachTestingContext } from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import {
type AdditionalPhoneMetadata,
type PhonesMetadata,
} from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type';
const FIELD_NAME = 'phonenumber';
type TestCaseInputAndExpected = Partial<
Omit<PhonesMetadata, 'additionalPhones'>
> & {
additionalPhones?: Array<Partial<AdditionalPhoneMetadata>> | null;
};
type CreatePhoneFieldMetadataTestCase = {
input: TestCaseInputAndExpected;
expected?: TestCaseInputAndExpected;
};
const FAILING_TEST_INPUTS: { input: Partial<PhonesMetadata>; label: string }[] =
[
{
label: 'phone field without country or calling code at all',
input: {
primaryPhoneNumber: '123456789',
additionalPhones: [],
},
},
{
label: 'phone field with invalid country code',
input: {
primaryPhoneNumber: '123456789',
primaryPhoneCallingCode: '+33',
primaryPhoneCountryCode: 'XX' as CountryCode,
additionalPhones: [],
},
},
{
label: 'phone field with invalid calling code',
input: {
primaryPhoneNumber: '123456789',
primaryPhoneCallingCode: '+999',
primaryPhoneCountryCode: 'FR',
additionalPhones: [],
},
},
{
label: 'phone field with conflicting country code and calling code',
input: {
primaryPhoneNumber: '123456789',
primaryPhoneCallingCode: '+33',
primaryPhoneCountryCode: 'US',
additionalPhones: [],
},
},
{
label: 'phone field with invalid phone number format',
input: {
primaryPhoneNumber: 'not-a-number',
additionalPhones: [],
},
},
{
label: 'phone field with conflicting phone number country code',
input: {
primaryPhoneNumber: '+33123456789',
primaryPhoneCountryCode: 'US',
additionalPhones: [],
},
},
{
label: 'phone field with conflicting phone number calling code',
input: {
primaryPhoneNumber: '+33123456789',
primaryPhoneCallingCode: '+1',
additionalPhones: [],
},
},
];
const primaryFailingTests = FAILING_TEST_INPUTS.map<
EachTestingContext<CreatePhoneFieldMetadataTestCase>
>(({ input, label }) => ({
title: `create primary ${label}`,
context: {
input: {
...input,
additionalPhones: [],
},
},
}));
const additionalPhonesNumberFailingTests = FAILING_TEST_INPUTS.map<
EachTestingContext<CreatePhoneFieldMetadataTestCase>
>(
({
input: {
primaryPhoneCallingCode,
primaryPhoneCountryCode,
primaryPhoneNumber,
},
label,
}) => ({
title: `create primary ${label}`,
context: {
input: {
additionalPhones: [
{
callingCode: primaryPhoneCallingCode,
countryCode: primaryPhoneCountryCode,
number: primaryPhoneNumber,
},
],
},
},
}),
);
const FAILING_TEST_CASES: EachTestingContext<CreatePhoneFieldMetadataTestCase>[] =
[...primaryFailingTests, ...additionalPhonesNumberFailingTests];
describe('failing create phone field metadata test suite', () => {
let createdObjectMetadataId: string;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
input: {
nameSingular: 'myTestObject',
namePlural: 'myTestObjects',
labelSingular: 'My Test Object',
labelPlural: 'My Test Objects',
icon: 'Icon123',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = data.createOneObject.id;
await createOneFieldMetadata({
input: {
name: FIELD_NAME,
label: 'Phone number',
type: FieldMetadataType.PHONES,
objectMetadataId: createdObjectMetadataId,
isLabelSyncedWithName: false,
},
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
});
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: {
isActive: false,
},
},
});
await deleteOneObjectMetadata({
input: { idToDelete: createdObjectMetadataId },
});
});
test.each(FAILING_TEST_CASES)(
'it should fail to $title',
async ({ context: { input } }) => {
const {
data: { createOneResponse },
errors,
} = await createOneOperation<{
id: string;
[FIELD_NAME]: any;
}>({
objectMetadataSingularName: 'myTestObject',
input: {
id: faker.string.uuid(),
[FIELD_NAME]: input,
},
gqlFields: `
id
${FIELD_NAME} {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
__typename
}
`,
});
expect(createOneResponse).toBeNull();
expect(errors).toBeDefined();
expect(errors).toMatchSnapshot();
},
);
});
@@ -0,0 +1,142 @@
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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
const SUCCESSFUL_TEST_CASES: EachTestingContext<{
input: Omit<CreateFieldInput, 'objectMetadataId' | 'type' | 'workspaceId'>;
}>[] = [
{
title: 'actor field with basic metadata (name, label, description, icon)',
context: {
input: {
name: 'processedBy',
label: 'Processed By',
description: 'Person who processed this',
icon: 'IconUser',
},
},
},
{
title: 'actor field with default value containing source and name',
context: {
input: {
name: 'initiatedBy',
label: 'Initiated By',
defaultValue: {
source: "'MANUAL'",
workspaceMemberId: null,
name: "'System'",
},
},
},
},
{
title: 'actor field with API source default',
context: {
input: {
name: 'importedBy',
label: 'Imported By',
defaultValue: {
source: "'API'",
workspaceMemberId: null,
name: "'API User'",
},
},
},
},
];
describe('Actor field metadata creation tests suite', () => {
let createdObjectMetadataId: string;
let createdFieldMetadataId: string | undefined = undefined;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'testActorObject',
namePlural: 'testActorObjects',
labelSingular: 'Test Actor Object',
labelPlural: 'Test Actor Objects',
icon: 'IconTestPipe',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = data.createOneObject.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: createdObjectMetadataId },
});
});
afterEach(async () => {
if (createdFieldMetadataId) {
await updateOneFieldMetadata({
input: {
updatePayload: { isActive: false },
idToUpdate: createdFieldMetadataId,
},
expectToFail: false,
});
await deleteOneFieldMetadata({
input: { idToDelete: createdFieldMetadataId },
expectToFail: false,
});
createdFieldMetadataId = undefined;
}
});
test.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'it should create $title',
async ({ context: { input } }) => {
const inputPayload = {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.ACTOR,
isLabelSyncedWithName: false,
...input,
};
const { data } = await createOneFieldMetadata({
expectToFail: false,
input: inputPayload,
gqlFields: `
id
type
name
label
description
icon
defaultValue
isLabelSyncedWithName
settings
`,
});
createdFieldMetadataId = data.createOneField.id;
const { objectMetadataId: _, ...expectedFields } = inputPayload;
expect(data.createOneField).toMatchObject(expectedFields);
},
);
});
@@ -0,0 +1,226 @@
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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
const SUCCESSFUL_TEST_CASES: EachTestingContext<{
input: Omit<CreateFieldInput, 'objectMetadataId' | 'type' | 'workspaceId'>;
}>[] = [
{
title: 'address field with basic metadata (name, label, description, icon)',
context: {
input: {
name: 'location',
label: 'Location',
description: 'Physical location',
icon: 'IconMapPin',
},
},
},
{
title: 'address field with subFields setting (street and city only)',
context: {
input: {
name: 'simpleAddress',
label: 'Simple Address',
settings: {
subFields: ['addressStreet1', 'addressCity'],
},
},
},
},
{
title: 'address field with all subFields',
context: {
input: {
name: 'fullAddress',
label: 'Full Address',
settings: {
subFields: [
'addressStreet1',
'addressStreet2',
'addressCity',
'addressState',
'addressPostcode',
'addressCountry',
'addressLat',
'addressLng',
],
},
},
},
},
{
title: 'address field with default value containing full address',
context: {
input: {
name: 'officeAddress',
label: 'Office Address',
defaultValue: {
addressStreet1: "'123 Main St'",
addressStreet2: "'Suite 100'",
addressCity: "'San Francisco'",
addressState: "'CA'",
addressPostcode: "'94102'",
addressCountry: "'USA'",
addressLat: null,
addressLng: null,
},
},
},
},
{
title: 'address field with partial default value',
context: {
input: {
name: 'shippingAddress',
label: 'Shipping Address',
defaultValue: {
addressStreet1: "'456 Oak Ave'",
addressStreet2: "''",
addressCity: "'New York'",
addressState: "''",
addressPostcode: "''",
addressCountry: "''",
addressLat: null,
addressLng: null,
},
},
},
},
{
title: 'address field with empty default value',
context: {
input: {
name: 'billingAddress',
label: 'Billing Address',
defaultValue: {
addressStreet1: "''",
addressStreet2: "''",
addressCity: "''",
addressState: "''",
addressPostcode: "''",
addressCountry: "''",
addressLat: null,
addressLng: null,
},
},
},
},
{
title: 'address field with settings and default value',
context: {
input: {
name: 'warehouseAddress',
label: 'Warehouse Address',
settings: {
subFields: ['addressStreet1', 'addressCity', 'addressCountry'],
},
defaultValue: {
addressStreet1: "''",
addressStreet2: "''",
addressCity: "''",
addressState: "''",
addressPostcode: "''",
addressCountry: "'USA'",
addressLat: null,
addressLng: null,
},
},
},
},
];
describe('Address field metadata creation tests suite', () => {
let createdObjectMetadataId: string;
let createdFieldMetadataId: string | undefined = undefined;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'testAddressObject',
namePlural: 'testAddressObjects',
labelSingular: 'Test Address Object',
labelPlural: 'Test Address Objects',
icon: 'IconTestPipe',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = data.createOneObject.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: createdObjectMetadataId },
});
});
afterEach(async () => {
if (createdFieldMetadataId) {
await updateOneFieldMetadata({
input: {
updatePayload: { isActive: false },
idToUpdate: createdFieldMetadataId,
},
expectToFail: false,
});
await deleteOneFieldMetadata({
input: { idToDelete: createdFieldMetadataId },
expectToFail: false,
});
createdFieldMetadataId = undefined;
}
});
test.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'it should create $title',
async ({ context: { input } }) => {
const inputPayload = {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.ADDRESS,
isLabelSyncedWithName: false,
...input,
};
const { data } = await createOneFieldMetadata({
expectToFail: false,
input: inputPayload,
gqlFields: `
id
type
name
label
description
icon
defaultValue
isLabelSyncedWithName
settings
`,
});
createdFieldMetadataId = data.createOneField.id;
const { objectMetadataId: _, ...expectedFields } = inputPayload;
expect(data.createOneField).toMatchObject(expectedFields);
},
);
});
@@ -0,0 +1,167 @@
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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
const SUCCESSFUL_TEST_CASES: EachTestingContext<{
input: Omit<CreateFieldInput, 'objectMetadataId' | 'type' | 'workspaceId'>;
}>[] = [
{
title:
'currency field with basic metadata (name, label, description, icon)',
context: {
input: {
name: 'amount',
label: 'Amount',
description: 'Transaction amount',
icon: 'IconCurrencyDollar',
},
},
},
{
title: 'currency field with default value containing amount and currency',
context: {
input: {
name: 'price',
label: 'Price',
defaultValue: {
amountMicros: "'1000000'",
currencyCode: "'USD'",
},
},
},
},
{
title: 'currency field with zero default amount',
context: {
input: {
name: 'discount',
label: 'Discount',
defaultValue: {
amountMicros: "'0'",
currencyCode: "'EUR'",
},
},
},
},
{
title: 'currency field with only currency code default',
context: {
input: {
name: 'budget',
label: 'Budget',
defaultValue: {
amountMicros: null,
currencyCode: "'GBP'",
},
},
},
},
{
title: 'currency field with empty default value',
context: {
input: {
name: 'cost',
label: 'Cost',
defaultValue: {
amountMicros: null,
currencyCode: "''",
},
},
},
},
];
describe('Currency field metadata creation tests suite', () => {
let createdObjectMetadataId: string;
let createdFieldMetadataId: string | undefined = undefined;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'testCurrencyObject',
namePlural: 'testCurrencyObjects',
labelSingular: 'Test Currency Object',
labelPlural: 'Test Currency Objects',
icon: 'IconTestPipe',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = data.createOneObject.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: createdObjectMetadataId },
});
});
afterEach(async () => {
if (createdFieldMetadataId) {
await updateOneFieldMetadata({
input: {
updatePayload: { isActive: false },
idToUpdate: createdFieldMetadataId,
},
expectToFail: false,
});
await deleteOneFieldMetadata({
input: { idToDelete: createdFieldMetadataId },
expectToFail: false,
});
createdFieldMetadataId = undefined;
}
});
test.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'it should create $title',
async ({ context: { input } }) => {
const inputPayload = {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.CURRENCY,
isLabelSyncedWithName: false,
...input,
};
const { data } = await createOneFieldMetadata({
expectToFail: false,
input: inputPayload,
gqlFields: `
id
type
name
label
description
icon
defaultValue
isLabelSyncedWithName
settings
`,
});
createdFieldMetadataId = data.createOneField.id;
const { objectMetadataId: _, ...expectedFields } = inputPayload;
expect(data.createOneField).toMatchObject(expectedFields);
},
);
});
@@ -0,0 +1,168 @@
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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
const SUCCESSFUL_TEST_CASES: EachTestingContext<{
input: Omit<CreateFieldInput, 'objectMetadataId' | 'type' | 'workspaceId'>;
}>[] = [
{
title: 'emails field with basic metadata (name, label, description, icon)',
context: {
input: {
name: 'contactEmails',
label: 'Contact Emails',
description: 'Contact email addresses',
icon: 'IconMail',
},
},
},
{
title: 'emails field with maxNumberOfValues setting',
context: {
input: {
name: 'limitedEmails',
label: 'Limited Emails',
settings: {
maxNumberOfValues: 3,
},
},
},
},
{
title: 'emails field with default value containing primary email',
context: {
input: {
name: 'defaultEmails',
label: 'Default Emails',
defaultValue: {
primaryEmail: "'contact@example.com'",
additionalEmails: null,
},
},
},
},
{
title: 'emails field with empty default value',
context: {
input: {
name: 'emptyEmails',
label: 'Empty Emails',
defaultValue: {
primaryEmail: "''",
additionalEmails: null,
},
},
},
},
{
title: 'emails field with settings and default value',
context: {
input: {
name: 'configuredEmails',
label: 'Configured Emails',
settings: {
maxNumberOfValues: 5,
},
defaultValue: {
primaryEmail: "'support@company.com'",
additionalEmails: null,
},
},
},
},
];
describe('Emails field metadata creation tests suite', () => {
let createdObjectMetadataId: string;
let createdFieldMetadataId: string | undefined = undefined;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'testEmailsObject',
namePlural: 'testEmailsObjects',
labelSingular: 'Test Emails Object',
labelPlural: 'Test Emails Objects',
icon: 'IconTestPipe',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = data.createOneObject.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: createdObjectMetadataId },
});
});
afterEach(async () => {
if (createdFieldMetadataId) {
await updateOneFieldMetadata({
input: {
updatePayload: { isActive: false },
idToUpdate: createdFieldMetadataId,
},
expectToFail: false,
});
await deleteOneFieldMetadata({
input: { idToDelete: createdFieldMetadataId },
expectToFail: false,
});
createdFieldMetadataId = undefined;
}
});
test.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'it should create $title',
async ({ context: { input } }) => {
const inputPayload = {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.EMAILS,
isLabelSyncedWithName: false,
...input,
};
const { data } = await createOneFieldMetadata({
expectToFail: false,
input: inputPayload,
gqlFields: `
id
type
name
label
description
icon
defaultValue
isLabelSyncedWithName
settings
`,
});
createdFieldMetadataId = data.createOneField.id;
const { objectMetadataId: _, ...expectedFields } = inputPayload;
expect(data.createOneField).toMatchObject(expectedFields);
},
);
});
@@ -0,0 +1,154 @@
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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
const SUCCESSFUL_TEST_CASES: EachTestingContext<{
input: Omit<CreateFieldInput, 'objectMetadataId' | 'type' | 'workspaceId'>;
}>[] = [
{
title:
'full name field with basic metadata (name, label, description, icon)',
context: {
input: {
name: 'contactName',
label: 'Contact Name',
description: 'Full name of contact',
icon: 'IconUser',
},
},
},
{
title: 'full name field with default value containing first and last name',
context: {
input: {
name: 'ownerName',
label: 'Owner Name',
defaultValue: {
firstName: "'John'",
lastName: "'Doe'",
},
},
},
},
{
title: 'full name field with only first name default',
context: {
input: {
name: 'customerName',
label: 'Customer Name',
defaultValue: {
firstName: "'Jane'",
lastName: "''",
},
},
},
},
{
title: 'full name field with empty default value',
context: {
input: {
name: 'authorName',
label: 'Author Name',
defaultValue: {
firstName: "''",
lastName: "''",
},
},
},
},
];
describe('Full name field metadata creation tests suite', () => {
let createdObjectMetadataId: string;
let createdFieldMetadataId: string | undefined = undefined;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'testFullNameObject',
namePlural: 'testFullNameObjects',
labelSingular: 'Test Full Name Object',
labelPlural: 'Test Full Name Objects',
icon: 'IconTestPipe',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = data.createOneObject.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: createdObjectMetadataId },
});
});
afterEach(async () => {
if (createdFieldMetadataId) {
await updateOneFieldMetadata({
input: {
updatePayload: { isActive: false },
idToUpdate: createdFieldMetadataId,
},
expectToFail: false,
});
await deleteOneFieldMetadata({
input: { idToDelete: createdFieldMetadataId },
expectToFail: false,
});
createdFieldMetadataId = undefined;
}
});
test.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'it should create $title',
async ({ context: { input } }) => {
const inputPayload = {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.FULL_NAME,
isLabelSyncedWithName: false,
...input,
};
const { data } = await createOneFieldMetadata({
expectToFail: false,
input: inputPayload,
gqlFields: `
id
type
name
label
description
icon
defaultValue
isLabelSyncedWithName
settings
`,
});
createdFieldMetadataId = data.createOneField.id;
const { objectMetadataId: _, ...expectedFields } = inputPayload;
expect(data.createOneField).toMatchObject(expectedFields);
},
);
});
@@ -0,0 +1,171 @@
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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
const SUCCESSFUL_TEST_CASES: EachTestingContext<{
input: Omit<CreateFieldInput, 'objectMetadataId' | 'type' | 'workspaceId'>;
}>[] = [
{
title: 'links field with basic metadata (name, label, description, icon)',
context: {
input: {
name: 'socialLinks',
label: 'Social Links',
description: 'Social media links',
icon: 'IconLink',
},
},
},
{
title: 'links field with maxNumberOfValues setting',
context: {
input: {
name: 'limitedLinks',
label: 'Limited Links',
settings: {
maxNumberOfValues: 5,
},
},
},
},
{
title: 'links field with default value containing primary link',
context: {
input: {
name: 'websiteLinks',
label: 'Website Links',
defaultValue: {
primaryLinkLabel: "'Website'",
primaryLinkUrl: "'https://example.com'",
secondaryLinks: null,
},
},
},
},
{
title: 'links field with empty default value',
context: {
input: {
name: 'emptyLinks',
label: 'Empty Links',
defaultValue: {
primaryLinkLabel: "''",
primaryLinkUrl: "''",
secondaryLinks: null,
},
},
},
},
{
title: 'links field with settings and default value',
context: {
input: {
name: 'configuredLinks',
label: 'Configured Links',
settings: {
maxNumberOfValues: 3,
},
defaultValue: {
primaryLinkLabel: "'Homepage'",
primaryLinkUrl: "'https://company.com'",
secondaryLinks: null,
},
},
},
},
];
describe('Links field metadata creation tests suite', () => {
let createdObjectMetadataId: string;
let createdFieldMetadataId: string | undefined = undefined;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'testLinksObject',
namePlural: 'testLinksObjects',
labelSingular: 'Test Links Object',
labelPlural: 'Test Links Objects',
icon: 'IconTestPipe',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = data.createOneObject.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: createdObjectMetadataId },
});
});
afterEach(async () => {
if (createdFieldMetadataId) {
await updateOneFieldMetadata({
input: {
updatePayload: { isActive: false },
idToUpdate: createdFieldMetadataId,
},
expectToFail: false,
});
await deleteOneFieldMetadata({
input: { idToDelete: createdFieldMetadataId },
expectToFail: false,
});
createdFieldMetadataId = undefined;
}
});
test.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'it should create $title',
async ({ context: { input } }) => {
const inputPayload = {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.LINKS,
isLabelSyncedWithName: false,
...input,
};
const { data } = await createOneFieldMetadata({
expectToFail: false,
input: inputPayload,
gqlFields: `
id
type
name
label
description
icon
defaultValue
isLabelSyncedWithName
settings
`,
});
createdFieldMetadataId = data.createOneField.id;
const { objectMetadataId: _, ...expectedFields } = inputPayload;
expect(data.createOneField).toMatchObject(expectedFields);
},
);
});
@@ -219,109 +219,7 @@ const SUCCESSFUL_TEST_CASES: EachTestingContext<CreatePhoneFieldMetadataTestCase
},
];
const FAILING_TEST_INPUTS: { input: Partial<PhonesMetadata>; label: string }[] =
[
{
label: 'phone field without country or calling code at all',
input: {
primaryPhoneNumber: '123456789',
additionalPhones: [],
},
},
{
label: 'phone field with invalid country code',
input: {
primaryPhoneNumber: '123456789',
primaryPhoneCallingCode: '+33',
primaryPhoneCountryCode: 'XX' as CountryCode,
additionalPhones: [],
},
},
{
label: 'phone field with invalid calling code',
input: {
primaryPhoneNumber: '123456789',
primaryPhoneCallingCode: '+999',
primaryPhoneCountryCode: 'FR',
additionalPhones: [],
},
},
{
label: 'phone field with conflicting country code and calling code',
input: {
primaryPhoneNumber: '123456789',
primaryPhoneCallingCode: '+33',
primaryPhoneCountryCode: 'US',
additionalPhones: [],
},
},
{
label: 'phone field with invalid phone number format',
input: {
primaryPhoneNumber: 'not-a-number',
additionalPhones: [],
},
},
{
label: 'phone field with conflicting phone number country code',
input: {
primaryPhoneNumber: '+33123456789',
primaryPhoneCountryCode: 'US',
additionalPhones: [],
},
},
{
label: 'phone field with conflicting phone number calling code',
input: {
primaryPhoneNumber: '+33123456789',
primaryPhoneCallingCode: '+1',
additionalPhones: [],
},
},
];
const primaryFailingTests = FAILING_TEST_INPUTS.map<
EachTestingContext<CreatePhoneFieldMetadataTestCase>
>(({ input, label }) => ({
title: `create primary ${label}`,
context: {
input: {
...input,
additionalPhones: [],
},
},
}));
const additionalPhonesNumberFailingTests = FAILING_TEST_INPUTS.map<
EachTestingContext<CreatePhoneFieldMetadataTestCase>
>(
({
input: {
primaryPhoneCallingCode,
primaryPhoneCountryCode,
primaryPhoneNumber,
},
label,
}) => ({
title: `create primary ${label}`,
context: {
input: {
additionalPhones: [
{
callingCode: primaryPhoneCallingCode,
countryCode: primaryPhoneCountryCode,
number: primaryPhoneNumber,
},
],
},
},
}),
);
const FAILING_TEST_CASES: EachTestingContext<CreatePhoneFieldMetadataTestCase>[] =
[...primaryFailingTests, ...additionalPhonesNumberFailingTests];
describe('Phone field metadata tests suite', () => {
describe('successful create phone field metadata test suite', () => {
let createdObjectMetadataId: string;
beforeAll(async () => {
@@ -371,7 +269,7 @@ describe('Phone field metadata tests suite', () => {
});
test.each(SUCCESSFUL_TEST_CASES)(
'It should succeed $title',
'it should succeed $title',
async ({ context: { input, expected } }) => {
const {
@@ -407,38 +305,4 @@ describe('Phone field metadata tests suite', () => {
});
},
);
test.each(FAILING_TEST_CASES)(
'It should fail to $title',
async ({ context: { input } }) => {
const {
data: { createOneResponse },
errors,
} = await createOneOperation<{
id: string;
[FIELD_NAME]: any;
}>({
objectMetadataSingularName: 'myTestObject',
input: {
id: faker.string.uuid(),
[FIELD_NAME]: input,
},
gqlFields: `
id
${FIELD_NAME} {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
__typename
}
`,
});
expect(createOneResponse).toBeNull();
expect(errors).toBeDefined();
expect(errors).toMatchSnapshot();
},
);
});
@@ -0,0 +1,154 @@
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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
const SUCCESSFUL_TEST_CASES: EachTestingContext<{
input: Omit<CreateFieldInput, 'objectMetadataId' | 'type' | 'workspaceId'>;
}>[] = [
{
title:
'rich text v2 field with basic metadata (name, label, description, icon)',
context: {
input: {
name: 'description',
label: 'Description',
description: 'Rich text description field',
icon: 'IconNotes',
},
},
},
{
title: 'rich text v2 field with default value containing markdown',
context: {
input: {
name: 'notes',
label: 'Notes',
defaultValue: {
blocknote: null,
markdown: "'# Default Title'",
},
},
},
},
{
title: 'rich text v2 field with default value containing blocknote',
context: {
input: {
name: 'content',
label: 'Content',
defaultValue: {
blocknote: '\'{"blocks":[{"type":"paragraph"}]}\'',
markdown: null,
},
},
},
},
{
title: 'rich text v2 field with empty default value',
context: {
input: {
name: 'body',
label: 'Body',
defaultValue: {
blocknote: null,
markdown: null,
},
},
},
},
];
describe('Rich text v2 field metadata creation tests suite', () => {
let createdObjectMetadataId: string;
let createdFieldMetadataId: string | undefined = undefined;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'testRichTextV2Object',
namePlural: 'testRichTextV2Objects',
labelSingular: 'Test Rich Text V2 Object',
labelPlural: 'Test Rich Text V2 Objects',
icon: 'IconTestPipe',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = data.createOneObject.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: createdObjectMetadataId },
});
});
afterEach(async () => {
if (createdFieldMetadataId) {
await updateOneFieldMetadata({
input: {
updatePayload: { isActive: false },
idToUpdate: createdFieldMetadataId,
},
expectToFail: false,
});
await deleteOneFieldMetadata({
input: { idToDelete: createdFieldMetadataId },
expectToFail: false,
});
createdFieldMetadataId = undefined;
}
});
test.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'it should create $title',
async ({ context: { input } }) => {
const inputPayload = {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.RICH_TEXT_V2,
isLabelSyncedWithName: false,
...input,
};
const { data } = await createOneFieldMetadata({
expectToFail: false,
input: inputPayload,
gqlFields: `
id
type
name
label
description
icon
defaultValue
isLabelSyncedWithName
settings
`,
});
createdFieldMetadataId = data.createOneField.id;
const { objectMetadataId: _, ...expectedFields } = inputPayload;
expect(data.createOneField).toMatchObject(expectedFields);
},
);
});
@@ -0,0 +1,156 @@
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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
const SUCCESSFUL_TEST_CASES: EachTestingContext<{
input: Partial<UpdateFieldInput>;
}>[] = [
{
title: 'actor field basic metadata (label, description, icon)',
context: {
input: {
label: 'Updated Actor',
description: 'Updated description',
icon: 'IconUsers',
},
},
},
{
title: 'actor field default value with MANUAL source',
context: {
input: {
defaultValue: {
source: "'MANUAL'",
workspaceMemberId: null,
name: "'System'",
},
},
},
},
{
title: 'actor field default value with API source',
context: {
input: {
defaultValue: {
source: "'API'",
workspaceMemberId: null,
name: "'API User'",
},
},
},
},
{
title: 'actor field default value set to null',
context: {
input: {
defaultValue: null,
},
},
},
];
describe('Actor field metadata update tests suite', () => {
let createdObjectMetadataId: string;
let createdFieldMetadataId: string;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'testActorUpdateObject',
namePlural: 'testActorUpdateObjects',
labelSingular: 'Test Actor Update Object',
labelPlural: 'Test Actor Update Objects',
icon: 'IconTestPipe',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = data.createOneObject.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: createdObjectMetadataId },
});
});
beforeEach(async () => {
const {
data: { createOneField },
} = await createOneFieldMetadata({
expectToFail: false,
input: {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.ACTOR,
name: 'testActor',
label: 'Test Actor',
description: 'Initial description',
icon: 'IconUser',
isLabelSyncedWithName: false,
},
gqlFields: `
id
`,
});
createdFieldMetadataId = createOneField.id;
});
afterEach(async () => {
await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneFieldMetadata({
expectToFail: false,
input: { idToDelete: createdFieldMetadataId },
});
});
test.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'it should update $title',
async ({ context: { input } }) => {
const { data } = await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: input,
},
gqlFields: `
id
type
name
label
description
icon
settings
defaultValue
`,
});
expect(data.updateOneField).toMatchObject(input);
},
);
});
@@ -0,0 +1,215 @@
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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
const SUCCESSFUL_TEST_CASES: EachTestingContext<{
input: Partial<UpdateFieldInput>;
}>[] = [
{
title: 'address field basic metadata (label, description, icon)',
context: {
input: {
label: 'Updated Address',
description: 'Updated description',
icon: 'IconHome',
},
},
},
{
title: 'address field settings with limited subFields',
context: {
input: {
settings: {
subFields: ['addressStreet1', 'addressCity', 'addressCountry'],
},
},
},
},
{
title: 'address field settings with all subFields',
context: {
input: {
settings: {
subFields: [
'addressStreet1',
'addressStreet2',
'addressCity',
'addressState',
'addressPostcode',
'addressCountry',
'addressLat',
'addressLng',
],
},
},
},
},
{
title: 'address field default value with full address',
context: {
input: {
defaultValue: {
addressStreet1: "'123 Main St'",
addressStreet2: "'Apt 4'",
addressCity: "'New York'",
addressState: "'NY'",
addressPostcode: "'10001'",
addressCountry: "'USA'",
addressLat: null,
addressLng: null,
},
},
},
},
{
title: 'address field default value with partial address',
context: {
input: {
defaultValue: {
addressStreet1: "'456 Oak Ave'",
addressStreet2: "''",
addressCity: "'Boston'",
addressState: "''",
addressPostcode: "''",
addressCountry: "''",
addressLat: null,
addressLng: null,
},
},
},
},
{
title: 'address field default value set to null',
context: {
input: {
defaultValue: null,
},
},
},
{
title: 'address field settings and default value together',
context: {
input: {
settings: {
subFields: ['addressStreet1', 'addressCity', 'addressPostcode'],
},
defaultValue: {
addressStreet1: "''",
addressStreet2: "''",
addressCity: "''",
addressState: "''",
addressPostcode: "''",
addressCountry: "'USA'",
addressLat: null,
addressLng: null,
},
},
},
},
];
describe('Address field metadata update tests suite', () => {
let createdObjectMetadataId: string;
let createdFieldMetadataId: string;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'testAddressUpdateObject',
namePlural: 'testAddressUpdateObjects',
labelSingular: 'Test Address Update Object',
labelPlural: 'Test Address Update Objects',
icon: 'IconTestPipe',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = data.createOneObject.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: createdObjectMetadataId },
});
});
beforeEach(async () => {
const {
data: { createOneField },
} = await createOneFieldMetadata({
expectToFail: false,
input: {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.ADDRESS,
name: 'testAddress',
label: 'Test Address',
description: 'Initial description',
icon: 'IconMapPin',
isLabelSyncedWithName: false,
},
gqlFields: `
id
`,
});
createdFieldMetadataId = createOneField.id;
});
afterEach(async () => {
await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneFieldMetadata({
expectToFail: false,
input: { idToDelete: createdFieldMetadataId },
});
});
test.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'it should update $title',
async ({ context: { input } }) => {
const { data } = await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: input,
},
gqlFields: `
id
type
name
label
description
icon
settings
defaultValue
`,
});
expect(data.updateOneField).toMatchObject(input);
},
);
});
@@ -0,0 +1,176 @@
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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
const SUCCESSFUL_TEST_CASES: EachTestingContext<{
input: Partial<UpdateFieldInput>;
}>[] = [
{
title: 'currency field basic metadata (label, description, icon)',
context: {
input: {
label: 'Updated Currency',
description: 'Updated description',
icon: 'IconCoin',
},
},
},
{
title: 'currency field default value with amount and currency',
context: {
input: {
defaultValue: {
amountMicros: "'1000000'",
currencyCode: "'USD'",
},
},
},
},
{
title: 'currency field default value with zero amount',
context: {
input: {
defaultValue: {
amountMicros: "'0'",
currencyCode: "'EUR'",
},
},
},
},
{
title: 'currency field default value with only currency code',
context: {
input: {
defaultValue: {
amountMicros: null,
currencyCode: "'GBP'",
},
},
},
},
{
title: 'currency field default value with empty values',
context: {
input: {
defaultValue: {
amountMicros: null,
currencyCode: "''",
},
},
},
},
{
title: 'currency field default value set to null',
context: {
input: {
defaultValue: null,
},
},
},
];
describe('Currency field metadata update tests suite', () => {
let createdObjectMetadataId: string;
let createdFieldMetadataId: string;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'testCurrencyUpdateObject',
namePlural: 'testCurrencyUpdateObjects',
labelSingular: 'Test Currency Update Object',
labelPlural: 'Test Currency Update Objects',
icon: 'IconTestPipe',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = data.createOneObject.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: createdObjectMetadataId },
});
});
beforeEach(async () => {
const {
data: { createOneField },
} = await createOneFieldMetadata({
expectToFail: false,
input: {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.CURRENCY,
name: 'testCurrency',
label: 'Test Currency',
description: 'Initial description',
icon: 'IconCurrencyDollar',
isLabelSyncedWithName: false,
},
gqlFields: `
id
`,
});
createdFieldMetadataId = createOneField.id;
});
afterEach(async () => {
await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneFieldMetadata({
expectToFail: false,
input: { idToDelete: createdFieldMetadataId },
});
});
test.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'it should update $title',
async ({ context: { input } }) => {
const { data } = await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: input,
},
gqlFields: `
id
type
name
label
description
icon
settings
defaultValue
`,
});
expect(data.updateOneField).toMatchObject(input);
},
);
});
@@ -0,0 +1,178 @@
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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
const SUCCESSFUL_TEST_CASES: EachTestingContext<{
input: Partial<UpdateFieldInput>;
}>[] = [
{
title: 'emails field basic metadata (label, description, icon)',
context: {
input: {
label: 'Updated Emails',
description: 'Updated description',
icon: 'IconMailOpened',
},
},
},
{
title: 'emails field settings with maxNumberOfValues',
context: {
input: {
settings: {
maxNumberOfValues: 3,
},
},
},
},
{
title: 'emails field default value with primary email',
context: {
input: {
defaultValue: {
primaryEmail: "'contact@example.com'",
additionalEmails: null,
},
},
},
},
{
title: 'emails field default value with empty values',
context: {
input: {
defaultValue: {
primaryEmail: "''",
additionalEmails: null,
},
},
},
},
{
title: 'emails field default value set to null',
context: {
input: {
defaultValue: null,
},
},
},
{
title: 'emails field settings and default value together',
context: {
input: {
settings: {
maxNumberOfValues: 5,
},
defaultValue: {
primaryEmail: "'support@company.com'",
additionalEmails: null,
},
},
},
},
];
describe('Emails field metadata update tests suite', () => {
let createdObjectMetadataId: string;
let createdFieldMetadataId: string;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'testEmailsUpdateObject',
namePlural: 'testEmailsUpdateObjects',
labelSingular: 'Test Emails Update Object',
labelPlural: 'Test Emails Update Objects',
icon: 'IconTestPipe',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = data.createOneObject.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: createdObjectMetadataId },
});
});
beforeEach(async () => {
const {
data: { createOneField },
} = await createOneFieldMetadata({
expectToFail: false,
input: {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.EMAILS,
name: 'testEmails',
label: 'Test Emails',
description: 'Initial description',
icon: 'IconMail',
isLabelSyncedWithName: false,
},
gqlFields: `
id
`,
});
createdFieldMetadataId = createOneField.id;
});
afterEach(async () => {
await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneFieldMetadata({
expectToFail: false,
input: { idToDelete: createdFieldMetadataId },
});
});
test.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'it should update $title',
async ({ context: { input } }) => {
const { data } = await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: input,
},
gqlFields: `
id
type
name
label
description
icon
settings
defaultValue
`,
});
expect(data.updateOneField).toMatchObject(input);
},
);
});
@@ -0,0 +1,165 @@
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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
const SUCCESSFUL_TEST_CASES: EachTestingContext<{
input: Partial<UpdateFieldInput>;
}>[] = [
{
title: 'full name field basic metadata (label, description, icon)',
context: {
input: {
label: 'Updated Full Name',
description: 'Updated description',
icon: 'IconUserCircle',
},
},
},
{
title: 'full name field default value with first and last name',
context: {
input: {
defaultValue: {
firstName: "'John'",
lastName: "'Doe'",
},
},
},
},
{
title: 'full name field default value with only first name',
context: {
input: {
defaultValue: {
firstName: "'Jane'",
lastName: "''",
},
},
},
},
{
title: 'full name field default value with empty values',
context: {
input: {
defaultValue: {
firstName: "''",
lastName: "''",
},
},
},
},
{
title: 'full name field default value set to null',
context: {
input: {
defaultValue: null,
},
},
},
];
describe('Full name field metadata update tests suite', () => {
let createdObjectMetadataId: string;
let createdFieldMetadataId: string;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'testFullNameUpdateObject',
namePlural: 'testFullNameUpdateObjects',
labelSingular: 'Test Full Name Update Object',
labelPlural: 'Test Full Name Update Objects',
icon: 'IconTestPipe',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = data.createOneObject.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: createdObjectMetadataId },
});
});
beforeEach(async () => {
const {
data: { createOneField },
} = await createOneFieldMetadata({
expectToFail: false,
input: {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.FULL_NAME,
name: 'testFullName',
label: 'Test Full Name',
description: 'Initial description',
icon: 'IconUser',
isLabelSyncedWithName: false,
},
gqlFields: `
id
`,
});
createdFieldMetadataId = createOneField.id;
});
afterEach(async () => {
await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneFieldMetadata({
expectToFail: false,
input: { idToDelete: createdFieldMetadataId },
});
});
test.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'it should update $title',
async ({ context: { input } }) => {
const { data } = await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: input,
},
gqlFields: `
id
type
name
label
description
icon
settings
defaultValue
`,
});
expect(data.updateOneField).toMatchObject(input);
},
);
});
@@ -0,0 +1,181 @@
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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
const SUCCESSFUL_TEST_CASES: EachTestingContext<{
input: Partial<UpdateFieldInput>;
}>[] = [
{
title: 'links field basic metadata (label, description, icon)',
context: {
input: {
label: 'Updated Links',
description: 'Updated description',
icon: 'IconExternalLink',
},
},
},
{
title: 'links field settings with maxNumberOfValues',
context: {
input: {
settings: {
maxNumberOfValues: 5,
},
},
},
},
{
title: 'links field default value with primary link',
context: {
input: {
defaultValue: {
primaryLinkLabel: "'Website'",
primaryLinkUrl: "'https://example.com'",
secondaryLinks: null,
},
},
},
},
{
title: 'links field default value with empty values',
context: {
input: {
defaultValue: {
primaryLinkLabel: "''",
primaryLinkUrl: "''",
secondaryLinks: null,
},
},
},
},
{
title: 'links field default value set to null',
context: {
input: {
defaultValue: null,
},
},
},
{
title: 'links field settings and default value together',
context: {
input: {
settings: {
maxNumberOfValues: 3,
},
defaultValue: {
primaryLinkLabel: "'Homepage'",
primaryLinkUrl: "'https://company.com'",
secondaryLinks: null,
},
},
},
},
];
describe('Links field metadata update tests suite', () => {
let createdObjectMetadataId: string;
let createdFieldMetadataId: string;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'testLinksUpdateObject',
namePlural: 'testLinksUpdateObjects',
labelSingular: 'Test Links Update Object',
labelPlural: 'Test Links Update Objects',
icon: 'IconTestPipe',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = data.createOneObject.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: createdObjectMetadataId },
});
});
beforeEach(async () => {
const {
data: { createOneField },
} = await createOneFieldMetadata({
expectToFail: false,
input: {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.LINKS,
name: 'testLinks',
label: 'Test Links',
description: 'Initial description',
icon: 'IconLink',
isLabelSyncedWithName: false,
},
gqlFields: `
id
`,
});
createdFieldMetadataId = createOneField.id;
});
afterEach(async () => {
await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneFieldMetadata({
expectToFail: false,
input: { idToDelete: createdFieldMetadataId },
});
});
test.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'it should update $title',
async ({ context: { input } }) => {
const { data } = await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: input,
},
gqlFields: `
id
type
name
label
description
icon
settings
defaultValue
`,
});
expect(data.updateOneField).toMatchObject(input);
},
);
});
@@ -0,0 +1,184 @@
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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
const SUCCESSFUL_TEST_CASES: EachTestingContext<{
input: Partial<UpdateFieldInput>;
}>[] = [
{
title: 'phones field basic metadata (label, description, icon)',
context: {
input: {
label: 'Updated Phones',
description: 'Updated description',
icon: 'IconPhoneCall',
},
},
},
{
title: 'phones field settings with maxNumberOfValues',
context: {
input: {
settings: {
maxNumberOfValues: 4,
},
},
},
},
{
title: 'phones field default value with primary phone',
context: {
input: {
defaultValue: {
primaryPhoneNumber: "'+33123456789'",
primaryPhoneCountryCode: "'FR'",
primaryPhoneCallingCode: "'+33'",
additionalPhones: null,
},
},
},
},
{
title: 'phones field default value with empty values',
context: {
input: {
defaultValue: {
primaryPhoneNumber: "''",
primaryPhoneCountryCode: "''",
primaryPhoneCallingCode: "''",
additionalPhones: null,
},
},
},
},
{
title: 'phones field default value set to null',
context: {
input: {
defaultValue: null,
},
},
},
{
title: 'phones field settings and default value together',
context: {
input: {
settings: {
maxNumberOfValues: 6,
},
defaultValue: {
primaryPhoneNumber: "'+1555123456'",
primaryPhoneCountryCode: "'US'",
primaryPhoneCallingCode: "'+1'",
additionalPhones: null,
},
},
},
},
];
describe('Phones field metadata update tests suite', () => {
let createdObjectMetadataId: string;
let createdFieldMetadataId: string;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'testPhonesUpdateObject',
namePlural: 'testPhonesUpdateObjects',
labelSingular: 'Test Phones Update Object',
labelPlural: 'Test Phones Update Objects',
icon: 'IconTestPipe',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = data.createOneObject.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: createdObjectMetadataId },
});
});
beforeEach(async () => {
const {
data: { createOneField },
} = await createOneFieldMetadata({
expectToFail: false,
input: {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.PHONES,
name: 'testPhones',
label: 'Test Phones',
description: 'Initial description',
icon: 'IconPhone',
isLabelSyncedWithName: false,
},
gqlFields: `
id
`,
});
createdFieldMetadataId = createOneField.id;
});
afterEach(async () => {
await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneFieldMetadata({
expectToFail: false,
input: { idToDelete: createdFieldMetadataId },
});
});
test.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'it should update $title',
async ({ context: { input } }) => {
const { data } = await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: input,
},
gqlFields: `
id
type
name
label
description
icon
settings
defaultValue
`,
});
expect(data.updateOneField).toMatchObject(input);
},
);
});
@@ -0,0 +1,165 @@
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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
const SUCCESSFUL_TEST_CASES: EachTestingContext<{
input: Partial<UpdateFieldInput>;
}>[] = [
{
title: 'rich text v2 field basic metadata (label, description, icon)',
context: {
input: {
label: 'Updated Rich Text',
description: 'Updated description',
icon: 'IconFileText',
},
},
},
{
title: 'rich text v2 field default value with markdown',
context: {
input: {
defaultValue: {
blocknote: null,
markdown: "'# Default Title'",
},
},
},
},
{
title: 'rich text v2 field default value with blocknote',
context: {
input: {
defaultValue: {
blocknote: '\'{"blocks":[{"type":"paragraph"}]}\'',
markdown: null,
},
},
},
},
{
title: 'rich text v2 field default value with both formats',
context: {
input: {
defaultValue: {
blocknote: '\'{"blocks":[]}\'',
markdown: "'## Heading'",
},
},
},
},
{
title: 'rich text v2 field default value set to null',
context: {
input: {
defaultValue: null,
},
},
},
];
describe('Rich text v2 field metadata update tests suite', () => {
let createdObjectMetadataId: string;
let createdFieldMetadataId: string;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'testRichTextV2UpdateObject',
namePlural: 'testRichTextV2UpdateObjects',
labelSingular: 'Test Rich Text V2 Update Object',
labelPlural: 'Test Rich Text V2 Update Objects',
icon: 'IconTestPipe',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = data.createOneObject.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: createdObjectMetadataId },
});
});
beforeEach(async () => {
const {
data: { createOneField },
} = await createOneFieldMetadata({
expectToFail: false,
input: {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.RICH_TEXT_V2,
name: 'testRichText',
label: 'Test Rich Text',
description: 'Initial description',
icon: 'IconNotes',
isLabelSyncedWithName: false,
},
gqlFields: `
id
`,
});
createdFieldMetadataId = createOneField.id;
});
afterEach(async () => {
await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneFieldMetadata({
expectToFail: false,
input: { idToDelete: createdFieldMetadataId },
});
});
test.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'it should update $title',
async ({ context: { input } }) => {
const { data } = await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: input,
},
gqlFields: `
id
type
name
label
description
icon
settings
defaultValue
`,
});
expect(data.updateOneField).toMatchObject(input);
},
);
});
@@ -11,11 +11,11 @@ import { eachTestingContextFilter } from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
import {
type FieldMetadataComplexOption,
type 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)(
'Successful update field metadata %s tests suite v2',
@@ -128,6 +128,7 @@ describe.each(fieldMetadataEnumTypes)(
: rawDefaultValue;
const { data: firstUpdate } = await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: {
@@ -146,6 +147,7 @@ describe.each(fieldMetadataEnumTypes)(
const updatedOptions = initialOptions.slice(1);
const { data: secondUpdate, errors } = await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: {
@@ -3,10 +3,7 @@ import {
NOT_EXISTING_TEST_PERSON_ID,
TEST_PERSON_1_ID,
} from 'test/integration/constants/test-person-ids.constants';
import {
TEST_PRIMARY_LINK_URL,
TEST_PRIMARY_LINK_URL_WIITHOUT_TRAILING_SLASH,
} from 'test/integration/constants/test-primary-link-url.constant';
import { TEST_PRIMARY_LINK_URL } from 'test/integration/constants/test-primary-link-url.constant';
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
import { deleteAllRecords } from 'test/integration/utils/delete-all-records';
import { generateRecordName } from 'test/integration/utils/generate-record-name';
@@ -98,9 +95,6 @@ describe('Core REST API Update One endpoint', () => {
const updatedPerson = res.body.data.updatePerson;
expect(updatedPerson.company).toBeDefined();
expect(updatedPerson.company.domainName.primaryLinkUrl).toBe(
TEST_PRIMARY_LINK_URL_WIITHOUT_TRAILING_SLASH,
);
expect(updatedPerson.company.people).not.toBeDefined();
});
});