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 3s

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,276 @@
import gql from 'graphql-tag';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { getAppProviderByClassName } from 'test/integration/utils/get-app-provider-by-class-name.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
type ObjectPage = {
data: {
objects: {
edges: {
cursor: string;
node: {
id: string;
fields: {
edges: {
cursor: string;
node: { id: string; objectMetadataId: string; isUnique: boolean };
}[];
pageInfo: {
hasNextPage: boolean;
hasPreviousPage: boolean;
};
};
indexMetadatas: {
edges: { node: { id: string } }[];
};
};
}[];
pageInfo: {
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
endCursor: string | null;
};
};
} | null;
errors?: { message: string }[];
};
const OBJECTS_WITH_FIELDS_QUERY = gql`
query ObjectsWithFields($objectPaging: CursorPaging!) {
objects(filter: { isActive: { is: true } }, paging: $objectPaging) {
edges {
cursor
node {
id
fields(paging: { first: 1 }, filter: { isActive: { is: true } }) {
edges {
cursor
node {
id
objectMetadataId
isUnique
}
}
pageInfo {
hasNextPage
hasPreviousPage
}
}
indexMetadatas(paging: { first: 1 }) {
edges {
node {
id
}
}
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`;
describe('metadata list pagination', () => {
const queryObjects = async (paging: Record<string, unknown>) => {
const response = await makeMetadataAPIRequest({
query: OBJECTS_WITH_FIELDS_QUERY,
variables: { objectPaging: paging },
});
return response.body as ObjectPage;
};
it('paginates top-level metadata forward and backward with opaque cursors', async () => {
const firstPage = await queryObjects({ first: 1 });
expect(firstPage.errors).toBeUndefined();
expect(firstPage.data?.objects.edges).toHaveLength(1);
expect(firstPage.data?.objects.pageInfo).toMatchObject({
hasNextPage: true,
hasPreviousPage: false,
});
const firstPageEndCursor = firstPage.data?.objects.pageInfo.endCursor;
expect(firstPageEndCursor).toBeTruthy();
const secondPage = await queryObjects({
first: 1,
after: firstPageEndCursor,
});
expect(secondPage.errors).toBeUndefined();
expect(secondPage.data?.objects.edges).toHaveLength(1);
expect(secondPage.data?.objects.pageInfo.hasPreviousPage).toBe(true);
expect(secondPage.data?.objects.edges[0].node.id).not.toBe(
firstPage.data?.objects.edges[0].node.id,
);
const secondPageStartCursor = secondPage.data?.objects.pageInfo.startCursor;
const backPage = await queryObjects({
last: 1,
before: secondPageStartCursor,
});
expect(backPage.errors).toBeUndefined();
expect(backPage.data?.objects.edges[0].node.id).toBe(
firstPage.data?.objects.edges[0].node.id,
);
expect(backPage.data?.objects.pageInfo.hasNextPage).toBe(true);
});
it('batches bounded nested pagination independently for every parent', async () => {
const cacheService =
getAppProviderByClassName<WorkspaceManyOrAllFlatEntityMapsCacheService>(
'WorkspaceManyOrAllFlatEntityMapsCacheService',
);
const cacheSpy = jest.spyOn(
cacheService,
'getOrRecomputeManyOrAllFlatEntityMaps',
);
try {
const page = await queryObjects({ first: 2 });
expect(page.errors).toBeUndefined();
expect(page.data?.objects.edges).toHaveLength(2);
for (const { node: objectMetadata } of page.data?.objects.edges ?? []) {
expect(objectMetadata.fields.edges).toHaveLength(1);
expect(objectMetadata.fields.edges[0].node.objectMetadataId).toBe(
objectMetadata.id,
);
expect(objectMetadata.fields.edges[0].cursor).toBeTruthy();
expect(objectMetadata.fields.pageInfo.hasPreviousPage).toBe(false);
}
const fieldBatchCalls = cacheSpy.mock.calls.filter(([parameters]) =>
parameters.flatMapsKeys?.includes('flatFieldMetadataMaps'),
);
const indexBatchCalls = cacheSpy.mock.calls.filter(([parameters]) =>
parameters.flatMapsKeys?.includes('flatIndexMaps'),
);
expect(fieldBatchCalls).toHaveLength(1);
expect(indexBatchCalls).toHaveLength(1);
} finally {
cacheSpy.mockRestore();
}
});
it('hydrates isUnique in top-level and nested field connections', async () => {
const fieldsResponse = await makeMetadataAPIRequest({
query: gql`
query UniqueFields {
fields(filter: { isActive: { is: true } }, paging: { first: 1000 }) {
edges {
node {
id
objectMetadataId
isUnique
}
}
}
}
`,
});
const uniqueField = fieldsResponse.body.data.fields.edges
.map(
({ node }: { node: Record<string, unknown> }) =>
node as {
id: string;
objectMetadataId: string;
isUnique: boolean;
},
)
.find(({ isUnique }: { isUnique: boolean }) => isUnique);
expect(fieldsResponse.body.errors).toBeUndefined();
expect(uniqueField).toBeDefined();
const objectResponse = await makeMetadataAPIRequest({
query: gql`
query NestedUniqueField($fieldId: UUID!, $objectId: UUID!) {
field(id: $fieldId) {
id
isUnique
}
object(id: $objectId) {
fields(filter: { id: { eq: $fieldId } }, paging: { first: 1 }) {
edges {
node {
id
isUnique
}
}
}
indexMetadatas(paging: { first: 1 }) {
edges {
node {
id
}
}
}
}
}
`,
variables: {
fieldId: uniqueField?.id,
objectId: uniqueField?.objectMetadataId,
},
});
expect(objectResponse.body.errors).toBeUndefined();
expect(objectResponse.body.data.field).toEqual({
id: uniqueField?.id,
isUnique: true,
});
expect(objectResponse.body.data.object.fields.edges[0].node).toEqual({
id: uniqueField?.id,
isUnique: true,
});
expect(
objectResponse.body.data.object.indexMetadatas.edges[0].node.id,
).toBeTruthy();
});
it('rejects malformed cursor IDs and invalid UUID operators as user input', async () => {
const malformedCursor = Buffer.from(
JSON.stringify({ id: 'not-a-uuid' }),
).toString('base64');
const malformedCursorResponse = await queryObjects({
first: 1,
after: malformedCursor,
});
expect(malformedCursorResponse.data).toBeNull();
expect(malformedCursorResponse.errors?.[0].message).toContain(
'Invalid cursor',
);
const invalidFilterResponse = await makeMetadataAPIRequest({
query: gql`
query InvalidUuidFilter {
objects(filter: { id: { is: true } }, paging: { first: 1 }) {
edges {
node {
id
}
}
}
}
`,
});
expect(invalidFilterResponse.body.data).toBeNull();
expect(invalidFilterResponse.body.errors[0].message).toContain(
'UUID is/isNot comparisons only support null values',
);
});
});