Fix deactivate objects impacts (#11185)

In this PR:
- Remove deactivated objects from ActivityTargetInlineCell record picker
- Prevent users to deactivate createdAt, updatedAt, deletedAt fields on
any objects

Still left:
- write unit tests on the assert utils
- write integration tests on field metadata service
- prevent users to deactivate createdAt, updatedAt, deletedAt on FE
This commit is contained in:
Charles Bochet
2025-03-26 20:45:46 +01:00
committed by GitHub
parent 90e884d33f
commit 5bd10d40cb
49 changed files with 861 additions and 531 deletions

View File

@ -28,6 +28,7 @@ export const useOpenActivityTargetInlineCellEditMode = () => {
.filter(
(objectMetadataItem) =>
objectMetadataItem.isSearchable &&
objectMetadataItem.isActive &&
objectMetadataItem.nameSingular !== CoreObjectNameSingular.Task &&
objectMetadataItem.nameSingular !== CoreObjectNameSingular.Note &&
objectMetadataItem.nameSingular !==

View File

@ -15,4 +15,5 @@ export enum FieldMetadataExceptionCode {
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
FIELD_METADATA_RELATION_NOT_ENABLED = 'FIELD_METADATA_RELATION_NOT_ENABLED',
FIELD_METADATA_RELATION_MALFORMED = 'FIELD_METADATA_RELATION_MALFORMED',
LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND = 'LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND',
}

View File

@ -30,6 +30,7 @@ import {
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { FieldMetadataRelatedRecordsService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service';
import { assertDoesNotNullifyDefaultValueForNonNullableField } from 'src/engine/metadata-modules/field-metadata/utils/assert-does-not-nullify-default-value-for-non-nullable-field.util';
import { checkCanDeactivateFieldOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/check-can-deactivate-field-or-throw';
import {
computeColumnName,
computeCompositeColumnName,
@ -154,6 +155,12 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
);
}
if (!objectMetadata.labelIdentifierFieldMetadataId) {
throw new FieldMetadataException(
'Label identifier field metadata id does not exist',
FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND,
);
}
assertMutationNotOnRemoteObject(objectMetadata);
assertDoesNotNullifyDefaultValueForNonNullableField({
@ -161,18 +168,13 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
defaultValueFromUpdate: fieldMetadataInput.defaultValue,
});
if (
objectMetadata.labelIdentifierFieldMetadataId ===
existingFieldMetadata.id &&
fieldMetadataInput.isActive === false
) {
throw new FieldMetadataException(
'Cannot deactivate label identifier field',
FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED,
);
}
if (fieldMetadataInput.isActive === false) {
checkCanDeactivateFieldOrThrow({
labelIdentifierFieldMetadataId:
objectMetadata.labelIdentifierFieldMetadataId,
existingFieldMetadata,
});
const viewsRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
fieldMetadataInput.workspaceId,

View File

@ -30,6 +30,8 @@ export interface FieldMetadataInterface<
relationTargetObjectMetadataId?: string;
relationTargetObjectMetadata?: ObjectMetadataEntity;
isCustom?: boolean;
isSystem?: boolean;
isActive?: boolean;
generatedType?: 'STORED' | 'VIRTUAL';
asExpression?: string;
}

View File

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`checkCanDeactivateFieldOrThrow throws if trying to deactivate createdAt field 1`] = `"Cannot deactivate createdAt, updatedAt or deletedAt field"`;
exports[`checkCanDeactivateFieldOrThrow throws if trying to deactivate deletedAt field 1`] = `"Cannot deactivate createdAt, updatedAt or deletedAt field"`;
exports[`checkCanDeactivateFieldOrThrow throws if trying to deactivate label identifier field 1`] = `"Cannot deactivate label identifier field"`;
exports[`checkCanDeactivateFieldOrThrow throws if trying to deactivate system field 1`] = `"Cannot deactivate system field"`;
exports[`checkCanDeactivateFieldOrThrow throws if trying to deactivate updatedAt field 1`] = `"Cannot deactivate createdAt, updatedAt or deletedAt field"`;

View File

@ -0,0 +1,106 @@
import { EachTestingContext } from 'twenty-shared/testing';
import { checkCanDeactivateFieldOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/check-can-deactivate-field-or-throw';
type CheckCanDeactivateFieldOrThrowTestContext = EachTestingContext<{
input: Parameters<typeof checkCanDeactivateFieldOrThrow>[0];
shouldNotThrow?: true;
}>;
const checkCanDeactivateFieldOrThrowTestCases: CheckCanDeactivateFieldOrThrowTestContext[] =
[
{
title: 'does not throw if nominal case',
context: {
input: {
labelIdentifierFieldMetadataId: 'fieldIdentifierId',
existingFieldMetadata: {
id: 'myFieldId',
isSystem: false,
name: 'myFieldName',
},
},
shouldNotThrow: true,
},
},
{
title: 'throws if trying to deactivate label identifier field',
context: {
input: {
labelIdentifierFieldMetadataId: 'fieldId',
existingFieldMetadata: {
id: 'fieldId',
isSystem: false,
name: 'name',
},
},
},
},
{
title: 'throws if trying to deactivate system field',
context: {
input: {
labelIdentifierFieldMetadataId: 'fieldIdentifierId',
existingFieldMetadata: {
id: 'systemFieldId',
isSystem: true,
name: 'systemField',
},
},
},
},
{
title: 'throws if trying to deactivate createdAt field',
context: {
input: {
labelIdentifierFieldMetadataId: 'fieldIdentifierId',
existingFieldMetadata: {
id: 'createdAtId',
isSystem: false,
name: 'createdAt',
},
},
},
},
{
title: 'throws if trying to deactivate updatedAt field',
context: {
input: {
labelIdentifierFieldMetadataId: 'fieldIdentifierId',
existingFieldMetadata: {
id: 'updatedAtId',
isSystem: false,
name: 'updatedAt',
},
},
},
},
{
title: 'throws if trying to deactivate deletedAt field',
context: {
input: {
labelIdentifierFieldMetadataId: 'fieldIdentifierId',
existingFieldMetadata: {
id: 'deletedAtId',
isSystem: false,
name: 'deletedAt',
},
},
},
},
];
describe('checkCanDeactivateFieldOrThrow', () => {
it.each(checkCanDeactivateFieldOrThrowTestCases)(
'$title',
({ context: { input, shouldNotThrow } }) => {
if (shouldNotThrow) {
expect(() => checkCanDeactivateFieldOrThrow(input)).not.toThrow();
} else {
expect(() =>
checkCanDeactivateFieldOrThrow(input),
).toThrowErrorMatchingSnapshot();
}
},
);
});

View File

@ -0,0 +1,42 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
type CheckCanDeactivateFieldOptions = {
labelIdentifierFieldMetadataId: string;
existingFieldMetadata: Pick<
FieldMetadataInterface,
'id' | 'isSystem' | 'name'
>;
};
export const checkCanDeactivateFieldOrThrow = ({
labelIdentifierFieldMetadataId,
existingFieldMetadata,
}: CheckCanDeactivateFieldOptions) => {
if (existingFieldMetadata.id === labelIdentifierFieldMetadataId) {
throw new FieldMetadataException(
'Cannot deactivate label identifier field',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
}
if (existingFieldMetadata.isSystem === true) {
throw new FieldMetadataException(
'Cannot deactivate system field',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
}
if (
['deletedAt', 'createdAt', 'updatedAt'].includes(existingFieldMetadata.name)
) {
throw new FieldMetadataException(
'Cannot deactivate createdAt, updatedAt or deletedAt field',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
}
};

View File

@ -30,6 +30,7 @@ export const fieldMetadataGraphqlApiExceptionHandler = (error: Error) => {
case FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR:
case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_NOT_ENABLED:
case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED:
case FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND:
default:
throw new InternalServerError(error.message);
}

View File

@ -1,4 +1,4 @@
import { getMockCreateObjectInput } from 'test/integration/utils/object-metadata/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 { UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';

View File

@ -14,9 +14,9 @@ import {
LISTING_NAME_PLURAL,
LISTING_NAME_SINGULAR,
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
import { createListingCustomObject } from 'test/integration/metadata/suites/object-metadata/utils/create-test-object-metadata.util';
import { deleteOneObjectMetadataItem } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { findManyObjectsMetadataItems } from 'test/integration/metadata/suites/object-metadata/utils/find-many-objects-metadata-items.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 { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
import { EachTestingContext } from 'twenty-shared/testing';
import { SearchRecordDTO } from 'src/engine/core-modules/search/dtos/search-record-dto';
@ -46,7 +46,14 @@ describe('SearchResolver', () => {
beforeAll(async () => {
try {
const objectsMetadata = await findManyObjectsMetadataItems();
const objectsMetadata = await findManyObjectMetadata({
input: {
filter: {},
paging: {
first: 1000,
},
},
});
const listingObjectMetadata = objectsMetadata.find(
(object) => object.nameSingular === LISTING_NAME_SINGULAR,
);
@ -56,7 +63,19 @@ describe('SearchResolver', () => {
objectMetadataId: listingObjectMetadata.id,
};
} else {
listingObjectMetadataId = await createListingCustomObject();
const { data } = await createOneObjectMetadata({
input: {
labelSingular: LISTING_NAME_SINGULAR,
labelPlural: LISTING_NAME_PLURAL,
nameSingular: LISTING_NAME_SINGULAR,
namePlural: LISTING_NAME_PLURAL,
icon: 'IconBuildingSkyscraper',
},
});
listingObjectMetadataId = {
objectMetadataId: data.createOneObject.id,
};
}
await performCreateManyOperation(
@ -102,9 +121,9 @@ describe('SearchResolver', () => {
console.log(error);
});
await deleteOneObjectMetadataItem(
listingObjectMetadataId.objectMetadataId,
).catch((error) => {
await deleteOneObjectMetadata({
input: { idToDelete: listingObjectMetadataId.objectMetadataId },
}).catch((error) => {
// eslint-disable-next-line no-console
console.log(error);
});

View File

@ -1,12 +1,12 @@
import { createCustomTextFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-custom-text-field-metadata.util';
import { createOneFieldMetadataFactory } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-factory.util';
import { deleteOneFieldMetadataItemFactory } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata-factory.util';
import { updateOneFieldMetadataFactory } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata-factory.util';
import { createOneObjectMetadataFactory } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-factory.util';
import { createListingCustomObject } from 'test/integration/metadata/suites/object-metadata/utils/create-test-object-metadata.util';
import { deleteOneObjectMetadataItemFactory } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata-factory.util';
import { deleteOneObjectMetadataItem } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { updateOneObjectMetadataItemFactory } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata-factory.util';
import { createOneFieldMetadataQueryFactory } 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 { deleteOneFieldMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata-query-factory.util';
import { updateOneFieldMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata-query-factory.util';
import { createOneObjectMetadataQueryFactory } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-query-factory.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { deleteOneObjectMetadataQueryFactory } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata-query-factory.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { updateOneObjectMetadataQueryFactory } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata-query-factory.util';
import { makeMetadataAPIRequestWithMemberRole } from 'test/integration/metadata/suites/utils/make-metadata-api-request-with-member-role.util';
import { FieldMetadataType } from 'twenty-shared/types';
@ -21,18 +21,33 @@ describe('datamodel permissions', () => {
let testFieldId = '';
beforeAll(async () => {
const { objectMetadataId: createdObjectId } =
await createListingCustomObject();
const { data } = await createOneObjectMetadata({
input: {
nameSingular: 'listing',
namePlural: 'listings',
labelSingular: 'Listing',
labelPlural: 'Listings',
icon: 'IconBuildingSkyscraper',
},
});
listingObjectId = createdObjectId;
listingObjectId = data.createOneObject.id;
const { fieldMetadataId: createdFieldMetadaId } =
await createCustomTextFieldMetadata(createdObjectId);
const { data: createdFieldData } = await createOneFieldMetadata({
input: {
name: 'house',
type: FieldMetadataType.TEXT,
label: 'House',
objectMetadataId: listingObjectId,
},
});
testFieldId = createdFieldMetadaId;
testFieldId = createdFieldData.createOneField.id;
});
afterAll(async () => {
await deleteOneObjectMetadataItem(listingObjectId);
await deleteOneObjectMetadata({
input: { idToDelete: listingObjectId },
});
});
describe('createOne', () => {
it('should throw a permission error when user does not have permission (member role)', async () => {
@ -46,8 +61,8 @@ describe('datamodel permissions', () => {
};
// Act
const graphqlOperation = createOneFieldMetadataFactory({
input: { field: createFieldInput },
const graphqlOperation = createOneFieldMetadataQueryFactory({
input: createFieldInput,
gqlFields: `
id
name
@ -77,8 +92,8 @@ describe('datamodel permissions', () => {
label: 'Updated Name',
};
const graphqlOperation = updateOneFieldMetadataFactory({
input: { id: testFieldId, update: updateFieldInput },
const graphqlOperation = updateOneFieldMetadataQueryFactory({
input: { idToUpdate: testFieldId, updatePayload: updateFieldInput },
gqlFields: `
id
name
@ -103,8 +118,8 @@ describe('datamodel permissions', () => {
describe('deleteOne', () => {
it('should throw a permission error when user does not have permission (member role)', async () => {
// Arrange
const graphqlOperation = deleteOneFieldMetadataItemFactory({
idToDelete: testFieldId,
const graphqlOperation = deleteOneFieldMetadataQueryFactory({
input: { idToDelete: testFieldId },
});
const response =
@ -127,17 +142,15 @@ describe('datamodel permissions', () => {
describe('createOne', () => {
it('should throw a permission error when user does not have permission (member role)', async () => {
// Arrange
const graphqlOperation = createOneObjectMetadataFactory({
const graphqlOperation = createOneObjectMetadataQueryFactory({
gqlFields: `
id
`,
input: {
object: {
labelPlural: 'Test Objects',
labelSingular: 'Test Object',
namePlural: 'testObjects',
nameSingular: 'testObject',
},
labelPlural: 'Test Objects',
labelSingular: 'Test Object',
namePlural: 'testObjects',
nameSingular: 'testObject',
},
});
@ -160,18 +173,27 @@ describe('datamodel permissions', () => {
let listingObjectId = '';
beforeAll(async () => {
const { objectMetadataId: createdObjectId } =
await createListingCustomObject();
const { data } = await createOneObjectMetadata({
input: {
labelPlural: 'Listings',
labelSingular: 'Listing',
namePlural: 'listings',
nameSingular: 'listing',
icon: 'IconBuildingSkyscraper',
},
});
listingObjectId = createdObjectId;
listingObjectId = data.createOneObject.id;
});
afterAll(async () => {
await deleteOneObjectMetadataItem(listingObjectId);
await deleteOneObjectMetadata({
input: { idToDelete: listingObjectId },
});
});
describe('updateOne', () => {
it('should throw a permission error when user does not have permission (member role)', async () => {
// Arrange
const graphqlOperation = updateOneObjectMetadataItemFactory({
const graphqlOperation = updateOneObjectMetadataQueryFactory({
gqlFields: `
id
`,
@ -201,8 +223,8 @@ describe('datamodel permissions', () => {
describe('deleteOne', () => {
it('should throw a permission error when user does not have permission (member role)', async () => {
// Arrange
const graphqlOperation = deleteOneObjectMetadataItemFactory({
idToDelete: listingObjectId,
const graphqlOperation = deleteOneObjectMetadataQueryFactory({
input: { idToDelete: listingObjectId },
});
const response =

View File

@ -2,8 +2,8 @@ import request from 'supertest';
import { deleteOneRoleOperationFactory } from 'test/integration/graphql/utils/delete-one-role-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
import { createListingCustomObject } from 'test/integration/metadata/suites/object-metadata/utils/create-test-object-metadata.util';
import { deleteOneObjectMetadataItem } from 'test/integration/metadata/suites/object-metadata/utils/delete-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 { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces';
import { DEV_SEED_WORKSPACE_MEMBER_IDS } from 'src/database/typeorm-seeds/workspace/workspace-members';
@ -38,19 +38,12 @@ describe('roles permissions', () => {
let guestRoleId: string;
beforeAll(async () => {
const enablePermissionsQuery = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IsPermissionsEnabled',
true,
);
const enablePermissionsV2Query = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IsPermissionsV2Enabled',
true,
);
await makeGraphqlAPIRequest(enablePermissionsQuery);
await makeGraphqlAPIRequest(enablePermissionsV2Query);
const query = {
@ -460,14 +453,23 @@ describe('roles permissions', () => {
let listingObjectId = '';
beforeAll(async () => {
const { objectMetadataId: createdObjectId } =
await createListingCustomObject();
const { data } = await createOneObjectMetadata({
input: {
nameSingular: 'house',
namePlural: 'houses',
labelSingular: 'House',
labelPlural: 'Houses',
icon: 'IconBuildingSkyscraper',
},
});
listingObjectId = createdObjectId;
listingObjectId = data.createOneObject.id;
});
afterAll(async () => {
await deleteOneObjectMetadataItem(listingObjectId);
await deleteOneObjectMetadata({
input: { idToDelete: listingObjectId },
});
});
const upsertObjectPermissionMutation = ({

View File

@ -1,21 +1,33 @@
import { createOneFieldMetadataFactory } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-factory.util';
import { createListingCustomObject } from 'test/integration/metadata/suites/object-metadata/utils/create-test-object-metadata.util';
import { deleteOneObjectMetadataItem } 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 { 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 { FieldMetadataType } from 'twenty-shared/types';
describe('createOne', () => {
describe('FieldMetadataService name/label sync', () => {
let listingObjectId = '';
let createdObjectMetadataId = '';
beforeEach(async () => {
const { objectMetadataId: createdObjectId } =
await createListingCustomObject();
const {
data: {
createOneObject: { id: objectMetadataId },
},
} = await createOneObjectMetadata({
input: {
nameSingular: 'myTestObject',
namePlural: 'myTestObjects',
labelSingular: 'My Test Object',
labelPlural: 'My Test Objects',
icon: 'Icon123',
},
});
listingObjectId = createdObjectId;
createdObjectMetadataId = objectMetadataId;
});
afterEach(async () => {
await deleteOneObjectMetadataItem(listingObjectId);
await deleteOneObjectMetadata({
input: { idToDelete: createdObjectMetadataId },
});
});
it('should create a field when name and label are synced correctly', async () => {
// Arrange
@ -24,13 +36,13 @@ describe('createOne', () => {
name: FIELD_NAME,
label: 'Test Field',
type: FieldMetadataType.TEXT,
objectMetadataId: listingObjectId,
objectMetadataId: createdObjectMetadataId,
isLabelSyncedWithName: true,
};
// Act
const graphqlOperation = createOneFieldMetadataFactory({
input: { field: createFieldInput },
const { data } = await createOneFieldMetadata({
input: createFieldInput,
gqlFields: `
id
name
@ -39,10 +51,8 @@ describe('createOne', () => {
`,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
// Assert
expect(response.body.data.createOneField.name).toBe(FIELD_NAME);
expect(data.createOneField.name).toBe(FIELD_NAME);
});
it('should set isLabelSyncWithName to false if not in input', async () => {
@ -51,26 +61,22 @@ describe('createOne', () => {
name: 'testField',
label: 'Test Field',
type: FieldMetadataType.TEXT,
objectMetadataId: listingObjectId,
objectMetadataId: createdObjectMetadataId,
};
// Act
const graphqlOperation = createOneFieldMetadataFactory({
input: { field: createFieldInput },
const { data } = await createOneFieldMetadata({
input: createFieldInput,
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
id
name
label
isLabelSyncedWithName
`,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
// Assert
expect(response.body.data.createOneField.isLabelSyncedWithName).toBe(
false,
);
expect(data.createOneField.isLabelSyncedWithName).toBe(false);
});
it('should return an error when name and label are not synced but isLabelSyncedWithName is true', async () => {
@ -79,25 +85,24 @@ describe('createOne', () => {
name: 'testField',
label: 'Different Label',
type: FieldMetadataType.TEXT,
objectMetadataId: listingObjectId,
objectMetadataId: createdObjectMetadataId,
isLabelSyncedWithName: true,
};
const graphqlOperation = createOneFieldMetadataFactory({
input: { field: createFieldInput },
// Act
const { errors } = await createOneFieldMetadata({
input: createFieldInput,
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
`,
expectToFail: true,
});
// Act
const response = await makeMetadataAPIRequest(graphqlOperation);
// Assert
expect(response.body.errors[0].message).toBe(
expect(errors[0].message).toBe(
'Name is not synced with label. Expected name: "differentLabel", got testField',
);
});

View File

@ -1,12 +1,16 @@
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { createCustomTextFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-custom-text-field-metadata.util';
import { deleteOneFieldMetadataItemFactory } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata-factory.util';
import { updateOneFieldMetadataFactory } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata-factory.util';
import { createListingCustomObject } from 'test/integration/metadata/suites/object-metadata/utils/create-test-object-metadata.util';
import { deleteOneObjectMetadataItem } 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 { 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 {
LISTING_NAME_PLURAL,
LISTING_NAME_SINGULAR,
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
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 { FieldMetadataType } from 'twenty-shared/types';
describe('deleteOne', () => {
describe('Kanban aggregate operation', () => {
@ -15,14 +19,27 @@ describe('deleteOne', () => {
let viewId = '';
beforeEach(async () => {
const { objectMetadataId: createdObjectId } =
await createListingCustomObject();
const { data } = await createOneObjectMetadata({
input: {
nameSingular: LISTING_NAME_SINGULAR,
namePlural: LISTING_NAME_PLURAL,
labelSingular: 'Listing',
labelPlural: 'Listings',
icon: 'IconBuildingSkyscraper',
},
});
listingObjectId = createdObjectId;
const { fieldMetadataId: createdFieldMetadaId } =
await createCustomTextFieldMetadata(createdObjectId);
listingObjectId = data.createOneObject.id;
const { data: createdFieldData } = await createOneFieldMetadata({
input: {
name: 'house',
type: FieldMetadataType.TEXT,
label: 'House',
objectMetadataId: listingObjectId,
},
});
testFieldId = createdFieldMetadaId;
testFieldId = createdFieldData.createOneField.id;
// create view
const graphqlOperation = createOneOperationFactory({
@ -49,7 +66,9 @@ describe('deleteOne', () => {
viewId = createdView.id;
});
afterEach(async () => {
await deleteOneObjectMetadataItem(listingObjectId);
await deleteOneObjectMetadata({
input: { idToDelete: listingObjectId },
});
});
it('should reset kanban aggregate operation when deleting a field used as kanbanAggregateOperationFieldMetadataId', async () => {
// Arrange
@ -76,25 +95,25 @@ describe('deleteOne', () => {
expect(viewResponse.body.data.view.kanbanAggregateOperation).toBe('MAX');
// Deactivate field to be able to delete it after
const deactivateFieldOperation = updateOneFieldMetadataFactory({
input: { id: testFieldId, update: { isActive: false } },
await updateOneFieldMetadata({
input: {
idToUpdate: testFieldId,
updatePayload: { isActive: false },
},
gqlFields: `
id
isActive
`,
});
await makeMetadataAPIRequest(deactivateFieldOperation);
// Act
const graphqlOperation = deleteOneFieldMetadataItemFactory({
idToDelete: testFieldId,
const { data } = await deleteOneFieldMetadata({
input: { idToDelete: testFieldId },
});
const response = await makeMetadataAPIRequest(graphqlOperation);
// Assert
// 1. Field is deleted
expect(response.body.data.deleteOneField.id).toBe(testFieldId);
expect(data.deleteOneField.id).toBe(testFieldId);
// 2. Kanban aggregate operation has been reset on view using this field as kanbanAggregateOperationFieldMetadataId
const updatedViewResponse =

View File

@ -1,8 +1,12 @@
import { createCustomTextFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-custom-text-field-metadata.util';
import { updateOneFieldMetadataFactory } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata-factory.util';
import { createListingCustomObject } from 'test/integration/metadata/suites/object-metadata/utils/create-test-object-metadata.util';
import { deleteOneObjectMetadataItem } 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 { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
import {
LISTING_NAME_PLURAL,
LISTING_NAME_SINGULAR,
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
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 { FieldMetadataType } from 'twenty-shared/types';
describe('updateOne', () => {
describe('FieldMetadataService name/label sync', () => {
@ -10,18 +14,35 @@ describe('updateOne', () => {
let testFieldId = '';
beforeEach(async () => {
const { objectMetadataId: createdObjectId } =
await createListingCustomObject();
const { data } = await createOneObjectMetadata({
input: {
labelSingular: LISTING_NAME_SINGULAR,
labelPlural: LISTING_NAME_PLURAL,
nameSingular: LISTING_NAME_SINGULAR,
namePlural: LISTING_NAME_PLURAL,
icon: 'IconBuildingSkyscraper',
isLabelSyncedWithName: true,
},
});
listingObjectId = createdObjectId;
listingObjectId = data.createOneObject.id;
const { fieldMetadataId: createdFieldMetadaId } =
await createCustomTextFieldMetadata(createdObjectId);
const { data: createdFieldMetadata } = await createOneFieldMetadata({
input: {
objectMetadataId: listingObjectId,
type: FieldMetadataType.TEXT,
name: 'testName',
label: 'Test name',
isLabelSyncedWithName: true,
},
});
testFieldId = createdFieldMetadaId;
testFieldId = createdFieldMetadata.createOneField.id;
});
afterEach(async () => {
await deleteOneObjectMetadataItem(listingObjectId);
await deleteOneObjectMetadata({
input: { idToDelete: listingObjectId },
});
});
it('should update a field name and label when they are synced correctly', async () => {
@ -29,11 +50,12 @@ describe('updateOne', () => {
const updateFieldInput = {
name: 'newName',
label: 'New name',
isLabelSyncedWithName: true,
};
// Act
const graphqlOperation = updateOneFieldMetadataFactory({
input: { id: testFieldId, update: updateFieldInput },
const { data } = await updateOneFieldMetadata({
input: { idToUpdate: testFieldId, updatePayload: updateFieldInput },
gqlFields: `
id
name
@ -42,10 +64,8 @@ describe('updateOne', () => {
`,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
// Assert
expect(response.body.data.updateOneField.name).toBe('newName');
expect(data.updateOneField.name).toBe('newName');
});
it('should update a field name and label when they are not synced correctly and labelSync is false', async () => {
@ -57,8 +77,8 @@ describe('updateOne', () => {
};
// Act
const graphqlOperation = updateOneFieldMetadataFactory({
input: { id: testFieldId, update: updateFieldInput },
const { data } = await updateOneFieldMetadata({
input: { idToUpdate: testFieldId, updatePayload: updateFieldInput },
gqlFields: `
id
name
@ -67,33 +87,31 @@ describe('updateOne', () => {
`,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
// Assert
expect(response.body.data.updateOneField.name).toBe('differentName');
expect(data.updateOneField.name).toBe('differentName');
});
it('should not update a field name if it is not synced correctly with label and labelSync is true', async () => {
// Arrange
const updateFieldInput = {
name: 'newName',
isLabelSyncedWithName: true,
};
// Act
const graphqlOperation = updateOneFieldMetadataFactory({
input: { id: testFieldId, update: updateFieldInput },
const { errors } = await updateOneFieldMetadata({
input: { idToUpdate: testFieldId, updatePayload: updateFieldInput },
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
expectToFail: true,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
// Assert
expect(response.body.errors[0].message).toBe(
expect(errors[0].message).toBe(
'Name is not synced with label. Expected name: "testName", got newName',
);
});

View File

@ -1,30 +0,0 @@
import { createOneFieldMetadataFactory } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { FieldMetadataType } from 'twenty-shared/types';
const FIELD_NAME = 'testName';
export const createCustomTextFieldMetadata = async (
objectMetadataItemId: string,
) => {
const createFieldInput = {
name: FIELD_NAME,
label: 'Test name',
type: FieldMetadataType.TEXT,
objectMetadataId: objectMetadataItemId,
isLabelSyncedWithName: true,
};
const graphqlOperation = createOneFieldMetadataFactory({
input: { field: createFieldInput },
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
return { fieldMetadataId: response.body.data.createOneField.id };
};

View File

@ -1,24 +0,0 @@
import gql from 'graphql-tag';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
type CreateOneFieldFactoryParams = {
gqlFields: string;
input?: { field: Omit<CreateFieldInput, 'workspaceId' | 'dataSourceId'> };
};
export const createOneFieldMetadataFactory = ({
gqlFields,
input,
}: CreateOneFieldFactoryParams) => ({
query: gql`
mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {
createOneField(input: $input) {
${gqlFields}
}
}
`,
variables: {
input,
},
});

View File

@ -0,0 +1,25 @@
import gql from 'graphql-tag';
import { PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
export type CreateOneFieldFactoryInput = Omit<
CreateFieldInput,
'workspaceId' | 'dataSourceId'
>;
export const createOneFieldMetadataQueryFactory = ({
input,
gqlFields = 'id',
}: PerformMetadataQueryParams<CreateOneFieldFactoryInput>) => ({
query: gql`
mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {
createOneField(input: $input) {
${gqlFields}
}
}
`,
variables: {
input: { field: input },
},
});

View File

@ -0,0 +1,29 @@
import {
CreateOneFieldFactoryInput,
createOneFieldMetadataQueryFactory,
} from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
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';
export const createOneFieldMetadata = async ({
input,
gqlFields,
expectToFail = false,
}: PerformMetadataQueryParams<CreateOneFieldFactoryInput>) => {
const graphqlOperation = createOneFieldMetadataQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
if (expectToFail) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Field Metadata creation should have failed but did not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@ -1,20 +0,0 @@
import gql from 'graphql-tag';
type DeleteOneFieldFactoryParams = {
idToDelete: string;
};
export const deleteOneFieldMetadataItemFactory = ({
idToDelete,
}: DeleteOneFieldFactoryParams) => ({
query: gql`
mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {
deleteOneField(input: { id: $idToDelete }) {
id
}
}
`,
variables: {
idToDelete,
},
});

View File

@ -0,0 +1,22 @@
import gql from 'graphql-tag';
import { PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
export type DeleteOneFieldFactoryInput = {
idToDelete: string;
};
export const deleteOneFieldMetadataQueryFactory = ({
input,
gqlFields = 'id',
}: PerformMetadataQueryParams<DeleteOneFieldFactoryInput>) => ({
query: gql`
mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {
deleteOneField(input: { id: $idToDelete }) {
${gqlFields}
}
}
`,
variables: {
idToDelete: input.idToDelete,
},
});

View File

@ -1,10 +1,29 @@
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { deleteOneFieldMetadataItemFactory } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata-factory.util';
import {
DeleteOneFieldFactoryInput,
deleteOneFieldMetadataQueryFactory,
} from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata-query-factory.util';
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';
export const deleteFieldMetadata = async (fieldMetadataId: string) => {
const graphqlOperation = deleteOneFieldMetadataItemFactory({
idToDelete: fieldMetadataId,
export const deleteOneFieldMetadata = async ({
input,
gqlFields,
expectToFail = false,
}: PerformMetadataQueryParams<DeleteOneFieldFactoryInput>) => {
const graphqlOperation = deleteOneFieldMetadataQueryFactory({
input,
gqlFields,
});
await makeGraphqlAPIRequest(graphqlOperation);
const response = await makeGraphqlAPIRequest(graphqlOperation);
if (expectToFail) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Field Metadata deletion should have failed but did not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@ -1,17 +1,15 @@
import gql from 'graphql-tag';
import { PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
type FieldsFactoryParams = {
gqlFields: string;
input: {
filter: object;
paging: object;
};
export type FindManyFieldsMetadataFactoryInput = {
filter: object;
paging: object;
};
export const fieldsMetadataFactory = ({
gqlFields,
export const findManyFieldsMetadataQueryFactory = ({
gqlFields = 'id',
input,
}: FieldsFactoryParams) => ({
}: PerformMetadataQueryParams<FindManyFieldsMetadataFactoryInput>) => ({
query: gql`
query FieldsMetadata($filter: FieldFilter!, $paging: CursorPaging!) {
fields(filter: $filter, paging: $paging) {

View File

@ -0,0 +1,29 @@
import {
FindManyFieldsMetadataFactoryInput,
findManyFieldsMetadataQueryFactory,
} from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
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';
export const findManyFieldsMetadata = async ({
input,
gqlFields,
expectToFail = false,
}: PerformMetadataQueryParams<FindManyFieldsMetadataFactoryInput>) => {
const graphqlOperation = findManyFieldsMetadataQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
if (expectToFail) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Field Metadata retrieval should have failed but did not',
});
}
return response.body.data.fields.edges.map((edge) => edge.node);
};

View File

@ -1,25 +0,0 @@
import gql from 'graphql-tag';
import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
type UpdateOneFieldFactoryParams = {
gqlFields: string;
input: { id: string; update: Omit<UpdateFieldInput, 'workspaceId' | 'id'> };
};
export const updateOneFieldMetadataFactory = ({
gqlFields,
input,
}: UpdateOneFieldFactoryParams) => ({
query: gql`
mutation UpdateOneFieldMetadataItem($idToUpdate: UUID!, $updatePayload: UpdateFieldInput!) {
updateOneField(input: {id: $idToUpdate, update: $updatePayload}) {
${gqlFields}
}
}
`,
variables: {
idToUpdate: input.id,
updatePayload: input.update,
},
});

View File

@ -0,0 +1,26 @@
import gql from 'graphql-tag';
import { PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
export type UpdateOneFieldFactoryInput = {
idToUpdate: string;
updatePayload: Omit<UpdateFieldInput, 'workspaceId' | 'id'>;
};
export const updateOneFieldMetadataQueryFactory = ({
gqlFields = 'id',
input,
}: PerformMetadataQueryParams<UpdateOneFieldFactoryInput>) => ({
query: gql`
mutation UpdateOneFieldMetadataItem($idToUpdate: UUID!, $updatePayload: UpdateFieldInput!) {
updateOneField(input: {id: $idToUpdate, update: $updatePayload}) {
${gqlFields}
}
}
`,
variables: {
idToUpdate: input.idToUpdate,
updatePayload: input.updatePayload,
},
});

View File

@ -0,0 +1,29 @@
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import {
UpdateOneFieldFactoryInput,
updateOneFieldMetadataQueryFactory,
} from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata-query-factory.util';
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';
export const updateOneFieldMetadata = async ({
input,
gqlFields,
expectToFail = false,
}: PerformMetadataQueryParams<UpdateOneFieldFactoryInput>) => {
const graphqlOperation = updateOneFieldMetadataQueryFactory({
input,
gqlFields,
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
if (expectToFail) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Field Metadata update should have failed but did not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@ -1,2 +1,2 @@
export const LISTING_NAME_SINGULAR = 'listing';
export const LISTING_NAME_PLURAL = 'listings';
export const LISTING_NAME_SINGULAR = 'listinga';
export const LISTING_NAME_PLURAL = 'listingas';

View File

@ -1,5 +1,5 @@
import { getMockCreateObjectInput } from 'test/integration/utils/object-metadata/generate-mock-create-object-metadata-input';
import { performFailingObjectMetadataCreation } from 'test/integration/utils/object-metadata/perform-failing-object-metadata-creation';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-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 { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@ -124,9 +124,10 @@ const allTestsUseCases = [
describe('Object metadata creation should fail', () => {
it.each(allTestsUseCases)('$title', async ({ context }) => {
const errors = await performFailingObjectMetadataCreation(
getMockCreateObjectInput(context),
);
const { errors } = await createOneObjectMetadata({
input: getMockCreateObjectInput(context),
expectToFail: true,
});
expect(errors.length).toBe(1);
const firstError = errors[0];

View File

@ -1,11 +1,10 @@
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { createOneObjectMetadataFactory } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-factory.util';
import { deleteOneObjectMetadataItemFactory } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata-factory.util';
import { objectsMetadataFactory } from 'test/integration/metadata/suites/object-metadata/utils/objects-metadata-factory.util';
import { updateOneObjectMetadataItemFactory } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata-factory.util';
import { findManyFieldsMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { findManyObjectMetadataQueryFactory } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-query-factory.util';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { createOneRelationMetadataFactory } from 'test/integration/metadata/suites/utils/create-one-relation-metadata-factory.util';
import { deleteOneRelationMetadataItemFactory } from 'test/integration/metadata/suites/utils/delete-one-relation-metadata-factory.util';
import { fieldsMetadataFactory } from 'test/integration/metadata/suites/utils/fields-metadata-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { FieldMetadataType } from 'twenty-shared/types';
@ -37,7 +36,7 @@ describe('Custom object renaming', () => {
{},
);
const standardObjectsGraphqlOperation = objectsMetadataFactory({
const standardObjectsGraphqlOperation = findManyObjectMetadataQueryFactory({
gqlFields: `
id
nameSingular
@ -50,7 +49,7 @@ describe('Custom object renaming', () => {
},
});
const fieldsGraphqlOperation = fieldsMetadataFactory({
const fieldsGraphqlOperation = findManyFieldsMetadataQueryFactory({
gqlFields: `
id
name
@ -94,22 +93,18 @@ describe('Custom object renaming', () => {
};
// Act
const graphqlOperation = createOneObjectMetadataFactory({
input: { object: LISTING_OBJECT },
const { data } = await createOneObjectMetadata({
input: LISTING_OBJECT,
gqlFields: `
id
nameSingular
`,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
// Assert
expect(response.body.data.createOneObject.nameSingular).toBe(
LISTING_NAME_SINGULAR,
);
expect(data.createOneObject.nameSingular).toBe(LISTING_NAME_SINGULAR);
listingObjectId = response.body.data.createOneObject.id;
listingObjectId = data.createOneObject.id;
const fields = await makeMetadataAPIRequest(fieldsGraphqlOperation);
@ -206,43 +201,31 @@ describe('Custom object renaming', () => {
const HOUSE_NAME_PLURAL = 'houses';
const HOUSE_LABEL_SINGULAR = 'House';
const HOUSE_LABEL_PLURAL = 'Houses';
const updateListingNameGraphqlOperation =
updateOneObjectMetadataItemFactory({
gqlFields: `
// Act
const { data } = await updateOneObjectMetadata({
gqlFields: `
nameSingular
labelSingular
namePlural
labelPlural
`,
input: {
idToUpdate: listingObjectId,
updatePayload: {
nameSingular: HOUSE_NAME_SINGULAR,
namePlural: HOUSE_NAME_PLURAL,
labelSingular: HOUSE_LABEL_SINGULAR,
labelPlural: HOUSE_LABEL_PLURAL,
},
input: {
idToUpdate: listingObjectId,
updatePayload: {
nameSingular: HOUSE_NAME_SINGULAR,
namePlural: HOUSE_NAME_PLURAL,
labelSingular: HOUSE_LABEL_SINGULAR,
labelPlural: HOUSE_LABEL_PLURAL,
},
});
// Act
const updateListingNameResponse = await makeMetadataAPIRequest(
updateListingNameGraphqlOperation,
);
},
});
// Assert
expect(
updateListingNameResponse.body.data.updateOneObject.nameSingular,
).toBe(HOUSE_NAME_SINGULAR);
expect(updateListingNameResponse.body.data.updateOneObject.namePlural).toBe(
HOUSE_NAME_PLURAL,
);
expect(
updateListingNameResponse.body.data.updateOneObject.labelSingular,
).toBe(HOUSE_LABEL_SINGULAR);
expect(
updateListingNameResponse.body.data.updateOneObject.labelPlural,
).toBe(HOUSE_LABEL_PLURAL);
expect(data.updateOneObject.nameSingular).toBe(HOUSE_NAME_SINGULAR);
expect(data.updateOneObject.namePlural).toBe(HOUSE_NAME_PLURAL);
expect(data.updateOneObject.labelSingular).toBe(HOUSE_LABEL_SINGULAR);
expect(data.updateOneObject.labelPlural).toBe(HOUSE_LABEL_PLURAL);
const fieldsResponse = await makeMetadataAPIRequest(fieldsGraphqlOperation);
@ -306,14 +289,12 @@ describe('Custom object renaming', () => {
});
it('5. should delete custom object', async () => {
const graphqlOperation = deleteOneObjectMetadataItemFactory({
idToDelete: listingObjectId,
const { data } = await deleteOneObjectMetadata({
input: {
idToDelete: listingObjectId,
},
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
const deleteListingResponse = response.body.data.deleteOneObject;
expect(deleteListingResponse.id).toBe(listingObjectId);
expect(data.deleteOneObject.id).toBe(listingObjectId);
});
});

View File

@ -1,6 +1,6 @@
import { deleteOneObjectMetadataItem } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { getMockCreateObjectInput } from 'test/integration/utils/object-metadata/generate-mock-create-object-metadata-input';
import { performObjectMetadataCreation } from 'test/integration/utils/object-metadata/perform-object-metadata-creation';
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 { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
@ -26,13 +26,13 @@ const allTestsUseCases = [...successfulObjectMetadataItemCreateOneUseCase];
describe('Object metadata creation should succeed', () => {
it.each(allTestsUseCases)('$title', async ({ context }) => {
const response = await performObjectMetadataCreation(
getMockCreateObjectInput(context),
);
const { data } = await createOneObjectMetadata({
input: getMockCreateObjectInput(context),
});
expect(response.body.data.createOneObject.id).toBeDefined();
await deleteOneObjectMetadataItem(
response.body.data.createOneObject.id,
).catch();
expect(data.createOneObject.id).toBeDefined();
await deleteOneObjectMetadata({
input: { idToDelete: data.createOneObject.id },
}).catch();
});
});

View File

@ -1,24 +0,0 @@
import gql from 'graphql-tag';
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
type CreateOneObjectFactoryParams = {
gqlFields: string;
input?: { object: Omit<CreateObjectInput, 'workspaceId' | 'dataSourceId'> };
};
export const createOneObjectMetadataFactory = ({
gqlFields,
input,
}: CreateOneObjectFactoryParams) => ({
query: gql`
mutation CreateOneObjectMetadataItem($input: CreateOneObjectInput!) {
createOneObject(input: $input) {
${gqlFields}
}
}
`,
variables: {
input,
},
});

View File

@ -0,0 +1,25 @@
import gql from 'graphql-tag';
import { PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
export type CreateOneObjectFactoryInput = Omit<
CreateObjectInput,
'workspaceId' | 'dataSourceId'
>;
export const createOneObjectMetadataQueryFactory = ({
input,
gqlFields = 'id',
}: PerformMetadataQueryParams<CreateOneObjectFactoryInput>) => ({
query: gql`
mutation CreateOneObjectMetadataItem($input: CreateOneObjectInput!) {
createOneObject(input: $input) {
${gqlFields}
}
}
`,
variables: {
input: { object: input },
},
});

View File

@ -0,0 +1,29 @@
import {
CreateOneObjectFactoryInput,
createOneObjectMetadataQueryFactory,
} from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
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';
export const createOneObjectMetadata = async ({
input,
gqlFields,
expectToFail = false,
}: PerformMetadataQueryParams<CreateOneObjectFactoryInput>) => {
const graphqlOperation = createOneObjectMetadataQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
if (expectToFail) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Object Metadata creation should have failed but did not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@ -1,32 +0,0 @@
import {
LISTING_NAME_PLURAL,
LISTING_NAME_SINGULAR,
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
import { createOneObjectMetadataFactory } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
const LISTING_OBJECT = {
namePlural: LISTING_NAME_PLURAL,
nameSingular: LISTING_NAME_SINGULAR,
labelPlural: 'Listings',
labelSingular: 'Listing',
description: 'Listing object',
icon: 'IconListNumbers',
isLabelSyncedWithName: false,
};
export const createListingCustomObject = async () => {
const createObjectOperation = createOneObjectMetadataFactory({
input: { object: LISTING_OBJECT },
gqlFields: `
id
nameSingular
`,
});
const response = await makeMetadataAPIRequest(createObjectOperation);
return {
objectMetadataId: response.body.data.createOneObject.id,
};
};

View File

@ -1,20 +0,0 @@
import gql from 'graphql-tag';
type DeleteOneObjectFactoryParams = {
idToDelete: string;
};
export const deleteOneObjectMetadataItemFactory = ({
idToDelete,
}: DeleteOneObjectFactoryParams) => ({
query: gql`
mutation DeleteOneObjectMetadataItem($idToDelete: UUID!) {
deleteOneObject(input: { id: $idToDelete }) {
id
}
}
`,
variables: {
idToDelete,
},
});

View File

@ -0,0 +1,22 @@
import gql from 'graphql-tag';
import { PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
export type DeleteOneObjectFactoryInput = {
idToDelete: string;
};
export const deleteOneObjectMetadataQueryFactory = ({
input,
gqlFields = 'id',
}: PerformMetadataQueryParams<DeleteOneObjectFactoryInput>) => ({
query: gql`
mutation DeleteOneObjectMetadataItem($idToDelete: UUID!) {
deleteOneObject(input: { id: $idToDelete }) {
${gqlFields}
}
}
`,
variables: {
idToDelete: input.idToDelete,
},
});

View File

@ -1,12 +1,29 @@
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { deleteOneObjectMetadataItemFactory } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata-factory.util';
import {
DeleteOneObjectFactoryInput,
deleteOneObjectMetadataQueryFactory,
} from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata-query-factory.util';
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';
export const deleteOneObjectMetadataItem = async (
objectMetadataItemId: string,
) => {
const graphqlOperation = deleteOneObjectMetadataItemFactory({
idToDelete: objectMetadataItemId,
export const deleteOneObjectMetadata = async ({
input,
gqlFields,
expectToFail = false,
}: PerformMetadataQueryParams<DeleteOneObjectFactoryInput>) => {
const graphqlOperation = deleteOneObjectMetadataQueryFactory({
input,
gqlFields,
});
await makeGraphqlAPIRequest(graphqlOperation);
const response = await makeGraphqlAPIRequest(graphqlOperation);
if (expectToFail) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Object Metadata deletion should have failed but did not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@ -1,17 +1,15 @@
import gql from 'graphql-tag';
import { PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
type ObjectsFactoryParams = {
gqlFields: string;
input: {
filter: object;
paging: object;
};
export type FindManyObjectMetadataFactoryInput = {
filter: object;
paging: object;
};
export const objectsMetadataFactory = ({
gqlFields,
export const findManyObjectMetadataQueryFactory = ({
gqlFields = 'id',
input,
}: ObjectsFactoryParams) => ({
}: PerformMetadataQueryParams<FindManyObjectMetadataFactoryInput>) => ({
query: gql`
query ObjectsMetadata($filter: ObjectFilter!, $paging: CursorPaging!) {
objects(filter: $filter, paging: $paging) {

View File

@ -0,0 +1,29 @@
import {
FindManyObjectMetadataFactoryInput,
findManyObjectMetadataQueryFactory,
} from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
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';
export const findManyObjectMetadata = async ({
input,
gqlFields,
expectToFail = false,
}: PerformMetadataQueryParams<FindManyObjectMetadataFactoryInput>) => {
const graphqlOperation = findManyObjectMetadataQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
if (expectToFail) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Object Metadata retrieval should have failed but did not',
});
}
return response.body.data.objects.edges.map((edge) => edge.node);
};

View File

@ -1,28 +0,0 @@
import gql from 'graphql-tag';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
export const findManyObjectsMetadataItems = async () => {
const query = {
query: gql`
query ObjectMetadataItems {
objects(paging: { first: 1000 }) {
edges {
node {
id
nameSingular
namePlural
}
}
}
}
`,
};
const response = await makeMetadataAPIRequest(query);
return response.body.data.objects.edges.map((edge) => edge.node) as {
id: string;
nameSingular: string;
namePlural: string;
}[];
};

View File

@ -4,8 +4,8 @@ import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/d
export const getMockCreateObjectInput = (
overrides?: Partial<Omit<CreateObjectInput, 'workspaceId' | 'dataSourceId'>>,
) => ({
namePlural: 'listings',
nameSingular: 'listing',
namePlural: 'listingas',
nameSingular: 'listinga',
labelPlural: 'Listings',
labelSingular: 'Listing',
description: 'Listing object',

View File

@ -1,28 +0,0 @@
import gql from 'graphql-tag';
import { UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
type UpdateOneObjectFactoryParams = {
gqlFields: string;
input: {
idToUpdate: string;
updatePayload: UpdateObjectPayload;
};
};
export const updateOneObjectMetadataItemFactory = ({
gqlFields,
input,
}: UpdateOneObjectFactoryParams) => ({
query: gql`
mutation UpdateOneObjectMetadataItem($idToUpdate: UUID!, $updatePayload: UpdateObjectPayload!) {
updateOneObject(input: {id: $idToUpdate, update: $updatePayload}) {
${gqlFields}
}
}
`,
variables: {
idToUpdate: input.idToUpdate,
updatePayload: input.updatePayload,
},
});

View File

@ -0,0 +1,26 @@
import gql from 'graphql-tag';
import { PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
export type UpdateOneObjectFactoryInput = {
idToUpdate: string;
updatePayload: UpdateObjectPayload;
};
export const updateOneObjectMetadataQueryFactory = ({
gqlFields = 'id',
input,
}: PerformMetadataQueryParams<UpdateOneObjectFactoryInput>) => ({
query: gql`
mutation UpdateOneObjectMetadataItem($idToUpdate: UUID!, $updatePayload: UpdateObjectPayload!) {
updateOneObject(input: {id: $idToUpdate, update: $updatePayload}) {
${gqlFields}
}
}
`,
variables: {
idToUpdate: input.idToUpdate,
updatePayload: input.updatePayload,
},
});

View File

@ -0,0 +1,29 @@
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import {
UpdateOneObjectFactoryInput,
updateOneObjectMetadataQueryFactory,
} from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata-query-factory.util';
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';
export const updateOneObjectMetadata = async ({
input,
gqlFields,
expectToFail = false,
}: PerformMetadataQueryParams<UpdateOneObjectFactoryInput>) => {
const graphqlOperation = updateOneObjectMetadataQueryFactory({
input,
gqlFields,
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
if (expectToFail) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Object Metadata update should have failed but did not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@ -0,0 +1,5 @@
export type PerformMetadataQueryParams<T> = {
input: T;
gqlFields?: string;
expectToFail?: boolean;
};

View File

@ -0,0 +1,17 @@
import { isDefined } from 'class-validator';
import { Response } from 'supertest';
type WarnIfNoErrorButExpectedToFailInput = {
response: Response;
errorMessage: string;
};
export const warnIfNoErrorButExpectedToFail = ({
response,
errorMessage,
}: WarnIfNoErrorButExpectedToFailInput) => {
if (isDefined(response.body.data)) {
expect(false).toEqual(errorMessage);
}
expect(response.body.errors.length).toBeGreaterThan(0);
};

View File

@ -1,28 +0,0 @@
import { deleteOneObjectMetadataItem } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { performObjectMetadataCreation } from 'test/integration/utils/object-metadata/perform-object-metadata-creation';
import { isDefined } from 'twenty-shared/utils';
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
export const performFailingObjectMetadataCreation = async (
objectInput: Omit<CreateObjectInput, 'workspaceId' | 'dataSourceId'>,
) => {
const response = await performObjectMetadataCreation(objectInput);
if (isDefined(response.body.data)) {
try {
const createdId = response.body.data.createOneObject.id;
await deleteOneObjectMetadataItem(createdId);
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
}
expect(false).toEqual(
'Object Metadata Item should have failed but did not',
);
}
expect(response.body.errors.length).toBeGreaterThan(0);
return response.body.errors;
};

View File

@ -1,18 +0,0 @@
import { createOneObjectMetadataFactory } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
export const performObjectMetadataCreation = async (
args: Omit<CreateObjectInput, 'workspaceId' | 'dataSourceId'>,
) => {
const graphqlOperation = createOneObjectMetadataFactory({
input: { object: args },
gqlFields: `
id
nameSingular
`,
});
return await makeMetadataAPIRequest(graphqlOperation);
};