Files
upstream-twenty/packages/twenty-server/test/integration/metadata/suites/application/successful-export-application-data-model.integration-spec.ts
upstream-mirror e7da97194a
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
2026-09-17 23:28:13 +05:00

1407 lines
48 KiB
TypeScript

import { findManyApplications } from 'test/integration/graphql/utils/find-many-applications.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 { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
import { exportApplication } from 'test/integration/metadata/suites/application/utils/export-application.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 { upsertFieldPermissions } from 'test/integration/metadata/suites/field-permission/utils/upsert-field-permissions.util';
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
import { upsertObjectPermissions } from 'test/integration/metadata/suites/object-permission/utils/upsert-object-permissions.util';
import { upsertPermissionFlags } from 'test/integration/metadata/suites/role-permission-flag/utils/upsert-permission-flags.util';
import { upsertRowLevelPermissionPredicates } from 'test/integration/metadata/suites/row-level-permission-predicate/utils/upsert-row-level-permission-predicates.util';
import { createOneRole } from 'test/integration/metadata/suites/role/utils/create-one-role.util';
import { deleteOneRole } from 'test/integration/metadata/suites/role/utils/delete-one-role.util';
import { createOneViewFilter } from 'test/integration/metadata/suites/view-filter/utils/create-one-view-filter.util';
import { destroyOneViewFilter } from 'test/integration/metadata/suites/view-filter/utils/destroy-one-view-filter.util';
import { createOneView } from 'test/integration/metadata/suites/view/utils/create-one-view.util';
import { destroyOneView } from 'test/integration/metadata/suites/view/utils/destroy-one-view.util';
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
import {
type FieldManifest,
getFieldUniversalIdentifier,
getIndexFieldUniversalIdentifier,
getSystemFormFieldPageLayoutWidgetUniversalIdentifier,
getSystemPageLayoutTabUniversalIdentifier,
getSystemPageLayoutWidgetUniversalIdentifier,
getSystemRecordFormPageLayoutUniversalIdentifier,
getSystemRecordPageLayoutUniversalIdentifier,
getSystemRelationFieldUniversalIdentifier,
getSystemViewFieldUniversalIdentifier,
getSystemViewUniversalIdentifier,
type NavigationMenuItemManifest,
type ObjectManifest,
type PageLayoutManifest,
type PageLayoutTabManifest,
type StandalonePageLayoutWidgetManifest,
type StandaloneViewFieldManifest,
SYSTEM_VIEW_KEYS,
type TranslationsManifest,
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
type ViewManifest,
} from 'twenty-shared/application';
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-shared/metadata';
import {
AggregateOperations,
FieldMetadataType,
NavigationMenuItemType,
PageLayoutTabLayoutMode,
RelationOnDeleteAction,
RelationType,
RowLevelPermissionPredicateGroupLogicalOperator,
RowLevelPermissionPredicateOperand,
ViewFilterGroupLogicalOperator,
ViewFilterOperand,
ViewSortDirection,
ViewType,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { version as getUuidVersion, v4 } from 'uuid';
import { WORKSPACE_CUSTOM_APPLICATION_NAME } from 'src/engine/core-modules/application/constants/workspace-custom-application.constant';
import { ApplicationExportCoverageStatus } from 'src/engine/core-modules/application/enums/application-export-coverage-status.enum';
import { fromObjectManifestToUniversalFlatObjectMetadata } from 'src/engine/core-modules/application/application-manifest/converters/from-object-manifest-to-universal-flat-object-metadata.util';
import { buildSearchVectorFlatFieldMetadataForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-search-vector-flat-field-metadata-for-custom-object.util';
import { buildSearchVectorGinIndexForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-search-vector-gin-index-for-custom-object.util';
const TEST_APP_ID = '7e3d1c2b-0000-4a7b-8c9d-0e1f2a3b4c5d';
const TEST_ROLE_ID = '7e3d1c2b-0001-4a7b-8c9d-0e1f2a3b4c5d';
const TICKET_OBJECT_ID = '7e3d1c2b-0002-4a7b-8c9d-0e1f2a3b4c5d';
const PROJECT_OBJECT_ID = '7e3d1c2b-0003-4a7b-8c9d-0e1f2a3b4c5d';
const TICKET_TITLE_FIELD_ID = '7e3d1c2b-0004-4a7b-8c9d-0e1f2a3b4c5d';
const TICKET_PROJECT_FIELD_ID = '7e3d1c2b-0005-4a7b-8c9d-0e1f2a3b4c5d';
const PROJECT_TICKETS_FIELD_ID = '7e3d1c2b-0006-4a7b-8c9d-0e1f2a3b4c5d';
const COMPANY_TAGLINE_FIELD_ID = '7e3d1c2b-0007-4a7b-8c9d-0e1f2a3b4c5d';
const INDEX_ID = '7e3d1c2b-0008-4a7b-8c9d-0e1f2a3b4c5d';
const INDEX_FIELD_ID = '7e3d1c2b-0009-4a7b-8c9d-0e1f2a3b4c5d';
const OPEN_TICKETS_VIEW_ID = '7e3d1c2b-0010-4a7b-8c9d-0e1f2a3b4c5d';
const TITLE_VIEW_FIELD_ID = '7e3d1c2b-0011-4a7b-8c9d-0e1f2a3b4c5d';
const PROJECT_VIEW_FIELD_ID = '7e3d1c2b-0012-4a7b-8c9d-0e1f2a3b4c5d';
const VIEW_FIELD_GROUP_ID = '7e3d1c2b-0013-4a7b-8c9d-0e1f2a3b4c5d';
const VIEW_FILTER_GROUP_ID = '7e3d1c2b-0014-4a7b-8c9d-0e1f2a3b4c5d';
const VIEW_FILTER_ID = '7e3d1c2b-0015-4a7b-8c9d-0e1f2a3b4c5d';
const VIEW_SORT_ID = '7e3d1c2b-0016-4a7b-8c9d-0e1f2a3b4c5d';
const VIEW_GROUP_ID = '7e3d1c2b-0017-4a7b-8c9d-0e1f2a3b4c5d';
const PROJECT_INDEX_VIEW_FIELD_ID = '7e3d1c2b-0018-4a7b-8c9d-0e1f2a3b4c5d';
const TICKET_PAGE_LAYOUT_ID = '7e3d1c2b-0019-4a7b-8c9d-0e1f2a3b4c5d';
const TICKET_PAGE_OVERVIEW_TAB_ID = '7e3d1c2b-0020-4a7b-8c9d-0e1f2a3b4c5d';
const TICKET_PAGE_FIELDS_WIDGET_ID = '7e3d1c2b-0021-4a7b-8c9d-0e1f2a3b4c5d';
const TICKET_BOARD_LAYOUT_ID = '7e3d1c2b-0022-4a7b-8c9d-0e1f2a3b4c5d';
const TICKET_BOARD_TAB_ID = '7e3d1c2b-0023-4a7b-8c9d-0e1f2a3b4c5d';
const TICKET_BOARD_DOCS_WIDGET_ID = '7e3d1c2b-0024-4a7b-8c9d-0e1f2a3b4c5d';
const TICKET_RECORD_PAGE_EXTRA_TAB_ID = '7e3d1c2b-0025-4a7b-8c9d-0e1f2a3b4c5d';
const TICKET_RECORD_PAGE_EXTRA_NOTES_WIDGET_ID =
'7e3d1c2b-0026-4a7b-8c9d-0e1f2a3b4c5d';
const TICKETS_NAVIGATION_FOLDER_ID = '7e3d1c2b-0029-4a7b-8c9d-0e1f2a3b4c5d';
const TICKETS_NAVIGATION_ITEM_ID = '7e3d1c2b-0030-4a7b-8c9d-0e1f2a3b4c5d';
const TICKETS_INDEX_VIEW_NAVIGATION_ITEM_ID =
'7e3d1c2b-0031-4a7b-8c9d-0e1f2a3b4c5d';
const COMPANY_RECORD_PAGE_TAGLINE_TAB_ID =
'7e3d1c2b-0027-4a7b-8c9d-0e1f2a3b4c5d';
const TICKET_RECORD_PAGE_HOME_DOCS_WIDGET_ID =
'7e3d1c2b-0028-4a7b-8c9d-0e1f2a3b4c5d';
const TEST_ROLE_TICKET_OBJECT_PERMISSION_ID =
'7e3d1c2b-0032-4a7b-8c9d-0e1f2a3b4c5d';
const TEST_ROLE_TICKET_TITLE_FIELD_PERMISSION_ID =
'7e3d1c2b-0033-4a7b-8c9d-0e1f2a3b4c5d';
const EXPORT_TICKETS_PERMISSION_FLAG_ID =
'7e3d1c2b-0034-4a7b-8c9d-0e1f2a3b4c5d';
const TEST_ROLE_TICKET_PREDICATE_GROUP_ID =
'7e3d1c2b-0035-4a7b-8c9d-0e1f2a3b4c5d';
const TEST_ROLE_TICKET_CHILD_PREDICATE_GROUP_ID =
'7e3d1c2b-0036-4a7b-8c9d-0e1f2a3b4c5d';
const TEST_ROLE_TICKET_TITLE_PREDICATE_ID =
'7e3d1c2b-0037-4a7b-8c9d-0e1f2a3b4c5d';
const ENGINE_DERIVED_FIELD_NAMES = [
'id',
'createdAt',
'updatedAt',
'deletedAt',
'createdBy',
'updatedBy',
'position',
'searchVector',
];
const SYSTEM_RELATION_TARGET_OBJECT_NAMES = [
'timelineActivity',
'attachment',
'noteTarget',
'taskTarget',
] as const;
const RECORD_PAGE_TABS = [
{ tabTitle: 'Home', widgetTitle: 'Fields' },
{ tabTitle: 'Timeline', widgetTitle: 'Timeline' },
{ tabTitle: 'Tasks', widgetTitle: 'Tasks' },
{ tabTitle: 'Notes', widgetTitle: 'Notes' },
{ tabTitle: 'Files', widgetTitle: 'Files' },
];
const RECORD_FORM_TAB_TITLE = 'Fields';
type SystemRowsOwner = {
objectName: string;
objectUniversalIdentifier: string;
objectMetadataApplicationUniversalIdentifier: string;
applicationFieldUniversalIdentifierByName: Record<string, string>;
};
const buildIdentifierNames = (): Map<string, string> => {
const names = new Map<string, string>([
[TEST_APP_ID, 'TEST_APP'],
[TEST_ROLE_ID, 'TEST_ROLE'],
[
TEST_ROLE_TICKET_OBJECT_PERMISSION_ID,
'TEST_ROLE_TICKET_OBJECT_PERMISSION',
],
[
TEST_ROLE_TICKET_TITLE_FIELD_PERMISSION_ID,
'TEST_ROLE_TICKET_TITLE_FIELD_PERMISSION',
],
[EXPORT_TICKETS_PERMISSION_FLAG_ID, 'EXPORT_TICKETS_PERMISSION_FLAG'],
[TEST_ROLE_TICKET_PREDICATE_GROUP_ID, 'TEST_ROLE_TICKET_PREDICATE_GROUP'],
[
TEST_ROLE_TICKET_CHILD_PREDICATE_GROUP_ID,
'TEST_ROLE_TICKET_CHILD_PREDICATE_GROUP',
],
[TEST_ROLE_TICKET_TITLE_PREDICATE_ID, 'TEST_ROLE_TICKET_TITLE_PREDICATE'],
[TICKET_OBJECT_ID, 'TICKET_OBJECT'],
[PROJECT_OBJECT_ID, 'PROJECT_OBJECT'],
[TICKET_TITLE_FIELD_ID, 'TICKET_TITLE_FIELD'],
[TICKET_PROJECT_FIELD_ID, 'TICKET_PROJECT_FIELD'],
[PROJECT_TICKETS_FIELD_ID, 'PROJECT_TICKETS_FIELD'],
[COMPANY_TAGLINE_FIELD_ID, 'COMPANY_TAGLINE_FIELD'],
[INDEX_ID, 'TICKET_TITLE_INDEX'],
[OPEN_TICKETS_VIEW_ID, 'OPEN_TICKETS_VIEW'],
[TITLE_VIEW_FIELD_ID, 'OPEN_TICKETS_TITLE_VIEW_FIELD'],
[PROJECT_VIEW_FIELD_ID, 'OPEN_TICKETS_PROJECT_VIEW_FIELD'],
[VIEW_FIELD_GROUP_ID, 'OPEN_TICKETS_TRIAGE_FIELD_GROUP'],
[VIEW_FILTER_GROUP_ID, 'OPEN_TICKETS_NOT_FILTER_GROUP'],
[VIEW_FILTER_ID, 'OPEN_TICKETS_TITLE_FILTER'],
[VIEW_SORT_ID, 'OPEN_TICKETS_TITLE_SORT'],
[VIEW_GROUP_ID, 'OPEN_TICKETS_OPEN_GROUP'],
[PROJECT_INDEX_VIEW_FIELD_ID, 'PROJECT_INDEX_DELETED_AT_VIEW_FIELD'],
[TICKET_PAGE_LAYOUT_ID, 'TICKET_PAGE_LAYOUT'],
[TICKET_PAGE_OVERVIEW_TAB_ID, 'TICKET_PAGE_OVERVIEW_TAB'],
[TICKET_PAGE_FIELDS_WIDGET_ID, 'TICKET_PAGE_FIELDS_WIDGET'],
[TICKET_BOARD_LAYOUT_ID, 'TICKET_BOARD_LAYOUT'],
[TICKET_BOARD_TAB_ID, 'TICKET_BOARD_TAB'],
[TICKET_BOARD_DOCS_WIDGET_ID, 'TICKET_BOARD_DOCS_WIDGET'],
[TICKET_RECORD_PAGE_EXTRA_TAB_ID, 'TICKET_RECORD_PAGE_EXTRA_TAB'],
[
TICKET_RECORD_PAGE_EXTRA_NOTES_WIDGET_ID,
'TICKET_RECORD_PAGE_EXTRA_NOTES_WIDGET',
],
[
COMPANY_RECORD_PAGE_TAGLINE_TAB_ID,
'STANDARD_COMPANY_RECORD_PAGE_TAGLINE_TAB',
],
[TICKETS_NAVIGATION_FOLDER_ID, 'TICKETS_NAVIGATION_FOLDER'],
[TICKETS_NAVIGATION_ITEM_ID, 'TICKETS_NAVIGATION_ITEM'],
[
TICKETS_INDEX_VIEW_NAVIGATION_ITEM_ID,
'TICKETS_INDEX_VIEW_NAVIGATION_ITEM',
],
[
TICKET_RECORD_PAGE_HOME_DOCS_WIDGET_ID,
'TICKET_RECORD_PAGE_HOME_DOCS_WIDGET',
],
[
getIndexFieldUniversalIdentifier({
applicationUniversalIdentifier: TEST_APP_ID,
indexUniversalIdentifier: INDEX_ID,
fieldUniversalIdentifier: TICKET_TITLE_FIELD_ID,
}),
'TICKET_TITLE_INDEX_FIELD_ENGINE_DERIVED',
],
[STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company, 'STANDARD_COMPANY_OBJECT'],
]);
const nameSystemRowsOfObject = ({
objectName,
objectUniversalIdentifier,
objectMetadataApplicationUniversalIdentifier,
applicationFieldUniversalIdentifierByName,
}: SystemRowsOwner) => {
const applicationFields = Object.entries(
applicationFieldUniversalIdentifierByName,
);
for (const viewKey of Object.values(SYSTEM_VIEW_KEYS)) {
const viewUniversalIdentifier = getSystemViewUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier,
objectUniversalIdentifier,
viewKey,
});
names.set(
viewUniversalIdentifier,
`${objectName}_${viewKey}_ENGINE_VIEW`,
);
for (const [
fieldName,
fieldMetadataUniversalIdentifier,
] of applicationFields) {
names.set(
getSystemViewFieldUniversalIdentifier({
fieldMetadataApplicationUniversalIdentifier: TEST_APP_ID,
viewUniversalIdentifier,
fieldMetadataUniversalIdentifier,
}),
`${objectName}_${viewKey}_ENGINE_VIEW_FIELD_${fieldName}`,
);
}
}
const recordPageLayoutUniversalIdentifier =
getSystemRecordPageLayoutUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier,
objectUniversalIdentifier,
});
names.set(
recordPageLayoutUniversalIdentifier,
`${objectName}_RECORD_PAGE_LAYOUT`,
);
for (const { tabTitle, widgetTitle } of RECORD_PAGE_TABS) {
const pageLayoutTabUniversalIdentifier =
getSystemPageLayoutTabUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier,
pageLayoutUniversalIdentifier: recordPageLayoutUniversalIdentifier,
title: tabTitle,
});
names.set(
pageLayoutTabUniversalIdentifier,
`${objectName}_RECORD_PAGE_TAB_${tabTitle}`,
);
names.set(
getSystemPageLayoutWidgetUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier,
pageLayoutTabUniversalIdentifier,
title: widgetTitle,
}),
`${objectName}_RECORD_PAGE_WIDGET_${widgetTitle}`,
);
}
const recordFormLayoutUniversalIdentifier =
getSystemRecordFormPageLayoutUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier,
objectUniversalIdentifier,
});
const recordFormTabUniversalIdentifier =
getSystemPageLayoutTabUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier,
pageLayoutUniversalIdentifier: recordFormLayoutUniversalIdentifier,
title: RECORD_FORM_TAB_TITLE,
});
names.set(
recordFormLayoutUniversalIdentifier,
`${objectName}_RECORD_FORM_LAYOUT`,
);
names.set(
recordFormTabUniversalIdentifier,
`${objectName}_RECORD_FORM_TAB`,
);
for (const [
fieldName,
fieldMetadataUniversalIdentifier,
] of applicationFields) {
names.set(
getSystemFormFieldPageLayoutWidgetUniversalIdentifier({
fieldMetadataApplicationUniversalIdentifier: TEST_APP_ID,
pageLayoutTabUniversalIdentifier: recordFormTabUniversalIdentifier,
fieldMetadataUniversalIdentifier,
}),
`${objectName}_RECORD_FORM_WIDGET_${fieldName}`,
);
}
};
for (const {
objectName,
objectUniversalIdentifier,
objectManifest,
authoredFieldUniversalIdentifierByName,
} of [
{
objectName: 'TICKET',
objectUniversalIdentifier: TICKET_OBJECT_ID,
objectManifest: ticketObject,
authoredFieldUniversalIdentifierByName: {
title: TICKET_TITLE_FIELD_ID,
project: TICKET_PROJECT_FIELD_ID,
},
},
{
objectName: 'PROJECT',
objectUniversalIdentifier: PROJECT_OBJECT_ID,
objectManifest: projectObject,
authoredFieldUniversalIdentifierByName: {
tickets: PROJECT_TICKETS_FIELD_ID,
},
},
]) {
const engineFieldUniversalIdentifierByName = Object.fromEntries(
ENGINE_DERIVED_FIELD_NAMES.map((fieldName) => [
fieldName,
getFieldUniversalIdentifier({
applicationUniversalIdentifier: TEST_APP_ID,
objectUniversalIdentifier,
name: fieldName,
}),
]),
);
for (const [fieldName, fieldUniversalIdentifier] of Object.entries(
engineFieldUniversalIdentifierByName,
)) {
names.set(
fieldUniversalIdentifier,
`${objectName}_${fieldName}_ENGINE_FIELD`,
);
}
const flatObjectMetadata = fromObjectManifestToUniversalFlatObjectMetadata({
objectManifest,
applicationUniversalIdentifier: TEST_APP_ID,
now: new Date().toISOString(),
});
names.set(
buildSearchVectorGinIndexForCustomObject({
flatObjectMetadata,
searchVectorFlatFieldMetadata:
buildSearchVectorFlatFieldMetadataForCustomObject({
flatObjectMetadata,
}),
}).universalIdentifier,
`${objectName}_SEARCH_VECTOR_ENGINE_INDEX`,
);
const systemRelationFieldUniversalIdentifierByName: Record<string, string> =
{};
for (const targetObjectName of SYSTEM_RELATION_TARGET_OBJECT_NAMES) {
const targetObjectUniversalIdentifier =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS[targetObjectName];
const forwardFieldUniversalIdentifier =
getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: TEST_APP_ID,
objectUniversalIdentifier,
relationTargetObjectUniversalIdentifier:
targetObjectUniversalIdentifier,
});
const reverseFieldUniversalIdentifier =
getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: TEST_APP_ID,
objectUniversalIdentifier: targetObjectUniversalIdentifier,
relationTargetObjectUniversalIdentifier: objectUniversalIdentifier,
});
systemRelationFieldUniversalIdentifierByName[targetObjectName] =
forwardFieldUniversalIdentifier;
names.set(
forwardFieldUniversalIdentifier,
`${objectName}_${targetObjectName}_SYSTEM_RELATION_FIELD`,
);
names.set(
reverseFieldUniversalIdentifier,
`STANDARD_${targetObjectName}_target${objectName}_SYSTEM_RELATION_FIELD`,
);
nameSystemRowsOfObject({
objectName: `STANDARD_${targetObjectName}`,
objectUniversalIdentifier: targetObjectUniversalIdentifier,
objectMetadataApplicationUniversalIdentifier:
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
applicationFieldUniversalIdentifierByName: {
[`target${objectName}`]: reverseFieldUniversalIdentifier,
},
});
}
nameSystemRowsOfObject({
objectName,
objectUniversalIdentifier,
objectMetadataApplicationUniversalIdentifier: TEST_APP_ID,
applicationFieldUniversalIdentifierByName: {
...authoredFieldUniversalIdentifierByName,
...engineFieldUniversalIdentifierByName,
...systemRelationFieldUniversalIdentifierByName,
},
});
}
nameSystemRowsOfObject({
objectName: 'STANDARD_COMPANY',
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company,
objectMetadataApplicationUniversalIdentifier:
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
applicationFieldUniversalIdentifierByName: {
tagline: COMPANY_TAGLINE_FIELD_ID,
},
});
return names;
};
const projectObject = buildDefaultObjectManifest({
universalIdentifier: PROJECT_OBJECT_ID,
applicationUniversalIdentifier: TEST_APP_ID,
nameSingular: 'exportProject',
namePlural: 'exportProjects',
labelSingular: 'Export Project',
labelPlural: 'Export Projects',
description: 'A project',
});
const ticketObject: ObjectManifest = {
...buildDefaultObjectManifest({
universalIdentifier: TICKET_OBJECT_ID,
applicationUniversalIdentifier: TEST_APP_ID,
nameSingular: 'exportTicket',
namePlural: 'exportTickets',
labelSingular: 'Export Ticket',
labelPlural: 'Export Tickets',
description: 'A ticket',
}),
color: 'blue',
isLabelSyncedWithName: true,
};
ticketObject.fields = [
...ticketObject.fields,
{
universalIdentifier: TICKET_TITLE_FIELD_ID,
type: FieldMetadataType.TEXT,
name: 'title',
label: 'Title',
isLabelSyncedWithName: true,
},
{
universalIdentifier: TICKET_PROJECT_FIELD_ID,
type: FieldMetadataType.RELATION,
name: 'project',
label: 'Project',
relationTargetFieldMetadataUniversalIdentifier: PROJECT_TICKETS_FIELD_ID,
relationTargetObjectMetadataUniversalIdentifier:
projectObject.universalIdentifier,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
joinColumnName: 'projectId',
onDelete: RelationOnDeleteAction.SET_NULL,
},
},
];
projectObject.fields = [
...projectObject.fields,
{
universalIdentifier: PROJECT_TICKETS_FIELD_ID,
type: FieldMetadataType.RELATION,
name: 'tickets',
label: 'Tickets',
relationTargetFieldMetadataUniversalIdentifier: TICKET_PROJECT_FIELD_ID,
relationTargetObjectMetadataUniversalIdentifier:
ticketObject.universalIdentifier,
universalSettings: { relationType: RelationType.ONE_TO_MANY },
},
];
const companyTaglineField: FieldManifest = {
universalIdentifier: COMPANY_TAGLINE_FIELD_ID,
type: FieldMetadataType.TEXT,
name: 'exportTagline',
label: 'Export Tagline',
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company,
};
const FIXTURE_TRANSLATIONS: TranslationsManifest = {
'fr-FR': {
'export.ticket.title': 'Titre du ticket',
'export.ticket.project': 'Projet',
},
};
const openTicketsView: ViewManifest = {
universalIdentifier: OPEN_TICKETS_VIEW_ID,
name: 'Open tickets',
objectUniversalIdentifier: ticketObject.universalIdentifier,
type: ViewType.TABLE,
icon: 'IconTicket',
position: 1,
isCompact: true,
anyFieldFilterValue: 'urgent',
fields: [
{
universalIdentifier: TITLE_VIEW_FIELD_ID,
fieldMetadataUniversalIdentifier: TICKET_TITLE_FIELD_ID,
position: 0,
size: 240,
aggregateOperation: AggregateOperations.COUNT,
},
{
universalIdentifier: PROJECT_VIEW_FIELD_ID,
fieldMetadataUniversalIdentifier: TICKET_PROJECT_FIELD_ID,
position: 1,
isVisible: false,
},
],
fieldGroups: [
{
universalIdentifier: VIEW_FIELD_GROUP_ID,
name: 'Triage',
position: 0,
},
],
filterGroups: [
{
universalIdentifier: VIEW_FILTER_GROUP_ID,
logicalOperator: ViewFilterGroupLogicalOperator.NOT,
},
],
filters: [
{
universalIdentifier: VIEW_FILTER_ID,
fieldMetadataUniversalIdentifier: TICKET_TITLE_FIELD_ID,
operand: ViewFilterOperand.CONTAINS,
value: 'bug',
viewFilterGroupUniversalIdentifier: VIEW_FILTER_GROUP_ID,
positionInViewFilterGroup: 0,
},
],
sorts: [
{
universalIdentifier: VIEW_SORT_ID,
fieldMetadataUniversalIdentifier: TICKET_TITLE_FIELD_ID,
direction: ViewSortDirection.DESC,
},
],
groups: [
{
universalIdentifier: VIEW_GROUP_ID,
fieldValue: 'open',
position: 0,
},
],
};
const PROJECT_INDEX_VIEW_ID = getSystemViewUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier: TEST_APP_ID,
objectUniversalIdentifier: PROJECT_OBJECT_ID,
viewKey: SYSTEM_VIEW_KEYS.INDEX,
});
const TICKET_INDEX_VIEW_ID = getSystemViewUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier: TEST_APP_ID,
objectUniversalIdentifier: TICKET_OBJECT_ID,
viewKey: SYSTEM_VIEW_KEYS.INDEX,
});
const projectIndexViewField: StandaloneViewFieldManifest = {
universalIdentifier: PROJECT_INDEX_VIEW_FIELD_ID,
viewUniversalIdentifier: PROJECT_INDEX_VIEW_ID,
fieldMetadataUniversalIdentifier: getFieldUniversalIdentifier({
applicationUniversalIdentifier: TEST_APP_ID,
objectUniversalIdentifier: PROJECT_OBJECT_ID,
name: 'deletedAt',
}),
position: 10,
isVisible: true,
size: 180,
};
const ticketPageLayout: PageLayoutManifest = {
universalIdentifier: TICKET_PAGE_LAYOUT_ID,
name: 'Ticket page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: TICKET_OBJECT_ID,
defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier:
TICKET_PAGE_OVERVIEW_TAB_ID,
tabs: [
{
universalIdentifier: TICKET_PAGE_OVERVIEW_TAB_ID,
title: 'Overview',
position: 0,
icon: 'IconHome',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
universalIdentifier: TICKET_PAGE_FIELDS_WIDGET_ID,
title: 'Fields',
type: 'FIELDS',
objectUniversalIdentifier: TICKET_OBJECT_ID,
position: {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 0,
},
configuration: {
configurationType: 'FIELDS',
viewUniversalIdentifier: OPEN_TICKETS_VIEW_ID,
newFieldDefaultVisibility: true,
},
},
],
},
],
};
const ticketBoardLayout: PageLayoutManifest = {
universalIdentifier: TICKET_BOARD_LAYOUT_ID,
name: 'Ticket board',
type: 'STANDALONE_PAGE',
tabs: [
{
universalIdentifier: TICKET_BOARD_TAB_ID,
title: 'Board',
position: 0,
layoutMode: PageLayoutTabLayoutMode.GRID,
widgets: [
{
universalIdentifier: TICKET_BOARD_DOCS_WIDGET_ID,
title: 'Docs',
type: 'IFRAME',
position: {
layoutMode: PageLayoutTabLayoutMode.GRID,
row: 0,
column: 0,
rowSpan: 4,
columnSpan: 6,
},
configuration: {
configurationType: 'IFRAME',
url: 'https://example.com/tickets',
},
},
],
},
],
};
const TICKET_RECORD_PAGE_LAYOUT_ID =
getSystemRecordPageLayoutUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier: TEST_APP_ID,
objectUniversalIdentifier: TICKET_OBJECT_ID,
});
const ticketRecordPageExtraTab: PageLayoutTabManifest = {
universalIdentifier: TICKET_RECORD_PAGE_EXTRA_TAB_ID,
pageLayoutUniversalIdentifier: TICKET_RECORD_PAGE_LAYOUT_ID,
title: 'Extra',
position: 60,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
universalIdentifier: TICKET_RECORD_PAGE_EXTRA_NOTES_WIDGET_ID,
title: 'Notes',
type: 'NOTES',
objectUniversalIdentifier: TICKET_OBJECT_ID,
position: { layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, index: 0 },
configuration: { configurationType: 'NOTES' },
},
],
};
const companyRecordPageTaglineTab: PageLayoutTabManifest = {
universalIdentifier: COMPANY_RECORD_PAGE_TAGLINE_TAB_ID,
pageLayoutUniversalIdentifier: getSystemRecordPageLayoutUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier:
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company,
}),
title: 'Tagline',
position: 60,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
};
const ticketRecordPageHomeDocsWidget: StandalonePageLayoutWidgetManifest = {
universalIdentifier: TICKET_RECORD_PAGE_HOME_DOCS_WIDGET_ID,
pageLayoutTabUniversalIdentifier: getSystemPageLayoutTabUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier: TEST_APP_ID,
pageLayoutUniversalIdentifier: TICKET_RECORD_PAGE_LAYOUT_ID,
title: 'Home',
}),
title: 'Docs',
type: 'IFRAME',
position: { layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, index: 1 },
configuration: {
configurationType: 'IFRAME',
url: 'https://example.com/tickets/docs',
},
};
const ticketsNavigationFolder: NavigationMenuItemManifest = {
universalIdentifier: TICKETS_NAVIGATION_FOLDER_ID,
type: NavigationMenuItemType.FOLDER,
name: 'Support',
position: 40,
};
const ticketsNavigationItem: NavigationMenuItemManifest = {
universalIdentifier: TICKETS_NAVIGATION_ITEM_ID,
type: NavigationMenuItemType.OBJECT,
position: 41,
icon: 'IconTicket',
folderUniversalIdentifier: TICKETS_NAVIGATION_FOLDER_ID,
targetObjectUniversalIdentifier: TICKET_OBJECT_ID,
};
const ticketsIndexViewNavigationItem: NavigationMenuItemManifest = {
universalIdentifier: TICKETS_INDEX_VIEW_NAVIGATION_ITEM_ID,
type: NavigationMenuItemType.VIEW,
position: 42,
name: 'All tickets',
icon: 'IconList',
folderUniversalIdentifier: TICKETS_NAVIGATION_FOLDER_ID,
viewUniversalIdentifier: TICKET_INDEX_VIEW_ID,
};
const manifest = buildBaseManifest({
appId: TEST_APP_ID,
roleId: TEST_ROLE_ID,
overrides: {
translations: FIXTURE_TRANSLATIONS,
permissionFlags: [
{
universalIdentifier: EXPORT_TICKETS_PERMISSION_FLAG_ID,
key: 'EXPORT_TICKETS',
label: 'Export tickets',
permissionType: 'settings',
},
],
roles: [
{
universalIdentifier: TEST_ROLE_ID,
label: 'Test Role',
description: 'A test role',
canReadAllObjectRecords: true,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
universalIdentifier: TEST_ROLE_TICKET_OBJECT_PERMISSION_ID,
objectUniversalIdentifier: TICKET_OBJECT_ID,
canReadObjectRecords: true,
canUpdateObjectRecords: false,
},
],
fieldPermissions: [
{
universalIdentifier: TEST_ROLE_TICKET_TITLE_FIELD_PERMISSION_ID,
objectUniversalIdentifier: TICKET_OBJECT_ID,
fieldUniversalIdentifier: TICKET_TITLE_FIELD_ID,
canUpdateFieldValue: false,
},
],
rowLevelPermissionPredicateGroups: [
{
universalIdentifier: TEST_ROLE_TICKET_PREDICATE_GROUP_ID,
objectUniversalIdentifier: TICKET_OBJECT_ID,
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.OR,
position: 0,
},
{
universalIdentifier: TEST_ROLE_TICKET_CHILD_PREDICATE_GROUP_ID,
objectUniversalIdentifier: TICKET_OBJECT_ID,
logicalOperator:
RowLevelPermissionPredicateGroupLogicalOperator.AND,
parentPredicateGroupUniversalIdentifier:
TEST_ROLE_TICKET_PREDICATE_GROUP_ID,
position: 1,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: TEST_ROLE_TICKET_TITLE_PREDICATE_ID,
objectUniversalIdentifier: TICKET_OBJECT_ID,
fieldUniversalIdentifier: TICKET_TITLE_FIELD_ID,
operand: RowLevelPermissionPredicateOperand.CONTAINS,
value: 'bug',
predicateGroupUniversalIdentifier:
TEST_ROLE_TICKET_CHILD_PREDICATE_GROUP_ID,
position: 0,
},
],
permissionFlagUniversalIdentifiers: [
EXPORT_TICKETS_PERMISSION_FLAG_ID,
SystemPermissionFlag.WORKSPACE,
],
},
],
objects: [ticketObject, projectObject],
fields: [companyTaglineField],
indexes: [
{
universalIdentifier: INDEX_ID,
objectUniversalIdentifier: ticketObject.universalIdentifier,
fields: [
{
universalIdentifier: INDEX_FIELD_ID,
fieldUniversalIdentifier: TICKET_TITLE_FIELD_ID,
},
],
},
],
views: [openTicketsView],
viewFields: [projectIndexViewField],
pageLayouts: [ticketPageLayout, ticketBoardLayout],
pageLayoutTabs: [ticketRecordPageExtraTab, companyRecordPageTaglineTab],
pageLayoutWidgets: [ticketRecordPageHomeDocsWidget],
navigationMenuItems: [
ticketsNavigationFolder,
ticketsNavigationItem,
ticketsIndexViewNavigationItem,
],
},
});
const IDENTIFIER_NAMES = buildIdentifierNames();
const nameIdentifiers = <TValue>(value: TValue): TValue => {
const named: TValue = JSON.parse(JSON.stringify(value), (_key, nodeValue) =>
typeof nodeValue === 'string' && IDENTIFIER_NAMES.has(nodeValue)
? `<${IDENTIFIER_NAMES.get(nodeValue)}>`
: nodeValue,
);
return named;
};
describe('Application export - data model', () => {
beforeAll(async () => {
await cleanupApplicationAndAppRegistration({
applicationUniversalIdentifier: TEST_APP_ID,
});
await setupApplicationForSync({
applicationUniversalIdentifier: TEST_APP_ID,
name: 'Export Data Model Test Application',
description: 'App for testing the data model export',
sourcePath: 'export-data-model',
});
await syncApplication({ manifest, expectToFail: false });
}, 60000);
afterAll(async () => {
await cleanupApplicationAndAppRegistration({
applicationUniversalIdentifier: TEST_APP_ID,
});
});
it('exports the whole application as a stable manifest and classifies every row', async () => {
const { data, errors } = await exportApplication({
universalIdentifier: TEST_APP_ID,
expectToFail: false,
});
expect(errors).toBeUndefined();
const exported = data.exportApplication;
expect(exported.application).toMatchObject({
universalIdentifier: TEST_APP_ID,
displayName: 'Test Application',
sourceType: 'LOCAL',
});
expect(exported.files).toEqual([]);
expect(nameIdentifiers(exported.manifest)).toMatchSnapshot('manifest');
const isNamedRow = ({
universalIdentifier,
}: {
universalIdentifier: string;
}) => IDENTIFIER_NAMES.has(universalIdentifier);
const hasRandomUniversalIdentifier = ({
universalIdentifier,
}: {
universalIdentifier: string;
}) => getUuidVersion(universalIdentifier) === 4;
const compareCoverageRows = (
left: { metadataName: string; universalIdentifier: string },
right: { metadataName: string; universalIdentifier: string },
) =>
left.metadataName.localeCompare(right.metadataName) ||
left.universalIdentifier.localeCompare(right.universalIdentifier);
const unnamedCoverage = exported.coverage.filter((row) => !isNamedRow(row));
const randomlyIdentifiedCoverageCounts = Object.fromEntries(
Object.entries(
unnamedCoverage
.filter(hasRandomUniversalIdentifier)
.reduce<Record<string, number>>((counts, entry) => {
const key = `${entry.metadataName} ${entry.status}${isDefined(entry.reason) ? ` (${entry.reason})` : ''}`;
counts[key] = (counts[key] ?? 0) + 1;
return counts;
}, {}),
).sort(([left], [right]) => left.localeCompare(right)),
);
expect({
named: nameIdentifiers(exported.coverage.filter(isNamedRow)).sort(
compareCoverageRows,
),
deterministicUnnamed: unnamedCoverage
.filter((row) => !hasRandomUniversalIdentifier(row))
.sort(compareCoverageRows),
randomlyIdentifiedByKindAndStatus: randomlyIdentifiedCoverageCounts,
}).toMatchSnapshot('coverage');
}, 60000);
it('keeps the engine-derived label identifier pointer and classifies the rows it cannot export', async () => {
const { data } = await exportApplication({
universalIdentifier: TEST_APP_ID,
expectToFail: false,
});
const exported = data.exportApplication;
const exportedTicket = exported.manifest.objects.find(
({ universalIdentifier }) => universalIdentifier === TICKET_OBJECT_ID,
);
const statusOf = (universalIdentifier: string) =>
exported.coverage.find(
(entry) => entry.universalIdentifier === universalIdentifier,
)?.status;
expect(
exportedTicket?.labelIdentifierFieldMetadataUniversalIdentifier,
).toBe(
getFieldUniversalIdentifier({
applicationUniversalIdentifier: TEST_APP_ID,
objectUniversalIdentifier: TICKET_OBJECT_ID,
name: 'id',
}),
);
expect(exported.manifest.translations).toEqual(FIXTURE_TRANSLATIONS);
expect(statusOf(TICKET_OBJECT_ID)).toBe(
ApplicationExportCoverageStatus.EXPORTED,
);
expect(statusOf(COMPANY_TAGLINE_FIELD_ID)).toBe(
ApplicationExportCoverageStatus.EXPORTED,
);
expect(statusOf(TEST_ROLE_ID)).toBe(
ApplicationExportCoverageStatus.EXPORTED,
);
expect(statusOf(TEST_ROLE_TICKET_OBJECT_PERMISSION_ID)).toBe(
ApplicationExportCoverageStatus.EXPORTED,
);
expect(statusOf(TEST_ROLE_TICKET_TITLE_FIELD_PERMISSION_ID)).toBe(
ApplicationExportCoverageStatus.EXPORTED,
);
expect(statusOf(EXPORT_TICKETS_PERMISSION_FLAG_ID)).toBe(
ApplicationExportCoverageStatus.EXPORTED,
);
expect(statusOf(TEST_ROLE_TICKET_PREDICATE_GROUP_ID)).toBe(
ApplicationExportCoverageStatus.EXPORTED,
);
expect(statusOf(TEST_ROLE_TICKET_CHILD_PREDICATE_GROUP_ID)).toBe(
ApplicationExportCoverageStatus.EXPORTED,
);
expect(statusOf(TEST_ROLE_TICKET_TITLE_PREDICATE_ID)).toBe(
ApplicationExportCoverageStatus.EXPORTED,
);
expect(statusOf(TICKET_INDEX_VIEW_ID)).toBe(
ApplicationExportCoverageStatus.ENGINE_DERIVED,
);
expect(statusOf(PROJECT_INDEX_VIEW_ID)).toBe(
ApplicationExportCoverageStatus.ENGINE_DERIVED,
);
expect(statusOf(PROJECT_INDEX_VIEW_FIELD_ID)).toBe(
ApplicationExportCoverageStatus.EXPORTED,
);
expect(statusOf(TICKET_RECORD_PAGE_LAYOUT_ID)).toBe(
ApplicationExportCoverageStatus.ENGINE_DERIVED,
);
expect(statusOf(TICKET_RECORD_PAGE_EXTRA_TAB_ID)).toBe(
ApplicationExportCoverageStatus.EXPORTED,
);
expect(statusOf(TICKET_RECORD_PAGE_HOME_DOCS_WIDGET_ID)).toBe(
ApplicationExportCoverageStatus.EXPORTED,
);
expect(statusOf(TICKETS_NAVIGATION_FOLDER_ID)).toBe(
ApplicationExportCoverageStatus.EXPORTED,
);
expect(statusOf(TICKETS_NAVIGATION_ITEM_ID)).toBe(
ApplicationExportCoverageStatus.EXPORTED,
);
expect(statusOf(TICKETS_INDEX_VIEW_NAVIGATION_ITEM_ID)).toBe(
ApplicationExportCoverageStatus.EXPORTED,
);
}, 60000);
it('round-trips the raw export through an additive dry-run sync without any action', async () => {
const { data } = await exportApplication({
universalIdentifier: TEST_APP_ID,
expectToFail: false,
});
const dryRun = await syncApplication({
manifest: data.exportApplication.manifest,
dryRun: true,
inferDeletionFromMissingEntities: false,
expectToFail: false,
});
expect(dryRun.errors).toBeUndefined();
expect(dryRun.data.syncApplication.actions).toEqual([]);
}, 60000);
it('refuses to export the standard application', async () => {
const { errors } = await exportApplication({
universalIdentifier: TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
expectToFail: true,
});
expect(errors?.[0]?.extensions?.subCode).toBe(
'STANDARD_APPLICATION_NOT_EXPORTABLE',
);
});
it('round-trips the workspace Custom application through an additive dry-run sync without any action', async () => {
const { data: applicationsData } = await findManyApplications({
expectToFail: false,
});
const customApplication = applicationsData.findManyApplications.find(
({ name }) => name === WORKSPACE_CUSTOM_APPLICATION_NAME,
);
expect(isDefined(customApplication)).toBe(true);
const { data } = await exportApplication({
universalIdentifier: customApplication!.universalIdentifier,
expectToFail: false,
});
expect(data.exportApplication.manifest.objects.length).toBeGreaterThan(0);
const dryRun = await syncApplication({
manifest: data.exportApplication.manifest,
dryRun: true,
inferDeletionFromMissingEntities: false,
expectToFail: false,
});
expect(dryRun.errors).toBeUndefined();
expect(dryRun.data.syncApplication.actions).toEqual([]);
}, 60000);
it('should round-trip a view with a filter created through the metadata API in the workspace Custom application without any action', async () => {
const { objects } = await findManyObjectMetadata({
expectToFail: false,
input: { filter: {}, paging: { first: 1000 } },
gqlFields: `
id
nameSingular
fieldsList {
id
name
}
`,
});
const personObject = objects.find(
({ nameSingular }) => nameSingular === 'person',
);
jestExpectToBeDefined(personObject);
const personJobTitleField = personObject.fieldsList?.find(
({ name }) => name === 'jobTitle',
);
jestExpectToBeDefined(personJobTitleField);
const { data: applicationsData } = await findManyApplications({
expectToFail: false,
});
const customApplication = applicationsData.findManyApplications.find(
({ name }) => name === WORKSPACE_CUSTOM_APPLICATION_NAME,
);
jestExpectToBeDefined(customApplication);
const { data: createdViewData } = await createOneView({
expectToFail: false,
input: {
name: 'Export Custom Application People',
objectMetadataId: personObject.id,
type: ViewType.TABLE,
icon: 'IconUser',
},
});
const createdViewId = createdViewData.createView.id;
try {
const { data: createdViewFilterData } = await createOneViewFilter({
expectToFail: false,
input: {
viewId: createdViewId,
fieldMetadataId: personJobTitleField.id,
operand: ViewFilterOperand.CONTAINS,
value: 'Engineer',
},
});
const createdViewFilterId = createdViewFilterData.createViewFilter.id;
try {
const { data } = await exportApplication({
universalIdentifier: customApplication.universalIdentifier,
expectToFail: false,
});
expect(data.exportApplication.manifest.views).toContainEqual(
expect.objectContaining({
name: 'Export Custom Application People',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person,
filters: [
expect.objectContaining({
operand: ViewFilterOperand.CONTAINS,
value: 'Engineer',
}),
],
}),
);
const dryRun = await syncApplication({
manifest: data.exportApplication.manifest,
dryRun: true,
inferDeletionFromMissingEntities: false,
expectToFail: false,
});
expect(dryRun.errors).toBeUndefined();
expect(dryRun.data.syncApplication.actions).toEqual([]);
} finally {
await destroyOneViewFilter({
expectToFail: false,
input: { id: createdViewFilterId },
});
}
} finally {
await destroyOneView({ expectToFail: false, viewId: createdViewId });
}
}, 60000);
it('should round-trip a role with object, field and flag permissions created through the metadata API in the workspace Custom application without any action', async () => {
const { objects } = await findManyObjectMetadata({
expectToFail: false,
input: { filter: {}, paging: { first: 1000 } },
gqlFields: `
id
nameSingular
fieldsList {
id
name
}
`,
});
const personObject = objects.find(
({ nameSingular }) => nameSingular === 'person',
);
jestExpectToBeDefined(personObject);
const personJobTitleField = personObject.fieldsList?.find(
({ name }) => name === 'jobTitle',
);
jestExpectToBeDefined(personJobTitleField);
const { data: applicationsData } = await findManyApplications({
expectToFail: false,
});
const customApplication = applicationsData.findManyApplications.find(
({ name }) => name === WORKSPACE_CUSTOM_APPLICATION_NAME,
);
jestExpectToBeDefined(customApplication);
const { data: createdRoleData } = await createOneRole({
expectToFail: false,
input: {
label: 'Export Custom Application Recruiter',
canUpdateAllSettings: false,
canAccessAllTools: false,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canBeAssignedToUsers: true,
},
});
const createdRoleId = createdRoleData.createOneRole.id;
try {
await upsertObjectPermissions({
expectToFail: false,
input: {
roleId: createdRoleId,
objectPermissions: [
{
objectMetadataId: personObject.id,
canReadObjectRecords: true,
canUpdateObjectRecords: false,
},
],
},
});
await upsertFieldPermissions({
expectToFail: false,
input: {
roleId: createdRoleId,
fieldPermissions: [
{
objectMetadataId: personObject.id,
fieldMetadataId: personJobTitleField.id,
canUpdateFieldValue: false,
},
],
},
});
await upsertPermissionFlags({
expectToFail: false,
input: {
roleId: createdRoleId,
permissionFlagKeys: [PermissionFlagType.EXPORT_CSV],
},
});
const predicateGroupId = v4();
const keptPredicateId = v4();
const removedPredicateId = v4();
const predicateGroupInput = {
id: predicateGroupId,
objectMetadataId: personObject.id,
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.AND,
};
const keptPredicateInput = {
id: keptPredicateId,
fieldMetadataId: personJobTitleField.id,
operand: RowLevelPermissionPredicateOperand.CONTAINS,
value: 'Engineer',
rowLevelPermissionPredicateGroupId: predicateGroupId,
positionInRowLevelPermissionPredicateGroup: 0,
};
await upsertRowLevelPermissionPredicates({
expectToFail: false,
input: {
roleId: createdRoleId,
objectMetadataId: personObject.id,
predicateGroups: [predicateGroupInput],
predicates: [
keptPredicateInput,
{
id: removedPredicateId,
fieldMetadataId: personJobTitleField.id,
operand: RowLevelPermissionPredicateOperand.CONTAINS,
value: 'Manager',
rowLevelPermissionPredicateGroupId: predicateGroupId,
positionInRowLevelPermissionPredicateGroup: 1,
},
],
},
});
await upsertRowLevelPermissionPredicates({
expectToFail: false,
input: {
roleId: createdRoleId,
objectMetadataId: personObject.id,
predicateGroups: [predicateGroupInput],
predicates: [keptPredicateInput],
},
});
const { data } = await exportApplication({
universalIdentifier: customApplication.universalIdentifier,
expectToFail: false,
});
expect(data.exportApplication.manifest.roles).toContainEqual(
expect.objectContaining({
label: 'Export Custom Application Recruiter',
objectPermissions: [
expect.objectContaining({
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person,
canReadObjectRecords: true,
canUpdateObjectRecords: false,
}),
],
fieldPermissions: [
expect.objectContaining({
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person,
canUpdateFieldValue: false,
}),
],
rowLevelPermissionPredicateGroups: [
expect.objectContaining({
universalIdentifier: predicateGroupId,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person,
logicalOperator:
RowLevelPermissionPredicateGroupLogicalOperator.AND,
}),
],
rowLevelPermissionPredicates: [
expect.objectContaining({
universalIdentifier: keptPredicateId,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person,
operand: RowLevelPermissionPredicateOperand.CONTAINS,
value: 'Engineer',
predicateGroupUniversalIdentifier: predicateGroupId,
position: 0,
}),
],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.EXPORT_CSV],
}),
);
expect(
data.exportApplication.coverage.find(
({ universalIdentifier }) =>
universalIdentifier === removedPredicateId,
),
).toMatchObject({
metadataName: 'rowLevelPermissionPredicate',
status: ApplicationExportCoverageStatus.EXCLUDED,
});
const dryRun = await syncApplication({
manifest: data.exportApplication.manifest,
dryRun: true,
inferDeletionFromMissingEntities: false,
expectToFail: false,
});
expect(dryRun.errors).toBeUndefined();
expect(dryRun.data.syncApplication.actions).toEqual([]);
} finally {
await upsertRowLevelPermissionPredicates({
expectToFail: false,
input: {
roleId: createdRoleId,
objectMetadataId: personObject.id,
predicateGroups: [],
predicates: [],
},
});
await deleteOneRole({
expectToFail: false,
input: { idToDelete: createdRoleId },
});
}
}, 60000);
});