Files
twenty/packages/twenty-server/test/integration/graphql/suites/group-by-resolver.integration-spec.ts
T
Thomas Trompette b4e924b671 Sync views and navigation items (#18003)
Both objects are necessary to fully enjoy objects within applications

<img width="770" height="311" alt="Capture d’écran 2026-02-17 à 15 19
43"
src="https://github.com/user-attachments/assets/48c51fa4-63f4-45b2-a40a-df73f3aa79be"
/>
2026-02-17 16:32:41 +01:00

1895 lines
59 KiB
TypeScript

import { randomUUID } from 'crypto';
import { gql } from 'apollo-server-core';
import { default as request } from 'supertest';
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 { createViewFilterGroupOperationFactory } from 'test/integration/graphql/utils/create-view-filter-group-operation-factory.util';
import { deleteRole } from 'test/integration/graphql/utils/delete-one-role.util';
import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-one-operation-factory.util';
import { groupByOperationFactory } from 'test/integration/graphql/utils/group-by-operation-factory.util';
import { makeGraphqlAPIRequestWithMemberRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-member-role.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { updateWorkspaceMemberRole } from 'test/integration/graphql/utils/update-workspace-member-role.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util';
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { createRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-relation-between-objects.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { createOneCoreViewFilter } from 'test/integration/metadata/suites/view-filter/utils/create-one-core-view-filter.util';
import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util';
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
import {
FieldMetadataType,
OrderByDirection,
RelationType,
ViewFilterGroupLogicalOperator,
ViewFilterOperand,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
const client = request(`http://localhost:${APP_PORT}`);
describe('group-by resolver (integration)', () => {
describe('standard case', () => {
const testPersonId = randomUUID();
const testPerson2Id = randomUUID();
const testPerson3Id = randomUUID();
afterEach(async () => {
// cleanup created people
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: 'id',
recordId: testPersonId,
}),
);
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: 'id',
recordId: testPerson2Id,
}),
);
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: 'id',
recordId: testPerson3Id,
}),
);
});
it('groups by city', async () => {
const cityA = 'City A';
const cityB = 'City B';
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: testPersonId, city: cityA },
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: testPerson2Id, city: cityB },
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: testPerson3Id, city: cityB },
}),
);
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [{ city: true }],
orderBy: [{ city: OrderByDirection.AscNullsFirst }], // needed for City groups to be in 300 first groups
limit: 300,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(groups).toEqual(
expect.arrayContaining([
expect.objectContaining({ groupByDimensionValues: [cityA] }),
expect.objectContaining({ groupByDimensionValues: [cityB] }),
]),
);
const groupWithCityA = groups.find(
(group: any) => group.groupByDimensionValues[0] === cityA,
);
expect(groupWithCityA.groupByDimensionValues).toEqual([cityA]);
expect(groupWithCityA.totalCount).toEqual(1);
const groupWithCityB = groups.find(
(group: any) => group.groupByDimensionValues[0] === cityB,
);
expect(groupWithCityB.groupByDimensionValues).toEqual([cityB]);
expect(groupWithCityB.totalCount).toEqual(2);
});
it('limits the number of groups when limit argument is provided', async () => {
const cityA = 'City A';
const cityB = 'City B';
const cityC = 'City C';
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: testPersonId, city: cityA },
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: testPerson2Id, city: cityB },
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: testPerson3Id, city: cityC },
}),
);
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [{ city: true }],
limit: 2,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(Array.isArray(groups)).toBe(true);
expect(groups).toHaveLength(2);
});
it('computes aggregated metrics on date time field', async () => {
const cityA = 'City A';
const cityB = 'City B';
const person1 = (
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: testPersonId, city: cityA },
}),
)
).body.data.createPerson;
const person2 = (
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: testPerson2Id, city: cityB },
}),
)
).body.data.createPerson;
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: testPerson3Id, city: cityB },
}),
);
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [{ city: true }],
orderBy: [{ city: OrderByDirection.AscNullsFirst }], // needed for City groups to be in 300 first groups
gqlFields: 'minCreatedAt',
limit: 300,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(groups).toEqual(
expect.arrayContaining([
expect.objectContaining({ groupByDimensionValues: [cityA] }),
expect.objectContaining({ groupByDimensionValues: [cityB] }),
]),
);
const groupWithCityA = groups.find(
(group: any) => group.groupByDimensionValues[0] === cityA,
);
expect(groupWithCityA.groupByDimensionValues).toEqual([cityA]);
expect(groupWithCityA.minCreatedAt).toEqual(person1.createdAt);
const groupWithCityB = groups.find(
(group: any) => group.groupByDimensionValues[0] === cityB,
);
expect(groupWithCityB.groupByDimensionValues).toEqual([cityB]);
expect(groupWithCityB.minCreatedAt).toEqual(person2.createdAt);
});
});
describe('date range', () => {
const testPersonId = randomUUID();
const testPerson2Id = randomUUID();
const testPerson3Id = randomUUID();
const idJan2 = testPersonId;
const idJan8 = testPerson2Id;
const idMar3 = testPerson3Id;
beforeAll(async () => {
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: {
id: idJan2,
createdAt: '2025-01-02T12:00:00.000Z', // thursday, january, Q1, 2025
},
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: idJan8, createdAt: '2025-01-08T08:00:00.000Z' }, // wednesday, january, Q1, 2025
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: idMar3, createdAt: '2025-03-03T09:30:00.000Z' }, // monday, march, Q1, 2025
}),
);
});
afterAll(async () => {
// cleanup created people
for (const id of [testPersonId, testPerson2Id, testPerson3Id]) {
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: 'id',
recordId: id,
}),
);
}
});
const filter2025 = {
and: [
{
createdAt: {
gte: '2025-01-01T00:00:00.000Z',
},
},
{
createdAt: {
lt: '2025-03-04T00:00:00.000Z',
},
},
],
};
it('datetime field - groups by createdAt MONTH', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [
{ createdAt: { granularity: 'MONTH', timeZone: 'Europe/Paris' } },
],
filter: filter2025,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(Array.isArray(groups)).toBe(true);
expect(groups.length).toBe(2);
const groupWith2Records = groups.find((g: any) => g.totalCount === 2);
const groupWith1Record = groups.find((g: any) => g.totalCount === 1);
expect(groupWith2Records).toBeDefined();
expect(groupWith1Record).toBeDefined();
expect(groupWith2Records.totalCount).toBe(2);
expect(groupWith1Record.totalCount).toBe(1);
});
describe('group by week', () => {
const idMarch1st = randomUUID();
const idMarch2nd = randomUUID();
const idMarch3rd = randomUUID();
beforeAll(async () => {
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: idMarch3rd, createdAt: '2025-03-03T09:30:00.000Z' }, // monday, march, Q1, 2025
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: idMarch2nd, createdAt: '2025-03-02T09:30:00.000Z' }, // sunday, march, Q1, 2025
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: idMarch1st, createdAt: '2025-03-01T09:30:00.000Z' }, // saturday, march, Q1, 2025
}),
);
});
afterAll(async () => {
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: 'id',
recordId: idMarch1st,
}),
);
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: 'id',
recordId: idMarch2nd,
}),
);
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: 'id',
recordId: idMarch3rd,
}),
);
});
it('datetime field - groups by createdAt WEEK with default (MONDAY)', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [
{ createdAt: { granularity: 'WEEK', timeZone: 'Europe/Paris' } },
],
gqlFields: `
edges {
node {
id
}
}
`,
filter: filter2025,
limit: 10,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(groups.length).toBe(4);
// Group starting week of monday dec 30th, 2024
const mondayDec30thGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2024-12-30'),
);
expect(mondayDec30thGroup).toBeDefined();
expect(mondayDec30thGroup.edges[0].node.id).toBe(idJan2);
expect(mondayDec30thGroup.totalCount).toBe(1);
// Group starting week of monday jan 6th, 2025
const mondayJan6thGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2025-01-06'),
);
expect(mondayJan6thGroup).toBeDefined();
expect(mondayJan6thGroup.edges[0].node.id).toBe(idJan8);
expect(mondayJan6thGroup.totalCount).toBe(1);
// Group starting week of monday feb 24th, 2025
const mondayFeb24thGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2025-02-24'),
);
expect(mondayFeb24thGroup).toBeDefined();
expect(mondayFeb24thGroup.edges.length).toBe(2);
expect(
mondayFeb24thGroup.edges.find(
(edge: any) => edge.node.id === idMarch2nd,
),
).toBeDefined;
expect(
mondayFeb24thGroup.edges.find(
(edge: any) => edge.node.id === idMarch1st,
),
).toBeDefined;
// Group starting week of monday march 3rd, 2025
const mondayMarch3rdGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2025-03-03'),
);
expect(mondayMarch3rdGroup).toBeDefined();
expect(mondayMarch3rdGroup.edges.length).toBe(2);
expect(
mondayMarch3rdGroup.edges.find(
(edge: any) => edge.node.id === idMarch3rd,
),
).toBeDefined;
expect(
mondayMarch3rdGroup.edges.find(
(edge: any) => edge.node.id === idMar3,
),
).toBeDefined;
});
it('datetime field - groups by createdAt WEEK with weekStartDay SUNDAY', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [
{
createdAt: {
granularity: 'WEEK',
weekStartDay: 'SUNDAY',
timeZone: 'Europe/Paris',
},
},
],
gqlFields: `
edges {
node {
id
}
}
`,
filter: filter2025,
limit: 10,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(groups.length).toBe(4);
// Group starting week of sunday dec 29th, 2024
const sundayDec29thGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2024-12-29'),
);
expect(sundayDec29thGroup).toBeDefined();
expect(sundayDec29thGroup.totalCount).toBe(1);
expect(
sundayDec29thGroup.edges.find((edge: any) => edge.node.id === idJan2),
).toBeDefined();
// Group starting week of sunday jan 5th, 2025
const sundayJan5thGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2025-01-05'),
);
expect(sundayJan5thGroup).toBeDefined();
expect(sundayJan5thGroup.totalCount).toBe(1);
expect(
sundayJan5thGroup.edges.find((edge: any) => edge.node.id === idJan8),
).toBeDefined();
// Group starting week of sunday feb 23rd, 2025
const sundayFeb23rdGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2025-02-23'),
);
expect(sundayFeb23rdGroup).toBeDefined();
expect(sundayFeb23rdGroup.totalCount).toBe(1);
expect(
sundayFeb23rdGroup.edges.find(
(edge: any) => edge.node.id === idMarch1st,
),
).toBeDefined();
// Group starting week of sunday march 2nd, 2025
const sundayMarch2ndGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2025-03-02'),
);
expect(sundayMarch2ndGroup).toBeDefined();
expect(sundayMarch2ndGroup.totalCount).toBe(3);
expect(
sundayMarch2ndGroup.edges.find(
(edge: any) => edge.node.id === idMarch2nd,
),
).toBeDefined();
expect(
sundayMarch2ndGroup.edges.find(
(edge: any) => edge.node.id === idMarch3rd,
),
).toBeDefined();
expect(
sundayMarch2ndGroup.edges.find(
(edge: any) => edge.node.id === idMar3,
),
).toBeDefined();
});
it('datetime field - groups by createdAt WEEK with weekStartDay SATURDAY', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [
{
createdAt: {
granularity: 'WEEK',
weekStartDay: 'SATURDAY',
timeZone: 'Europe/Paris',
},
},
],
gqlFields: `
edges {
node {
id
}
}
`,
filter: filter2025,
limit: 10,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(groups.length).toBe(3);
// Group starting week of saturday dec 28th, 2024
const saturdayDec28thGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2024-12-28'),
);
expect(saturdayDec28thGroup).toBeDefined();
expect(saturdayDec28thGroup.totalCount).toBe(1);
expect(
saturdayDec28thGroup.edges.find(
(edge: any) => edge.node.id === idJan2,
),
).toBeDefined();
// Group starting week of saturday jan 4th, 2025
const saturdayJan4thGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2025-01-04'),
);
expect(saturdayJan4thGroup).toBeDefined();
expect(saturdayJan4thGroup.totalCount).toBe(1);
expect(
saturdayJan4thGroup.edges.find(
(edge: any) => edge.node.id === idJan8,
),
).toBeDefined();
// Group starting week of saturday march 1st, 2025
const saturdayMarch1stGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2025-03-01'),
);
expect(saturdayMarch1stGroup).toBeDefined();
expect(saturdayMarch1stGroup.totalCount).toBe(4);
expect(
saturdayMarch1stGroup.edges.find(
(edge: any) => edge.node.id === idMarch1st,
),
).toBeDefined();
expect(
saturdayMarch1stGroup.edges.find(
(edge: any) => edge.node.id === idMarch2nd,
),
).toBeDefined();
expect(
saturdayMarch1stGroup.edges.find(
(edge: any) => edge.node.id === idMarch3rd,
),
).toBeDefined();
expect(
saturdayMarch1stGroup.edges.find(
(edge: any) => edge.node.id === idMar3,
),
).toBeDefined();
});
});
describe('cyclic date', () => {
const filter2024And2025 = {
and: [
{
createdAt: {
gte: '2024-01-01T00:00:00.000Z',
},
},
{
createdAt: {
lt: '2025-03-04T00:00:00.000Z',
},
},
],
};
const testPersonId2024 = randomUUID();
beforeAll(async () => {
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: {
id: testPersonId2024,
createdAt: '2024-04-11T12:00:00.000Z', // thursday, april, Q2, 2024
},
}),
);
});
afterAll(async () => {
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: 'id',
recordId: testPersonId2024,
}),
);
});
it('datetime field - groups by createdAt DAY_OF_THE_WEEK', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [{ createdAt: { granularity: 'DAY_OF_THE_WEEK' } }],
// adding a filter for test not to fail when we are in january again
filter: filter2024And2025,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(Array.isArray(groups)).toBe(true);
const thursdayGroup = groups.find((g: any) =>
g.groupByDimensionValues?.[0]?.startsWith?.('Thursday'),
);
const mondayGroup = groups.find((g: any) =>
g.groupByDimensionValues?.[0]?.startsWith?.('Monday'),
);
const wednesdayGroup = groups.find((g: any) =>
g.groupByDimensionValues?.[0]?.startsWith?.('Wednesday'),
);
expect(thursdayGroup).toBeDefined();
expect(mondayGroup).toBeDefined();
expect(wednesdayGroup).toBeDefined();
expect(thursdayGroup.totalCount).toBe(2);
expect(mondayGroup.totalCount).toBe(1);
expect(wednesdayGroup.totalCount).toBe(1);
});
it('datetime field - groups by createdAt MONTH_OF_THE_YEAR', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [{ createdAt: { granularity: 'MONTH_OF_THE_YEAR' } }],
filter: filter2024And2025,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(Array.isArray(groups)).toBe(true);
const janGroup = groups.find((g: any) =>
g.groupByDimensionValues?.[0]?.startsWith?.('January'),
);
const marGroup = groups.find((g: any) =>
g.groupByDimensionValues?.[0]?.startsWith?.('March'),
);
const aprGroup = groups.find((g: any) =>
g.groupByDimensionValues?.[0]?.startsWith?.('April'),
);
expect(janGroup).toBeDefined();
expect(marGroup).toBeDefined();
expect(aprGroup).toBeDefined();
expect(janGroup.totalCount).toBe(2);
expect(marGroup.totalCount).toBe(1);
expect(aprGroup.totalCount).toBe(1);
});
it('datetime field - groups by createdAt QUARTER_OF_THE_YEAR', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [{ createdAt: { granularity: 'QUARTER_OF_THE_YEAR' } }],
filter: filter2024And2025,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(Array.isArray(groups)).toBe(true);
const q1Group = groups.find((g: any) =>
g.groupByDimensionValues?.[0]?.startsWith?.('Q1'),
);
const q2Group = groups.find((g: any) =>
g.groupByDimensionValues?.[0]?.startsWith?.('Q2'),
);
expect(q1Group).toBeDefined();
expect(q2Group).toBeDefined();
expect(q1Group.totalCount).toBe(3);
expect(q2Group.totalCount).toBe(1);
});
});
});
describe('with viewId defined', () => {
const testPersonId = randomUUID();
const testPerson2Id = randomUUID();
const testPerson3Id = randomUUID();
let viewId: string;
let personObjectMetadataId: string;
let personObject: ObjectMetadataDTO & {
fieldsList?: FieldMetadataDTO[];
};
beforeAll(async () => {
const { objects } = await findManyObjectMetadata({
input: {
filter: {},
paging: {
first: 100,
},
},
gqlFields: 'id nameSingular fieldsList { id name }',
expectToFail: false,
});
const person = objects.find((o) => o.nameSingular === 'person');
if (!person || !person.id) {
throw new Error('Person object not found');
}
personObject = person;
personObjectMetadataId = personObject.id;
});
afterEach(async () => {
// cleanup created people
for (const id of [testPersonId, testPerson2Id, testPerson3Id]) {
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: 'id',
recordId: id,
}),
);
}
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'view',
gqlFields: 'id',
recordId: viewId,
}),
);
});
it('groups by city', async () => {
const cityFieldMetadata = personObject?.fieldsList?.find(
(f: FieldMetadataDTO) => f.name === 'city',
);
const cityFieldMetadataId = cityFieldMetadata?.id;
const cityToKeep = 'City To Keep';
const cityToExclude = 'City To Exclude';
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: testPersonId, city: cityToKeep },
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: testPerson2Id, city: cityToExclude },
}),
);
// create a view with a filter: city eq cityToKeep
const { data: createViewData } = await createOneCoreView({
input: {
name: 'People View City Keep',
objectMetadataId: personObjectMetadataId,
icon: 'Icon123',
},
expectToFail: false,
});
viewId = createViewData.createCoreView.id;
// create a filter group and a filter for the view
const viewFilterGroupResponse = await makeMetadataAPIRequest(
createViewFilterGroupOperationFactory({
data: {
viewId,
logicalOperator: ViewFilterGroupLogicalOperator.AND,
positionInViewFilterGroup: 0,
},
}),
);
const viewFilterGroupId = viewFilterGroupResponse.body.data
.createCoreViewFilterGroup.id as string;
jestExpectToBeDefined(cityFieldMetadataId);
await createOneCoreViewFilter({
input: {
viewId,
viewFilterGroupId,
fieldMetadataId: cityFieldMetadataId,
operand: ViewFilterOperand.CONTAINS,
value: cityToKeep,
positionInViewFilterGroup: 0,
},
expectToFail: false,
});
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [{ city: true }],
viewId,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(groups).toEqual(
expect.arrayContaining([
expect.objectContaining({ groupByDimensionValues: [cityToKeep] }),
]),
);
// Ensure excluded city is not present
expect(groups).toEqual(
expect.not.arrayContaining([
expect.objectContaining({ groupByDimensionValues: [cityToExclude] }),
]),
);
});
it('groups by city with any field filter', async () => {
const cityA = 'City A';
const cityB = 'City B';
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: testPersonId, city: cityA },
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: testPerson2Id, city: cityB },
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: testPerson3Id, city: cityB },
}),
);
// create a view with any field filter
const { data: createViewData } = await createOneCoreView({
input: {
name: 'People View City Keep',
objectMetadataId: personObjectMetadataId,
icon: 'Icon123',
anyFieldFilterValue: cityA,
},
expectToFail: false,
});
viewId = createViewData.createCoreView.id;
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [{ city: true }],
viewId,
orderBy: [{ city: OrderByDirection.AscNullsFirst }], // needed for City groups to be in 300 first groups
limit: 300,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(groups).toEqual(
expect.arrayContaining([
expect.objectContaining({ groupByDimensionValues: [cityA] }),
]),
);
// Ensure excluded city is not present
expect(groups).toEqual(
expect.not.arrayContaining([
expect.objectContaining({ groupByDimensionValues: [cityB] }),
]),
);
});
});
describe('relation fields', () => {
describe('group by relation fields', () => {
const testPersonId = randomUUID();
const testPerson2Id = randomUUID();
const testPerson3Id = randomUUID();
const testCompanyId = randomUUID();
const testCompany2Id = randomUUID();
const filter2025 = {
and: [
{
createdAt: {
gte: '2025-01-01T00:00:00.000Z',
},
},
{
createdAt: {
lte: '2025-03-03T23:59:59.999Z',
},
},
],
};
beforeAll(async () => {
// Create companies with different createdAt dates for grouping
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: COMPANY_GQL_FIELDS,
data: {
id: testCompanyId,
name: 'Company A',
createdAt: '2025-01-02T12:00:00.000Z', // Thursday
address: {
addressCity: 'City A',
},
},
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: COMPANY_GQL_FIELDS,
data: {
id: testCompany2Id,
name: 'Company B',
createdAt: '2025-01-08T08:00:00.000Z', // Wednesday
address: {
addressCity: 'City B',
},
},
}),
);
// Create people linked to companies
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: {
id: testPersonId,
companyId: testCompanyId,
createdAt: '2025-03-03T09:30:00.000Z',
},
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: {
id: testPerson2Id,
companyId: testCompanyId,
createdAt: '2025-03-03T09:30:00.000Z',
},
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: {
id: testPerson3Id,
companyId: testCompany2Id,
createdAt: '2025-03-03T09:30:00.000Z',
},
}),
);
});
afterAll(async () => {
// Cleanup people
for (const id of [testPersonId, testPerson2Id, testPerson3Id]) {
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: 'id',
recordId: id,
}),
);
}
// Cleanup companies
for (const id of [testCompanyId, testCompany2Id]) {
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: 'id',
recordId: id,
}),
);
}
});
it('groups by one relation field - company createdAt with DAY_OF_THE_WEEK granularity', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [
{
company: {
createdAt: {
granularity: 'DAY_OF_THE_WEEK',
},
},
},
],
filter: filter2025,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(Array.isArray(groups)).toBe(true);
const thursdayGroup = groups.find((g: any) =>
g.groupByDimensionValues?.[0]?.startsWith?.('Thursday'),
);
const wednesdayGroup = groups.find((g: any) =>
g.groupByDimensionValues?.[0]?.startsWith?.('Wednesday'),
);
expect(thursdayGroup).toBeDefined();
expect(wednesdayGroup).toBeDefined();
expect(thursdayGroup.totalCount).toBe(2);
expect(wednesdayGroup.totalCount).toBe(1);
});
it('groups by one relation field - company createdAt with WEEK granularity and weekStartDay SUNDAY', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [
{
company: {
createdAt: {
granularity: 'WEEK',
weekStartDay: 'SUNDAY',
},
},
},
],
filter: filter2025,
limit: 10,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(Array.isArray(groups)).toBe(true);
// Verify that all records are grouped
const totalCount = groups.reduce(
(sum: number, group: any) => sum + group.totalCount,
0,
);
expect(totalCount).toBe(3);
});
it('groups by two relation fields from the same joined table', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [
{
company: {
createdAt: {
granularity: 'DAY_OF_THE_WEEK',
},
},
},
{
company: {
address: {
addressCity: true,
},
},
},
],
filter: filter2025,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(Array.isArray(groups)).toBe(true);
const thursdayCityAGroup = groups.find(
(g: any) =>
g.groupByDimensionValues?.[0]?.startsWith?.('Thursday') &&
g.groupByDimensionValues?.[1] === 'City A',
);
const wednesdayCityBGroup = groups.find(
(g: any) =>
g.groupByDimensionValues?.[0]?.startsWith?.('Wednesday') &&
g.groupByDimensionValues?.[1] === 'City B',
);
expect(thursdayCityAGroup).toBeDefined();
expect(wednesdayCityBGroup).toBeDefined();
expect(thursdayCityAGroup.totalCount).toBe(2);
expect(wednesdayCityBGroup.totalCount).toBe(1);
});
});
describe('group by relation fields from different tables', () => {
const testPersonId = randomUUID();
const testPerson2Id = randomUUID();
const testPerson3Id = randomUUID();
const testCompanyId = randomUUID();
const testCompany2Id = randomUUID();
const testlistingId = randomUUID();
const testlisting2Id = randomUUID();
let listingObjectMetadataId: string;
let personlistingRelationFieldId: string;
const filter2025 = {
and: [
{
createdAt: {
gte: '2025-01-01T00:00:00.000Z',
},
},
{
createdAt: {
lte: '2025-03-03T23:59:59.999Z',
},
},
],
};
beforeAll(async () => {
// Find person and company object metadata
const { objects } = await findManyObjectMetadata({
input: {
filter: {},
paging: {
first: 100,
},
},
gqlFields: 'id nameSingular',
expectToFail: false,
});
const personObject = objects.find((o) => o.nameSingular === 'person');
const companyObject = objects.find((o) => o.nameSingular === 'company');
if (!personObject?.id || !companyObject?.id) {
throw new Error('Person or Company object not found');
}
// Create listing object
const { data: createListingObjectData } = await createOneObjectMetadata(
{
input: {
nameSingular: 'listing',
namePlural: 'listings',
labelSingular: 'Listing',
labelPlural: 'Listings',
icon: 'IconPaw',
},
expectToFail: false,
},
);
listingObjectMetadataId = createListingObjectData.createOneObject.id;
// Create relation from person to listing
const personlistingRelation = await createRelationBetweenObjects({
objectMetadataId: personObject.id,
targetObjectMetadataId: listingObjectMetadataId,
relationType: RelationType.MANY_TO_ONE,
type: FieldMetadataType.RELATION,
name: 'assignedListing',
});
personlistingRelationFieldId = personlistingRelation.id;
// Create companies
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: COMPANY_GQL_FIELDS,
data: {
id: testCompanyId,
name: 'Company A',
},
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: COMPANY_GQL_FIELDS,
data: {
id: testCompany2Id,
name: 'Company B',
},
}),
);
// Create listings
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'listing',
gqlFields: 'id name',
data: {
id: testlistingId,
name: 'Flat in Paris',
},
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'listing',
gqlFields: 'id name',
data: {
id: testlisting2Id,
name: 'House in Marseille',
},
}),
);
// Create people linked to companies and listings
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: {
id: testPersonId,
companyId: testCompanyId,
assignedListingId: testlistingId,
createdAt: '2025-03-03T09:30:00.000Z',
},
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: {
id: testPerson2Id,
companyId: testCompanyId,
assignedListingId: testlisting2Id,
createdAt: '2025-03-03T09:30:00.000Z',
},
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: {
id: testPerson3Id,
companyId: testCompanyId,
assignedListingId: testlisting2Id,
createdAt: '2025-03-03T09:30:00.000Z',
},
}),
);
});
afterAll(async () => {
// Cleanup people
for (const id of [testPersonId, testPerson2Id, testPerson3Id]) {
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: 'id',
recordId: id,
}),
);
}
// Cleanup listings
for (const id of [testlistingId, testlisting2Id]) {
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'listing',
gqlFields: 'id',
recordId: id,
}),
);
}
// Cleanup company
for (const id of [testCompanyId, testCompany2Id]) {
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: 'id',
recordId: id,
}),
);
}
// Cleanup relation field
if (isDefined(personlistingRelationFieldId)) {
await updateOneFieldMetadata({
input: {
idToUpdate: personlistingRelationFieldId,
updatePayload: {
isActive: false,
},
},
expectToFail: false,
});
await deleteOneFieldMetadata({
input: { idToDelete: personlistingRelationFieldId },
expectToFail: false,
});
}
// Cleanup listing object
if (isDefined(listingObjectMetadataId)) {
await updateOneObjectMetadata({
input: {
idToUpdate: listingObjectMetadataId,
updatePayload: {
isActive: false,
},
},
expectToFail: false,
});
await deleteOneObjectMetadata({
input: { idToDelete: listingObjectMetadataId },
expectToFail: false,
});
}
});
it('groups by two relation fields from different tables', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [
{
company: {
name: true,
},
},
{
assignedListing: {
name: true,
},
},
],
filter: filter2025,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(Array.isArray(groups)).toBe(true);
const companyAFlatInParisGroup = groups.find(
(g: any) =>
g.groupByDimensionValues?.[0] === 'Company A' &&
g.groupByDimensionValues?.[1] === 'Flat in Paris',
);
const companyAHouseInMarseilleGroup = groups.find(
(g: any) =>
g.groupByDimensionValues?.[0] === 'Company A' &&
g.groupByDimensionValues?.[1] === 'House in Marseille',
);
expect(companyAFlatInParisGroup).toBeDefined();
expect(companyAHouseInMarseilleGroup).toBeDefined();
expect(companyAFlatInParisGroup.totalCount).toBe(1);
expect(companyAHouseInMarseilleGroup.totalCount).toBe(2);
});
});
describe('group by morph relation fields', () => {
const testPetId = randomUUID();
const testPet2Id = randomUUID();
const testPet3Id = randomUUID();
const testRocketId = randomUUID();
const testRocket2Id = randomUUID();
const filter2025 = {
and: [
{
createdAt: {
gte: '2025-01-01T00:00:00.000Z',
},
},
{
createdAt: {
lte: '2025-03-03T23:59:59.999Z',
},
},
],
};
beforeAll(async () => {
// Create rockets with different names
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'rocket',
gqlFields: 'id name',
data: {
id: testRocketId,
name: 'Rocket A',
},
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'rocket',
gqlFields: 'id name',
data: {
id: testRocket2Id,
name: 'Rocket B',
},
}),
);
// Create pets linked to rockets via morph relation
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'pet',
gqlFields: 'id name polymorphicOwnerRocket { id name }',
data: {
id: testPetId,
name: 'Pet 1',
polymorphicOwnerRocketId: testRocketId,
createdAt: '2025-03-03T09:30:00.000Z',
},
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'pet',
gqlFields: 'id name polymorphicOwnerRocket { id name }',
data: {
id: testPet2Id,
name: 'Pet 2',
polymorphicOwnerRocketId: testRocketId,
createdAt: '2025-03-03T09:30:00.000Z',
},
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'pet',
gqlFields: 'id name polymorphicOwnerRocket { id name }',
data: {
id: testPet3Id,
name: 'Pet 3',
polymorphicOwnerRocketId: testRocket2Id,
createdAt: '2025-03-03T09:30:00.000Z',
},
}),
);
});
afterAll(async () => {
// Cleanup pets
for (const id of [testPetId, testPet2Id, testPet3Id]) {
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'pet',
gqlFields: 'id',
recordId: id,
}),
);
}
// Cleanup rockets
for (const id of [testRocketId, testRocket2Id]) {
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'rocket',
gqlFields: 'id',
recordId: id,
}),
);
}
});
it('groups by morph relation field - polymorphicOwnerRocket name', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'pet',
objectMetadataPluralName: 'pets',
groupBy: [
{
polymorphicOwnerRocket: {
name: true,
},
},
],
filter: filter2025,
}),
);
const groups = response.body.data.petsGroupBy;
expect(groups).toBeDefined();
expect(Array.isArray(groups)).toBe(true);
const rocketAGroup = groups.find(
(g: any) => g.groupByDimensionValues?.[0] === 'Rocket A',
);
const rocketBGroup = groups.find(
(g: any) => g.groupByDimensionValues?.[0] === 'Rocket B',
);
expect(rocketAGroup).toBeDefined();
expect(rocketBGroup).toBeDefined();
expect(rocketAGroup.totalCount).toBe(2);
expect(rocketBGroup.totalCount).toBe(1);
});
});
describe('should throw if group by a relation field user does not have reading rights on', () => {
const testPetId = randomUUID();
const testRocketId = randomUUID();
let customRoleId: string;
let petObjectId: string;
let rocketObjectId: string;
let originalMemberRoleId: string;
beforeAll(async () => {
// Get the original Member role ID for restoration later
const getRolesQuery = {
query: `
query GetRoles {
getRoles {
id
label
}
}
`,
};
const rolesResponse = await client
.post('/metadata')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(getRolesQuery);
originalMemberRoleId = rolesResponse.body.data.getRoles.find(
(role: any) => role.label === 'Member',
).id;
// Get object metadata IDs for pet and rocket
const { objects } = await findManyObjectMetadata({
input: {
filter: {},
paging: {
first: 100,
},
},
gqlFields: 'id nameSingular',
expectToFail: false,
});
const petObject = objects.find((o) => o.nameSingular === 'pet');
const rocketObject = objects.find((o) => o.nameSingular === 'rocket');
if (!petObject?.id || !rocketObject?.id) {
throw new Error('Pet or Rocket object not found');
}
petObjectId = petObject.id;
rocketObjectId = rocketObject.id;
// Create a custom role with pet read permission but no rocket read permission
const createRoleOperation = {
query: gql`
mutation CreateOneRole {
createOneRole(
createRoleInput: {
label: "PetOnlyRole"
description: "Test role with pet read permission but no rocket read permission"
canUpdateAllSettings: false
canReadAllObjectRecords: false
canUpdateAllObjectRecords: false
canSoftDeleteAllObjectRecords: false
canDestroyAllObjectRecords: false
}
) {
id
label
}
}
`,
};
const createRoleResponse =
await makeMetadataAPIRequest(createRoleOperation);
customRoleId = createRoleResponse.body.data.createOneRole.id;
// Set object permissions: allow reading pets but not rockets
const upsertObjectPermissionsOperation = {
query: gql`
mutation UpsertObjectPermissions(
$roleId: UUID!
$objectPermissions: [ObjectPermissionInput!]!
) {
upsertObjectPermissions(
upsertObjectPermissionsInput: {
roleId: $roleId
objectPermissions: $objectPermissions
}
) {
objectMetadataId
canReadObjectRecords
}
}
`,
variables: {
roleId: customRoleId,
objectPermissions: [
{
objectMetadataId: petObjectId,
canReadObjectRecords: true,
canUpdateObjectRecords: false,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
{
objectMetadataId: rocketObjectId,
canReadObjectRecords: false,
canUpdateObjectRecords: false,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
},
};
await makeMetadataAPIRequest(upsertObjectPermissionsOperation);
// Assign the custom role to a workspace member
await updateWorkspaceMemberRole({
client,
roleId: customRoleId,
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
});
// Create a rocket
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'rocket',
gqlFields: 'id name',
data: {
id: testRocketId,
name: 'Test Rocket',
},
}),
);
// Create a pet linked to the rocket
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'pet',
gqlFields: 'id name polymorphicOwnerRocket { id name }',
data: {
id: testPetId,
name: 'Test Pet',
polymorphicOwnerRocketId: testRocketId,
createdAt: '2025-03-03T09:30:00.000Z',
},
}),
);
});
afterAll(async () => {
// Cleanup pet
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'pet',
gqlFields: 'id',
recordId: testPetId,
}),
);
// Cleanup rocket
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'rocket',
gqlFields: 'id',
recordId: testRocketId,
}),
);
// // Restore original role
const restoreMemberRoleQuery = {
query: `
mutation UpdateWorkspaceMemberRole {
updateWorkspaceMemberRole(
workspaceMemberId: "${WORKSPACE_MEMBER_DATA_SEED_IDS.JONY}"
roleId: "${originalMemberRoleId}"
) {
id
}
}
`,
};
await client
.post('/metadata')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(restoreMemberRoleQuery);
// // Delete custom role
if (isDefined(customRoleId)) {
await deleteRole(client, customRoleId);
}
});
it('should throw a permission error when grouping by polymorphicOwnerRocket field without read permission', async () => {
const filter2025 = {
and: [
{
createdAt: {
gte: '2025-01-01T00:00:00.000Z',
},
},
{
createdAt: {
lte: '2025-03-03T23:59:59.999Z',
},
},
],
};
const response = await makeGraphqlAPIRequestWithMemberRole(
groupByOperationFactory({
objectMetadataSingularName: 'pet',
objectMetadataPluralName: 'pets',
groupBy: [
{
polymorphicOwnerRocket: {
name: true,
},
},
],
filter: filter2025,
}),
);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toBe(
PermissionsExceptionMessage.PERMISSION_DENIED,
);
expect(response.body.errors[0].extensions.code).toBe(
ErrorCode.FORBIDDEN,
);
});
});
});
});