Common API - create common find one query (#14720)

closes https://github.com/twentyhq/core-team-issues/issues/1418

Tested : 
- findOne on Rest and Gql

### Vision
#### Common
- Common is kind of renamed Gql base resolver
- Common handles args (filter, values, ...) validation
- Common accepts depth or raw gql selected fields to compute
selectedFields
- Common is directly called by each CommonQueries (findOne, ...)
service, which extend CommonBaseQuery service
- Common sequence : 
| - Parse & Validate args (args-handlers, to create)
| - Build query (query-parsers : currently in gql-query-parsers, to
move)
| - Execute query
| - Fetch relation + format
#### Rest
- Simple parsing (without metadata validation)
- Calling Common API
- Simple rest response formatting
#### Gql
- Calling Common API
This commit is contained in:
Etienne
2025-10-08 12:18:42 +02:00
committed by GitHub
parent 8914ba9fec
commit c693c4d9cf
49 changed files with 1230 additions and 110 deletions
@@ -1,3 +1,4 @@
//TODO : Refacto-common - To delete
import { Injectable } from '@nestjs/common';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
@@ -1,3 +1,4 @@
//TODO : Refacto-common - Should be moved to common api layer
export interface ObjectRecord {
id: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -7,7 +7,7 @@ import {
type ObjectRecord,
type ObjectRecordFilter,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import {
type CreateManyResolverArgs,
type CreateOneResolverArgs,
@@ -23,9 +23,9 @@ import {
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
@Injectable()
export class QueryRunnerArgsFactory {
@@ -214,7 +214,7 @@ export class QueryRunnerArgsFactory {
return allOverriddenRecords;
}
private overrideFilterByFieldMetadata(
public overrideFilterByFieldMetadata(
filter: ObjectRecordFilter | undefined,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
) {
@@ -6,7 +6,7 @@ import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/objec
export interface WorkspaceQueryRunnerOptions {
authContext: AuthContext;
info: GraphQLResolveInfo;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
info: GraphQLResolveInfo;
}
@@ -1,5 +1,7 @@
import { type QueryFailedError } from 'typeorm';
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { commonQueryRunnerToGraphqlApiExceptionHandler } from 'src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util';
import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { graphqlQueryRunnerExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/graphql-query-runner-exception-handler.util';
import { workspaceExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-exception-handler.util';
@@ -18,6 +20,7 @@ import { twentyORMGraphqlApiExceptionHandler } from 'src/engine/twenty-orm/utils
interface QueryFailedErrorWithCode extends QueryFailedError {
code: string;
}
//TODO : Refacto-common - Should be handle first in common api layer
export const workspaceQueryRunnerGraphqlApiExceptionHandler = (
error: QueryFailedErrorWithCode,
@@ -33,6 +36,8 @@ export const workspaceQueryRunnerGraphqlApiExceptionHandler = (
return graphqlQueryRunnerExceptionHandler(error);
case error instanceof TwentyORMException:
return twentyORMGraphqlApiExceptionHandler(error);
case error instanceof CommonQueryRunnerException:
return commonQueryRunnerToGraphqlApiExceptionHandler(error);
case error instanceof AuthException:
return authGraphqlApiExceptionHandler(error);
case error instanceof ApiKeyException:
@@ -5,6 +5,7 @@ import merge from 'lodash.merge';
import { type QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
import { type WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type';
import { type WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage';
import { type WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
@@ -18,8 +19,9 @@ export class WorkspaceQueryHookService {
private readonly workspaceQueryHookExplorer: WorkspaceQueryHookExplorer,
) {}
//TODO : Refacto-common - Should be Common
public async executePreQueryHooks<
T extends WorkspaceResolverBuilderMethodNames,
T extends WorkspaceResolverBuilderMethodNames | CommonQueryNames,
>(
authContext: AuthContext,
// TODO: We should allow wildcard for object name
@@ -13,8 +13,8 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listener';
@@ -1,15 +1,21 @@
import { Injectable } from '@nestjs/common';
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import graphqlFields from 'graphql-fields';
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
type FindOneResolverArgs,
type Resolver,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@Injectable()
export class FindOneResolverFactory
@@ -18,6 +24,8 @@ export class FindOneResolverFactory
public static methodName = RESOLVER_METHOD_NAMES.FIND_ONE;
constructor(
private readonly commonFindOneQueryRunnerService: CommonFindOneQueryRunnerService,
private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryFindOneResolverService,
) {}
@@ -27,17 +35,45 @@ export class FindOneResolverFactory
const internalContext = context;
return async (_source, args, _context, info) => {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_COMMON_API_ENABLED,
internalContext.authContext.workspace?.id as string,
);
if (isCommonApiEnabled) {
try {
const graphqlQueryParser = new GraphqlQueryParser(
internalContext.objectMetadataItemWithFieldMaps,
internalContext.objectMetadataMaps,
);
const selectedFieldsResult = graphqlQueryParser.parseSelectedFields(
internalContext.objectMetadataItemWithFieldMaps,
graphqlFields(info),
internalContext.objectMetadataMaps,
);
return await this.commonFindOneQueryRunnerService.run({
args: { ...args, selectedFieldsResult },
authContext: internalContext.authContext,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
});
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
}
return await this.graphqlQueryRunnerService.execute(
args,
options,
{
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
},
FindOneResolverFactory.methodName,
);
};
@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';
import { CoreCommonApiModule } from 'src/engine/api/common/core-common-api.module';
import { GraphqlQueryRunnerModule } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module';
import { WorkspaceResolverBuilderService } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.service';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
@@ -9,7 +10,7 @@ import { WorkspaceResolverFactory } from './workspace-resolver.factory';
import { workspaceResolverBuilderFactories } from './factories/factories';
@Module({
imports: [GraphqlQueryRunnerModule, FeatureFlagModule],
imports: [GraphqlQueryRunnerModule, FeatureFlagModule, CoreCommonApiModule],
providers: [
...workspaceResolverBuilderFactories,
WorkspaceResolverFactory,