Add manifest integration tests (#18203)
- add integration test on sync manifest endpoint - fix invalid standard uuid + migration command
This commit is contained in:
+79
-74
@@ -79,80 +79,6 @@ exports[`Sync application should fail due to object system fields integrity shou
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Sync application should fail due to object system fields integrity when object has createdAt field with wrong type (TEXT instead of DATE_TIME) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"fieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Field to create related object not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "title",
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "fieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Field to create related object not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "createdAt",
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "fieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_OBJECT_INPUT",
|
||||
"message": "Names are not synced with labels",
|
||||
"userFriendlyMessage": "Names are not synced with labels",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"namePlural": "wrongCreatedAtObjects",
|
||||
"nameSingular": "wrongCreatedAtObject",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "objectMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 2 fieldMetadatas, 1 objectMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 2,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 3,
|
||||
},
|
||||
"userFriendlyMessage": "Metadata validation failed",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Sync application should fail due to object system fields integrity when object has id field with wrong type (TEXT instead of UUID) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
@@ -389,3 +315,82 @@ exports[`Sync application should fail due to object system fields integrity when
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Sync application should fail due to object system fields integrity when object miss default fields 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"objectMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field id is missing",
|
||||
"userFriendlyMessage": "System field id is missing",
|
||||
"value": "id",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field createdAt is missing",
|
||||
"userFriendlyMessage": "System field createdAt is missing",
|
||||
"value": "createdAt",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field updatedAt is missing",
|
||||
"userFriendlyMessage": "System field updatedAt is missing",
|
||||
"value": "updatedAt",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field deletedAt is missing",
|
||||
"userFriendlyMessage": "System field deletedAt is missing",
|
||||
"value": "deletedAt",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field createdBy is missing",
|
||||
"userFriendlyMessage": "System field createdBy is missing",
|
||||
"value": "createdBy",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field updatedBy is missing",
|
||||
"userFriendlyMessage": "System field updatedBy is missing",
|
||||
"value": "updatedBy",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field position is missing",
|
||||
"userFriendlyMessage": "System field position is missing",
|
||||
"value": "position",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field searchVector is missing",
|
||||
"userFriendlyMessage": "System field searchVector is missing",
|
||||
"value": "searchVector",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"namePlural": "wrongCreatedAtObjects",
|
||||
"nameSingular": "wrongCreatedAtObject",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "objectMetadata",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 objectMetadata",
|
||||
"summary": {
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Metadata validation failed",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
+1
-98
@@ -78,7 +78,7 @@ exports[`syncApplication should return workspace migration actions on initial sy
|
||||
"isActive": true,
|
||||
"isAuditLogged": true,
|
||||
"isCustom": true,
|
||||
"isLabelSyncedWithName": true,
|
||||
"isLabelSyncedWithName": false,
|
||||
"isRemote": false,
|
||||
"isSearchable": false,
|
||||
"isSystem": false,
|
||||
@@ -364,81 +364,6 @@ exports[`syncApplication should return workspace migration actions on initial sy
|
||||
"metadataName": "role",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"applicationUniversalIdentifier": Any<String>,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`syncApplication should return workspace migration actions on initial sync then on second sync with field rename and new role 2`] = `
|
||||
{
|
||||
"syncApplication": {
|
||||
"actions": [
|
||||
{
|
||||
"metadataName": "fieldMetadata",
|
||||
"type": "update",
|
||||
"universalIdentifier": Any<String>,
|
||||
"update": {
|
||||
"label": "Body",
|
||||
"name": "body",
|
||||
},
|
||||
},
|
||||
{
|
||||
"flatEntity": {
|
||||
"applicationUniversalIdentifier": Any<String>,
|
||||
"canAccessAllTools": false,
|
||||
"canBeAssignedToAgents": true,
|
||||
"canBeAssignedToApiKeys": true,
|
||||
"canBeAssignedToUsers": true,
|
||||
"canDestroyAllObjectRecords": false,
|
||||
"canReadAllObjectRecords": false,
|
||||
"canSoftDeleteAllObjectRecords": false,
|
||||
"canUpdateAllObjectRecords": false,
|
||||
"canUpdateAllSettings": false,
|
||||
"createdAt": Any<String>,
|
||||
"description": "A read-only role",
|
||||
"icon": null,
|
||||
"isEditable": true,
|
||||
"label": "Viewer Role",
|
||||
"universalIdentifier": Any<String>,
|
||||
"updatedAt": Any<String>,
|
||||
},
|
||||
"metadataName": "role",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"applicationUniversalIdentifier": Any<String>,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`syncApplication should sync a skill then update it on second sync 1`] = `
|
||||
{
|
||||
"syncApplication": {
|
||||
"actions": [
|
||||
{
|
||||
"flatEntity": {
|
||||
"applicationUniversalIdentifier": Any<String>,
|
||||
"canAccessAllTools": false,
|
||||
"canBeAssignedToAgents": true,
|
||||
"canBeAssignedToApiKeys": true,
|
||||
"canBeAssignedToUsers": true,
|
||||
"canDestroyAllObjectRecords": false,
|
||||
"canReadAllObjectRecords": false,
|
||||
"canSoftDeleteAllObjectRecords": false,
|
||||
"canUpdateAllObjectRecords": false,
|
||||
"canUpdateAllSettings": false,
|
||||
"createdAt": Any<String>,
|
||||
"description": "A test role",
|
||||
"icon": null,
|
||||
"isEditable": true,
|
||||
"label": "Test Role",
|
||||
"universalIdentifier": Any<String>,
|
||||
"updatedAt": Any<String>,
|
||||
},
|
||||
"metadataName": "role",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"flatEntity": {
|
||||
"applicationUniversalIdentifier": Any<String>,
|
||||
@@ -463,25 +388,3 @@ This is a test skill.",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`syncApplication should sync a skill then update it on second sync 2`] = `
|
||||
{
|
||||
"syncApplication": {
|
||||
"actions": [
|
||||
{
|
||||
"metadataName": "skill",
|
||||
"type": "update",
|
||||
"universalIdentifier": Any<String>,
|
||||
"update": {
|
||||
"content": "# Test Skill
|
||||
|
||||
This is an updated test skill with more content.",
|
||||
"description": "An updated skill for testing",
|
||||
"label": "Test Skill Updated",
|
||||
},
|
||||
},
|
||||
],
|
||||
"applicationUniversalIdentifier": Any<String>,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
+16
-51
@@ -1,4 +1,5 @@
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { buildDefaultObjectManifest } from 'test/integration/metadata/suites/application/utils/build-default-object-manifest.util';
|
||||
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
@@ -20,37 +21,12 @@ type TestContext = {
|
||||
|
||||
type SyncApplicationTestingContext = EachTestingContext<TestContext>[];
|
||||
|
||||
const buildBaseManifest = (
|
||||
overrides: Pick<Manifest, 'objects' | 'fields'>,
|
||||
): Manifest => ({
|
||||
application: {
|
||||
apiClientChecksum: '',
|
||||
marketplaceData: undefined,
|
||||
universalIdentifier: TEST_APP_ID,
|
||||
defaultRoleUniversalIdentifier: TEST_ROLE_ID,
|
||||
displayName: 'Test System Fields App',
|
||||
description: 'App for testing system field validation',
|
||||
icon: 'IconTestPipe',
|
||||
applicationVariables: {},
|
||||
packageJsonChecksum: null,
|
||||
yarnLockChecksum: null,
|
||||
},
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_ID,
|
||||
label: 'Test Role',
|
||||
description: 'A test role',
|
||||
},
|
||||
],
|
||||
logicFunctions: [],
|
||||
frontComponents: [],
|
||||
publicAssets: [],
|
||||
views: [],
|
||||
navigationMenuItems: [],
|
||||
pageLayouts: [],
|
||||
skills: [],
|
||||
...overrides,
|
||||
});
|
||||
const buildManifest = (overrides: Pick<Manifest, 'objects' | 'fields'>) =>
|
||||
buildBaseManifest({
|
||||
appId: TEST_APP_ID,
|
||||
roleId: TEST_ROLE_ID,
|
||||
overrides,
|
||||
});
|
||||
|
||||
const buildObjectWithLabelField = ({
|
||||
nameSingular,
|
||||
@@ -104,7 +80,7 @@ const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext
|
||||
title:
|
||||
'when object is created without any system fields (missing all 8 system fields)',
|
||||
context: {
|
||||
manifest: buildBaseManifest(
|
||||
manifest: buildManifest(
|
||||
buildObjectWithLabelField({
|
||||
nameSingular: 'noSystemFieldsObject',
|
||||
namePlural: 'noSystemFieldsObjects',
|
||||
@@ -118,7 +94,7 @@ const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext
|
||||
{
|
||||
title: 'when object has id field with wrong type (TEXT instead of UUID)',
|
||||
context: {
|
||||
manifest: buildBaseManifest(
|
||||
manifest: buildManifest(
|
||||
buildObjectWithLabelField({
|
||||
nameSingular: 'wrongIdTypeObject',
|
||||
namePlural: 'wrongIdTypeObjects',
|
||||
@@ -140,26 +116,15 @@ const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when object has createdAt field with wrong type (TEXT instead of DATE_TIME)',
|
||||
title: 'when object miss default fields',
|
||||
context: {
|
||||
manifest: buildBaseManifest(
|
||||
manifest: buildManifest(
|
||||
buildObjectWithLabelField({
|
||||
nameSingular: 'wrongCreatedAtObject',
|
||||
namePlural: 'wrongCreatedAtObjects',
|
||||
labelSingular: 'Wrong CreatedAt Object',
|
||||
labelPlural: 'Wrong CreatedAt Objects',
|
||||
description: 'Object with wrong createdAt field type',
|
||||
additionalFields: [
|
||||
{
|
||||
universalIdentifier: uuidv4(),
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'createdAt',
|
||||
label: 'Created At',
|
||||
description: 'Created at field with wrong type',
|
||||
icon: 'IconCalendar',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
},
|
||||
@@ -168,7 +133,7 @@ const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext
|
||||
title:
|
||||
'when object has position field with wrong type (TEXT instead of POSITION)',
|
||||
context: {
|
||||
manifest: buildBaseManifest(
|
||||
manifest: buildManifest(
|
||||
buildObjectWithLabelField({
|
||||
nameSingular: 'wrongPositionObject',
|
||||
namePlural: 'wrongPositionObjects',
|
||||
@@ -240,7 +205,7 @@ describe('Sync application should fail due to object system fields integrity', (
|
||||
description: 'Object for testing system field deletion',
|
||||
});
|
||||
|
||||
const validManifest = buildBaseManifest({
|
||||
const validManifest = buildManifest({
|
||||
objects: [testObject],
|
||||
fields: [],
|
||||
});
|
||||
@@ -250,7 +215,7 @@ describe('Sync application should fail due to object system fields integrity', (
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const manifestWithDeletedIdField = buildBaseManifest({
|
||||
const manifestWithDeletedIdField = buildManifest({
|
||||
objects: [
|
||||
{
|
||||
...testObject,
|
||||
@@ -277,7 +242,7 @@ describe('Sync application should fail due to object system fields integrity', (
|
||||
description: 'Object for testing system field update',
|
||||
});
|
||||
|
||||
const validManifest = buildBaseManifest({
|
||||
const validManifest = buildManifest({
|
||||
objects: [testObject],
|
||||
fields: [],
|
||||
});
|
||||
@@ -287,7 +252,7 @@ describe('Sync application should fail due to object system fields integrity', (
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const manifestWithUpdatedIdField = buildBaseManifest({
|
||||
const manifestWithUpdatedIdField = buildManifest({
|
||||
objects: [
|
||||
{
|
||||
...testObject,
|
||||
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { buildDefaultObjectManifest } from 'test/integration/metadata/suites/application/utils/build-default-object-manifest.util';
|
||||
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { findManyObjectMetadataWithIndexes } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util';
|
||||
|
||||
const TEST_APP_ID = uuidv4();
|
||||
const TEST_ROLE_ID = uuidv4();
|
||||
const TEST_FIELD_ID = uuidv4();
|
||||
const TEST_SECOND_FIELD_ID = uuidv4();
|
||||
|
||||
const TEST_OBJECT = buildDefaultObjectManifest({
|
||||
nameSingular: 'ticket',
|
||||
namePlural: 'tickets',
|
||||
labelSingular: 'Ticket',
|
||||
labelPlural: 'Tickets',
|
||||
description: 'A support ticket',
|
||||
icon: 'IconTicket',
|
||||
});
|
||||
|
||||
const buildManifest = (overrides?: Partial<Pick<Manifest, 'fields'>>) =>
|
||||
buildBaseManifest({
|
||||
appId: TEST_APP_ID,
|
||||
roleId: TEST_ROLE_ID,
|
||||
overrides: { objects: [TEST_OBJECT], ...overrides },
|
||||
});
|
||||
|
||||
const findObjectFields = async () => {
|
||||
const objects = await findManyObjectMetadataWithIndexes({
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const object = objects.find(
|
||||
(o) => o.universalIdentifier === TEST_OBJECT.universalIdentifier,
|
||||
);
|
||||
|
||||
return object?.fieldsList ?? [];
|
||||
};
|
||||
|
||||
describe('Manifest update - fields', () => {
|
||||
beforeEach(async () => {
|
||||
await setupApplicationForSync({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
name: 'Test Application',
|
||||
description: 'App for testing field manifest updates',
|
||||
sourcePath: 'test-manifest-update-field',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
afterEach(async () => {
|
||||
await uninstallApplication({
|
||||
universalIdentifier: TEST_APP_ID,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a new field when added to manifest on second sync', async () => {
|
||||
await syncApplication({
|
||||
manifest: buildManifest({ fields: [] }),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const fieldsAfterFirstSync = await findObjectFields();
|
||||
|
||||
const descriptionBefore = fieldsAfterFirstSync.find(
|
||||
(field: { name: string }) => field.name === 'description',
|
||||
);
|
||||
|
||||
expect(descriptionBefore).toBeUndefined();
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: TEST_FIELD_ID,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
description: 'Ticket description',
|
||||
icon: 'IconFileDescription',
|
||||
objectUniversalIdentifier: TEST_OBJECT.universalIdentifier,
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const fieldsAfterSecondSync = await findObjectFields();
|
||||
const descriptionAfter = fieldsAfterSecondSync.find(
|
||||
(field: { name: string }) => field.name === 'description',
|
||||
);
|
||||
|
||||
expect(descriptionAfter).toBeDefined();
|
||||
expect(descriptionAfter).toMatchObject({
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
type: FieldMetadataType.TEXT,
|
||||
description: 'Ticket description',
|
||||
icon: 'IconFileDescription',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
it('should update a field when properties change in manifest on second sync', async () => {
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: TEST_FIELD_ID,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
description: 'Ticket description',
|
||||
icon: 'IconFileDescription',
|
||||
objectUniversalIdentifier: TEST_OBJECT.universalIdentifier,
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const fieldsAfterFirstSync = await findObjectFields();
|
||||
const fieldBefore = fieldsAfterFirstSync.find(
|
||||
(field: { name: string }) => field.name === 'description',
|
||||
);
|
||||
|
||||
expect(fieldBefore).toBeDefined();
|
||||
expect(fieldBefore).toMatchObject({
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
});
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: TEST_FIELD_ID,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'body',
|
||||
label: 'Body',
|
||||
description: 'Ticket body content',
|
||||
icon: 'IconFileDescription',
|
||||
objectUniversalIdentifier: TEST_OBJECT.universalIdentifier,
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const fieldsAfterSecondSync = await findObjectFields();
|
||||
const renamedField = fieldsAfterSecondSync.find(
|
||||
(field: { name: string }) => field.name === 'body',
|
||||
);
|
||||
|
||||
expect(renamedField).toBeDefined();
|
||||
expect(renamedField).toMatchObject({
|
||||
name: 'body',
|
||||
label: 'Body',
|
||||
type: FieldMetadataType.TEXT,
|
||||
description: 'Ticket body content',
|
||||
});
|
||||
|
||||
const oldField = fieldsAfterSecondSync.find(
|
||||
(field: { name: string }) => field.name === 'description',
|
||||
);
|
||||
|
||||
expect(oldField).toBeUndefined();
|
||||
}, 60000);
|
||||
|
||||
it('should delete a field when removed from manifest on second sync', async () => {
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: TEST_FIELD_ID,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
description: 'Ticket description',
|
||||
icon: 'IconFileDescription',
|
||||
objectUniversalIdentifier: TEST_OBJECT.universalIdentifier,
|
||||
},
|
||||
{
|
||||
universalIdentifier: TEST_SECOND_FIELD_ID,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'priority',
|
||||
label: 'Priority',
|
||||
description: 'Ticket priority',
|
||||
icon: 'IconFlag',
|
||||
objectUniversalIdentifier: TEST_OBJECT.universalIdentifier,
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const fieldsAfterFirstSync = await findObjectFields();
|
||||
|
||||
expect(
|
||||
fieldsAfterFirstSync.find(
|
||||
(field: { name: string }) => field.name === 'description',
|
||||
),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
fieldsAfterFirstSync.find(
|
||||
(field: { name: string }) => field.name === 'priority',
|
||||
),
|
||||
).toBeDefined();
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: TEST_FIELD_ID,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
description: 'Ticket description',
|
||||
icon: 'IconFileDescription',
|
||||
objectUniversalIdentifier: TEST_OBJECT.universalIdentifier,
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const fieldsAfterSecondSync = await findObjectFields();
|
||||
|
||||
expect(
|
||||
fieldsAfterSecondSync.find(
|
||||
(field: { name: string }) => field.name === 'description',
|
||||
),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
fieldsAfterSecondSync.find(
|
||||
(field: { name: string }) => field.name === 'priority',
|
||||
),
|
||||
).toBeUndefined();
|
||||
}, 60000);
|
||||
});
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { buildDefaultObjectManifest } from 'test/integration/metadata/suites/application/utils/build-default-object-manifest.util';
|
||||
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util';
|
||||
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const TEST_APP_ID = uuidv4();
|
||||
const TEST_ROLE_ID = uuidv4();
|
||||
|
||||
const buildManifest = (
|
||||
overrides?: Partial<Pick<Manifest, 'objects' | 'fields'>>,
|
||||
) => buildBaseManifest({ appId: TEST_APP_ID, roleId: TEST_ROLE_ID, overrides });
|
||||
|
||||
const OBJECT_GQL_FIELDS =
|
||||
'id nameSingular namePlural labelSingular labelPlural description icon isCustom isActive';
|
||||
|
||||
const findCustomObjects = async () => {
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
input: {
|
||||
filter: { isCustom: { is: true } },
|
||||
paging: { first: 100 },
|
||||
},
|
||||
gqlFields: OBJECT_GQL_FIELDS,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
return objects;
|
||||
};
|
||||
|
||||
describe('Manifest update - objects', () => {
|
||||
beforeEach(async () => {
|
||||
await setupApplicationForSync({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
name: 'Test Application',
|
||||
description: 'App for testing object manifest updates',
|
||||
sourcePath: 'test-manifest-update-object',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
afterEach(async () => {
|
||||
await uninstallApplication({
|
||||
universalIdentifier: TEST_APP_ID,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a new object when added to manifest on second sync', async () => {
|
||||
const ticketObject = buildDefaultObjectManifest({
|
||||
nameSingular: 'ticket',
|
||||
namePlural: 'tickets',
|
||||
labelSingular: 'Ticket',
|
||||
labelPlural: 'Tickets',
|
||||
description: 'A support ticket',
|
||||
icon: 'IconTicket',
|
||||
});
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({ objects: [ticketObject] }),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const objectsAfterFirstSync = await findCustomObjects();
|
||||
const ticket = objectsAfterFirstSync.find(
|
||||
(obj) => obj.nameSingular === 'ticket',
|
||||
);
|
||||
|
||||
expect(ticket).toBeDefined();
|
||||
expect(ticket).toMatchObject({
|
||||
nameSingular: 'ticket',
|
||||
labelSingular: 'Ticket',
|
||||
description: 'A support ticket',
|
||||
icon: 'IconTicket',
|
||||
});
|
||||
|
||||
const invoiceObject = buildDefaultObjectManifest({
|
||||
nameSingular: 'invoice',
|
||||
namePlural: 'invoices',
|
||||
labelSingular: 'Invoice',
|
||||
labelPlural: 'Invoices',
|
||||
description: 'A billing invoice',
|
||||
icon: 'IconFileInvoice',
|
||||
});
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
objects: [ticketObject, invoiceObject],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const objectsAfterSecondSync = await findCustomObjects();
|
||||
const ticketAfter = objectsAfterSecondSync.find(
|
||||
(obj) => obj.nameSingular === 'ticket',
|
||||
);
|
||||
const invoiceAfter = objectsAfterSecondSync.find(
|
||||
(obj) => obj.nameSingular === 'invoice',
|
||||
);
|
||||
|
||||
expect(ticketAfter).toBeDefined();
|
||||
expect(invoiceAfter).toBeDefined();
|
||||
expect(invoiceAfter).toMatchObject({
|
||||
nameSingular: 'invoice',
|
||||
namePlural: 'invoices',
|
||||
labelSingular: 'Invoice',
|
||||
labelPlural: 'Invoices',
|
||||
description: 'A billing invoice',
|
||||
icon: 'IconFileInvoice',
|
||||
isCustom: true,
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
it('should update object properties when changed in manifest on second sync', async () => {
|
||||
const ticketObject = buildDefaultObjectManifest({
|
||||
nameSingular: 'ticket',
|
||||
namePlural: 'tickets',
|
||||
labelSingular: 'Ticket',
|
||||
labelPlural: 'Tickets',
|
||||
description: 'A support ticket',
|
||||
icon: 'IconTicket',
|
||||
});
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({ objects: [ticketObject] }),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const objectsAfterFirstSync = await findCustomObjects();
|
||||
const ticketBefore = objectsAfterFirstSync.find(
|
||||
(obj) => obj.nameSingular === 'ticket',
|
||||
);
|
||||
|
||||
expect(ticketBefore).toMatchObject({
|
||||
labelSingular: 'Ticket',
|
||||
description: 'A support ticket',
|
||||
icon: 'IconTicket',
|
||||
});
|
||||
|
||||
const updatedTicketObject = {
|
||||
...ticketObject,
|
||||
labelSingular: 'Support Ticket',
|
||||
labelPlural: 'Support Tickets',
|
||||
description: 'A customer support ticket',
|
||||
icon: 'IconHeadset',
|
||||
};
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({ objects: [updatedTicketObject] }),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const objectsAfterSecondSync = await findCustomObjects();
|
||||
const ticketAfter = objectsAfterSecondSync.find(
|
||||
(obj) => obj.nameSingular === 'ticket',
|
||||
);
|
||||
|
||||
expect(ticketAfter).toBeDefined();
|
||||
expect(ticketAfter).toMatchObject({
|
||||
nameSingular: 'ticket',
|
||||
labelSingular: 'Support Ticket',
|
||||
labelPlural: 'Support Tickets',
|
||||
description: 'A customer support ticket',
|
||||
icon: 'IconHeadset',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
it('should delete an object when removed from manifest on second sync', async () => {
|
||||
const ticketObject = buildDefaultObjectManifest({
|
||||
nameSingular: 'ticket',
|
||||
namePlural: 'tickets',
|
||||
labelSingular: 'Ticket',
|
||||
labelPlural: 'Tickets',
|
||||
description: 'A support ticket',
|
||||
icon: 'IconTicket',
|
||||
});
|
||||
|
||||
const invoiceObject = buildDefaultObjectManifest({
|
||||
nameSingular: 'invoice',
|
||||
namePlural: 'invoices',
|
||||
labelSingular: 'Invoice',
|
||||
labelPlural: 'Invoices',
|
||||
description: 'A billing invoice',
|
||||
icon: 'IconFileInvoice',
|
||||
});
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
objects: [ticketObject, invoiceObject],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const objectsAfterFirstSync = await findCustomObjects();
|
||||
|
||||
expect(
|
||||
objectsAfterFirstSync.find((obj) => obj.nameSingular === 'ticket'),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
objectsAfterFirstSync.find((obj) => obj.nameSingular === 'invoice'),
|
||||
).toBeDefined();
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({ objects: [ticketObject] }),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const objectsAfterSecondSync = await findCustomObjects();
|
||||
|
||||
expect(
|
||||
objectsAfterSecondSync.find((obj) => obj.nameSingular === 'ticket'),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
objectsAfterSecondSync.find((obj) => obj.nameSingular === 'invoice'),
|
||||
).toBeUndefined();
|
||||
}, 60000);
|
||||
});
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util';
|
||||
import { findRoles } from 'test/integration/metadata/suites/role/utils/find-roles.util';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const TEST_APP_ID = uuidv4();
|
||||
const TEST_ROLE_ID = uuidv4();
|
||||
const TEST_SECOND_ROLE_ID = uuidv4();
|
||||
|
||||
const buildManifest = (overrides?: Partial<Pick<Manifest, 'roles'>>) =>
|
||||
buildBaseManifest({
|
||||
appId: TEST_APP_ID,
|
||||
roleId: TEST_ROLE_ID,
|
||||
overrides: {
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_ID,
|
||||
label: 'Editor',
|
||||
description: 'Can edit',
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
|
||||
const findAppRoles = async () => {
|
||||
const { data } = await findRoles({
|
||||
gqlFields: 'id label description universalIdentifier',
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
return data.getRoles.filter(
|
||||
(role) =>
|
||||
role.universalIdentifier === TEST_ROLE_ID ||
|
||||
role.universalIdentifier === TEST_SECOND_ROLE_ID,
|
||||
);
|
||||
};
|
||||
|
||||
describe('Manifest update - roles', () => {
|
||||
beforeEach(async () => {
|
||||
await setupApplicationForSync({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
name: 'Test Application',
|
||||
description: 'App for testing role manifest updates',
|
||||
sourcePath: 'test-manifest-update-role',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
afterEach(async () => {
|
||||
await uninstallApplication({
|
||||
universalIdentifier: TEST_APP_ID,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a new role when added to manifest on second sync', async () => {
|
||||
await syncApplication({
|
||||
manifest: buildManifest({}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const rolesAfterFirstSync = await findAppRoles();
|
||||
|
||||
expect(rolesAfterFirstSync).toHaveLength(1);
|
||||
expect(rolesAfterFirstSync[0]).toMatchObject({
|
||||
label: 'Editor',
|
||||
description: 'Can edit',
|
||||
});
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_ID,
|
||||
label: 'Editor',
|
||||
description: 'Can edit',
|
||||
},
|
||||
{
|
||||
universalIdentifier: TEST_SECOND_ROLE_ID,
|
||||
label: 'Viewer',
|
||||
description: 'Read-only access',
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const rolesAfterSecondSync = await findAppRoles();
|
||||
|
||||
expect(rolesAfterSecondSync).toHaveLength(2);
|
||||
expect(
|
||||
rolesAfterSecondSync.find((r) => r.label === 'Editor'),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
rolesAfterSecondSync.find((r) => r.label === 'Viewer'),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
rolesAfterSecondSync.find((r) => r.label === 'Viewer'),
|
||||
).toMatchObject({
|
||||
description: 'Read-only access',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
it('should update a role when properties change in manifest on second sync', async () => {
|
||||
await syncApplication({
|
||||
manifest: buildManifest({}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const rolesAfterFirstSync = await findAppRoles();
|
||||
|
||||
expect(rolesAfterFirstSync).toHaveLength(1);
|
||||
expect(rolesAfterFirstSync[0]).toMatchObject({
|
||||
label: 'Editor',
|
||||
description: 'Can edit',
|
||||
});
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_ID,
|
||||
label: 'Senior Editor',
|
||||
description: 'Can edit everything',
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const rolesAfterSecondSync = await findAppRoles();
|
||||
|
||||
expect(rolesAfterSecondSync).toHaveLength(1);
|
||||
expect(rolesAfterSecondSync[0]).toMatchObject({
|
||||
label: 'Senior Editor',
|
||||
description: 'Can edit everything',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
it('should delete a role when removed from manifest on second sync', async () => {
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_ID,
|
||||
label: 'Editor',
|
||||
description: 'Can edit',
|
||||
},
|
||||
{
|
||||
universalIdentifier: TEST_SECOND_ROLE_ID,
|
||||
label: 'Viewer',
|
||||
description: 'Read-only access',
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const rolesAfterFirstSync = await findAppRoles();
|
||||
|
||||
expect(rolesAfterFirstSync).toHaveLength(2);
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_ID,
|
||||
label: 'Editor',
|
||||
description: 'Can edit',
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const rolesAfterSecondSync = await findAppRoles();
|
||||
|
||||
expect(rolesAfterSecondSync).toHaveLength(1);
|
||||
expect(rolesAfterSecondSync[0]).toMatchObject({
|
||||
label: 'Editor',
|
||||
description: 'Can edit',
|
||||
});
|
||||
}, 60000);
|
||||
});
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util';
|
||||
import { findSkills } from 'test/integration/metadata/suites/skill/utils/find-skills.util';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const TEST_APP_ID = uuidv4();
|
||||
const TEST_ROLE_ID = uuidv4();
|
||||
const TEST_SKILL_ID = uuidv4();
|
||||
const TEST_SECOND_SKILL_ID = uuidv4();
|
||||
|
||||
const SKILL_GQL_FIELDS = 'id name label description content icon applicationId';
|
||||
|
||||
const buildManifest = (overrides?: Partial<Pick<Manifest, 'skills'>>) =>
|
||||
buildBaseManifest({ appId: TEST_APP_ID, roleId: TEST_ROLE_ID, overrides });
|
||||
|
||||
const findAppSkills = async () => {
|
||||
const { data } = await findSkills({
|
||||
gqlFields: SKILL_GQL_FIELDS,
|
||||
expectToFail: false,
|
||||
input: undefined,
|
||||
});
|
||||
|
||||
return data.skills.filter(
|
||||
(skill) => skill.name === 'enrichment' || skill.name === 'scoring',
|
||||
);
|
||||
};
|
||||
|
||||
describe('Manifest update - skills', () => {
|
||||
beforeEach(async () => {
|
||||
await setupApplicationForSync({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
name: 'Test Application',
|
||||
description: 'App for testing skill manifest updates',
|
||||
sourcePath: 'test-manifest-update-skill',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
afterEach(async () => {
|
||||
await uninstallApplication({
|
||||
universalIdentifier: TEST_APP_ID,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a new skill when added to manifest on second sync', async () => {
|
||||
await syncApplication({
|
||||
manifest: buildManifest({ skills: [] }),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const skillsAfterFirstSync = await findAppSkills();
|
||||
|
||||
expect(skillsAfterFirstSync).toHaveLength(0);
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
skills: [
|
||||
{
|
||||
universalIdentifier: TEST_SKILL_ID,
|
||||
name: 'enrichment',
|
||||
label: 'Enrichment',
|
||||
description: 'Enrich contact data',
|
||||
icon: 'IconSparkles',
|
||||
content: '# Enrichment\n\nEnrich your data.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const skillsAfterSecondSync = await findAppSkills();
|
||||
|
||||
expect(skillsAfterSecondSync).toHaveLength(1);
|
||||
expect(skillsAfterSecondSync[0]).toMatchObject({
|
||||
name: 'enrichment',
|
||||
label: 'Enrichment',
|
||||
description: 'Enrich contact data',
|
||||
icon: 'IconSparkles',
|
||||
content: '# Enrichment\n\nEnrich your data.',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
it('should update a skill when properties change in manifest on second sync', async () => {
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
skills: [
|
||||
{
|
||||
universalIdentifier: TEST_SKILL_ID,
|
||||
name: 'enrichment',
|
||||
label: 'Enrichment',
|
||||
description: 'Enrich contact data',
|
||||
icon: 'IconSparkles',
|
||||
content: '# Enrichment\n\nEnrich your data.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const skillsAfterFirstSync = await findAppSkills();
|
||||
|
||||
expect(skillsAfterFirstSync).toHaveLength(1);
|
||||
expect(skillsAfterFirstSync[0]).toMatchObject({
|
||||
label: 'Enrichment',
|
||||
description: 'Enrich contact data',
|
||||
});
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
skills: [
|
||||
{
|
||||
universalIdentifier: TEST_SKILL_ID,
|
||||
name: 'enrichment',
|
||||
label: 'Data Enrichment',
|
||||
description: 'Enrich contact and company data',
|
||||
icon: 'IconSparkles',
|
||||
content:
|
||||
'# Data Enrichment\n\nEnrich your contact and company data from multiple sources.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const skillsAfterSecondSync = await findAppSkills();
|
||||
|
||||
expect(skillsAfterSecondSync).toHaveLength(1);
|
||||
expect(skillsAfterSecondSync[0]).toMatchObject({
|
||||
name: 'enrichment',
|
||||
label: 'Data Enrichment',
|
||||
description: 'Enrich contact and company data',
|
||||
content:
|
||||
'# Data Enrichment\n\nEnrich your contact and company data from multiple sources.',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
it('should delete a skill when removed from manifest on second sync', async () => {
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
skills: [
|
||||
{
|
||||
universalIdentifier: TEST_SKILL_ID,
|
||||
name: 'enrichment',
|
||||
label: 'Enrichment',
|
||||
description: 'Enrich contact data',
|
||||
icon: 'IconSparkles',
|
||||
content: '# Enrichment\n\nEnrich your data.',
|
||||
},
|
||||
{
|
||||
universalIdentifier: TEST_SECOND_SKILL_ID,
|
||||
name: 'scoring',
|
||||
label: 'Lead Scoring',
|
||||
description: 'Score your leads',
|
||||
icon: 'IconChartBar',
|
||||
content: '# Lead Scoring\n\nScore leads automatically.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const skillsAfterFirstSync = await findAppSkills();
|
||||
|
||||
expect(skillsAfterFirstSync).toHaveLength(2);
|
||||
expect(
|
||||
skillsAfterFirstSync.find((s) => s.name === 'enrichment'),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
skillsAfterFirstSync.find((s) => s.name === 'scoring'),
|
||||
).toBeDefined();
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildManifest({
|
||||
skills: [
|
||||
{
|
||||
universalIdentifier: TEST_SKILL_ID,
|
||||
name: 'enrichment',
|
||||
label: 'Enrichment',
|
||||
description: 'Enrich contact data',
|
||||
icon: 'IconSparkles',
|
||||
content: '# Enrichment\n\nEnrich your data.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const skillsAfterSecondSync = await findAppSkills();
|
||||
|
||||
expect(skillsAfterSecondSync).toHaveLength(1);
|
||||
expect(skillsAfterSecondSync[0]).toMatchObject({
|
||||
name: 'enrichment',
|
||||
label: 'Enrichment',
|
||||
});
|
||||
expect(
|
||||
skillsAfterSecondSync.find((s) => s.name === 'scoring'),
|
||||
).toBeUndefined();
|
||||
}, 60000);
|
||||
});
|
||||
+111
-167
@@ -2,15 +2,19 @@ import { buildDefaultObjectManifest } from 'test/integration/metadata/suites/app
|
||||
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util';
|
||||
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
|
||||
import { findRoles } from 'test/integration/metadata/suites/role/utils/find-roles.util';
|
||||
import { findSkills } from 'test/integration/metadata/suites/skill/utils/find-skills.util';
|
||||
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { type FieldManifest, type Manifest } from 'twenty-shared/application';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { findManyObjectMetadataWithIndexes } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util';
|
||||
|
||||
const TEST_APP_ID = uuidv4();
|
||||
const TEST_ROLE_ID = uuidv4();
|
||||
const TEST_SECOND_ROLE_ID = uuidv4();
|
||||
const TEST_FIELD_ID = uuidv4();
|
||||
const TEST_SKILL_ID = uuidv4();
|
||||
|
||||
@@ -23,6 +27,39 @@ const TEST_OBJECT = buildDefaultObjectManifest({
|
||||
icon: 'IconTicket',
|
||||
});
|
||||
|
||||
const TEST_SKILL = {
|
||||
universalIdentifier: TEST_SKILL_ID,
|
||||
name: 'test-skill',
|
||||
label: 'Test Skill',
|
||||
description: 'A skill for testing',
|
||||
icon: 'IconBrain',
|
||||
content: '# Test Skill\n\nThis is a test skill.',
|
||||
};
|
||||
|
||||
const TEST_FIELD: FieldManifest = {
|
||||
universalIdentifier: TEST_FIELD_ID,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
description: 'Ticket description',
|
||||
icon: 'IconFileDescription',
|
||||
objectUniversalIdentifier: TEST_OBJECT.universalIdentifier,
|
||||
};
|
||||
|
||||
const buildManifest = (
|
||||
overrides?: Partial<Pick<Manifest, 'fields' | 'skills' | 'objects'>>,
|
||||
) =>
|
||||
buildBaseManifest({
|
||||
appId: TEST_APP_ID,
|
||||
roleId: TEST_ROLE_ID,
|
||||
overrides: {
|
||||
objects: [TEST_OBJECT],
|
||||
skills: [TEST_SKILL],
|
||||
fields: [TEST_FIELD],
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
|
||||
describe('syncApplication', () => {
|
||||
let appCreated = false;
|
||||
|
||||
@@ -49,48 +86,8 @@ describe('syncApplication', () => {
|
||||
});
|
||||
|
||||
it('should return workspace migration actions on initial sync then on second sync with field rename and new role', async () => {
|
||||
const initialManifest: Manifest = {
|
||||
application: {
|
||||
universalIdentifier: TEST_APP_ID,
|
||||
defaultRoleUniversalIdentifier: TEST_ROLE_ID,
|
||||
displayName: 'Test Application',
|
||||
description: 'A test application for workspace migration',
|
||||
icon: 'IconTestPipe',
|
||||
applicationVariables: {},
|
||||
packageJsonChecksum: null,
|
||||
yarnLockChecksum: null,
|
||||
apiClientChecksum: null,
|
||||
},
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_ID,
|
||||
label: 'Test Role',
|
||||
description: 'A test role',
|
||||
},
|
||||
],
|
||||
skills: [],
|
||||
objects: [TEST_OBJECT],
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: TEST_FIELD_ID,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
description: 'Ticket description',
|
||||
icon: 'IconFileDescription',
|
||||
objectUniversalIdentifier: TEST_OBJECT.universalIdentifier,
|
||||
},
|
||||
],
|
||||
logicFunctions: [],
|
||||
frontComponents: [],
|
||||
publicAssets: [],
|
||||
views: [],
|
||||
navigationMenuItems: [],
|
||||
pageLayouts: [],
|
||||
};
|
||||
|
||||
const { data: firstSyncData } = await syncApplication({
|
||||
manifest: initialManifest,
|
||||
manifest: buildManifest(),
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
@@ -98,139 +95,92 @@ describe('syncApplication', () => {
|
||||
extractRecordIdsAndDatesAsExpectAny(firstSyncData),
|
||||
);
|
||||
|
||||
const updatedManifest: Manifest = {
|
||||
...initialManifest,
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_ID,
|
||||
label: 'Test Role',
|
||||
description: 'A test role',
|
||||
},
|
||||
{
|
||||
universalIdentifier: TEST_SECOND_ROLE_ID,
|
||||
label: 'Viewer Role',
|
||||
description: 'A read-only role',
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: TEST_FIELD_ID,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'body',
|
||||
label: 'Body',
|
||||
description: 'Ticket description',
|
||||
icon: 'IconFileDescription',
|
||||
objectUniversalIdentifier: TEST_OBJECT.universalIdentifier,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { data: secondSyncData } = await syncApplication({
|
||||
manifest: updatedManifest,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(secondSyncData).toMatchSnapshot(
|
||||
extractRecordIdsAndDatesAsExpectAny(secondSyncData),
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('should sync a skill then update it on second sync', async () => {
|
||||
const initialManifest: Manifest = {
|
||||
application: {
|
||||
universalIdentifier: TEST_APP_ID,
|
||||
defaultRoleUniversalIdentifier: TEST_ROLE_ID,
|
||||
displayName: 'Test Application',
|
||||
description: 'A test application for workspace migration',
|
||||
icon: 'IconTestPipe',
|
||||
applicationVariables: {},
|
||||
packageJsonChecksum: null,
|
||||
yarnLockChecksum: null,
|
||||
apiClientChecksum: null,
|
||||
// Verify database state after first sync
|
||||
const { objects: objectsAfterSync } = await findManyObjectMetadata({
|
||||
input: {
|
||||
filter: { isCustom: { is: true } },
|
||||
paging: { first: 100 },
|
||||
},
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_ID,
|
||||
label: 'Test Role',
|
||||
description: 'A test role',
|
||||
},
|
||||
],
|
||||
skills: [
|
||||
{
|
||||
universalIdentifier: TEST_SKILL_ID,
|
||||
name: 'test-skill',
|
||||
label: 'Test Skill',
|
||||
description: 'A skill for testing',
|
||||
icon: 'IconBrain',
|
||||
content: '# Test Skill\n\nThis is a test skill.',
|
||||
},
|
||||
],
|
||||
objects: [],
|
||||
fields: [],
|
||||
logicFunctions: [],
|
||||
frontComponents: [],
|
||||
publicAssets: [],
|
||||
views: [],
|
||||
navigationMenuItems: [],
|
||||
pageLayouts: [],
|
||||
};
|
||||
|
||||
const { data: firstSyncData } = await syncApplication({
|
||||
manifest: initialManifest,
|
||||
gqlFields:
|
||||
'id nameSingular namePlural labelSingular labelPlural description icon isCustom',
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(firstSyncData).toMatchSnapshot(
|
||||
extractRecordIdsAndDatesAsExpectAny(firstSyncData),
|
||||
const ticketObject = objectsAfterSync.find(
|
||||
(obj) => obj.nameSingular === 'ticket',
|
||||
);
|
||||
|
||||
const updatedManifest: Manifest = {
|
||||
...initialManifest,
|
||||
skills: [
|
||||
{
|
||||
universalIdentifier: TEST_SKILL_ID,
|
||||
name: 'test-skill',
|
||||
label: 'Test Skill Updated',
|
||||
description: 'An updated skill for testing',
|
||||
icon: 'IconBrain',
|
||||
content:
|
||||
'# Test Skill\n\nThis is an updated test skill with more content.',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(ticketObject).toBeDefined();
|
||||
expect(ticketObject).toMatchObject({
|
||||
nameSingular: 'ticket',
|
||||
namePlural: 'tickets',
|
||||
labelSingular: 'Ticket',
|
||||
labelPlural: 'Tickets',
|
||||
description: 'A support ticket',
|
||||
icon: 'IconTicket',
|
||||
isCustom: true,
|
||||
});
|
||||
|
||||
const { data: secondSyncData } = await syncApplication({
|
||||
manifest: updatedManifest,
|
||||
const objects = await findManyObjectMetadataWithIndexes({
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(secondSyncData).toMatchSnapshot(
|
||||
extractRecordIdsAndDatesAsExpectAny(secondSyncData),
|
||||
const fieldsAfterSync = objects.find(
|
||||
(o) => o.universalIdentifier === TEST_OBJECT.universalIdentifier,
|
||||
)?.fieldsList;
|
||||
|
||||
const descriptionField = fieldsAfterSync?.find(
|
||||
(f) => f.universalIdentifier === TEST_FIELD_ID,
|
||||
);
|
||||
|
||||
expect(descriptionField).toBeDefined();
|
||||
expect(descriptionField).toMatchObject({
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
type: FieldMetadataType.TEXT,
|
||||
description: 'Ticket description',
|
||||
icon: 'IconFileDescription',
|
||||
});
|
||||
|
||||
const { data: rolesAfterSync } = await findRoles({
|
||||
gqlFields: 'id label description universalIdentifier',
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const testRole = rolesAfterSync.getRoles.find(
|
||||
(role) => role.universalIdentifier === TEST_ROLE_ID,
|
||||
);
|
||||
|
||||
const { data: skillsAfterSync } = await findSkills({
|
||||
gqlFields: 'id name label description content icon',
|
||||
expectToFail: false,
|
||||
input: undefined,
|
||||
});
|
||||
|
||||
const testSkill = skillsAfterSync.skills.find(
|
||||
(skill) => skill.name === 'test-skill',
|
||||
);
|
||||
|
||||
expect(testRole).toBeDefined();
|
||||
expect(testRole).toMatchObject({
|
||||
label: 'Test Role',
|
||||
description: 'A test role',
|
||||
});
|
||||
|
||||
expect(testSkill).toBeDefined();
|
||||
expect(testSkill).toMatchObject({
|
||||
name: 'test-skill',
|
||||
label: 'Test Skill',
|
||||
description: 'A skill for testing',
|
||||
icon: 'IconBrain',
|
||||
content: '# Test Skill\n\nThis is a test skill.',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
it('should create a TEXT field on the standard Company object', async () => {
|
||||
const companyFieldId = uuidv4();
|
||||
|
||||
const manifest: Manifest = {
|
||||
application: {
|
||||
universalIdentifier: TEST_APP_ID,
|
||||
defaultRoleUniversalIdentifier: TEST_ROLE_ID,
|
||||
displayName: 'Test Application',
|
||||
description: 'A test application for workspace migration',
|
||||
icon: 'IconTestPipe',
|
||||
applicationVariables: {},
|
||||
packageJsonChecksum: null,
|
||||
yarnLockChecksum: null,
|
||||
apiClientChecksum: null,
|
||||
},
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_ID,
|
||||
label: 'Test Role',
|
||||
description: 'A test role',
|
||||
},
|
||||
],
|
||||
const manifest = buildManifest({
|
||||
skills: [],
|
||||
objects: [],
|
||||
fields: [
|
||||
@@ -245,13 +195,7 @@ describe('syncApplication', () => {
|
||||
STANDARD_OBJECTS.company.universalIdentifier,
|
||||
},
|
||||
],
|
||||
logicFunctions: [],
|
||||
frontComponents: [],
|
||||
publicAssets: [],
|
||||
views: [],
|
||||
navigationMenuItems: [],
|
||||
pageLayouts: [],
|
||||
};
|
||||
});
|
||||
|
||||
const { data: syncData } = await syncApplication({
|
||||
manifest,
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
export const buildBaseManifest = ({
|
||||
appId,
|
||||
roleId,
|
||||
overrides,
|
||||
}: {
|
||||
appId: string;
|
||||
roleId: string;
|
||||
overrides?: Partial<Manifest>;
|
||||
}): Manifest => ({
|
||||
application: {
|
||||
universalIdentifier: appId,
|
||||
defaultRoleUniversalIdentifier: roleId,
|
||||
displayName: 'Test Application',
|
||||
description: 'Test application',
|
||||
icon: 'IconTestPipe',
|
||||
applicationVariables: {},
|
||||
packageJsonChecksum: null,
|
||||
yarnLockChecksum: null,
|
||||
apiClientChecksum: null,
|
||||
},
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: roleId,
|
||||
label: 'Test Role',
|
||||
description: 'A test role',
|
||||
},
|
||||
],
|
||||
skills: [],
|
||||
objects: [],
|
||||
fields: [],
|
||||
logicFunctions: [],
|
||||
frontComponents: [],
|
||||
publicAssets: [],
|
||||
views: [],
|
||||
navigationMenuItems: [],
|
||||
pageLayouts: [],
|
||||
...overrides,
|
||||
});
|
||||
+5
@@ -22,10 +22,15 @@ export const findManyObjectMetadataWithIndexes = async ({
|
||||
gqlFields: `
|
||||
id
|
||||
nameSingular
|
||||
universalIdentifier
|
||||
fieldsList {
|
||||
id
|
||||
type
|
||||
name
|
||||
description
|
||||
icon
|
||||
label
|
||||
universalIdentifier
|
||||
relation {
|
||||
type
|
||||
sourceObjectMetadata {
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ const DEFAULT_ROLE_GQL_FIELDS = `
|
||||
label
|
||||
description
|
||||
icon
|
||||
universalIdentifier
|
||||
isEditable
|
||||
canUpdateAllSettings
|
||||
canAccessAllTools
|
||||
|
||||
Reference in New Issue
Block a user