fixes on search (#11955)

In this PR
- enable search by email
- search with ' ' (spaces) string and special characters do not throw
entry error

closes https://github.com/twentyhq/twenty/issues/11447 &
https://github.com/twentyhq/core-team-issues/issues/860
This commit is contained in:
Etienne
2025-05-12 10:59:10 +02:00
committed by GitHub
parent 650f8f5963
commit ca6e979ead
8 changed files with 104 additions and 22 deletions

View File

@ -14,6 +14,8 @@ const StyledTableWithPointerEvents = styled(StyledTable)<{
& > * { & > * {
pointer-events: ${({ isDragging }) => (isDragging ? 'none' : 'auto')}; pointer-events: ${({ isDragging }) => (isDragging ? 'none' : 'auto')};
} }
min-height: ${({ isDragging }) => (isDragging ? '100%' : 'auto')};
height: ${({ isDragging }) => (isDragging ? '100%' : 'auto')};
`; `;
export interface RecordTableContentProps { export interface RecordTableContentProps {

View File

@ -0,0 +1,76 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SearchVectorService } from 'src/engine/metadata-modules/search-vector/search-vector.service';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { SEARCH_FIELDS_FOR_PERSON } from 'src/modules/person/standard-objects/person.workspace-entity';
@Command({
name: 'upgrade:0-53:upgrade-search-vector-on-person-entity',
description: 'Upgrade search vector on person entity',
})
export class UpgradeSearchVectorOnPersonEntityCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(ObjectMetadataEntity, 'metadata')
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly searchVectorService: SearchVectorService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
index,
total,
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
);
const personObjectMetadata =
await this.objectMetadataRepository.findOneOrFail({
select: ['id'],
where: {
workspaceId,
nameSingular: STANDARD_OBJECT_IDS.person,
},
});
if (!options.dryRun) {
await this.searchVectorService.updateSearchVector(
personObjectMetadata.id,
SEARCH_FIELDS_FOR_PERSON,
workspaceId,
);
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
}
this.logger.log(
`Migrated search vector on person entity for workspace ${workspaceId}`,
);
}
}

View File

@ -5,27 +5,40 @@ import { BackfillWorkflowNextStepIdsCommand } from 'src/database/commands/upgrad
import { CopyTypeormMigrationsCommand } from 'src/database/commands/upgrade-version-command/0-53/0-53-copy-typeorm-migrations.command'; import { CopyTypeormMigrationsCommand } from 'src/database/commands/upgrade-version-command/0-53/0-53-copy-typeorm-migrations.command';
import { MigrateWorkflowEventListenersToAutomatedTriggersCommand } from 'src/database/commands/upgrade-version-command/0-53/0-53-migrate-workflow-event-listeners-to-automated-triggers.command'; import { MigrateWorkflowEventListenersToAutomatedTriggersCommand } from 'src/database/commands/upgrade-version-command/0-53/0-53-migrate-workflow-event-listeners-to-automated-triggers.command';
import { RemoveRelationForeignKeyFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/0-53/0-53-remove-relation-foreign-key-field-metadata.command'; import { RemoveRelationForeignKeyFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/0-53/0-53-remove-relation-foreign-key-field-metadata.command';
import { UpgradeSearchVectorOnPersonEntityCommand } from 'src/database/commands/upgrade-version-command/0-53/0-53-upgrade-search-vector-on-person-entity.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SearchVectorModule } from 'src/engine/metadata-modules/search-vector/search-vector.module';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Workspace], 'core'), TypeOrmModule.forFeature([Workspace], 'core'),
TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'), TypeOrmModule.forFeature(
[FieldMetadataEntity, ObjectMetadataEntity],
'metadata',
),
WorkspaceDataSourceModule, WorkspaceDataSourceModule,
SearchVectorModule,
WorkspaceMigrationRunnerModule,
WorkspaceMetadataVersionModule,
], ],
providers: [ providers: [
MigrateWorkflowEventListenersToAutomatedTriggersCommand, MigrateWorkflowEventListenersToAutomatedTriggersCommand,
CopyTypeormMigrationsCommand, CopyTypeormMigrationsCommand,
BackfillWorkflowNextStepIdsCommand, BackfillWorkflowNextStepIdsCommand,
RemoveRelationForeignKeyFieldMetadataCommand, RemoveRelationForeignKeyFieldMetadataCommand,
UpgradeSearchVectorOnPersonEntityCommand,
], ],
exports: [ exports: [
MigrateWorkflowEventListenersToAutomatedTriggersCommand, MigrateWorkflowEventListenersToAutomatedTriggersCommand,
RemoveRelationForeignKeyFieldMetadataCommand, RemoveRelationForeignKeyFieldMetadataCommand,
BackfillWorkflowNextStepIdsCommand, BackfillWorkflowNextStepIdsCommand,
CopyTypeormMigrationsCommand, CopyTypeormMigrationsCommand,
UpgradeSearchVectorOnPersonEntityCommand,
], ],
}) })
export class V0_53_UpgradeVersionCommandModule {} export class V0_53_UpgradeVersionCommandModule {}

View File

