- 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>
604 lines
17 KiB
TypeScript
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);
|
|
});
|
|
});
|