Common Api - createOne/Many (#15083)

closes https://github.com/twentyhq/core-team-issues/issues/1578
This commit is contained in:
Etienne
2025-10-15 14:13:07 +02:00
committed by GitHub
parent f65783f900
commit 4ae299973b
31 changed files with 1573 additions and 45 deletions
@@ -18,6 +18,7 @@ import {
type UpdateOneResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
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';
@@ -39,35 +40,40 @@ export class QueryRunnerArgsFactory {
const fieldMetadataMapByNameByName =
options.objectMetadataItemWithFieldMaps.fieldsById;
const { objectMetadataItemWithFieldMaps, authContext } = options;
switch (resolverArgsType) {
case ResolverArgsType.CreateOne:
return {
...args,
data: (
await this.overrideDataByFieldMetadata(
[(args as CreateOneResolverArgs).data],
options,
)
await this.overrideDataByFieldMetadata({
partialRecordInputs: [(args as CreateOneResolverArgs).data],
authContext,
objectMetadataItemWithFieldMaps,
})
)[0],
} satisfies CreateOneResolverArgs;
case ResolverArgsType.CreateMany:
return {
...args,
data: await this.overrideDataByFieldMetadata(
(args as CreateManyResolverArgs).data,
options,
),
data: await this.overrideDataByFieldMetadata({
partialRecordInputs: (args as CreateManyResolverArgs).data,
authContext,
objectMetadataItemWithFieldMaps,
}),
} satisfies CreateManyResolverArgs;
case ResolverArgsType.UpdateOne:
return {
...args,
id: (args as UpdateOneResolverArgs).id,
data: (
await this.overrideDataByFieldMetadata(
[(args as UpdateOneResolverArgs).data],
options,
false,
)
await this.overrideDataByFieldMetadata({
partialRecordInputs: [(args as UpdateOneResolverArgs).data],
authContext,
objectMetadataItemWithFieldMaps,
shouldBackfillPositionIfUndefined: false,
})
)[0],
} satisfies UpdateOneResolverArgs;
case ResolverArgsType.UpdateMany:
@@ -78,11 +84,12 @@ export class QueryRunnerArgsFactory {
options.objectMetadataItemWithFieldMaps,
),
data: (
await this.overrideDataByFieldMetadata(
[(args as UpdateManyResolverArgs).data],
options,
false,
)
await this.overrideDataByFieldMetadata({
partialRecordInputs: [(args as UpdateManyResolverArgs).data],
authContext,
objectMetadataItemWithFieldMaps,
shouldBackfillPositionIfUndefined: false,
})
)[0],
} satisfies UpdateManyResolverArgs;
case ResolverArgsType.FindOne:
@@ -115,11 +122,12 @@ export class QueryRunnerArgsFactory {
),
) ?? [],
)) as string[],
data: await this.overrideDataByFieldMetadata(
(args as FindDuplicatesResolverArgs).data,
options,
false,
),
data: await this.overrideDataByFieldMetadata({
partialRecordInputs: (args as FindDuplicatesResolverArgs).data,
authContext,
objectMetadataItemWithFieldMaps,
shouldBackfillPositionIfUndefined: false,
}),
} satisfies FindDuplicatesResolverArgs;
case ResolverArgsType.MergeMany:
return {
@@ -143,18 +151,24 @@ export class QueryRunnerArgsFactory {
}
}
private async overrideDataByFieldMetadata(
partialRecordInputs: Partial<ObjectRecord>[] | undefined,
options: WorkspaceQueryRunnerOptions,
async overrideDataByFieldMetadata({
partialRecordInputs,
authContext,
objectMetadataItemWithFieldMaps,
shouldBackfillPositionIfUndefined = true,
): Promise<Partial<ObjectRecord>[]> {
}: {
partialRecordInputs: Partial<ObjectRecord>[] | undefined;
authContext: AuthContext;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
shouldBackfillPositionIfUndefined?: boolean;
}): Promise<Partial<ObjectRecord>[]> {
if (!isDefined(partialRecordInputs)) {
return [];
}
const allOverriddenRecords: Partial<ObjectRecord>[] = [];
const workspace = options.authContext.workspace;
const workspace = authContext.workspace;
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
@@ -163,9 +177,9 @@ export class QueryRunnerArgsFactory {
partialRecordInputs,
workspaceId: workspace.id,
objectMetadata: {
isCustom: options.objectMetadataItemWithFieldMaps.isCustom,
nameSingular: options.objectMetadataItemWithFieldMaps.nameSingular,
fieldIdByName: options.objectMetadataItemWithFieldMaps.fieldIdByName,
isCustom: objectMetadataItemWithFieldMaps.isCustom,
nameSingular: objectMetadataItemWithFieldMaps.nameSingular,
fieldIdByName: objectMetadataItemWithFieldMaps.fieldIdByName,
},
shouldBackfillPositionIfUndefined,
});
@@ -175,9 +189,9 @@ export class QueryRunnerArgsFactory {
const createArgByArgKey: [string, any][] = await Promise.all(
Object.entries(record).map(async ([key, value]) => {
const fieldMetadataId =
options.objectMetadataItemWithFieldMaps.fieldIdByName[key];
objectMetadataItemWithFieldMaps.fieldIdByName[key];
const fieldMetadata =
options.objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
if (!fieldMetadata) {
return [key, value];
@@ -193,8 +207,7 @@ export class QueryRunnerArgsFactory {
const transformedRecord =
await this.recordInputTransformerService.process({
recordInput: { [key]: value },
objectMetadataMapItem:
options.objectMetadataItemWithFieldMaps,
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
});
return [key, transformedRecord[key]];
@@ -1,5 +1,8 @@
import { Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields';
import { ObjectRecord } 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 { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
@@ -8,8 +11,14 @@ import {
} 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 { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Injectable()
export class CreateManyResolverFactory
@@ -19,6 +28,8 @@ export class CreateManyResolverFactory
constructor(
private readonly graphqlQueryRunnerService: GraphqlQueryCreateManyResolverService,
private readonly commonCreateManyQueryRunnerService: CommonCreateManyQueryRunnerService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
create(
@@ -27,6 +38,53 @@ export class CreateManyResolverFactory
const internalContext = context;
return async (_source, args, _context, info) => {
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId: internalContext.authContext.workspace?.id as string,
});
const featureFlagsMap = workspaceDataSource.featureFlagMap;
if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) {
const graphqlQueryParser = new GraphqlQueryParser(
internalContext.objectMetadataItemWithFieldMaps,
internalContext.objectMetadataMaps,
);
const selectedFieldsResult = graphqlQueryParser.parseSelectedFields(
internalContext.objectMetadataItemWithFieldMaps,
graphqlFields(info),
internalContext.objectMetadataMaps,
);
try {
const records = await this.commonCreateManyQueryRunnerService.run({
args: { ...args, selectedFieldsResult },
authContext: internalContext.authContext,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
});
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(
internalContext.objectMetadataMaps,
);
return records.map((record: ObjectRecord) =>
typeORMObjectRecordsParser.processRecord({
objectRecord: record,
objectName:
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
}),
);
} catch (error) {
return workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
}
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
@@ -1,5 +1,7 @@
import { Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields';
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
@@ -8,8 +10,14 @@ import {
} 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 { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { GraphqlQueryCreateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-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 CreateOneResolverFactory
@@ -19,6 +27,8 @@ export class CreateOneResolverFactory
constructor(
private readonly graphqlQueryRunnerService: GraphqlQueryCreateOneResolverService,
private readonly featureFlagService: FeatureFlagService,
private readonly commonCreateOneQueryRunnerService: CommonCreateOneQueryRunnerService,
) {}
create(
@@ -27,6 +37,49 @@ export class CreateOneResolverFactory
const internalContext = context;
return async (_source, args, _context, info) => {
const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_COMMON_API_ENABLED,
internalContext.authContext.workspace?.id as string,
);
if (isCommonApiEnabled) {
const graphqlQueryParser = new GraphqlQueryParser(
internalContext.objectMetadataItemWithFieldMaps,
internalContext.objectMetadataMaps,
);
const selectedFieldsResult = graphqlQueryParser.parseSelectedFields(
internalContext.objectMetadataItemWithFieldMaps,
graphqlFields(info),
internalContext.objectMetadataMaps,
);
try {
const record = await this.commonCreateOneQueryRunnerService.run({
args: { ...args, selectedFieldsResult },
authContext: internalContext.authContext,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
});
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(
internalContext.objectMetadataMaps,
);
return typeORMObjectRecordsParser.processRecord({
objectRecord: record,
objectName:
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
});
} catch (error) {
return workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
}
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,