@ -21,6 +21,7 @@ import { UpgradeDateAndDateTimeFieldsSettingsJsonCommand } from 'src/database/co
import { BackfillWorkflowNextStepIdsCommand } from 'src/database/commands/upgrade-version-command/0-53/0-53-backfill-workflow-next-step-ids.command'; import { BackfillWorkflowNextStepIdsCommand } from 'src/database/commands/upgrade-version-command/0-53/0-53-backfill-workflow-next-step-ids.command';
import { CopyTypeormMigrationsCommand } from 'src/database/commands/upgrade-version-command/0-53/0-53-copy-typeorm-migrations.command'; import { CopyTypeormMigrationsCommand } from 'src/database/commands/upgrade-version-command/0-53/0-53-copy-typeorm-migrations.command';
import { MigrateWorkflowEventListenersToAutomatedTriggersCommand } from 'src/database/commands/upgrade-version-command/0-53/0-53-migrate-workflow-event-listeners-to-automated-triggers.command'; import { MigrateWorkflowEventListenersToAutomatedTriggersCommand } from 'src/database/commands/upgrade-version-command/0-53/0-53-migrate-workflow-event-listeners-to-automated-triggers.command';
import { UpgradeSearchVectorOnPersonEntityCommand } from 'src/database/commands/upgrade-version-command/0-53/0-53-upgrade-search-vector-on-person-entity.command';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@ -62,6 +63,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly migrateWorkflowEventListenersToAutomatedTriggersCommand: MigrateWorkflowEventListenersToAutomatedTriggersCommand, protected readonly migrateWorkflowEventListenersToAutomatedTriggersCommand: MigrateWorkflowEventListenersToAutomatedTriggersCommand,
protected readonly backfillWorkflowNextStepIdsCommand: BackfillWorkflowNextStepIdsCommand, protected readonly backfillWorkflowNextStepIdsCommand: BackfillWorkflowNextStepIdsCommand,
protected readonly copyTypeormMigrationsCommand: CopyTypeormMigrationsCommand, protected readonly copyTypeormMigrationsCommand: CopyTypeormMigrationsCommand,
protected readonly upgradeSearchVectorOnPersonEntityCommand: UpgradeSearchVectorOnPersonEntityCommand,
) { ) {
super( super(
workspaceRepository, workspaceRepository,
@ -114,6 +116,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.migrateWorkflowEventListenersToAutomatedTriggersCommand, this.migrateWorkflowEventListenersToAutomatedTriggersCommand,
this.backfillWorkflowNextStepIdsCommand, this.backfillWorkflowNextStepIdsCommand,
this.copyTypeormMigrationsCommand, this.copyTypeormMigrationsCommand,
this.upgradeSearchVectorOnPersonEntityCommand,
], ],
}; };

View File

@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { getLogoUrlFromDomainName } from 'twenty-shared/utils'; import { getLogoUrlFromDomainName } from 'twenty-shared/utils';
import { Brackets, ObjectLiteral } from 'typeorm'; import { Brackets, ObjectLiteral } from 'typeorm';
@ -96,7 +97,7 @@ export class SearchService {
...(imageIdentifierField ? [imageIdentifierField] : []), ...(imageIdentifierField ? [imageIdentifierField] : []),
].map((field) => `"${field}"`); ].map((field) => `"${field}"`);
const searchQuery = searchTerms const searchQuery = isNonEmptyString(searchTerms)
? queryBuilder ? queryBuilder
.select(fieldsToSelect) .select(fieldsToSelect)
.addSelect( .addSelect(

View File

@ -2,12 +2,12 @@ export const formatSearchTerms = (
searchTerm: string, searchTerm: string,
operator: 'and' | 'or' = 'and', operator: 'and' | 'or' = 'and',
) => { ) => {
if (searchTerm === '') { if (searchTerm.trim() === '') {
return ''; return '';
} }
const words = searchTerm.trim().split(/\s+/); const words = searchTerm.trim().split(/\s+/);
const formattedWords = words.map((word) => { const formattedWords = words.map((word) => {
const escapedWord = word.replace(/[\\:'&|!()]/g, '\\$&'); const escapedWord = word.replace(/[\\:'&|!()@<>]/g, '\\$&');
return `${escapedWord}:*`; return `${escapedWord}:*`;
}); });

View File

@ -30,15 +30,8 @@ describe('getTsVectorColumnExpressionFromFields', () => {
const result = getTsVectorColumnExpressionFromFields(fields); const result = getTsVectorColumnExpressionFromFields(fields);
const expected = ` const expected = `
to_tsvector('simple', COALESCE("nameFirstName", '') || ' ' || COALESCE("nameLastName", '') || ' ' || COALESCE("jobTitle", '') || ' ' || to_tsvector('simple', COALESCE("nameFirstName", '') || ' ' || COALESCE("nameLastName", '') || ' ' || COALESCE("jobTitle", '') || ' ' ||
COALESCE( COALESCE("emailsPrimaryEmail", '') || ' ' ||
replace( COALESCE(SPLIT_PART("emailsPrimaryEmail", '@', 2), ''))
"emailsPrimaryEmail",
'@',
' '
),
''
)
)
`.trim(); `.trim();
expect(result.trim()).toBe(expected); expect(result.trim()).toBe(expected);

View File

@ -82,15 +82,9 @@ const getColumnExpression = (
switch (fieldType) { switch (fieldType) {
case FieldMetadataType.EMAILS: case FieldMetadataType.EMAILS:
return ` return `
COALESCE( COALESCE(${quotedColumnName}, '') || ' ' ||
replace( COALESCE(SPLIT_PART(${quotedColumnName}, '@', 2), '')`;
${quotedColumnName},
'@',
' '
),
''
)
`;
default: default:
return `COALESCE(${quotedColumnName}, '')`; return `COALESCE(${quotedColumnName}, '')`;
} }