CreateMany optim - .save -> .updateMany + position (#13704)

fixes : https://github.com/twentyhq/core-team-issues/issues/1312

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Etienne
2025-08-07 00:52:28 +02:00
committed by GitHub
parent 723f1016a6
commit 58d25ebd83
5 changed files with 256 additions and 172 deletions
@@ -6,16 +6,27 @@ import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-qu
import { ResolverArgsType } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
import {
RecordPositionService,
RecordPositionServiceCreateArgs,
} from 'src/engine/core-modules/record-position/services/record-position.service';
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 { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
describe('QueryRunnerArgsFactory', () => {
const recordPositionService = {
buildRecordPosition: jest.fn().mockResolvedValue(2),
overridePositionOnRecords: jest
.fn()
.mockImplementation(
({ partialRecordInputs }: { partialRecordInputs: any[] }) => {
return Promise.resolve(
partialRecordInputs.map((record: any) => ({
...record,
position:
record.position === 'last' || !record.position
? 2
: record.position,
})),
);
},
),
};
const workspaceId = 'workspaceId';
const options = {
@@ -96,16 +107,24 @@ describe('QueryRunnerArgsFactory', () => {
ResolverArgsType.CreateMany,
);
const expectedArgs: RecordPositionServiceCreateArgs = {
value: 'last',
objectMetadata: { isCustom: true, nameSingular: 'testNumber' },
const expectedArgs = {
partialRecordInputs: [{ position: 'last', testNumber: 1 }],
objectMetadata: {
isCustom: true,
nameSingular: 'testNumber',
fieldIdByName: {
position: 'position-id',
testNumber: 'testNumber-id',
otherField: 'otherField-id',
},
},
workspaceId,
index: 0,
shouldBackfillPositionIfUndefined: true,
};
expect(recordPositionService.buildRecordPosition).toHaveBeenCalledWith(
expectedArgs,
);
expect(
recordPositionService.overridePositionOnRecords,
).toHaveBeenCalledWith(expectedArgs);
expect(result).toEqual({
id: 'uuid',
data: [{ position: 2, testNumber: 1 }],
@@ -124,16 +143,24 @@ describe('QueryRunnerArgsFactory', () => {
ResolverArgsType.CreateMany,
);
const expectedArgs: RecordPositionServiceCreateArgs = {
value: 'first',
objectMetadata: { isCustom: true, nameSingular: 'testNumber' },
const expectedArgs = {
partialRecordInputs: [{ testNumber: 1 }],
objectMetadata: {
isCustom: true,
nameSingular: 'testNumber',
fieldIdByName: {
position: 'position-id',
testNumber: 'testNumber-id',
otherField: 'otherField-id',
},
},
workspaceId,
index: 0,
shouldBackfillPositionIfUndefined: true,
};
expect(recordPositionService.buildRecordPosition).toHaveBeenCalledWith(
expectedArgs,
);
expect(
recordPositionService.overridePositionOnRecords,
).toHaveBeenCalledWith(expectedArgs);
expect(result).toEqual({
id: 'uuid',
data: [{ position: 2, testNumber: 1 }],
@@ -27,11 +27,6 @@ import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
type ArgPositionBackfillInput = {
argIndex?: number;
shouldBackfillPosition: boolean;
};
@Injectable()
export class QueryRunnerArgsFactory {
constructor(
@@ -47,50 +42,36 @@ export class QueryRunnerArgsFactory {
const fieldMetadataMapByNameByName =
options.objectMetadataItemWithFieldMaps.fieldsById;
const shouldBackfillPosition = Object.values(
options.objectMetadataItemWithFieldMaps.fieldsById,
).some(
(field) =>
field.type === FieldMetadataType.POSITION && field.name === 'position',
);
switch (resolverArgsType) {
case ResolverArgsType.CreateOne:
return {
...args,
data: await this.overrideDataByFieldMetadata(
(args as CreateOneResolverArgs).data,
options,
{
argIndex: 0,
shouldBackfillPosition,
},
),
data: (
await this.overrideDataByFieldMetadata(
[(args as CreateOneResolverArgs).data],
options,
)
)[0],
} satisfies CreateOneResolverArgs;
case ResolverArgsType.CreateMany:
return {
...args,
data: await Promise.all(
(args as CreateManyResolverArgs).data?.map((arg, index) =>
this.overrideDataByFieldMetadata(arg, options, {
argIndex: index,
shouldBackfillPosition,
}),
) ?? [],
data: await this.overrideDataByFieldMetadata(
(args as CreateManyResolverArgs).data,
options,
),
} satisfies CreateManyResolverArgs;
case ResolverArgsType.UpdateOne:
return {
...args,
id: (args as UpdateOneResolverArgs).id,
data: await this.overrideDataByFieldMetadata(
(args as UpdateOneResolverArgs).data,
options,
{
argIndex: 0,
shouldBackfillPosition: false,
},
),
data: (
await this.overrideDataByFieldMetadata(
[(args as UpdateOneResolverArgs).data],
options,
false,
)
)[0],
} satisfies UpdateOneResolverArgs;
case ResolverArgsType.UpdateMany:
return {
@@ -99,14 +80,13 @@ export class QueryRunnerArgsFactory {
(args as UpdateManyResolverArgs).filter,
options.objectMetadataItemWithFieldMaps,
),
data: await this.overrideDataByFieldMetadata(
(args as UpdateManyResolverArgs).data,
options,
{
argIndex: 0,
shouldBackfillPosition: false,
},
),
data: (
await this.overrideDataByFieldMetadata(
[(args as UpdateManyResolverArgs).data],
options,
false,
)
)[0],
} satisfies UpdateManyResolverArgs;
case ResolverArgsType.FindOne:
return {
@@ -137,13 +117,10 @@ export class QueryRunnerArgsFactory {
),
) ?? [],
)) as string[],
data: await Promise.all(
(args as FindDuplicatesResolverArgs).data?.map((arg, index) =>
this.overrideDataByFieldMetadata(arg, options, {
argIndex: index,
shouldBackfillPosition,
}),
) ?? [],
data: await this.overrideDataByFieldMetadata(
(args as FindDuplicatesResolverArgs).data,
options,
false,
),
} satisfies FindDuplicatesResolverArgs;
case ResolverArgsType.MergeMany:
@@ -168,97 +145,70 @@ export class QueryRunnerArgsFactory {
}
private async overrideDataByFieldMetadata(
data: Partial<ObjectRecord> | undefined,
partialRecordInputs: Partial<ObjectRecord>[] | undefined,
options: WorkspaceQueryRunnerOptions,
argPositionBackfillInput: ArgPositionBackfillInput,
): Promise<Partial<ObjectRecord>> {
if (!isDefined(data)) {
return Promise.resolve({});
shouldBackfillPositionIfUndefined = true,
): Promise<Partial<ObjectRecord>[]> {
if (!isDefined(partialRecordInputs)) {
return [];
}
const allOverriddenRecords: Partial<ObjectRecord>[] = [];
const workspace = options.authContext.workspace;
workspaceValidator.assertIsDefinedOrThrow(workspace);
let isFieldPositionPresent = false;
const overriddenPositionRecords =
await this.recordPositionService.overridePositionOnRecords({
partialRecordInputs,
workspaceId: workspace.id,
objectMetadata: {
isCustom: options.objectMetadataItemWithFieldMaps.isCustom,
nameSingular: options.objectMetadataItemWithFieldMaps.nameSingular,
fieldIdByName: options.objectMetadataItemWithFieldMaps.fieldIdByName,
},
shouldBackfillPositionIfUndefined,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const createArgByArgKeyPromises: Promise<[string, any]>[] = Object.entries(
data,
for (const record of overriddenPositionRecords) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
).map(async ([key, value]): Promise<[string, any]> => {
const fieldMetadataId =
options.objectMetadataItemWithFieldMaps.fieldIdByName[key];
const fieldMetadata =
options.objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
const createArgByArgKey: [string, any][] = await Promise.all(
Object.entries(record).map(async ([key, value]) => {
const fieldMetadataId =
options.objectMetadataItemWithFieldMaps.fieldIdByName[key];
const fieldMetadata =
options.objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
if (!fieldMetadata) {
return [key, value];
}
if (!fieldMetadata) {
return [key, value];
}
switch (fieldMetadata.type) {
case FieldMetadataType.POSITION: {
isFieldPositionPresent = true;
switch (fieldMetadata.type) {
case FieldMetadataType.NUMBER:
case FieldMetadataType.RICH_TEXT:
case FieldMetadataType.PHONES:
case FieldMetadataType.RICH_TEXT_V2:
case FieldMetadataType.LINKS:
case FieldMetadataType.EMAILS: {
const transformedValue =
await this.recordInputTransformerService.transformFieldValue(
fieldMetadata.type,
value,
);
const newValue = await this.recordPositionService.buildRecordPosition(
{
value,
workspaceId: workspace.id,
objectMetadata: {
isCustom: options.objectMetadataItemWithFieldMaps.isCustom,
nameSingular:
options.objectMetadataItemWithFieldMaps.nameSingular,
},
index: argPositionBackfillInput.argIndex,
},
);
return [key, transformedValue];
}
default:
return [key, value];
}
}),
);
return [key, newValue];
}
case FieldMetadataType.NUMBER:
case FieldMetadataType.RICH_TEXT:
case FieldMetadataType.PHONES:
case FieldMetadataType.RICH_TEXT_V2:
case FieldMetadataType.LINKS:
case FieldMetadataType.EMAILS: {
const transformedValue =
await this.recordInputTransformerService.transformFieldValue(
fieldMetadata.type,
value,
);
return [key, transformedValue];
}
default:
return [key, value];
}
});
const newArgEntries = await Promise.all(createArgByArgKeyPromises);
if (
!isFieldPositionPresent &&
argPositionBackfillInput.shouldBackfillPosition
) {
return Object.fromEntries([
...newArgEntries,
[
'position',
await this.recordPositionService.buildRecordPosition({
value: 'first',
workspaceId: workspace.id,
objectMetadata: {
isCustom: options.objectMetadataItemWithFieldMaps.isCustom,
nameSingular:
options.objectMetadataItemWithFieldMaps.nameSingular,
},
index: argPositionBackfillInput.argIndex,
}),
],
]);
allOverriddenRecords.push(Object.fromEntries(createArgByArgKey));
}
return Object.fromEntries(newArgEntries);
return allOverriddenRecords;
}
private overrideFilterByFieldMetadata(