snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
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

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
import gql from 'graphql-tag';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
type ActivateWorkspaceUtilArgs = {
accessToken: string;
expectToFail?: boolean;
};
export const activateWorkspace = async ({
accessToken,
expectToFail,
}: ActivateWorkspaceUtilArgs): CommonResponseBody<{
activateWorkspace: WorkspaceEntity;
}> => {
const mutation = gql`
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
activateWorkspace(data: $input) {
id
displayName
activationStatus
subdomain
inviteHash
logo
}
}
`;
const response = await makeMetadataAPIRequest(
{
query: mutation,
variables: {
input: {},
},
},
accessToken,
);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Activate workspace should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Activate workspace has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,20 @@
// Workspace-selection origin for a seeded workspace, passed as the GraphQL
// `origin` variable on auth exchanges. The server resolves which workspace to
// authenticate into from this URL's subdomain
// (getWorkspaceByOriginOrDefaultWorkspace); it is routing data, unrelated to
// the HTTP Origin header the CSRF and cookie-issuance gates read.
export const buildWorkspaceOriginForSubdomain = (
subdomain: string,
): string => {
const origin = new URL(`http://localhost:${APP_PORT}`);
origin.hostname =
process.env.IS_MULTIWORKSPACE_ENABLED === 'true'
? `${subdomain}.${origin.hostname}`
: origin.hostname;
return origin.toString();
};
export const buildAppleWorkspaceOrigin = (): string =>
buildWorkspaceOriginForSubdomain('apple');

View File

@@ -0,0 +1,39 @@
import gql from 'graphql-tag';
export const createRoleOperation = ({
label,
description,
canUpdateAllSettings,
canReadAllObjectRecords,
canDestroyAllObjectRecords,
canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords,
}: {
label: string;
description: string;
canUpdateAllSettings: boolean;
canReadAllObjectRecords: boolean;
canDestroyAllObjectRecords: boolean;
canUpdateAllObjectRecords: boolean;
canSoftDeleteAllObjectRecords: boolean;
}) => ({
query: gql`
mutation CreateOneRole($createRoleInput: CreateRoleInput!) {
createOneRole(createRoleInput: $createRoleInput) {
id
label
}
}
`,
variables: {
createRoleInput: {
label,
description,
canUpdateAllSettings,
canReadAllObjectRecords,
canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords,
},
},
});

View File

@@ -0,0 +1,150 @@
import gql from 'graphql-tag';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
export const createCustomRoleWithObjectPermissions = async (options: {
label: string;
canReadPerson?: boolean;
canReadCompany?: boolean;
canReadOpportunities?: boolean;
canUpdatePerson?: boolean;
canUpdateCompany?: boolean;
canUpdateOpportunities?: boolean;
hasAllObjectRecordsReadPermission?: boolean;
}) => {
const createRoleOperation = {
query: gql`
mutation CreateOneRole {
createOneRole(createRoleInput: {
label: "${options.label}"
description: "Test role for permission testing"
canUpdateAllSettings: ${options.hasAllObjectRecordsReadPermission ?? true}
canReadAllObjectRecords: ${options.hasAllObjectRecordsReadPermission ?? true}
canUpdateAllObjectRecords: ${options.hasAllObjectRecordsReadPermission ?? true}
canSoftDeleteAllObjectRecords: ${options.hasAllObjectRecordsReadPermission ?? true}
canDestroyAllObjectRecords: ${options.hasAllObjectRecordsReadPermission ?? true}
}) {
id
label
}
}
`,
};
const response = await makeMetadataAPIRequest(createRoleOperation);
expect(response.body.errors).toBeUndefined();
expect(response.body.data.createOneRole).toBeDefined();
const roleId = response.body.data.createOneRole.id;
const getObjectMetadataOperation = {
query: gql`
query {
objects(paging: { first: 1000 }) {
edges {
node {
id
nameSingular
}
}
}
}
`,
};
const objectMetadataResponse = await makeMetadataAPIRequest(
getObjectMetadataOperation,
);
const objects = objectMetadataResponse.body.data.objects.edges;
const personObjectId = objects.find(
(obj: any) => obj.node.nameSingular === 'person',
)?.node.id;
const companyObjectId = objects.find(
(obj: any) => obj.node.nameSingular === 'company',
)?.node.id;
const opportunityObjectId = objects.find(
(obj: any) => obj.node.nameSingular === 'opportunity',
)?.node.id;
const objectPermissions = [];
if (
options.canReadPerson !== undefined ||
options.canUpdatePerson !== undefined
) {
objectPermissions.push({
objectMetadataId: personObjectId,
canReadObjectRecords: options.canReadPerson,
canUpdateObjectRecords:
options.canUpdatePerson === undefined ? false : options.canUpdatePerson,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
});
}
if (
options.canReadCompany !== undefined ||
options.canUpdateCompany !== undefined
) {
objectPermissions.push({
objectMetadataId: companyObjectId,
canReadObjectRecords: options.canReadCompany,
canUpdateObjectRecords:
options.canUpdateCompany === undefined
? false
: options.canUpdateCompany,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
});
}
if (
options.canReadOpportunities !== undefined ||
options.canUpdateOpportunities !== undefined
) {
objectPermissions.push({
objectMetadataId: opportunityObjectId,
canReadObjectRecords: options.canReadOpportunities,
canUpdateObjectRecords:
options.canUpdateOpportunities === undefined
? false
: options.canUpdateOpportunities,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
});
}
if (objectPermissions.length > 0) {
const upsertObjectPermissionsOperation = {
query: gql`
mutation UpsertObjectPermissions(
$roleId: UUID!
$objectPermissions: [ObjectPermissionInput!]!
) {
upsertObjectPermissions(
upsertObjectPermissionsInput: {
roleId: $roleId
objectPermissions: $objectPermissions
}
) {
objectMetadataId
canReadObjectRecords
}
}
`,
variables: {
roleId,
objectPermissions,
},
};
await makeMetadataAPIRequest(upsertObjectPermissionsOperation);
}
return {
roleId,
personObjectId,
companyObjectId,
opportunityObjectId,
};
};

View File

@@ -0,0 +1,30 @@
import gql from 'graphql-tag';
import { capitalize } from 'twenty-shared/utils';
type CreateManyOperationFactoryParams = {
objectMetadataSingularName: string;
objectMetadataPluralName: string;
gqlFields: string;
data?: object;
upsert?: boolean;
};
export const createManyOperationFactory = ({
objectMetadataSingularName,
objectMetadataPluralName,
gqlFields,
data = {},
upsert = false,
}: CreateManyOperationFactoryParams) => ({
query: gql`
mutation Create${capitalize(objectMetadataSingularName)}($data: [${capitalize(objectMetadataSingularName)}CreateInput!]!, $upsert: Boolean) {
create${capitalize(objectMetadataPluralName)}(data: $data, upsert: $upsert) {
${gqlFields}
}
}
`,
variables: {
data,
upsert,
},
});

View File

@@ -0,0 +1,66 @@
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type ObjectRecord } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
import { v4 } from 'uuid';
type CreateManyOperationInput = {
objectMetadataSingularName: string;
objectMetadataPluralName: string;
gqlFields?: string;
data: object[];
upsert?: boolean;
expectToFail?: boolean;
token?: string;
};
export const createManyOperation = async ({
objectMetadataSingularName,
objectMetadataPluralName,
gqlFields = 'id',
data,
upsert = false,
expectToFail = false,
token,
}: CreateManyOperationInput): CommonResponseBody<{
createdRecords: ObjectRecord[];
}> => {
const graphqlOperation = createManyOperationFactory({
objectMetadataSingularName,
objectMetadataPluralName,
gqlFields,
data: data.map((item) => ({
id: v4(),
...item,
})),
upsert,
});
const response = await makeGraphqlAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Create many operation should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Create many operation failed but should not have',
});
}
return {
data: {
createdRecords:
response.body.data?.[`create${capitalize(objectMetadataPluralName)}`] ??
[],
},
errors: response.body.errors,
};
};

View File

@@ -0,0 +1,25 @@
import gql from 'graphql-tag';
import { capitalize } from 'twenty-shared/utils';
type CreateOneOperationFactoryParams = {
objectMetadataSingularName: string;
gqlFields: string;
data?: object;
};
export const createOneOperationFactory = ({
objectMetadataSingularName,
gqlFields,
data = {},
}: CreateOneOperationFactoryParams) => ({
query: gql`
mutation CreateOne${capitalize(objectMetadataSingularName)}($input: ${capitalize(objectMetadataSingularName)}CreateInput!) {
create${capitalize(objectMetadataSingularName)}(data: $input) {
${gqlFields}
}
}
`,
variables: {
input: data,
},
});

View File

@@ -0,0 +1,42 @@
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type ObjectRecord } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
type CreateOneOperationArgs<T> = PerformMetadataQueryParams<T> & {
objectMetadataSingularName: string;
};
export const createOneOperation = async <T = object>({
input,
gqlFields = 'id',
objectMetadataSingularName,
expectToFail = false,
}: CreateOneOperationArgs<T>): CommonResponseBody<{
createOneResponse: ObjectRecord;
}> => {
const graphqlOperation = createOneOperationFactory({
data: input as object, // TODO default generic does not work
objectMetadataSingularName,
gqlFields,
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
if (expectToFail) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Create one operation should have failed but did not',
});
}
return {
data: {
createOneResponse:
response.body.data[`create${capitalize(objectMetadataSingularName)}`],
},
errors: response.body.errors,
};
};

