Enables phone number search in the global search (Command Menu) for person records. (#14636)
## Changes Made - Added phone fields to search indexing: Extended searchable field types to include `FieldMetadataType.PHONES` - Updated person entity search configuration: Added `phones` to the fields indexed for person records - Enhanced search format support: Phone numbers are now indexed in multiple formats: - Raw number: `2071234567` - International with plus: `+442071234567` - International without plus: `442071234567` - Optimized for phone data: Removed unnecessary text processing (e.g. unaccenting) for numeric phone fields - Created workspace migration: New command to regenerate search vectors for existing workspaces ## Technical Details The implementation modifies PostgreSQL `tsvector` generation to index both `primaryPhoneNumber` and `primaryPhoneCallingCode` fields, combining them into international formats. This enables users to search phone numbers using the formats they naturally type. ### Modified Files - `is-searchable-field.util.ts` – Added `PHONES` to searchable types - `person.workspace-entity.ts` – Included `phones` in person search fields - `get-ts-vector-column-expression.util.ts` – Enhanced expression generation to support multiple phone number formats - `is-searchable-subfield.util.ts` – Added subfield filtering logic for phone fields ## Testing - **Unit tests**: Validated `tsvector` expression generation and phone-specific logic - **Integration tests**: Covered phone search scenarios across multiple formats ## Migration Includes the `upgrade:1-7:regenerate-person-search-vector-with-phones` command, which safely updates existing workspaces by dropping and recreating search vectors with phone indexing support. ## Note Frontend and Backend are both storing normalized phone numbers, as they should. The issue turned out to be with the seed file instead, which contained outdated records. I relied on the database as the source of truth without testing via the creation of a new record and it was an incorrect evaluation on my part. Note taken, I will be more comprehensive with my analysis from here on since I now understand I must check comprehensively before reaching a conclusion.
This commit is contained in:
+118
@@ -0,0 +1,118 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
||||
import { SEARCH_FIELDS_FOR_PERSON } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-7:regenerate-person-search-vector-with-phones',
|
||||
description:
|
||||
'Regenerate person search vector to include phone number indexing for existing workspaces',
|
||||
})
|
||||
export class RegeneratePersonSearchVectorWithPhonesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace)
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
|
||||
this.logger.log(
|
||||
`Regenerating person search vector for workspace ${workspaceId} in schema ${schemaName}`,
|
||||
);
|
||||
|
||||
const personTableExists = await this.coreDataSource.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = '${schemaName}'
|
||||
AND table_name = 'person'
|
||||
);
|
||||
`);
|
||||
|
||||
if (!personTableExists[0]?.exists) {
|
||||
this.logger.log(
|
||||
`Person table does not exist in workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const searchVectorColumnExists = await this.coreDataSource.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.columns
|
||||
WHERE table_schema = '${schemaName}'
|
||||
AND table_name = 'person'
|
||||
AND column_name = 'searchVector'
|
||||
);
|
||||
`);
|
||||
|
||||
if (!searchVectorColumnExists[0]?.exists) {
|
||||
this.logger.log(
|
||||
`searchVector column does not exist in workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newSearchVectorExpression = getTsVectorColumnExpressionFromFields(
|
||||
SEARCH_FIELDS_FOR_PERSON,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Dropping existing searchVector column for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.coreDataSource.query(`
|
||||
ALTER TABLE "${schemaName}"."person"
|
||||
DROP COLUMN "searchVector"
|
||||
`);
|
||||
|
||||
this.logger.log(
|
||||
`Creating new searchVector column with phone indexing for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.coreDataSource.query(`
|
||||
ALTER TABLE "${schemaName}"."person"
|
||||
ADD COLUMN "searchVector" tsvector
|
||||
GENERATED ALWAYS AS (${newSearchVectorExpression}) STORED
|
||||
`);
|
||||
|
||||
this.logger.log(
|
||||
`Recreating GIN index on searchVector for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.coreDataSource.query(`
|
||||
CREATE INDEX "IDX_person_searchVector"
|
||||
ON "${schemaName}"."person"
|
||||
USING GIN ("searchVector")
|
||||
`);
|
||||
|
||||
this.logger.log(
|
||||
`Successfully regenerated person search vector for workspace ${workspaceId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to regenerate person search vector for workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { RegeneratePersonSearchVectorWithPhonesCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-regenerate-person-search-vector-with-phones.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Workspace]), WorkspaceDataSourceModule],
|
||||
providers: [RegeneratePersonSearchVectorWithPhonesCommand],
|
||||
exports: [RegeneratePersonSearchVectorWithPhonesCommand],
|
||||
})
|
||||
export class V1_7_UpgradeVersionCommandModule {}
|
||||
+2
@@ -8,6 +8,7 @@ import { V1_2_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-
|
||||
import { V1_3_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-3/1-3-upgrade-version-command.module';
|
||||
import { V1_5_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-5/1-5-upgrade-version-command.module';
|
||||
import { V1_6_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-6/1-6-upgrade-version-command.module';
|
||||
import { V1_7_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-7/1-7-upgrade-version-command.module';
|
||||
import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
||||
@@ -22,6 +23,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
|
||||
V1_3_UpgradeVersionCommandModule,
|
||||
V1_5_UpgradeVersionCommandModule,
|
||||
V1_6_UpgradeVersionCommandModule,
|
||||
V1_7_UpgradeVersionCommandModule,
|
||||
WorkspaceSyncMetadataModule,
|
||||
],
|
||||
providers: [UpgradeCommand],
|
||||
|
||||
+1200
-1200
File diff suppressed because it is too large
Load Diff
+40
@@ -12,6 +12,7 @@ const nameFullNameField = {
|
||||
};
|
||||
const jobTitleTextField = { name: 'jobTitle', type: FieldMetadataType.TEXT };
|
||||
const emailsEmailsField = { name: 'emails', type: FieldMetadataType.EMAILS };
|
||||
const phonesPhonesField = { name: 'phones', type: FieldMetadataType.PHONES };
|
||||
|
||||
describe('getTsVectorColumnExpressionFromFields', () => {
|
||||
it('should generate correct expression for simple text field', () => {
|
||||
@@ -69,4 +70,43 @@ describe('getTsVectorColumnExpressionFromFields', () => {
|
||||
"to_tsvector('simple', COALESCE(public.unaccent_immutable(\"bodyV2Markdown\"), ''))",
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle phone fields without unaccenting', () => {
|
||||
const fields = [phonesPhonesField] as FieldTypeAndNameMetadata[];
|
||||
const result = getTsVectorColumnExpressionFromFields(fields);
|
||||
|
||||
expect(result).toContain('COALESCE("phonesPrimaryPhoneNumber", \'\')');
|
||||
expect(result).toContain('COALESCE("phonesPrimaryPhoneCallingCode", \'\')');
|
||||
expect(result).not.toContain('unaccent_immutable');
|
||||
});
|
||||
|
||||
it('should generate international format expressions for phone fields', () => {
|
||||
const fields = [phonesPhonesField] as FieldTypeAndNameMetadata[];
|
||||
const result = getTsVectorColumnExpressionFromFields(fields);
|
||||
|
||||
expect(result).toContain(
|
||||
'COALESCE("phonesPrimaryPhoneCallingCode" || "phonesPrimaryPhoneNumber", \'\')',
|
||||
);
|
||||
expect(result).toContain(
|
||||
"COALESCE(REPLACE(\"phonesPrimaryPhoneCallingCode\", '+', '') || \"phonesPrimaryPhoneNumber\", '')",
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate trunk prefix format expression for phone fields', () => {
|
||||
const fields = [phonesPhonesField] as FieldTypeAndNameMetadata[];
|
||||
const result = getTsVectorColumnExpressionFromFields(fields);
|
||||
|
||||
expect(result).toContain(
|
||||
"COALESCE('0' || \"phonesPrimaryPhoneNumber\", '')",
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly index phone subfields', () => {
|
||||
const fields = [phonesPhonesField] as FieldTypeAndNameMetadata[];
|
||||
const result = getTsVectorColumnExpressionFromFields(fields);
|
||||
|
||||
expect(result).toContain('phonesPrimaryPhoneNumber');
|
||||
expect(result).toContain('phonesPrimaryPhoneCallingCode');
|
||||
expect(result).not.toContain('phonesAdditionalPhones');
|
||||
});
|
||||
});
|
||||
|
||||
+19
-1
@@ -55,7 +55,7 @@ const getColumnExpressionsFromField = (
|
||||
);
|
||||
}
|
||||
|
||||
return compositeType.properties
|
||||
const baseExpressions = compositeType.properties
|
||||
.filter((property) =>
|
||||
isSearchableSubfield(compositeType.type, property.type, property.name),
|
||||
)
|
||||
@@ -67,6 +67,21 @@ const getColumnExpressionsFromField = (
|
||||
|
||||
return getColumnExpression(columnName, fieldMetadataTypeAndName.type);
|
||||
});
|
||||
|
||||
if (fieldMetadataTypeAndName.type === FieldMetadataType.PHONES) {
|
||||
const phoneNumberColumn = `"${fieldMetadataTypeAndName.name}PrimaryPhoneNumber"`;
|
||||
const callingCodeColumn = `"${fieldMetadataTypeAndName.name}PrimaryPhoneCallingCode"`;
|
||||
|
||||
const internationalFormats = [
|
||||
`COALESCE(${callingCodeColumn} || ${phoneNumberColumn}, '')`,
|
||||
`COALESCE(REPLACE(${callingCodeColumn}, '+', '') || ${phoneNumberColumn}, '')`,
|
||||
`COALESCE('0' || ${phoneNumberColumn}, '')`,
|
||||
];
|
||||
|
||||
return [...baseExpressions, ...internationalFormats];
|
||||
}
|
||||
|
||||
return baseExpressions;
|
||||
}
|
||||
const columnName = computeColumnName(fieldMetadataTypeAndName.name);
|
||||
|
||||
@@ -85,6 +100,9 @@ const getColumnExpression = (
|
||||
COALESCE(public.unaccent_immutable(${quotedColumnName}), '') || ' ' ||
|
||||
COALESCE(public.unaccent_immutable(SPLIT_PART(${quotedColumnName}, '@', 2)), '')`;
|
||||
|
||||
case FieldMetadataType.PHONES:
|
||||
return `COALESCE(${quotedColumnName}, '')`;
|
||||
|
||||
default:
|
||||
return `COALESCE(public.unaccent_immutable(${quotedColumnName}), '')`;
|
||||
}
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ const SEARCHABLE_FIELD_TYPES = [
|
||||
FieldMetadataType.EMAILS,
|
||||
FieldMetadataType.ADDRESS,
|
||||
FieldMetadataType.LINKS,
|
||||
FieldMetadataType.PHONES,
|
||||
FieldMetadataType.RICH_TEXT,
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
] as const;
|
||||
|
||||
+4
@@ -11,6 +11,10 @@ export const isSearchableSubfield = (
|
||||
switch (compositeFieldMetadataType) {
|
||||
case FieldMetadataType.RICH_TEXT_V2:
|
||||
return ['markdown'].includes(subFieldName);
|
||||
case FieldMetadataType.PHONES:
|
||||
return ['primaryPhoneNumber', 'primaryPhoneCallingCode'].includes(
|
||||
subFieldName,
|
||||
);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -44,11 +44,13 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o
|
||||
|
||||
const NAME_FIELD_NAME = 'name';
|
||||
const EMAILS_FIELD_NAME = 'emails';
|
||||
const PHONES_FIELD_NAME = 'phones';
|
||||
const JOB_TITLE_FIELD_NAME = 'jobTitle';
|
||||
|
||||
export const SEARCH_FIELDS_FOR_PERSON: FieldTypeAndNameMetadata[] = [
|
||||
{ name: NAME_FIELD_NAME, type: FieldMetadataType.FULL_NAME },
|
||||
{ name: EMAILS_FIELD_NAME, type: FieldMetadataType.EMAILS },
|
||||
{ name: PHONES_FIELD_NAME, type: FieldMetadataType.PHONES },
|
||||
{ name: JOB_TITLE_FIELD_NAME, type: FieldMetadataType.TEXT },
|
||||
];
|
||||
|
||||
|
||||
+224
-2
@@ -36,14 +36,35 @@ import { type SearchCursor } from 'src/engine/core-modules/search/services/searc
|
||||
|
||||
describe('SearchResolver', () => {
|
||||
const persons = [
|
||||
{ id: TEST_PERSON_1_ID, name: { firstName: 'searchInput1' } },
|
||||
{ id: TEST_PERSON_2_ID, name: { firstName: 'searchInput2' } },
|
||||
{
|
||||
id: TEST_PERSON_1_ID,
|
||||
name: { firstName: 'searchInput1' },
|
||||
phones: {
|
||||
primaryPhoneNumber: '2071234567',
|
||||
primaryPhoneCallingCode: '+44',
|
||||
primaryPhoneCountryCode: 'GB',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: TEST_PERSON_2_ID,
|
||||
name: { firstName: 'searchInput2' },
|
||||
phones: {
|
||||
primaryPhoneNumber: '5551234567',
|
||||
primaryPhoneCallingCode: '+1',
|
||||
primaryPhoneCountryCode: 'US',
|
||||
},
|
||||
},
|
||||
{ id: TEST_PERSON_3_ID, name: { firstName: 'searchInput3' } },
|
||||
{
|
||||
id: TEST_PERSON_4_ID,
|
||||
name: { firstName: 'José', lastName: 'García' },
|
||||
jobTitle: 'Café Manager',
|
||||
emails: { primaryEmail: 'josé@café.com' },
|
||||
phones: {
|
||||
primaryPhoneNumber: '123456789',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: TEST_PERSON_5_ID,
|
||||
@@ -684,6 +705,207 @@ describe('SearchResolver', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should find person by raw phone number',
|
||||
context: {
|
||||
input: {
|
||||
searchInput: '2071234567',
|
||||
excludedObjectNameSingulars: ['workspaceMember'],
|
||||
limit: 50,
|
||||
},
|
||||
eval: {
|
||||
orderedRecordIds: [searchInput1Person.id],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
decodedEndCursor: {
|
||||
lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 },
|
||||
lastRecordIdsPerObject: {
|
||||
person: searchInput1Person.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should find person by international phone number with plus',
|
||||
context: {
|
||||
input: {
|
||||
searchInput: '+442071234567',
|
||||
excludedObjectNameSingulars: ['workspaceMember'],
|
||||
limit: 50,
|
||||
},
|
||||
eval: {
|
||||
orderedRecordIds: [searchInput1Person.id],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
decodedEndCursor: {
|
||||
lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 },
|
||||
lastRecordIdsPerObject: {
|
||||
person: searchInput1Person.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should find person by international phone number without plus',
|
||||
context: {
|
||||
input: {
|
||||
searchInput: '442071234567',
|
||||
excludedObjectNameSingulars: ['workspaceMember'],
|
||||
limit: 50,
|
||||
},
|
||||
eval: {
|
||||
orderedRecordIds: [searchInput1Person.id],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
decodedEndCursor: {
|
||||
lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 },
|
||||
lastRecordIdsPerObject: {
|
||||
person: searchInput1Person.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should find person by trunk prefix phone number (UK)',
|
||||
context: {
|
||||
input: {
|
||||
searchInput: '02071234567',
|
||||
excludedObjectNameSingulars: ['workspaceMember'],
|
||||
limit: 50,
|
||||
},
|
||||
eval: {
|
||||
orderedRecordIds: [searchInput1Person.id],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
decodedEndCursor: {
|
||||
lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 },
|
||||
lastRecordIdsPerObject: {
|
||||
person: searchInput1Person.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should find person by trunk prefix phone number (France)',
|
||||
context: {
|
||||
input: {
|
||||
searchInput: '0123456789',
|
||||
excludedObjectNameSingulars: ['workspaceMember'],
|
||||
limit: 50,
|
||||
},
|
||||
eval: {
|
||||
orderedRecordIds: [josePerson.id],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
decodedEndCursor: {
|
||||
lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 },
|
||||
lastRecordIdsPerObject: {
|
||||
person: josePerson.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should find person by US phone number (raw national)',
|
||||
context: {
|
||||
input: {
|
||||
searchInput: '5551234567',
|
||||
excludedObjectNameSingulars: ['workspaceMember'],
|
||||
limit: 50,
|
||||
},
|
||||
eval: {
|
||||
orderedRecordIds: [searchInput2Person.id],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
decodedEndCursor: {
|
||||
lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 },
|
||||
lastRecordIdsPerObject: {
|
||||
person: searchInput2Person.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should find person by partial phone number',
|
||||
context: {
|
||||
input: {
|
||||
searchInput: '555123',
|
||||
excludedObjectNameSingulars: ['workspaceMember'],
|
||||
limit: 50,
|
||||
},
|
||||
eval: {
|
||||
orderedRecordIds: [searchInput2Person.id],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
decodedEndCursor: {
|
||||
lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 },
|
||||
lastRecordIdsPerObject: {
|
||||
person: searchInput2Person.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should find multiple persons when phone search matches multiple records',
|
||||
context: {
|
||||
input: {
|
||||
searchInput: '123456789',
|
||||
excludedObjectNameSingulars: ['workspaceMember'],
|
||||
limit: 50,
|
||||
},
|
||||
eval: {
|
||||
orderedRecordIds: [josePerson.id],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
decodedEndCursor: {
|
||||
lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 },
|
||||
lastRecordIdsPerObject: {
|
||||
person: josePerson.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should rank phone search results appropriately vs other field matches',
|
||||
context: {
|
||||
input: {
|
||||
searchInput: 'searchInput1',
|
||||
excludedObjectNameSingulars: ['workspaceMember'],
|
||||
limit: 50,
|
||||
},
|
||||
eval: {
|
||||
orderedRecordIds: [searchInput1Person.id, searchInput1Pet.id],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
decodedEndCursor: {
|
||||
lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 },
|
||||
lastRecordIdsPerObject: {
|
||||
person: searchInput1Person.id,
|
||||
pet: searchInput1Pet.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it.each(testsUseCases)('$title', async ({ context }) => {
|
||||
|
||||
Reference in New Issue
Block a user