Files
twenty/packages/twenty-server/test/integration/graphql/suites/search/search-resolver.integration-spec.ts
Félix Malfait a47a6be4a8 Improve seeds (#12675)
- Add seeds for notes/tasks
- Adds account manager to companies
- A companies and phone numbers to people
- Add many more opportunities

TODO: add timeline activities

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-06-17 15:25:05 +02:00

604 lines
17 KiB
TypeScript

import { OBJECT_MODEL_COMMON_FIELDS } from 'test/integration/constants/object-model-common-fields';
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
import { TEST_API_KEY_1_ID } from 'test/integration/constants/test-api-key-ids.constant';
import {
TEST_PERSON_1_ID,
TEST_PERSON_2_ID,
TEST_PERSON_3_ID,
} from 'test/integration/constants/test-person-ids.constants';
import {
TEST_PET_ID_1,
TEST_PET_ID_2,
} from 'test/integration/constants/test-pet-ids.constants';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { performCreateManyOperation } from 'test/integration/graphql/utils/perform-create-many-operation.utils';
import { searchFactory } from 'test/integration/graphql/utils/search-factory.util';
import { deleteAllRecords } from 'test/integration/utils/delete-all-records';
import { EachTestingContext } from 'twenty-shared/testing';
import {
decodeCursor,
encodeCursorData,
} from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
import { SearchArgs } from 'src/engine/core-modules/search/dtos/search-args';
import { SearchResultEdgeDTO } from 'src/engine/core-modules/search/dtos/search-result-edge.dto';
import { SearchCursor } from 'src/engine/core-modules/search/services/search.service';
describe('SearchResolver', () => {
const [firstPerson, secondPerson, thirdPerson] = [
{ id: TEST_PERSON_1_ID, name: { firstName: 'searchInput1' } },
{ id: TEST_PERSON_2_ID, name: { firstName: 'searchInput2' } },
{ id: TEST_PERSON_3_ID, name: { firstName: 'searchInput3' } },
];
const [apiKey] = [
{
id: TEST_API_KEY_1_ID,
name: 'record not searchable',
expiresAt: new Date(Date.now()),
},
];
const [firstPet, secondPet] = [
{ id: TEST_PET_ID_1, name: 'searchInput1' },
{ id: TEST_PET_ID_2, name: 'searchInput2' },
];
beforeAll(async () => {
await deleteAllRecords('person');
await deleteAllRecords('company');
await deleteAllRecords('opportunity');
await deleteAllRecords('note');
await deleteAllRecords('task');
await deleteAllRecords('noteTarget');
await deleteAllRecords('taskTarget');
await deleteAllRecords('_pet');
await deleteAllRecords('_surveyResult');
try {
await performCreateManyOperation(
'pet',
'pets',
OBJECT_MODEL_COMMON_FIELDS,
[firstPet, secondPet],
);
await performCreateManyOperation('person', 'people', PERSON_GQL_FIELDS, [
firstPerson,
secondPerson,
thirdPerson,
]);
await performCreateManyOperation(
'apiKey',
'apiKeys',
OBJECT_MODEL_COMMON_FIELDS,
[apiKey],
);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
throw new Error('beforeAll failed');
}
});
const testsUseCases: EachTestingContext<{
input: SearchArgs;
eval: {
orderedRecordIds: string[];
pageInfo: {
hasNextPage: boolean;
decodedEndCursor: SearchCursor | null;
};
};
}>[] = [
{
title:
'should return all records for "isSearchable:true" objects when no search input is provided',
context: {
input: {
searchInput: '',
excludedObjectNameSingulars: ['workspaceMember'],
limit: 50,
},
eval: {
orderedRecordIds: [
firstPerson.id,
secondPerson.id,
thirdPerson.id,
firstPet.id,
secondPet.id,
],
pageInfo: {
hasNextPage: false,
decodedEndCursor: {
lastRanks: { tsRank: 0, tsRankCD: 0 },
lastRecordIdsPerObject: {
person: thirdPerson.id,
pet: secondPet.id,
},
},
},
},
},
},
{
title: 'should return filtered records when search input is provided',
context: {
input: {
searchInput: 'searchInput1',
excludedObjectNameSingulars: ['workspaceMember'],
limit: 50,
},
eval: {
orderedRecordIds: [firstPerson.id, firstPet.id],
pageInfo: {
hasNextPage: false,
decodedEndCursor: {
lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 },
lastRecordIdsPerObject: {
person: firstPerson.id,
pet: firstPet.id,
},
},
},
},
},
},
{
title: 'should return record from included Objects only',
context: {
input: {
searchInput: '',
excludedObjectNameSingulars: ['workspaceMember'],
includedObjectNameSingulars: ['pet'],
limit: 50,
},
eval: {
orderedRecordIds: [firstPet.id, secondPet.id],
pageInfo: {
hasNextPage: false,
decodedEndCursor: {
lastRanks: { tsRank: 0, tsRankCD: 0 },
lastRecordIdsPerObject: {
pet: secondPet.id,
},
},
},
},
},
},
{
title: 'should not return record from excludedObject',
context: {
input: {
searchInput: '',
excludedObjectNameSingulars: ['workspaceMember', 'person'],
limit: 50,
},
eval: {
orderedRecordIds: [firstPet.id, secondPet.id],
pageInfo: {
hasNextPage: false,
decodedEndCursor: {
lastRanks: { tsRank: 0, tsRankCD: 0 },
lastRecordIdsPerObject: {
pet: secondPet.id,
},
},
},
},
},
},
{
title: 'should return filtered records when filter is provided',
context: {
input: {
searchInput: '',
excludedObjectNameSingulars: ['workspaceMember'],
filter: { id: { eq: firstPet.id } },
limit: 50,
},
eval: {
orderedRecordIds: [firstPet.id],
pageInfo: {
hasNextPage: false,
decodedEndCursor: {
lastRanks: { tsRank: 0, tsRankCD: 0 },
lastRecordIdsPerObject: {
pet: firstPet.id,
},
},
},
},
},
},
{
title: 'should limit records number with limit',
context: {
input: {
searchInput: '',
excludedObjectNameSingulars: ['workspaceMember'],
limit: 4,
},
eval: {
orderedRecordIds: [
firstPerson.id,
secondPerson.id,
thirdPerson.id,
firstPet.id,
],
pageInfo: {
hasNextPage: true,
decodedEndCursor: {
lastRanks: { tsRank: 0, tsRankCD: 0 },
lastRecordIdsPerObject: {
pet: firstPet.id,
person: thirdPerson.id,
},
},
},
},
},
},
{
title: 'should return endCursor when paginating',
context: {
input: {
searchInput: '',
excludedObjectNameSingulars: ['workspaceMember'],
limit: 2,
},
eval: {
orderedRecordIds: [firstPerson.id, secondPerson.id],
pageInfo: {
hasNextPage: true,
decodedEndCursor: {
lastRanks: { tsRank: 0, tsRankCD: 0 },
lastRecordIdsPerObject: {
person: secondPerson.id,
},
},
},
},
},
},
{
title: 'should return endCursor when paginating with Cursor',
context: {
input: {
searchInput: '',
excludedObjectNameSingulars: ['workspaceMember'],
after: encodeCursorData({
lastRanks: { tsRank: 0, tsRankCD: 0 },
lastRecordIdsPerObject: {
person: secondPerson.id,
},
}),
limit: 2,
},
eval: {
orderedRecordIds: [thirdPerson.id, firstPet.id],
pageInfo: {
hasNextPage: true,
decodedEndCursor: {
lastRanks: { tsRank: 0, tsRankCD: 0 },
lastRecordIdsPerObject: {
pet: firstPet.id,
person: thirdPerson.id,
},
},
},
},
},
},
{
title: 'should limit records number with limit and searchInput',
context: {
input: {
searchInput: 'searchInput',
excludedObjectNameSingulars: ['workspaceMember'],
limit: 4,
},
eval: {
orderedRecordIds: [
firstPerson.id,
secondPerson.id,
thirdPerson.id,
firstPet.id,
],
pageInfo: {
hasNextPage: true,
decodedEndCursor: {
lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 },
lastRecordIdsPerObject: {
pet: firstPet.id,
person: thirdPerson.id,
},
},
},
},
},
},
{
title: 'should return endCursor when paginating with searchInput',
context: {
input: {
searchInput: 'searchInput',
excludedObjectNameSingulars: ['workspaceMember'],
limit: 2,
},
eval: {
orderedRecordIds: [firstPerson.id, secondPerson.id],
pageInfo: {
hasNextPage: true,
decodedEndCursor: {
lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 },
lastRecordIdsPerObject: {
person: secondPerson.id,
},
},
},
},
},
},
{
title:
'should return endCursor when paginating with searchInput with Cursor',
context: {
input: {
searchInput: 'searchInput',
excludedObjectNameSingulars: ['workspaceMember'],
after: encodeCursorData({
lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 },
lastRecordIdsPerObject: {
person: secondPerson.id,
},
}),
limit: 2,
},
eval: {
orderedRecordIds: [thirdPerson.id, firstPet.id],
pageInfo: {
hasNextPage: true,
decodedEndCursor: {
lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 },
lastRecordIdsPerObject: {
pet: firstPet.id,
person: thirdPerson.id,
},
},
},
},
},
},
{
title:
'should return endCursor when paginating with searchInput with Cursor and filter',
context: {
input: {
searchInput: 'searchInput',
excludedObjectNameSingulars: ['workspaceMember'],
after: encodeCursorData({
lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 },
lastRecordIdsPerObject: {
person: secondPerson.id,
},
}),
limit: 2,
filter: { id: { neq: firstPet.id } },
},
eval: {
orderedRecordIds: [thirdPerson.id, secondPet.id],
pageInfo: {
hasNextPage: false,
decodedEndCursor: {
lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 },
lastRecordIdsPerObject: {
person: thirdPerson.id,
pet: secondPet.id,
},
},
},
},
},
},
{
title: 'should paginate properly with excludedObject',
context: {
input: {
searchInput: '',
excludedObjectNameSingulars: ['workspaceMember', 'person'],
limit: 1,
},
eval: {
orderedRecordIds: [firstPet.id],
pageInfo: {
hasNextPage: true,
decodedEndCursor: {
lastRanks: { tsRank: 0, tsRankCD: 0 },
lastRecordIdsPerObject: {
pet: firstPet.id,
},
},
},
},
},
},
{
title: 'should paginate properly with included Objects only',
context: {
input: {
searchInput: '',
excludedObjectNameSingulars: ['workspaceMember'],
includedObjectNameSingulars: ['pet'],
limit: 1,
},
eval: {
orderedRecordIds: [firstPet.id],
pageInfo: {
hasNextPage: true,
decodedEndCursor: {
lastRanks: { tsRank: 0, tsRankCD: 0 },
lastRecordIdsPerObject: {
pet: firstPet.id,
},
},
},
},
},
},
{
title: 'should paginate properly when no records are returned',
context: {
input: {
searchInput: '',
excludedObjectNameSingulars: ['workspaceMember'],
limit: 0,
},
eval: {
orderedRecordIds: [],
pageInfo: {
hasNextPage: true,
decodedEndCursor: null,
},
},
},
},
];
it.each(testsUseCases)('$title', async ({ context }) => {
const graphqlOperation = searchFactory(context.input);
const response = await makeGraphqlAPIRequest(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.search).toBeDefined();
const search = response.body.data.search;
const edges = search.edges;
const pageInfo = search.pageInfo;
context.eval.orderedRecordIds.length > 0
? expect(edges).not.toHaveLength(0)
: expect(edges).toHaveLength(0);
expect(
edges.map((edge: SearchResultEdgeDTO) => edge.node.recordId),
).toEqual(context.eval.orderedRecordIds);
expect(pageInfo).toBeDefined();
expect(context.eval.pageInfo.hasNextPage).toEqual(pageInfo.hasNextPage);
expect(context.eval.pageInfo.decodedEndCursor).toEqual(
pageInfo.endCursor
? decodeCursor(pageInfo.endCursor)
: pageInfo.endCursor,
);
});
it('should return cursor for each search edge', async () => {
const graphqlOperation = searchFactory({
searchInput: 'searchInput',
excludedObjectNameSingulars: ['workspaceMember'],
limit: 2,
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
const expectedResult = {
edges: [
{
cursor: encodeCursorData({
lastRanks: { tsRankCD: 0.1, tsRank: 0.06079271 },
lastRecordIdsPerObject: {
person: firstPerson.id,
},
}),
},
{
cursor: encodeCursorData({
lastRanks: { tsRankCD: 0.1, tsRank: 0.06079271 },
lastRecordIdsPerObject: {
person: secondPerson.id,
},
}),
},
],
pageInfo: {
hasNextPage: true,
endCursor: encodeCursorData({
lastRanks: { tsRankCD: 0.1, tsRank: 0.06079271 },
lastRecordIdsPerObject: {
person: secondPerson.id,
},
}),
},
};
expect({
...response.body.data.search,
edges: response.body.data.search.edges.map(
(edge: SearchResultEdgeDTO) => ({
cursor: edge.cursor,
}),
),
}).toEqual(expectedResult);
});
it('should return cursor for each search edge with after cursor input', async () => {
const graphqlOperation = searchFactory({
searchInput: 'searchInput',
excludedObjectNameSingulars: ['workspaceMember'],
limit: 2,
after: encodeCursorData({
lastRanks: { tsRankCD: 0.1, tsRank: 0.06079271 },
lastRecordIdsPerObject: {
person: secondPerson.id,
},
}),
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
const expectedResult = {
edges: [
{
cursor: encodeCursorData({
lastRanks: { tsRankCD: 0.1, tsRank: 0.06079271 },
lastRecordIdsPerObject: {
person: thirdPerson.id,
},
}),
},
{
cursor: encodeCursorData({
lastRanks: { tsRankCD: 0.1, tsRank: 0.06079271 },
lastRecordIdsPerObject: {
person: thirdPerson.id,
pet: firstPet.id,
},
}),
},
],
pageInfo: {
hasNextPage: true,
endCursor: encodeCursorData({
lastRanks: { tsRankCD: 0.1, tsRank: 0.06079271 },
lastRecordIdsPerObject: {
person: thirdPerson.id,
pet: firstPet.id,
},
}),
},
};
expect({
...response.body.data.search,
edges: response.body.data.search.edges.map(
(edge: SearchResultEdgeDTO) => ({
cursor: edge.cursor,
}),
),
}).toEqual(expectedResult);
});
});