View File

@@ -0,0 +1,25 @@
import gql from 'graphql-tag';
import { VIEW_FILTER_GROUP_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
import { type ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
type CreateViewFilterGroupOperationFactoryParams = {
gqlFields?: string;
data?: Partial<ViewFilterGroupEntity>;
};
export const createViewFilterGroupOperationFactory = ({
gqlFields = VIEW_FILTER_GROUP_GQL_FIELDS,
data = {},
}: CreateViewFilterGroupOperationFactoryParams = {}) => ({
query: gql`
mutation CreateViewFilterGroup($input: CreateViewFilterGroupInput!) {
createViewFilterGroup(input: $input) {
${gqlFields}
}
}
`,
variables: {
input: data,
},
});

View File

@@ -0,0 +1,25 @@
import gql from 'graphql-tag';
import { VIEW_SORT_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
import { type ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
type CreateViewSortOperationFactoryParams = {
gqlFields?: string;
data?: Partial<ViewSortEntity>;
};
export const createViewSortOperationFactory = ({
gqlFields = VIEW_SORT_GQL_FIELDS,
data = {},
}: CreateViewSortOperationFactoryParams = {}) => ({
query: gql`
mutation CreateViewSort($input: CreateViewSortInput!) {
createViewSort(input: $input) {
${gqlFields}
}
}
`,
variables: {
input: data,
},
});

View File

@@ -0,0 +1,12 @@
import * as jwt from 'jsonwebtoken';
import { isDefined } from 'twenty-shared/utils';
export const decodeJwtCompleteOrThrow = (token: string) => {
const decoded = jwt.decode(token, { complete: true });
if (!isDefined(decoded)) {
throw new Error('Failed to decode JWT');
}
return decoded;
};

View File

@@ -0,0 +1,29 @@
import gql from 'graphql-tag';
import { capitalize } from 'twenty-shared/utils';
type DeleteManyOperationFactoryParams = {
objectMetadataSingularName: string;
objectMetadataPluralName: string;
gqlFields: string;
filter?: object;
};
export const deleteManyOperationFactory = ({
objectMetadataSingularName,
objectMetadataPluralName,
gqlFields,
filter = {},
}: DeleteManyOperationFactoryParams) => ({
query: gql`
mutation Delete${capitalize(objectMetadataPluralName)}(
$filter: ${capitalize(objectMetadataSingularName)}FilterInput!
) {
delete${capitalize(objectMetadataPluralName)}(filter: $filter) {
${gqlFields}
}
}
`,
variables: {
filter,
},
});

View File

@@ -0,0 +1,25 @@
import gql from 'graphql-tag';
import { capitalize } from 'twenty-shared/utils';
type DeleteOneOperationFactoryParams = {
objectMetadataSingularName: string;
gqlFields: string;
recordId: string;
};
export const deleteOneOperationFactory = ({
objectMetadataSingularName,
gqlFields,
recordId,
}: DeleteOneOperationFactoryParams) => ({
query: gql`
mutation Delete${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: UUID!) {
delete${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id) {
${gqlFields}
}
}
`,
variables: {
[`${objectMetadataSingularName}Id`]: recordId,
},
});

View File

@@ -0,0 +1,7 @@
export const deleteOneRoleOperationFactory = (roleId: string) => ({
query: `
mutation DeleteOneRole {
deleteOneRole(roleId: "${roleId}")
}
`,
});

View File

@@ -0,0 +1,10 @@
import { deleteOneRoleOperationFactory } from 'test/integration/graphql/utils/delete-one-role-operation-factory.util';
export const deleteRole = async (client: any, roleId: string) => {
const deleteRoleQuery = deleteOneRoleOperationFactory(roleId);
await client
.post('/metadata')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(deleteRoleQuery);
};

View File

@@ -0,0 +1,55 @@
import gql from 'graphql-tag';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type UserEntity } from 'src/engine/core-modules/user/user.entity';
type DeleteUserUtilArgs = {
accessToken: string;
expectToFail?: boolean;
};
export const deleteUser = async ({
accessToken,
expectToFail,
}: DeleteUserUtilArgs): CommonResponseBody<{
deleteUser: UserEntity;
}> => {
const mutation = gql`
mutation DeleteUser {
deleteUser {
id
email
firstName
lastName
deletedAt
}
}
`;
const response = await makeMetadataAPIRequest(
{
query: mutation,
variables: {},
},
accessToken,
);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Delete user should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Delete user has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,16 @@
import gql from 'graphql-tag';
export const deleteViewFilterGroupOperationFactory = ({
viewFilterGroupId,
}: {
viewFilterGroupId: string;
}) => ({
query: gql`
mutation DeleteViewFilterGroup($id: String!) {
deleteViewFilterGroup(id: $id)
}
`,
variables: {
id: viewFilterGroupId,
},
});

View File

@@ -0,0 +1,16 @@
import gql from 'graphql-tag';
export const deleteViewSortOperationFactory = ({
viewSortId,
}: {
viewSortId: string;
}) => ({
query: gql`
mutation DeleteViewSort($input: DeleteViewSortInput!) {
deleteViewSort(input: $input)
}
`,
variables: {
input: { id: viewSortId },
},
});

View File

@@ -0,0 +1,29 @@
import gql from 'graphql-tag';
import { capitalize } from 'twenty-shared/utils';
type DestroyManyOperationFactoryParams = {
objectMetadataSingularName: string;
objectMetadataPluralName: string;
gqlFields: string;
filter?: object;
};
export const destroyManyOperationFactory = ({
objectMetadataSingularName,
objectMetadataPluralName,
gqlFields,
filter = {},
}: DestroyManyOperationFactoryParams) => ({
query: gql`
mutation Destroy${capitalize(objectMetadataPluralName)}(
$filter: ${capitalize(objectMetadataSingularName)}FilterInput!
) {
destroy${capitalize(objectMetadataPluralName)}(filter: $filter) {
${gqlFields}
}
}
`,
variables: {
filter,
},
});

View File

@@ -0,0 +1,25 @@
import gql from 'graphql-tag';
import { capitalize } from 'twenty-shared/utils';
type DestroyOneOperationFactoryParams = {
objectMetadataSingularName: string;
gqlFields: string;
recordId: string;
};
export const destroyOneOperationFactory = ({
objectMetadataSingularName,
gqlFields,
recordId,
}: DestroyOneOperationFactoryParams) => ({
query: gql`
mutation Destroy${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: UUID!) {
destroy${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id) {
${gqlFields}
}
}
`,
variables: {
[`${objectMetadataSingularName}Id`]: recordId,
},
});

View File

@@ -0,0 +1,16 @@
import gql from 'graphql-tag';
export const destroyViewFilterGroupOperationFactory = ({
viewFilterGroupId,
}: {
viewFilterGroupId: string;
}) => ({
query: gql`
mutation DestroyViewFilterGroup($id: String!) {
destroyViewFilterGroup(id: $id)
}
`,
variables: {
id: viewFilterGroupId,
},
});

View File

@@ -0,0 +1,16 @@
import gql from 'graphql-tag';
export const destroyViewSortOperationFactory = ({
viewSortId,
}: {
viewSortId: string;
}) => ({
query: gql`
mutation DestroyViewSort($input: DestroyViewSortInput!) {
destroyViewSort(input: $input)
}
`,
variables: {
input: { id: viewSortId },
},
});

View File

@@ -0,0 +1,25 @@
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { isDefined } from 'twenty-shared/utils';
import { type BaseGraphQLError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
export const expectOneNotInternalServerErrorSnapshot = ({
errors,
normalizeMessage,
}: {
errors: BaseGraphQLError[];
normalizeMessage?: (message: string) => string;
}) => {
expect(errors.length).toBe(1);
const [firstError] = errors;
expect(firstError.extensions.code).not.toBe('INTERNAL_SERVER_ERROR');
const errorForSnapshot = isDefined(normalizeMessage)
? { ...firstError, message: normalizeMessage(firstError.message) }
: firstError;
expect(errorForSnapshot).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(errorForSnapshot),
);
};

View File

@@ -0,0 +1,59 @@
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import gql from 'graphql-tag';
import { type ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
export const APPLICATION_GQL_FIELDS = `
id
name
description
logoUrl
version
universalIdentifier
canBeUninstalled
`;
export const findManyApplications = async ({
gqlFields = APPLICATION_GQL_FIELDS,
expectToFail,
accessToken,
}: {
gqlFields?: string;
expectToFail?: boolean;
accessToken?: string;
}): CommonResponseBody<{
findManyApplications: ApplicationDTO[];
}> => {
const response = await makeMetadataAPIRequest(
{
query: gql`
query FindManyApplications {
findManyApplications {
${gqlFields}
}
}
`,
variables: {},
},
accessToken,
);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Application search should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Application search has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,53 @@
import gql from 'graphql-tag';
import { capitalize } from 'twenty-shared/utils';
type FindManyOperationFactoryParams = {
objectMetadataSingularName: string;
objectMetadataPluralName: string;
gqlFields: string;
filter?: object;
orderBy?: object;
after?: string;
before?: string;
first?: number;
last?: number;
};
export const findManyOperationFactory = ({
objectMetadataSingularName,
objectMetadataPluralName,
gqlFields,
filter = {},
orderBy = {},
after,
before,
first,
last,
}: FindManyOperationFactoryParams) => ({
query: gql`
query ${capitalize(objectMetadataPluralName)}($filter: ${capitalize(objectMetadataSingularName)}FilterInput, $orderBy: [${capitalize(objectMetadataSingularName)}OrderByInput], $after: String, $before: String, $first: Int, $last: Int) {
${objectMetadataPluralName}(filter: $filter, orderBy: $orderBy, after: $after, before: $before, first: $first, last: $last) {
edges {
node {
${gqlFields}
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`,
variables: {
filter,
orderBy,
after,
before,
first,
last,
},
});

View File

@@ -0,0 +1,25 @@
import gql from 'graphql-tag';
import { capitalize } from 'twenty-shared/utils';
type FindOneOperationFactoryParams = {
objectMetadataSingularName: string;
gqlFields: string;
filter?: unknown;
};
export const findOneOperationFactory = ({
objectMetadataSingularName,
gqlFields,
filter = {},
}: FindOneOperationFactoryParams) => ({
query: gql`
query FindOne${capitalize(objectMetadataSingularName)}($filter: ${capitalize(objectMetadataSingularName)}FilterInput!) {
${objectMetadataSingularName}(filter: $filter) {
${gqlFields}
}
}
`,
variables: {
filter,
},
});

View File

@@ -0,0 +1,39 @@
import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type ObjectRecord } from 'twenty-shared/types';
type FindOneOperationArgs = Parameters<typeof findOneOperationFactory>[0] & {
expectToFail?: boolean;
};
export const findOneOperation = async ({
gqlFields = 'id',
objectMetadataSingularName,
expectToFail = false,
filter,
}: FindOneOperationArgs): CommonResponseBody<{
findResponse: ObjectRecord;
}> => {
const graphqlOperation = findOneOperationFactory({
objectMetadataSingularName,
gqlFields,
filter,
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
if (expectToFail) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Find one operation should have failed but did not',
});
}
return {
data: {
findResponse: response.body.data[objectMetadataSingularName],
},
errors: response.body.errors,
};
};

View File

@@ -0,0 +1,21 @@
import gql from 'graphql-tag';
import { VIEW_FILTER_GROUP_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
export const findViewFilterGroupOperationFactory = ({
gqlFields = VIEW_FILTER_GROUP_GQL_FIELDS,
viewFilterGroupId,
}: {
gqlFields?: string;
viewFilterGroupId: string;
}) => ({
query: gql`
query GetViewFilterGroup($id: String!) {
getViewFilterGroup(id: $id) {
${gqlFields}
}
}
`,
variables: {
id: viewFilterGroupId,
},
});

View File

@@ -0,0 +1,19 @@
import gql from 'graphql-tag';
import { VIEW_FILTER_GROUP_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
export const findViewFilterGroupsOperationFactory = ({
gqlFields = VIEW_FILTER_GROUP_GQL_FIELDS,
viewId,
}: {
gqlFields?: string;
viewId?: string;
} = {}) => ({
query: gql`
query GetViewFilterGroups($viewId: String) {
getViewFilterGroups(viewId: $viewId) {
${gqlFields}
}
}
`,
variables: viewId ? { viewId } : {},
});

View File

@@ -0,0 +1,21 @@
import gql from 'graphql-tag';
import { VIEW_SORT_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
export const findViewSortsOperationFactory = ({
gqlFields = VIEW_SORT_GQL_FIELDS,
viewId,
}: {
gqlFields?: string;
viewId: string;
}) => ({
query: gql`
query GetViewSorts($viewId: String!) {
getViewSorts(viewId: $viewId) {
${gqlFields}
}
}
`,
variables: {
viewId,
},
});

View File

@@ -0,0 +1,28 @@
import { createHash } from 'crypto';
import * as jwt from 'jsonwebtoken';
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/jwt-token-type.enum';
// Mirrors JwtWrapperService.generateAppSecret + extractAppSecretBody so tests
// can hand-craft tokens that match what a pre-2.5 server would have signed.
// extractAppSecretBody resolves to workspaceId when present, else userId.
const HS256_APP_SECRET = 'replace_me_with_a_random_string';
const generateLegacyHs256Secret = (
type: JwtTokenTypeEnum,
appSecretBody: string,
): string =>
createHash('sha256')
.update(`${HS256_APP_SECRET}${appSecretBody}${type}`)
.digest('hex');
export const forgeLegacyHs256Token = <TPayload extends Record<string, unknown>>(
payload: TPayload & { type: JwtTokenTypeEnum },
appSecretBody: string,
options: jwt.SignOptions = { expiresIn: '5m' },
): string =>
jwt.sign(payload, generateLegacyHs256Secret(payload.type, appSecretBody), {
algorithm: 'HS256',
...options,
});

View File

@@ -0,0 +1,25 @@
import gql from 'graphql-tag';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
export const generateApiKeyToken = async ({
apiKeyId,
accessToken,
expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString(),
}: {
apiKeyId: string;
accessToken: string;
expiresAt?: string;
}) => {
const mutation = gql`
mutation GenerateApiKeyToken($apiKeyId: UUID!, $expiresAt: String!) {
generateApiKeyToken(apiKeyId: $apiKeyId, expiresAt: $expiresAt) {
token
}
}
`;
return await makeMetadataAPIRequest(
{ query: mutation, variables: { apiKeyId, expiresAt } },
accessToken,
);
};

View File

@@ -0,0 +1,57 @@
import request from 'supertest';
import { buildAppleWorkspaceOrigin } from 'test/integration/graphql/utils/build-apple-workspace-origin.util';
import { getAuthTokensFromLoginToken } from 'test/integration/graphql/utils/get-auth-tokens-from-login-token.util';
const SERVER_URL = `http://localhost:${APP_PORT}`;
type GetAccessTokenForCredentialsArgs = {
email: string;
password?: string;
};
// Logs a seeded Apple user in with email/password and returns a usable
// access token, minting it dynamically rather than relying on a pre-baked
// entry in test-tokens.json.
export const getAccessTokenForCredentials = async ({
email,
password = 'tim@apple.dev',
}: GetAccessTokenForCredentialsArgs): Promise<string> => {
const origin = buildAppleWorkspaceOrigin();
const loginResponse = await request(SERVER_URL)
.post('/metadata')
.set('Origin', origin)
.send({
query: `
mutation GetLoginTokenFromCredentials(
$email: String!
$password: String!
$origin: String!
) {
getLoginTokenFromCredentials(
email: $email
password: $password
origin: $origin
) {
loginToken {
token
}
}
}
`,
variables: { email, password, origin },
})
.expect(200);
const loginToken =
loginResponse.body.data.getLoginTokenFromCredentials.loginToken.token;
const { data } = await getAuthTokensFromLoginToken({
loginToken,
origin,
expectToFail: false,
});
return data.getAuthTokensFromLoginToken.tokens.accessOrWorkspaceAgnosticToken
.token;
};

View File

@@ -0,0 +1,34 @@
import { gql } from 'graphql-tag';
export type GetAuthTokensFromLoginTokenFactoryInput = {
loginToken: string;
origin: string;
};
export const getAuthTokensFromLoginTokenQueryFactory = ({
loginToken,
origin,
}: GetAuthTokensFromLoginTokenFactoryInput) => {
return {
query: gql`
mutation GetAuthTokensFromLoginToken($loginToken: String!, $origin: String!) {
getAuthTokensFromLoginToken(loginToken: $loginToken, origin: $origin) {
tokens {
accessOrWorkspaceAgnosticToken {
token
expiresAt
}
refreshToken {
token
expiresAt
}
}
}
}
`,
variables: {
loginToken,
origin,
},
};
};

View File

@@ -0,0 +1,70 @@
import gql from 'graphql-tag';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type AuthTokens } from 'src/engine/core-modules/auth/dto/auth-tokens.dto';
type GetAuthTokensFromLoginTokenUtilArgs = {
loginToken: string;
origin?: string;
expectToFail?: boolean;
};
export const getAuthTokensFromLoginToken = async ({
loginToken,
origin = 'http://localhost:3001',
expectToFail,
}: GetAuthTokensFromLoginTokenUtilArgs): CommonResponseBody<{
getAuthTokensFromLoginToken: AuthTokens;
}> => {
const mutation = gql`
mutation GetAuthTokensFromLoginToken(
$loginToken: String!
$origin: String!
) {
getAuthTokensFromLoginToken(loginToken: $loginToken, origin: $origin) {
tokens {
accessOrWorkspaceAgnosticToken {
token
expiresAt
}
refreshToken {
token
expiresAt
}
}
}
}
`;
const response = await makeMetadataAPIRequest(
{
query: mutation,
variables: {
loginToken,
origin,
},
},
undefined, // Public endpoint - no authentication required
);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage:
'Get auth tokens from login token should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage:
'Get auth tokens from login token has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,62 @@
import gql from 'graphql-tag';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type AuthTokens } from 'src/engine/core-modules/auth/dto/auth-tokens.dto';
type GetAuthTokensFromSsoExchangeTokenUtilArgs = {
ssoExchangeToken: string;
expectToFail?: boolean;
};
export const getAuthTokensFromSsoExchangeToken = async ({
ssoExchangeToken,
expectToFail,
}: GetAuthTokensFromSsoExchangeTokenUtilArgs): CommonResponseBody<{
getAuthTokensFromSSOExchangeToken: AuthTokens;
}> => {
const mutation = gql`
mutation GetAuthTokensFromSSOExchangeToken($ssoExchangeToken: String!) {
getAuthTokensFromSSOExchangeToken(ssoExchangeToken: $ssoExchangeToken) {
tokens {
accessOrWorkspaceAgnosticToken {
token
expiresAt
}
refreshToken {
token
expiresAt
}
}
}
}
`;
const response = await makeMetadataAPIRequest(
{
query: mutation,
variables: { ssoExchangeToken },
},
undefined, // Public endpoint - no authentication required
);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage:
'Get auth tokens from sso exchange token should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage:
'Get auth tokens from sso exchange token has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,74 @@
import gql from 'graphql-tag';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type UserEntity } from 'src/engine/core-modules/user/user.entity';
type CurrentUserUtilArgs = {
accessToken: string;
expectToFail?: boolean;
};
export const getCurrentUser = async ({
accessToken,
expectToFail,
}: CurrentUserUtilArgs): CommonResponseBody<{
currentUser: UserEntity;
}> => {
const query = gql`
query CurrentUser {
currentUser {
id
email
firstName
lastName
isEmailVerified
disabled
canImpersonate
canAccessFullAdminPanel
locale
createdAt
updatedAt
deletedAt
currentWorkspace {
id
displayName
subdomain
activationStatus
logo
workspaceCustomApplicationId
}
currentUserWorkspace {
id
userId
}
}
}
`;
const response = await makeMetadataAPIRequest(
{
query,
variables: {},
},
accessToken,
);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Get current user should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Get current user has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,38 @@
import { gql } from 'graphql-tag';
export type GetLoginTokenFromCredentialsFactoryInput = {
email: string;
password: string;
origin: string;
};
export const getLoginTokenFromCredentialsQueryFactory = ({
email,
password,
origin,
}: GetLoginTokenFromCredentialsFactoryInput) => {
return {
query: gql`
mutation GetLoginTokenFromCredentials(
$email: String!
$password: String!
$origin: String!
) {
getLoginTokenFromCredentials(
email: $email
password: $password
origin: $origin
) {
loginToken {
token
}
}
}
`,
variables: {
email,
password,
origin,
},
};
};

View File

@@ -0,0 +1,51 @@
import gql from 'graphql-tag';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
type GetOnboardingStatusUtilArgs = {
accessToken: string;
expectToFail?: boolean;
};
export const getOnboardingStatus = async ({
accessToken,
expectToFail,
}: GetOnboardingStatusUtilArgs): CommonResponseBody<{
currentUser: { onboardingStatus: OnboardingStatus | null };
}> => {
const query = gql`
query CurrentUserOnboardingStatus {
currentUser {
onboardingStatus
}
}
`;
const response = await makeMetadataAPIRequest(
{
query,
variables: {},
},
accessToken,
);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Get onboarding status should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Get onboarding status has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,65 @@
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
import {
type BaseGraphQLError,
type ErrorCode,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
export interface GraphQLResponse<T extends Record<string, unknown>> {
status: number;
body: {
data?: T;
errors?: BaseGraphQLError[];
};
}
export const assertGraphQLSuccessfulResponse = <
T extends Record<string, unknown>,
>(
response: GraphQLResponse<T>,
expectedData?: Partial<T>,
) => {
expect(response.status).toBe(200);
expect(response.body.data).toBeDefined();
expect(response.body.errors).toBeUndefined();
if (expectedData) {
expect(response.body.data).toMatchObject(expectedData);
}
};
export const assertGraphQLErrorResponse = <T extends Record<string, unknown>>(
response: GraphQLResponse<T>,
expectedErrorCode: ErrorCode,
expectedErrorMessage?: string,
) => {
expect(response.status).toBe(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors).toHaveLength(1);
if (expectedErrorCode && response.body.errors) {
expect(response.body.errors[0].extensions?.code).toBe(expectedErrorCode);
}
if (expectedErrorMessage && response.body.errors) {
expect(response.body.errors[0].message).toBe(expectedErrorMessage);
}
};
export const assertGraphQLErrorResponseWithSnapshot = <
T extends Record<string, unknown>,
>(
response: GraphQLResponse<T>,
) => {
expect(response.status).toBe(200);
jestExpectToBeDefined(response.body.errors);
expect(response.body.errors).toHaveLength(1);
const firstError = response.body.errors[0];
jestExpectToBeDefined(firstError);
expect(firstError).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(firstError),
);
};

View File

@@ -0,0 +1,47 @@
import gql from 'graphql-tag';
import { capitalize } from 'twenty-shared/utils';
type GroupByOperationFactoryParams = {
objectMetadataSingularName: string;
objectMetadataPluralName: string;
orderByForRecords?: object[];
groupBy: object[];
filter?: object;
orderBy?: object[];
viewId?: string;
gqlFields?: string;
limit?: number;
offsetForRecords?: number;
};
export const groupByOperationFactory = ({
objectMetadataSingularName,
objectMetadataPluralName,
groupBy,
filter = {},
orderBy = [],
orderByForRecords = [],
viewId,
gqlFields,
limit,
offsetForRecords,
}: GroupByOperationFactoryParams) => ({
query: gql`
query ${capitalize(objectMetadataPluralName)}GroupBy($groupBy: [${capitalize(objectMetadataSingularName)}GroupByInput!]!, $filter: ${capitalize(objectMetadataSingularName)}FilterInput, $orderBy: [${capitalize(objectMetadataSingularName)}OrderByWithGroupByInput!], $orderByForRecords: [${capitalize(objectMetadataSingularName)}OrderByInput!], $viewId: UUID, $limit: Int, $offsetForRecords: Int) {
${objectMetadataPluralName}GroupBy(groupBy: $groupBy, filter: $filter, orderBy: $orderBy, orderByForRecords: $orderByForRecords, viewId: $viewId, limit: $limit, offsetForRecords: $offsetForRecords) {
${gqlFields ? gqlFields : ''}
groupByDimensionValues
totalCount
}
}
`,
variables: {
groupBy,
filter,
orderBy,
orderByForRecords,
limit,
offsetForRecords,
...(viewId && { viewId }),
},
});

View File

@@ -0,0 +1,65 @@
import gql from 'graphql-tag';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type ImpersonateDTO } from 'src/engine/core-modules/admin-panel/dtos/impersonate.dto';
type ImpersonateUtilArgs = {
userId: string;
workspaceId: string;
accessToken: string;
expectToFail?: boolean;
};
export const impersonate = async ({
userId,
workspaceId,
accessToken,
expectToFail,
}: ImpersonateUtilArgs): CommonResponseBody<{
impersonate: ImpersonateDTO;
}> => {
const mutation = gql`
mutation Impersonate($userId: UUID!, $workspaceId: UUID!) {
impersonate(userId: $userId, workspaceId: $workspaceId) {
loginToken {
token
expiresAt
}
workspace {
id
workspaceUrls {
subdomainUrl
customUrl
}
}
}
}
`;
const response = await makeMetadataAPIRequest(
{
query: mutation,
variables: { userId, workspaceId },
},
accessToken,
);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Impersonate should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Impersonate has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,50 @@
import gql from 'graphql-tag';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type InitiateTwoFactorAuthenticationProvisioningDTO } from 'src/engine/core-modules/two-factor-authentication/dto/initiate-two-factor-authentication-provisioning.dto';
type InitiateOtpProvisioningForAuthenticatedUserUtilArgs = {
accessToken: string;
expectToFail?: boolean;
};
export const initiateOtpProvisioningForAuthenticatedUser = async ({
accessToken,
expectToFail,
}: InitiateOtpProvisioningForAuthenticatedUserUtilArgs): CommonResponseBody<{
initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioningDTO;
}> => {
const mutation = gql`
mutation InitiateOTPProvisioningForAuthenticatedUser {
initiateOTPProvisioningForAuthenticatedUser {
uri
}
}
`;
const response = await makeMetadataAPIRequest(
{
query: mutation,
},
accessToken,
);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'OTP provisioning should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'OTP provisioning has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,21 @@
import { type ASTNode, print } from 'graphql';
import request from 'supertest';
type GraphqlOperation = {
query: ASTNode;
variables?: Record<string, unknown>;
};
export const makeGraphqlAPIRequestWithApiKey = (
graphqlOperation: GraphqlOperation,
) => {
const client = request(`http://localhost:${APP_PORT}`);
return client
.post('/graphql')
.set('Authorization', `Bearer ${API_KEY_ACCESS_TOKEN}`)
.send({
query: print(graphqlOperation.query),
variables: graphqlOperation.variables || {},
});
};

View File

@@ -0,0 +1,21 @@
import { type ASTNode, print } from 'graphql';
import request from 'supertest';
type GraphqlOperation = {
query: ASTNode;
variables?: Record<string, unknown>;
};
export const makeGraphqlAPIRequestWithGuestRole = (
graphqlOperation: GraphqlOperation,
) => {
const client = request(`http://localhost:${APP_PORT}`);
return client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_PHIL_GUEST_ACCESS_TOKEN}`)
.send({
query: print(graphqlOperation.query),
variables: graphqlOperation.variables || {},
});
};

View File

@@ -0,0 +1,21 @@
import { type ASTNode, print } from 'graphql';
import request from 'supertest';
type GraphqlOperation = {
query: ASTNode;
variables?: Record<string, unknown>;
};
export const makeGraphqlAPIRequestWithMemberRole = (
graphqlOperation: GraphqlOperation,
) => {
const client = request(`http://localhost:${APP_PORT}`);
return client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JONY_MEMBER_ACCESS_TOKEN}`)
.send({
query: print(graphqlOperation.query),
variables: graphqlOperation.variables || {},
});
};

View File

@@ -0,0 +1,26 @@
import { type ASTNode, print } from 'graphql';
import request from 'supertest';
import { isDefined } from 'twenty-shared/utils';
type GraphqlOperation = {
query: ASTNode;
variables?: Record<string, unknown>;
};
export const makeGraphqlAPIRequest = (
graphqlOperation: GraphqlOperation,
token: string | undefined = APPLE_JANE_ADMIN_ACCESS_TOKEN,
) => {
const client = request(`http://localhost:${APP_PORT}`);
const clientInstance = client.post('/graphql');
if (isDefined(token)) {
clientInstance.set('Authorization', `Bearer ${token}`);
}
return clientInstance.send({
query: print(graphqlOperation.query),
variables: graphqlOperation.variables || {},
});
};

View File

@@ -0,0 +1,36 @@
import gql from 'graphql-tag';
import { capitalize } from 'twenty-shared/utils';
type MergeManyOperationFactoryParams = {
objectMetadataPluralName: string;
gqlFields: string;
ids: string[];
conflictPriorityIndex: number;
dryRun?: boolean;
};
export const mergeManyOperationFactory = ({
objectMetadataPluralName,
gqlFields,
ids,
conflictPriorityIndex,
dryRun = false,
}: MergeManyOperationFactoryParams) => {
const capitalizedObjectName = capitalize(objectMetadataPluralName);
const mutationName = `merge${capitalizedObjectName}`;
return {
query: gql`
mutation Merge${capitalizedObjectName}($ids: [UUID!]!, $conflictPriorityIndex: Int!, $dryRun: Boolean! = false) {
${mutationName}(ids: $ids, conflictPriorityIndex: $conflictPriorityIndex, dryRun: $dryRun) {
${gqlFields}
}
}
`,
variables: {
ids,
conflictPriorityIndex,
dryRun,
},
};
};

View File

@@ -0,0 +1,28 @@
import gql from 'graphql-tag';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
export const renewApplicationToken = async ({
applicationRefreshToken,
accessToken,
}: {
applicationRefreshToken: string;
accessToken: string;
}) => {
const mutation = gql`
mutation RenewApplicationToken($applicationRefreshToken: String!) {
renewApplicationToken(applicationRefreshToken: $applicationRefreshToken) {
applicationAccessToken {
token
}
applicationRefreshToken {
token
}
}
}
`;
return await makeMetadataAPIRequest(
{ query: mutation, variables: { applicationRefreshToken } },
accessToken,
);
};

View File

@@ -0,0 +1,24 @@
import gql from 'graphql-tag';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
export const renewToken = async (appToken: string) => {
const mutation = gql`
mutation RenewToken($appToken: String!) {
renewToken(appToken: $appToken) {
tokens {
accessOrWorkspaceAgnosticToken {
token
}
refreshToken {
token
}
}
}
}
`;
return await makeMetadataAPIRequest(
{ query: mutation, variables: { appToken } },
undefined,
);
};

View File

@@ -0,0 +1,16 @@
import gql from 'graphql-tag';
export const resendWorkspaceInvitationOperationFactory = ({
appTokenId,
}: {
appTokenId: string;
}) => ({
query: gql`
mutation ResendWorkspaceInvitation($appTokenId: String!) {
resendWorkspaceInvitation(appTokenId: $appTokenId) {
success
}
}
`,
variables: { appTokenId },
});

View File

@@ -0,0 +1,27 @@
import gql from 'graphql-tag';
import { capitalize } from 'twenty-shared/utils';
type RestoreManyOperationFactoryParams = {
objectMetadataSingularName: string;
objectMetadataPluralName: string;
gqlFields: string;
filter: object;
};
export const restoreManyOperationFactory = ({
objectMetadataSingularName,
objectMetadataPluralName,
gqlFields,
filter,
}: RestoreManyOperationFactoryParams) => ({
query: gql`
mutation Restore${capitalize(objectMetadataPluralName)}($filter: ${capitalize(objectMetadataSingularName)}FilterInput!) {
restore${capitalize(objectMetadataPluralName)}(filter: $filter) {
${gqlFields}
}
}
`,
variables: {
filter,
},
});

View File

@@ -0,0 +1,25 @@
import gql from 'graphql-tag';
import { capitalize } from 'twenty-shared/utils';
type RestoreOneOperationFactoryParams = {
objectMetadataSingularName: string;
gqlFields: string;
recordId: string;
};
export const restoreOneOperationFactory = ({
objectMetadataSingularName,
gqlFields,
recordId,
}: RestoreOneOperationFactoryParams) => ({
query: gql`
mutation Restore${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: UUID!) {
restore${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id) {
${gqlFields}
}
}
`,
variables: {
[`${objectMetadataSingularName}Id`]: recordId,
},
});

View File

@@ -0,0 +1,56 @@
import gql from 'graphql-tag';
import { type SearchArgs } from 'src/engine/core-modules/search/dtos/search-args';
export const searchFactory = ({
searchInput,
excludedObjectNameSingulars,
includedObjectNameSingulars,
filter,
after,
limit = 50,
}: SearchArgs) => ({
query: gql`
query Search(
$searchInput: String!
$limit: Int!
$after: String
$excludedObjectNameSingulars: [String!]
$includedObjectNameSingulars: [String!]
$filter: ObjectRecordFilterInput
) {
search(
searchInput: $searchInput
limit: $limit
after: $after
excludedObjectNameSingulars: $excludedObjectNameSingulars
includedObjectNameSingulars: $includedObjectNameSingulars
filter: $filter
) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
recordId
objectNameSingular
label
imageUrl
tsRankCD
tsRank
}
cursor
}
}
}
`,
variables: {
searchInput,
limit,
after,
excludedObjectNameSingulars,
includedObjectNameSingulars,
filter,
},
});

View File

@@ -0,0 +1,53 @@
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { searchFactory } from 'test/integration/graphql/utils/search-factory.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type SearchArgs } from 'src/engine/core-modules/search/dtos/search-args';
import { type SearchResultConnectionDTO } from 'src/engine/core-modules/search/dtos/search-result-connection.dto';
type SearchOperationArgs = SearchArgs & {
expectToFail?: boolean;
accessToken?: string;
};
export const search = async ({
searchInput,
limit,
after,
includedObjectNameSingulars,
excludedObjectNameSingulars,
filter,
expectToFail,
accessToken,
}: SearchOperationArgs): CommonResponseBody<{
search: SearchResultConnectionDTO;
}> => {
const graphqlOperation = searchFactory({
searchInput,
limit,
after,
includedObjectNameSingulars,
excludedObjectNameSingulars,
filter,
});
const response = await makeGraphqlAPIRequest(graphqlOperation, accessToken);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Search should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Search has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,55 @@
import { AppTokenType } from 'src/engine/core-modules/app-token/app-token.entity';
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
export const seedWorkspaceInvitation = async ({
email,
value,
expiresAt,
roleId,
workspaceId = SEED_APPLE_WORKSPACE_ID,
}: {
email: string;
value: string;
expiresAt: Date;
roleId?: string;
workspaceId?: string;
}): Promise<string> => {
const insertedRows = await testDataSource.query(
`INSERT INTO core."appToken" ("workspaceId", "type", "value", "expiresAt", "context")
VALUES ($1, $2, $3, $4, $5::jsonb)
RETURNING "id"`,
[
workspaceId,
AppTokenType.InvitationToken,
value,
expiresAt.toISOString(),
JSON.stringify({ email, ...(roleId ? { roleId } : {}) }),
],
);
return insertedRows[0].id;
};
export const findWorkspaceInvitationsByEmail = ({
email,
workspaceId = SEED_APPLE_WORKSPACE_ID,
}: {
email: string;
workspaceId?: string;
}): Promise<{ value: string; expiresAt: string }[]> =>
testDataSource.query(
`SELECT "value", "expiresAt" FROM core."appToken" WHERE "workspaceId" = $1 AND context->>'email' = $2`,
[workspaceId, email],
);
export const deleteWorkspaceInvitationsByEmail = ({
email,
workspaceId = SEED_APPLE_WORKSPACE_ID,
}: {
email: string;
workspaceId?: string;
}) =>
testDataSource.query(
`DELETE FROM core."appToken" WHERE "workspaceId" = $1 AND context->>'email' = $2`,
[workspaceId, email],
);

View File

@@ -0,0 +1,18 @@
import gql from 'graphql-tag';
export const sendInvitationsOperationFactory = ({
emails,
roleId,
}: {
emails: string[];
roleId?: string;
}) => ({
query: gql`
mutation SendInvitations($emails: [String!]!, $roleId: UUID) {
sendInvitations(emails: $emails, roleId: $roleId) {
success
}
}
`,
variables: { emails, roleId },
});

View File

@@ -0,0 +1,144 @@
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.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 { findOneRoleByLabel } from 'test/integration/metadata/suites/role/utils/find-one-role-by-label.util';
import { updateWorkspaceMemberRole } from 'test/integration/metadata/suites/role/utils/update-workspace-member-role.util';
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
import {
type RowLevelPermissionPredicateValue,
RowLevelPermissionPredicateOperand,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
export type AccountOwnerRlsRoleSetup = {
customRoleId: string;
originalMemberRoleId: string;
companyObjectMetadataId: string;
accountOwnerFieldMetadataId: string;
};
export const setupAccountOwnerRlsRole = async ({
label,
description,
}: {
label: string;
description: string;
}): Promise<AccountOwnerRlsRoleSetup> => {
const memberRole = await findOneRoleByLabel({ label: 'Member' });
const { data: roleData } = await createOneRole({
expectToFail: false,
input: {
label,
description,
icon: 'IconSettings',
canUpdateAllSettings: false,
canAccessAllTools: true,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canBeAssignedToUsers: true,
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: false,
},
});
const customRoleId = roleData?.createOneRole?.id;
jestExpectToBeDefined(customRoleId);
const { objects } = await findManyObjectMetadata({
expectToFail: false,
input: { filter: {}, paging: { first: 1000 } },
gqlFields: `
id
nameSingular
fieldsList {
id
name
}
`,
});
jestExpectToBeDefined(objects);
const companyObjectMetadata = objects.find(
(object) => object.nameSingular === 'company',
);
jestExpectToBeDefined(companyObjectMetadata);
const accountOwnerFieldMetadata = companyObjectMetadata.fieldsList?.find(
(field) => field.name === 'accountOwner',
);
jestExpectToBeDefined(accountOwnerFieldMetadata);
await updateWorkspaceMemberRole({
input: {
roleId: customRoleId,
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
},
expectToFail: false,
});
return {
customRoleId,
originalMemberRoleId: memberRole.id,
companyObjectMetadataId: companyObjectMetadata.id,
accountOwnerFieldMetadataId: accountOwnerFieldMetadata.id,
};
};
export const upsertAccountOwnerRlsPredicate = async ({
setup,
predicateId,
value,
expectToFail = false,
}: {
setup: AccountOwnerRlsRoleSetup;
predicateId: string;
value: RowLevelPermissionPredicateValue;
expectToFail?: boolean;
}) =>
upsertRowLevelPermissionPredicates({
expectToFail,
input: {
roleId: setup.customRoleId,
objectMetadataId: setup.companyObjectMetadataId,
predicates: [
{
id: predicateId,
fieldMetadataId: setup.accountOwnerFieldMetadataId,
operand: RowLevelPermissionPredicateOperand.IS,
value,
},
],
predicateGroups: [],
},
});
export const cleanupAccountOwnerRlsRole = async (
setup: Partial<AccountOwnerRlsRoleSetup> = {},
): Promise<void> => {
if (isDefined(setup.originalMemberRoleId)) {
await updateWorkspaceMemberRole({
input: {
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
roleId: setup.originalMemberRoleId,
},
expectToFail: false,
});
}
if (isDefined(setup.customRoleId)) {
await deleteOneRole({
expectToFail: false,
input: { idToDelete: setup.customRoleId },
});
}
};

View File

@@ -0,0 +1,86 @@
import { upsertContainsRlsPredicate } from 'test/integration/graphql/utils/upsert-contains-rls-predicate.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 { findOneRoleByLabel } from 'test/integration/metadata/suites/role/utils/find-one-role-by-label.util';
import { updateWorkspaceMemberRole } from 'test/integration/metadata/suites/role/utils/update-workspace-member-role.util';
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
import { isDefined } from 'twenty-shared/utils';
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
export const VISIBLE_COMPANY_NAME_TOKEN = 'Visible';
export type CompanyNameRlsRoleSetup = {
customRoleId: string;
originalMemberRoleId: string;
};
export const setupCompanyNameRlsRole = async ({
label,
description,
}: {
label: string;
description: string;
}): Promise<CompanyNameRlsRoleSetup> => {
const memberRole = await findOneRoleByLabel({ label: 'Member' });
const { data: roleData } = await createOneRole({
expectToFail: false,
input: {
label,
description,
icon: 'IconSettings',
canUpdateAllSettings: false,
canAccessAllTools: true,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canBeAssignedToUsers: true,
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: false,
},
});
const customRoleId = roleData?.createOneRole?.id;
jestExpectToBeDefined(customRoleId);
await upsertContainsRlsPredicate({
roleId: customRoleId,
objectNameSingular: 'company',
fieldName: 'name',
value: VISIBLE_COMPANY_NAME_TOKEN,
});
await updateWorkspaceMemberRole({
input: {
roleId: customRoleId,
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
},
expectToFail: false,
});
return { customRoleId, originalMemberRoleId: memberRole.id };
};
export const cleanupCompanyNameRlsRole = async (
setup: Partial<CompanyNameRlsRoleSetup> = {},
): Promise<void> => {
if (isDefined(setup.originalMemberRoleId)) {
await updateWorkspaceMemberRole({
input: {
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
roleId: setup.originalMemberRoleId,
},
expectToFail: false,
});
}
if (isDefined(setup.customRoleId)) {
await deleteOneRole({
expectToFail: false,
input: { idToDelete: setup.customRoleId },
});
}
};

View File

@@ -0,0 +1,110 @@
import { randomUUID } from 'crypto';
import { COMPANY_GQL_FIELDS } from 'test/integration/constants/company-gql-fields.constants';
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-one-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { VISIBLE_COMPANY_NAME_TOKEN } from 'test/integration/graphql/utils/setup-company-name-rls-role.util';
import { isDefined } from 'twenty-shared/utils';
export type RlsCompanyRelationRecords = {
visibleCompanyId: string;
hiddenCompanyId: string;
visibleCompanyName: string;
hiddenCompanyName: string;
personWithVisibleCompanyId: string;
personWithHiddenCompanyId: string;
personWithoutCompanyId: string;
};
export const setupRlsCompanyRelationRecords = async ({
companyNamePrefix,
createdAt,
}: {
companyNamePrefix: string;
createdAt: string;
}): Promise<RlsCompanyRelationRecords> => {
const records: RlsCompanyRelationRecords = {
visibleCompanyId: randomUUID(),
hiddenCompanyId: randomUUID(),
visibleCompanyName: `${companyNamePrefix} ${VISIBLE_COMPANY_NAME_TOKEN} Co`,
hiddenCompanyName: `${companyNamePrefix} Hidden Co`,
personWithVisibleCompanyId: randomUUID(),
personWithHiddenCompanyId: randomUUID(),
personWithoutCompanyId: randomUUID(),
};
for (const { id, name } of [
{ id: records.visibleCompanyId, name: records.visibleCompanyName },
{ id: records.hiddenCompanyId, name: records.hiddenCompanyName },
]) {
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: COMPANY_GQL_FIELDS,
data: { id, name, createdAt },
}),
);
}
for (const { id, companyId } of [
{
id: records.personWithVisibleCompanyId,
companyId: records.visibleCompanyId,
},
{
id: records.personWithHiddenCompanyId,
companyId: records.hiddenCompanyId,
},
{ id: records.personWithoutCompanyId, companyId: undefined },
]) {
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: {
id,
createdAt,
...(isDefined(companyId) ? { companyId } : {}),
},
}),
);
}
return records;
};
export const cleanupRlsCompanyRelationRecords = async (
records: RlsCompanyRelationRecords | undefined,
): Promise<void> => {
if (!isDefined(records)) {
return;
}
const peopleIds = [
records.personWithVisibleCompanyId,
records.personWithHiddenCompanyId,
records.personWithoutCompanyId,
];
for (const recordId of peopleIds) {
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: 'id',
recordId,
}),
);
}
for (const recordId of [records.visibleCompanyId, records.hiddenCompanyId]) {
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: 'id',
recordId,
}),
);
}
};

