Search (#7237)
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:
@ -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],
|
||||
})
|
||||
|
||||
@ -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>>,
|
||||
|
||||
@ -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(' | ');
|
||||
}
|
||||
}
|
||||
@ -2,18 +2,18 @@ import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/wor
|
||||
|
||||
import { ArgsAliasFactory } from './args-alias.factory';
|
||||
import { ArgsStringFactory } from './args-string.factory';
|
||||
import { RelationFieldAliasFactory } from './relation-field-alias.factory';
|
||||
import { CreateManyQueryFactory } from './create-many-query.factory';
|
||||
import { DeleteManyQueryFactory } from './delete-many-query.factory';
|
||||
import { DeleteOneQueryFactory } from './delete-one-query.factory';
|
||||
import { FieldAliasFactory } from './field-alias.factory';
|
||||
import { FieldsStringFactory } from './fields-string.factory';
|
||||
import { FindDuplicatesQueryFactory } from './find-duplicates-query.factory';
|
||||
import { FindManyQueryFactory } from './find-many-query.factory';
|
||||
import { FindOneQueryFactory } from './find-one-query.factory';
|
||||
import { UpdateOneQueryFactory } from './update-one-query.factory';
|
||||
import { UpdateManyQueryFactory } from './update-many-query.factory';
|
||||
import { DeleteManyQueryFactory } from './delete-many-query.factory';
|
||||
import { FindDuplicatesQueryFactory } from './find-duplicates-query.factory';
|
||||
import { RecordPositionQueryFactory } from './record-position-query.factory';
|
||||
import { RelationFieldAliasFactory } from './relation-field-alias.factory';
|
||||
import { UpdateManyQueryFactory } from './update-many-query.factory';
|
||||
import { UpdateOneQueryFactory } from './update-one-query.factory';
|
||||
|
||||
export const workspaceQueryBuilderFactories = [
|
||||
ArgsAliasFactory,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
|
||||
import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory';
|
||||
import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory';
|
||||
import { SearchResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory';
|
||||
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
||||
|
||||
import { CreateManyResolverFactory } from './create-many-resolver.factory';
|
||||
@ -25,6 +26,7 @@ export const workspaceResolverBuilderFactories = [
|
||||
DestroyOneResolverFactory,
|
||||
DestroyManyResolverFactory,
|
||||
RestoreManyResolverFactory,
|
||||
SearchResolverFactory,
|
||||
];
|
||||
|
||||
export const workspaceResolverBuilderMethodNames = {
|
||||
@ -32,6 +34,7 @@ export const workspaceResolverBuilderMethodNames = {
|
||||
FindManyResolverFactory.methodName,
|
||||
FindOneResolverFactory.methodName,
|
||||
FindDuplicatesResolverFactory.methodName,
|
||||
SearchResolverFactory.methodName,
|
||||
],
|
||||
mutations: [
|
||||
CreateManyResolverFactory.methodName,
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import {
|
||||
Resolver,
|
||||
SearchResolverArgs,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
|
||||
@Injectable()
|
||||
export class SearchResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'search' as const;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
|
||||
) {}
|
||||
|
||||
create(context: WorkspaceSchemaBuilderContext): Resolver<SearchResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
try {
|
||||
return await this.graphqlQueryRunnerService.search(args, {
|
||||
authContext: internalContext.authContext,
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -48,6 +48,11 @@ export interface FindDuplicatesResolverArgs<
|
||||
data?: Data[];
|
||||
}
|
||||
|
||||
export interface SearchResolverArgs {
|
||||
searchInput?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface CreateOneResolverArgs<
|
||||
Data extends Partial<Record> = Partial<Record>,
|
||||
> {
|
||||
@ -123,4 +128,5 @@ export type ResolverArgs =
|
||||
| UpdateManyResolverArgs
|
||||
| UpdateOneResolverArgs
|
||||
| DestroyManyResolverArgs
|
||||
| RestoreManyResolverArgs;
|
||||
| RestoreManyResolverArgs
|
||||
| SearchResolverArgs;
|
||||
|
||||
@ -8,6 +8,7 @@ import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-reso
|
||||
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
|
||||
import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory';
|
||||
import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory';
|
||||
import { SearchResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory';
|
||||
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
|
||||
@ -42,6 +43,7 @@ export class WorkspaceResolverFactory {
|
||||
private readonly deleteManyResolverFactory: DeleteManyResolverFactory,
|
||||
private readonly restoreManyResolverFactory: RestoreManyResolverFactory,
|
||||
private readonly destroyManyResolverFactory: DestroyManyResolverFactory,
|
||||
private readonly searchResolverFactory: SearchResolverFactory,
|
||||
) {}
|
||||
|
||||
async create(
|
||||
@ -65,6 +67,7 @@ export class WorkspaceResolverFactory {
|
||||
['deleteMany', this.deleteManyResolverFactory],
|
||||
['restoreMany', this.restoreManyResolverFactory],
|
||||
['destroyMany', this.destroyManyResolverFactory],
|
||||
['search', this.searchResolverFactory],
|
||||
]);
|
||||
const resolvers: IResolvers = {
|
||||
Query: {},
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import { Inject, Injectable, forwardRef } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
|
||||
import { GraphQLInputObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
import { generateFields } from 'src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
import { InputTypeFactory } from './input-type.factory';
|
||||
@ -55,7 +53,12 @@ export class InputTypeDefinitionFactory {
|
||||
});
|
||||
|
||||
return {
|
||||
...this.generateFields(objectMetadata, kind, options),
|
||||
...generateFields(
|
||||
objectMetadata,
|
||||
kind,
|
||||
options,
|
||||
this.inputTypeFactory,
|
||||
),
|
||||
and: {
|
||||
type: andOrType,
|
||||
},
|
||||
@ -73,7 +76,12 @@ export class InputTypeDefinitionFactory {
|
||||
* Other input types are generated with fields only
|
||||
*/
|
||||
default:
|
||||
return this.generateFields(objectMetadata, kind, options);
|
||||
return generateFields(
|
||||
objectMetadata,
|
||||
kind,
|
||||
options,
|
||||
this.inputTypeFactory,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -84,46 +92,4 @@ export class InputTypeDefinitionFactory {
|
||||
type: inputType,
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: InputTypeDefinitionKind,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLInputFieldConfigMap {
|
||||
const fields: GraphQLInputFieldConfigMap = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Relation field types are generated during extension of object type definition
|
||||
if (isRelationFieldMetadataType(fieldMetadata.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const target = isCompositeFieldMetadataType(fieldMetadata.type)
|
||||
? fieldMetadata.type.toString()
|
||||
: fieldMetadata.id;
|
||||
|
||||
const isIdField = fieldMetadata.name === 'id';
|
||||
|
||||
const type = this.inputTypeFactory.create(
|
||||
target,
|
||||
fieldMetadata.type,
|
||||
kind,
|
||||
options,
|
||||
{
|
||||
nullable: fieldMetadata.isNullable,
|
||||
defaultValue: fieldMetadata.defaultValue,
|
||||
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
||||
settings: fieldMetadata.settings,
|
||||
isIdField,
|
||||
},
|
||||
);
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
|
||||
import { GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { generateFields } from 'src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
import { OutputTypeFactory } from './output-type.factory';
|
||||
|
||||
@ -39,48 +37,13 @@ export class ObjectTypeDefinitionFactory {
|
||||
type: new GraphQLObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
|
||||
description: objectMetadata.description,
|
||||
fields: this.generateFields(objectMetadata, kind, options),
|
||||
fields: generateFields(
|
||||
objectMetadata,
|
||||
kind,
|
||||
options,
|
||||
this.outputTypeFactory,
|
||||
),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: ObjectTypeDefinitionKind,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigMap<any, any> {
|
||||
const fields: GraphQLFieldConfigMap<any, any> = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Relation field types are generated during extension of object type definition
|
||||
if (isRelationFieldMetadataType(fieldMetadata.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const target = isCompositeFieldMetadataType(fieldMetadata.type)
|
||||
? fieldMetadata.type.toString()
|
||||
: fieldMetadata.id;
|
||||
|
||||
const type = this.outputTypeFactory.create(
|
||||
target,
|
||||
fieldMetadata.type,
|
||||
kind,
|
||||
options,
|
||||
{
|
||||
nullable: fieldMetadata.isNullable,
|
||||
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
||||
settings: fieldMetadata.settings,
|
||||
// Scalar type is already defined in the entity itself.
|
||||
isIdField: false,
|
||||
},
|
||||
);
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,9 +74,7 @@ export class RootTypeFactory {
|
||||
const args = getResolverArgs(methodName);
|
||||
const objectType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
objectMetadata.id,
|
||||
['findMany', 'findDuplicates'].includes(methodName)
|
||||
? ObjectTypeDefinitionKind.Connection
|
||||
: ObjectTypeDefinitionKind.Plain,
|
||||
this.getObjectTypeDefinitionKindByMethodName(methodName),
|
||||
);
|
||||
const argsType = this.argsFactory.create(
|
||||
{
|
||||
@ -124,4 +122,17 @@ export class RootTypeFactory {
|
||||
|
||||
return fieldConfigMap;
|
||||
}
|
||||
|
||||
private getObjectTypeDefinitionKindByMethodName(
|
||||
methodName: WorkspaceResolverBuilderMethodNames,
|
||||
): ObjectTypeDefinitionKind {
|
||||
switch (methodName) {
|
||||
case 'findMany':
|
||||
case 'findDuplicates':
|
||||
case 'search':
|
||||
return ObjectTypeDefinitionKind.Connection;
|
||||
default:
|
||||
return ObjectTypeDefinitionKind.Plain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,100 @@
|
||||
import {
|
||||
GraphQLFieldConfigMap,
|
||||
GraphQLInputFieldConfigMap,
|
||||
GraphQLInputType,
|
||||
GraphQLOutputType,
|
||||
} from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
|
||||
import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
|
||||
type TypeFactory<T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind> =
|
||||
{
|
||||
create: (
|
||||
target: string,
|
||||
fieldType: FieldMetadataType,
|
||||
kind: T,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
additionalOptions: {
|
||||
nullable?: boolean;
|
||||
defaultValue?: any;
|
||||
isArray: boolean;
|
||||
settings: any;
|
||||
isIdField: boolean;
|
||||
},
|
||||
) => T extends InputTypeDefinitionKind
|
||||
? GraphQLInputType
|
||||
: GraphQLOutputType;
|
||||
};
|
||||
|
||||
export const generateFields = <
|
||||
T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind,
|
||||
>(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: T,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
typeFactory: TypeFactory<T>,
|
||||
): T extends InputTypeDefinitionKind
|
||||
? GraphQLInputFieldConfigMap
|
||||
: GraphQLFieldConfigMap<any, any> => {
|
||||
const fields = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
if (
|
||||
isRelationFieldMetadataType(fieldMetadata.type) ||
|
||||
fieldMetadata.type === FieldMetadataType.TS_VECTOR
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const target = isCompositeFieldMetadataType(fieldMetadata.type)
|
||||
? fieldMetadata.type.toString()
|
||||
: fieldMetadata.id;
|
||||
|
||||
const typeFactoryOptions = isInputTypeDefinitionKind(kind)
|
||||
? {
|
||||
nullable: fieldMetadata.isNullable,
|
||||
defaultValue: fieldMetadata.defaultValue,
|
||||
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
||||
settings: fieldMetadata.settings,
|
||||
isIdField: fieldMetadata.name === 'id',
|
||||
}
|
||||
: {
|
||||
nullable: fieldMetadata.isNullable,
|
||||
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
||||
settings: fieldMetadata.settings,
|
||||
// Scalar type is already defined in the entity itself.
|
||||
isIdField: false,
|
||||
};
|
||||
|
||||
const type = typeFactory.create(
|
||||
target,
|
||||
fieldMetadata.type,
|
||||
kind,
|
||||
options,
|
||||
typeFactoryOptions,
|
||||
);
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
};
|
||||
|
||||
// Type guard
|
||||
const isInputTypeDefinitionKind = (
|
||||
kind: InputTypeDefinitionKind | ObjectTypeDefinitionKind,
|
||||
): kind is InputTypeDefinitionKind => {
|
||||
return Object.values(InputTypeDefinitionKind).includes(
|
||||
kind as InputTypeDefinitionKind,
|
||||
);
|
||||
};
|
||||
@ -137,6 +137,17 @@ export const getResolverArgs = (
|
||||
isNullable: false,
|
||||
},
|
||||
};
|
||||
case 'search':
|
||||
return {
|
||||
searchInput: {
|
||||
type: GraphQLString,
|
||||
isNullable: true,
|
||||
},
|
||||
limit: {
|
||||
type: GraphQLInt,
|
||||
isNullable: true,
|
||||
},
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unknown resolver type: ${type}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user