Steps to test

1. Run metadata migrations
2. Run sync-metadata on your workspace
3. Enable the following feature flags: 
IS_SEARCH_ENABLED
IS_QUERY_RUNNER_TWENTY_ORM_ENABLED
IS_WORKSPACE_MIGRATED_FOR_SEARCH
4. Type Cmd + K and search anything
This commit is contained in:
Marie
2024-10-03 17:18:49 +02:00
committed by GitHub
parent 4c250dd811
commit 5f9435c718
71 changed files with 1517 additions and 209 deletions

View File

@ -3,9 +3,13 @@ import { Module } from '@nestjs/common';
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
@Module({
imports: [WorkspaceQueryHookModule, WorkspaceQueryRunnerModule],
imports: [
WorkspaceQueryHookModule,
WorkspaceQueryRunnerModule,
FeatureFlagModule,
],
providers: [GraphqlQueryRunnerService],
exports: [GraphqlQueryRunnerService],
})

View File

@ -14,6 +14,7 @@ import {
FindManyResolverArgs,
FindOneResolverArgs,
ResolverArgsType,
SearchResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
@ -21,6 +22,7 @@ import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/gr
import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service';
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
import { GraphqlQuerySearchResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service';
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
import {
CallWebhookJobsJob,
@ -36,6 +38,7 @@ import {
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
@ -48,6 +51,7 @@ import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/worksp
export class GraphqlQueryRunnerService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly featureFlagService: FeatureFlagService,
private readonly workspaceQueryHookService: WorkspaceQueryHookService,
private readonly queryRunnerArgsFactory: QueryRunnerArgsFactory,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
@ -178,6 +182,20 @@ export class GraphqlQueryRunnerService {
return results?.[0] as ObjectRecord;
}
@LogExecutionTime()
async search<ObjectRecord extends IRecord = IRecord>(
args: SearchResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<ObjectRecord>> {
const graphqlQuerySearchResolverService =
new GraphqlQuerySearchResolverService(
this.twentyORMGlobalManager,
this.featureFlagService,
);
return graphqlQuerySearchResolverService.search(args, options);
}
@LogExecutionTime()
async createMany<ObjectRecord extends IRecord = IRecord>(
args: CreateManyResolverArgs<Partial<ObjectRecord>>,

View File

@ -0,0 +1,132 @@
import {
Record as IRecord,
OrderByDirection,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { SearchResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
export class GraphqlQuerySearchResolverService {
private twentyORMGlobalManager: TwentyORMGlobalManager;
private featureFlagService: FeatureFlagService;
constructor(
twentyORMGlobalManager: TwentyORMGlobalManager,
featureFlagService: FeatureFlagService,
) {
this.twentyORMGlobalManager = twentyORMGlobalManager;
this.featureFlagService = featureFlagService;
}
async search<ObjectRecord extends IRecord = IRecord>(
args: SearchResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<ObjectRecord>> {
const { authContext, objectMetadataItem, objectMetadataCollection } =
options;
const featureFlagsForWorkspace =
await this.featureFlagService.getWorkspaceFeatureFlags(
authContext.workspace.id,
);
const isQueryRunnerTwentyORMEnabled =
featureFlagsForWorkspace.IS_QUERY_RUNNER_TWENTY_ORM_ENABLED;
const isSearchEnabled = featureFlagsForWorkspace.IS_SEARCH_ENABLED;
if (!isQueryRunnerTwentyORMEnabled || !isSearchEnabled) {
throw new GraphqlQueryRunnerException(
'This endpoint is not available yet, please use findMany instead.',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
authContext.workspace.id,
objectMetadataItem.nameSingular,
);
const objectMetadataMap = generateObjectMetadataMap(
objectMetadataCollection,
);
const objectMetadata = objectMetadataMap[objectMetadataItem.nameSingular];
if (!objectMetadata) {
throw new GraphqlQueryRunnerException(
`Object metadata not found for ${objectMetadataItem.nameSingular}`,
GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
if (!args.searchInput) {
return typeORMObjectRecordsParser.createConnection(
[],
objectMetadataItem.nameSingular,
0,
0,
[{ id: OrderByDirection.AscNullsFirst }],
false,
false,
);
}
const searchTerms = this.formatSearchTerms(args.searchInput);
const limit = args?.limit ?? QUERY_MAX_RECORDS;
const resultsWithTsVector = (await repository
.createQueryBuilder()
.where(`"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery(:searchTerms)`, {
searchTerms,
})
.orderBy(
`ts_rank("${SEARCH_VECTOR_FIELD.name}", to_tsquery(:searchTerms))`,
'DESC',
)
.setParameter('searchTerms', searchTerms)
.limit(limit)
.getMany()) as ObjectRecord[];
const objectRecords = await repository.formatResult(resultsWithTsVector);
const totalCount = await repository.count();
const order = undefined;
return typeORMObjectRecordsParser.createConnection(
objectRecords ?? [],
objectMetadataItem.nameSingular,
limit,
totalCount,
order,
false,
false,
);
}
private formatSearchTerms(searchTerm: string) {
const words = searchTerm.trim().split(/\s+/);
const formattedWords = words.map((word) => {
const escapedWord = word.replace(/[\\:'&|!()]/g, '\\$&');
return `${escapedWord}:*`;
});
return formattedWords.join(' | ');
}
}