View File

@@ -0,0 +1,63 @@
import gql from 'graphql-tag';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type SignUpDTO } from 'src/engine/core-modules/auth/dto/sign-up.dto';
type SignUpOnNewWorkspaceUtilArgs = {
accessToken: string;
displayName?: string;
expectToFail?: boolean;
};
export const signUpInNewWorkspace = async ({
accessToken,
displayName = 'Test Workspace',
expectToFail,
}: SignUpOnNewWorkspaceUtilArgs): CommonResponseBody<{
signUpInNewWorkspace: SignUpDTO;
}> => {
const mutation = gql`
mutation SignUpInNewWorkspace($input: SignUpInNewWorkspaceInput) {
signUpInNewWorkspace(input: $input) {
loginToken {
token
expiresAt
}
workspace {
id
workspaceUrls {
customUrl
subdomainUrl
}
}
}
}
`;
const response = await makeMetadataAPIRequest(
{
query: mutation,
variables: { input: { displayName } },
},
accessToken,
);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Sign up on new workspace should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Sign up on new workspace has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,99 @@
import gql from 'graphql-tag';
import request from 'supertest';
import { getAuthTokensFromLoginToken } from 'test/integration/graphql/utils/get-auth-tokens-from-login-token.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
const APPLE_WORKSPACE_INVITE_HASH = 'apple.dev-invite-hash';
// Signs up a user in the Apple workspace, marks their email as verified,
// and returns a valid access token for that user.
export const signUpInWorkspaceAndGetAccessToken = async (
email: string,
password = 'Password123!',
): Promise<string> => {
const client = request(`http://localhost:${APP_PORT}`);
const enablePublicInviteLinkMutation = {
query: `
mutation updateWorkspace {
updateWorkspace(data: { isPublicInviteLinkEnabled: true }) {
id
isPublicInviteLinkEnabled
}
}
`,
};
await client
.post('/metadata')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(enablePublicInviteLinkMutation)
.expect(200);
const signUpInWorkspaceMutation = gql`
mutation SignUpInWorkspace(
$email: String!
$password: String!
$workspaceInviteHash: String
$workspaceId: UUID
) {
signUpInWorkspace(
email: $email
password: $password
workspaceInviteHash: $workspaceInviteHash
workspaceId: $workspaceId
) {
loginToken {
token
}
workspace {
id
workspaceUrls {
subdomainUrl
}
}
}
}
`;
const signUpResponse = await makeMetadataAPIRequest(
{
query: signUpInWorkspaceMutation,
variables: {
email,
password,
workspaceInviteHash: APPLE_WORKSPACE_INVITE_HASH,
workspaceId: SEED_APPLE_WORKSPACE_ID,
},
},
undefined,
);
expect(signUpResponse.status).toBe(200);
expect(signUpResponse.body.errors).toBeUndefined();
const signUpPayload = signUpResponse.body.data.signUpInWorkspace;
expect(signUpPayload.loginToken.token).toBeDefined();
await testDataSource.query(
'UPDATE core."user" SET "isEmailVerified" = true WHERE email = $1',
[email],
);
const origin =
signUpPayload.workspace.workspaceUrls?.subdomainUrl ??
'http://localhost:3001';
const {
data: { getAuthTokensFromLoginToken: authTokensData },
} = await getAuthTokensFromLoginToken({
loginToken: signUpPayload.loginToken.token,
origin,
expectToFail: false,
});
return authTokensData.tokens.accessOrWorkspaceAgnosticToken.token;
};

View File

@@ -0,0 +1,50 @@
import gql from 'graphql-tag';
export const signUpInWorkspaceOperationFactory = ({
email,
password = 'Test123!@#',
workspaceId,
workspaceInviteHash,
workspacePersonalInviteToken,
}: {
email: string;
password?: string;
workspaceId?: string;
workspaceInviteHash?: string;
workspacePersonalInviteToken?: string;
}) => ({
query: gql`
mutation SignUpInWorkspace(
$email: String!
$password: String!
$workspaceId: UUID
$workspaceInviteHash: String
$workspacePersonalInviteToken: String
) {
signUpInWorkspace(
email: $email
password: $password
workspaceId: $workspaceId
workspaceInviteHash: $workspaceInviteHash
workspacePersonalInviteToken: $workspacePersonalInviteToken
) {
loginToken {
token
}
workspace {
id
workspaceUrls {
subdomainUrl
}
}
}
}
`,
variables: {
email,
password,
workspaceId,
workspaceInviteHash,
workspacePersonalInviteToken,
},
});

View File

@@ -0,0 +1,26 @@
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
export const signUpOperationFactory = ({
email,
password,
workspaceInviteHash,
workspaceId,
}: {
email: string;
password: string;
workspaceInviteHash?: string;
workspaceId?: string;
}) => ({
query: `
mutation SignUpInWorkspace {
signUpInWorkspace(
email: "${email}"
password: "${password}"
workspaceInviteHash: "${workspaceInviteHash ?? 'apple.dev-invite-hash'}"
workspaceId: "${workspaceId ?? SEED_APPLE_WORKSPACE_ID}"
) {
workspace { id }
}
}
`,
});

View File

@@ -0,0 +1,87 @@
import gql from 'graphql-tag';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type AvailableWorkspacesAndAccessTokensDTO } from 'src/engine/core-modules/auth/dto/available-workspaces-and-access-tokens.dto';
import { type UserCredentialsInput } from 'src/engine/core-modules/auth/dto/user-credentials.input';
type SignUpUtilArgs = {
input: UserCredentialsInput;
expectToFail?: boolean;
};
export const signUp = async ({
input,
expectToFail,
}: SignUpUtilArgs): CommonResponseBody<{
signUp: AvailableWorkspacesAndAccessTokensDTO;
}> => {
const mutation = gql`
mutation SignUp(
$email: String!
$password: String!
$captchaToken: String
) {
signUp(email: $email, password: $password, captchaToken: $captchaToken) {
tokens {
accessOrWorkspaceAgnosticToken {
token
expiresAt
}
refreshToken {
token
expiresAt
}
}
availableWorkspaces {
availableWorkspacesForSignIn {
id
displayName
logo
workspaceUrls {
customUrl
subdomainUrl
}
}
availableWorkspacesForSignUp {
id
displayName
logo
workspaceUrls {
customUrl
subdomainUrl
}
}
}
}
}
`;
const response = await makeMetadataAPIRequest(
{
query: mutation,
variables: {
...input,
},
},
undefined, // Public endpoint - no authentication required
);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Sign up should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Sign up has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,50 @@
import gql from 'graphql-tag';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
type SkipSyncEmailOnboardingStepUtilArgs = {
accessToken: string;
expectToFail?: boolean;
};
export const skipSyncEmailOnboardingStep = async ({
accessToken,
expectToFail,
}: SkipSyncEmailOnboardingStepUtilArgs): CommonResponseBody<{
skipSyncEmailOnboardingStep: { success: boolean };
}> => {
const mutation = gql`
mutation SkipSyncEmailOnboardingStep {
skipSyncEmailOnboardingStep {
success
}
}
`;
const response = await makeMetadataAPIRequest(
{
query: mutation,
variables: {},
},
accessToken,
);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage:
'Skip sync email onboarding step should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Skip sync email onboarding step has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,27 @@
import gql from 'graphql-tag';
import { type FeatureFlagKey } from 'twenty-shared/types';
export const updateFeatureFlagFactory = (
workspaceId: string,
featureFlag: FeatureFlagKey,
value: boolean,
) => ({
query: gql`
mutation UpdateWorkspaceFeatureFlag(
$workspaceId: UUID!
$featureFlag: String!
$value: Boolean!
) {
updateWorkspaceFeatureFlag(
workspaceId: $workspaceId
featureFlag: $featureFlag
value: $value
)
}
`,
variables: {
workspaceId,
featureFlag,
value,
},
});

View File

@@ -0,0 +1,33 @@
import gql from 'graphql-tag';
import { capitalize } from 'twenty-shared/utils';
type UpdateManyOperationFactoryParams = {
objectMetadataSingularName: string;
objectMetadataPluralName: string;
gqlFields: string;
data?: object;
filter?: object;
};
export const updateManyOperationFactory = ({
objectMetadataSingularName,
objectMetadataPluralName,
gqlFields,
data = {},
filter = {},
}: UpdateManyOperationFactoryParams) => ({
query: gql`
mutation Update${capitalize(objectMetadataPluralName)}(
$data: ${capitalize(objectMetadataSingularName)}UpdateInput!
$filter: ${capitalize(objectMetadataSingularName)}FilterInput!
) {
update${capitalize(objectMetadataPluralName)}(data: $data, filter: $filter) {
${gqlFields}
}
}
`,
variables: {
data,
filter,
},
});

View File

@@ -0,0 +1,28 @@
import gql from 'graphql-tag';
import { capitalize } from 'twenty-shared/utils';
type UpdateOneOperationFactoryParams = {
objectMetadataSingularName: string;
gqlFields: string;
data?: object;
recordId: string;
};
export const updateOneOperationFactory = ({
objectMetadataSingularName,
gqlFields,
data = {},
recordId,
}: UpdateOneOperationFactoryParams) => ({
query: gql`
mutation Update${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: UUID!, $data: ${capitalize(objectMetadataSingularName)}UpdateInput!) {
update${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id, data: $data) {
${gqlFields}
}
}
`,
variables: {
data,
[`${objectMetadataSingularName}Id`]: recordId,
},
});

View File

@@ -0,0 +1,24 @@
import gql from 'graphql-tag';
import { VIEW_FILTER_GROUP_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
export const updateViewFilterGroupOperationFactory = ({
gqlFields = VIEW_FILTER_GROUP_GQL_FIELDS,
viewFilterGroupId,
data = {},
}: {
gqlFields?: string;
viewFilterGroupId: string;
data?: object;
}) => ({
query: gql`
mutation UpdateViewFilterGroup($id: String!, $input: UpdateViewFilterGroupInput!) {
updateViewFilterGroup(id: $id, input: $input) {
${gqlFields}
}
}
`,
variables: {
id: viewFilterGroupId,
input: data,
},
});

View File

@@ -0,0 +1,23 @@
import gql from 'graphql-tag';
import { VIEW_SORT_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
export const updateViewSortOperationFactory = ({
gqlFields = VIEW_SORT_GQL_FIELDS,
viewSortId,
data = {},
}: {
gqlFields?: string;
viewSortId: string;
data?: object;
}) => ({
query: gql`
mutation UpdateViewSort($input: UpdateViewSortInput!) {
updateViewSort(input: $input) {
${gqlFields}
}
}
`,
variables: {
input: { id: viewSortId, update: data },
},
});

View File

@@ -0,0 +1,31 @@
export const updateWorkspaceMemberRole = async ({
client,
roleId,
workspaceMemberId,
}: {
client: any;
roleId: string;
workspaceMemberId: string;
}) => {
const updateMemberRoleQuery = {
query: `
mutation UpdateWorkspaceMemberRole {
updateWorkspaceMemberRole(
workspaceMemberId: "${workspaceMemberId}"
roleId: "${roleId}"
) {
id
roles {
id
label
}
}
}
`,
};
await client
.post('/metadata')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(updateMemberRoleQuery);
};

View File

@@ -0,0 +1,18 @@
import gql from 'graphql-tag';
import { type UpdateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/update-workspace-input';
export const updateWorkspaceOperationFactory = ({
data,
}: {
data: UpdateWorkspaceInput;
}) => ({
query: gql`
mutation UpdateWorkspace($data: UpdateWorkspaceInput!) {
updateWorkspace(data: $data) {
id
}
}
`,
variables: { data },
});

View File

@@ -0,0 +1,42 @@
import { updateWorkspaceOperationFactory } from 'test/integration/graphql/utils/update-workspace-operation-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type UpdateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/update-workspace-input';
type UpdateWorkspaceUtilArgs = {
data: UpdateWorkspaceInput;
expectToFail?: boolean;
token?: string;
};
export const updateWorkspace = async ({
data,
expectToFail = false,
token,
}: UpdateWorkspaceUtilArgs): CommonResponseBody<{
updateWorkspace: { id: string };
}> => {
const response = await makeMetadataAPIRequest(
updateWorkspaceOperationFactory({ data }),
token,
);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'updateWorkspace should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'updateWorkspace has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,93 @@
import gql from 'graphql-tag';
import request from 'supertest';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { FileFolder } from 'twenty-shared/types';
const createFileUploadMutation = gql`
mutation CreateFileUpload(
$filename: String!
$size: Float!
$fileFolder: FileFolder!
$fieldMetadataId: String
) {
createFileUpload(
filename: $filename
size: $size
fileFolder: $fileFolder
fieldMetadataId: $fieldMetadataId
) {
fileId
uploadUrl
contentType
}
}
`;
const completeFileUploadMutation = gql`
mutation CompleteFileUpload($fileId: String!) {
completeFileUpload(fileId: $fileId) {
id
path
size
createdAt
url
}
}
`;
type UploadFileWithDirectUploadArgs = {
filename: string;
content: Buffer;
fileFolder: keyof typeof FileFolder;
fieldMetadataId?: string;
};
type DirectUploadedFile = {
id: string;
path: string;
size: number;
createdAt: string;
url: string;
};
export const uploadFileWithDirectUpload = async ({
filename,
content,
fileFolder,
fieldMetadataId,
}: UploadFileWithDirectUploadArgs): Promise<DirectUploadedFile> => {
const createResponse = await makeMetadataAPIRequest({
query: createFileUploadMutation,
variables: {
filename,
size: content.length,
fileFolder,
fieldMetadataId,
},
});
expect(createResponse.body.errors).toBeUndefined();
const { fileId, uploadUrl, contentType } =
createResponse.body.data.createFileUpload;
// Integration tests run on the local storage driver, so the upload url
// targets the server's streaming endpoint: replay it against the test app.
const { pathname, search } = new URL(uploadUrl);
const putResponse = await request(global.app.getHttpServer())
.put(`${pathname}${search}`)
.set('Content-Type', contentType)
.send(content);
expect(putResponse.status).toBe(204);
const completeResponse = await makeMetadataAPIRequest({
query: completeFileUploadMutation,
variables: { fileId },
});
expect(completeResponse.body.errors).toBeUndefined();
return completeResponse.body.data.completeFileUpload;
};

View File

@@ -0,0 +1,10 @@
import gql from 'graphql-tag';
export const uploadWorkspaceLogoMutation = gql`
mutation UploadWorkspaceLogo($file: Upload!) {
uploadWorkspaceLogo(file: $file) {
id
url
}
}
`;

View File

@@ -0,0 +1,59 @@
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
import { upsertRowLevelPermissionPredicates } from 'test/integration/metadata/suites/row-level-permission-predicate/utils/upsert-row-level-permission-predicates.util';
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
import { RowLevelPermissionPredicateOperand } from 'twenty-shared/types';
export const upsertContainsRlsPredicate = async ({
roleId,
objectNameSingular,
fieldName,
value,
}: {
roleId: string;
objectNameSingular: string;
fieldName: string;
value: string;
}): Promise<void> => {
const { objects } = await findManyObjectMetadata({
expectToFail: false,
input: { filter: {}, paging: { first: 1000 } },
gqlFields: `
id
nameSingular
fieldsList {
id
name
}
`,
});
jestExpectToBeDefined(objects);
const objectMetadata = objects.find(
(object) => object.nameSingular === objectNameSingular,
);
jestExpectToBeDefined(objectMetadata);
const fieldMetadata = objectMetadata.fieldsList?.find(
(field) => field.name === fieldName,
);
jestExpectToBeDefined(fieldMetadata);
await upsertRowLevelPermissionPredicates({
expectToFail: false,
input: {
roleId,
objectMetadataId: objectMetadata.id,
predicates: [
{
fieldMetadataId: fieldMetadata.id,
operand: RowLevelPermissionPredicateOperand.CONTAINS,
value,
},
],
predicateGroups: [],
},
});
};

View File

@@ -0,0 +1,17 @@
import { upsertFieldPermissionsQueryFactory } from 'test/integration/metadata/suites/field-permission/utils/upsert-field-permissions-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
export const upsertFieldPermissions = async (
input: UpsertFieldPermissionsInput,
) => {
const graphqlOperation = upsertFieldPermissionsQueryFactory({ input });
const response = await makeMetadataAPIRequest(graphqlOperation);
return {
data: response.body.data,
errors: response.body.errors,
};
};

View File

@@ -0,0 +1,39 @@
import gql from 'graphql-tag';
export const createUpsertObjectPermissionsOperation = (
roleId: string,
objectPermissions: Array<{
objectMetadataId: string;
canReadObjectRecords?: boolean;
canUpdateObjectRecords?: boolean;
canSoftDeleteObjectRecords?: boolean;
canDestroyObjectRecords?: boolean;
}>,
selectedFields: string[] = [
'objectMetadataId',
'canReadObjectRecords',
'canUpdateObjectRecords',
'canSoftDeleteObjectRecords',
'canDestroyObjectRecords',
],
) => ({
query: gql`
mutation UpsertObjectPermissions(
$roleId: UUID!
$objectPermissions: [ObjectPermissionInput!]!
) {
upsertObjectPermissions(
upsertObjectPermissionsInput: {
roleId: $roleId
objectPermissions: $objectPermissions
}
) {
${selectedFields.join('\n')}
}
}
`,
variables: {
roleId,
objectPermissions,
},
});

View File

@@ -0,0 +1,55 @@
import gql from 'graphql-tag';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type VerifyTwoFactorAuthenticationMethodDTO } from 'src/engine/core-modules/two-factor-authentication/dto/verify-two-factor-authentication-method.dto';
type VerifyTwoFactorAuthenticationMethodUtilArgs = {
otp: string;
accessToken: string;
expectToFail?: boolean;
};
export const verifyTwoFactorAuthenticationMethod = async ({
otp,
accessToken,
expectToFail,
}: VerifyTwoFactorAuthenticationMethodUtilArgs): CommonResponseBody<{
verifyTwoFactorAuthenticationMethodForAuthenticatedUser: VerifyTwoFactorAuthenticationMethodDTO;
}> => {
const mutation = gql`
mutation VerifyTwoFactorAuthenticationMethodForAuthenticatedUser(
$otp: String!
) {
verifyTwoFactorAuthenticationMethodForAuthenticatedUser(otp: $otp) {
success
}
}
`;
const response = await makeMetadataAPIRequest(
{
query: mutation,
variables: { otp },
},
accessToken,
);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'OTP verification should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'OTP verification has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,55 @@
import {
ViewFilterGroupLogicalOperator,
ViewOpenRecordIn,
ViewSortDirection,
ViewType,
ViewVisibility,
} from 'twenty-shared/types';
import { type ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
import { type ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
export const createViewData = (overrides: Partial<ViewEntity> = {}) => ({
name: 'Test View',
icon: 'IconTable',
type: ViewType.TABLE,
key: null,
position: 0,
isCompact: false,
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
visibility: ViewVisibility.WORKSPACE,
...overrides,
});
export const createViewSortData = (
viewId: string,
overrides: Partial<ViewSortEntity> = {},
) => ({
viewId,
direction: ViewSortDirection.ASC,
...overrides,
});
export const updateViewSortData = (
overrides: Partial<ViewSortEntity> = {},
) => ({
direction: ViewSortDirection.DESC,
...overrides,
});
export const createViewFilterGroupData = (
viewId: string,
overrides: Partial<ViewFilterGroupEntity> = {},
) => ({
viewId,
logicalOperator: ViewFilterGroupLogicalOperator.AND,
...overrides,
});
export const updateViewFilterGroupData = (
overrides: Partial<ViewFilterGroupEntity> = {},
) => ({
logicalOperator: ViewFilterGroupLogicalOperator.OR,
...overrides,
});

View File

@@ -0,0 +1,39 @@
import { createOneView } from 'test/integration/metadata/suites/view/utils/create-one-view.util';
import { type CreateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/create-view.input';
import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { createViewData } from './view-data-factory.util';
export const createTestViewWithGraphQL = async (
overrides: Partial<ViewEntity> = {},
): Promise<ViewEntity> => {
const viewData = createViewData(overrides);
const input: CreateViewInput = {
name: viewData.name,
objectMetadataId: viewData.objectMetadataId as string,
icon: viewData.icon,
type: viewData.type,
position: viewData.position,
mainGroupByFieldMetadataId:
viewData.mainGroupByFieldMetadataId ?? undefined,
isCompact: viewData.isCompact,
openRecordIn: viewData.openRecordIn,
visibility: viewData.visibility,
};
const { data, errors } = await createOneView({
input,
expectToFail: false,
});
if (errors) {
throw new Error(`Failed to create test view: ${JSON.stringify(errors)}`);
}
if (!data) {
throw new Error('No data returned from createTestViewWithGraphQL');
}
return data.createView;
};