Set record position on workflow creation (#11308)

- Migrate record position factory to core-modules 
- set position on record creation
This commit is contained in:
Thomas Trompette
2025-04-01 11:50:43 +02:00
committed by GitHub
parent b2012229f4
commit 023d071103
20 changed files with 303 additions and 317 deletions
@@ -7,14 +7,14 @@ import { ResolverArgsType } from 'src/engine/api/graphql/workspace-resolver-buil
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
import {
RecordPositionFactory,
RecordPositionFactoryCreateArgs,
} from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory';
RecordPositionService,
RecordPositionServiceCreateArgs,
} from 'src/engine/core-modules/record-position/services/record-position.service';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
describe('QueryRunnerArgsFactory', () => {
const recordPositionFactory = {
create: jest.fn().mockResolvedValue(2),
const recordPositionService = {
buildRecordPosition: jest.fn().mockResolvedValue(2),
};
const workspaceId = 'workspaceId';
const options = {
@@ -66,10 +66,8 @@ describe('QueryRunnerArgsFactory', () => {
providers: [
QueryRunnerArgsFactory,
{
provide: RecordPositionFactory,
useValue: {
create: recordPositionFactory.create,
},
provide: RecordPositionService,
useValue: recordPositionService,
},
],
}).compile();
@@ -107,14 +105,16 @@ describe('QueryRunnerArgsFactory', () => {
ResolverArgsType.CreateMany,
);
const expectedArgs: RecordPositionFactoryCreateArgs = {
const expectedArgs: RecordPositionServiceCreateArgs = {
value: 'last',
objectMetadata: { isCustom: true, nameSingular: 'testNumber' },
workspaceId,
index: 0,
};
expect(recordPositionFactory.create).toHaveBeenCalledWith(expectedArgs);
expect(recordPositionService.buildRecordPosition).toHaveBeenCalledWith(
expectedArgs,
);
expect(result).toEqual({
id: 'uuid',
data: [{ position: 2, testNumber: 1 }],
@@ -133,14 +133,16 @@ describe('QueryRunnerArgsFactory', () => {
ResolverArgsType.CreateMany,
);
const expectedArgs: RecordPositionFactoryCreateArgs = {
const expectedArgs: RecordPositionServiceCreateArgs = {
value: 'first',
objectMetadata: { isCustom: true, nameSingular: 'testNumber' },
workspaceId,
index: 0,
};
expect(recordPositionFactory.create).toHaveBeenCalledWith(expectedArgs);
expect(recordPositionService.buildRecordPosition).toHaveBeenCalledWith(
expectedArgs,
);
expect(result).toEqual({
id: 'uuid',
data: [{ position: 2, testNumber: 1 }],
@@ -1,80 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RecordPositionQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
import { RecordPositionFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
describe('RecordPositionFactory', () => {
const recordPositionQueryFactory = {
create: jest.fn().mockReturnValue(['query', []]),
};
let workspaceDataSourceService;
let factory: RecordPositionFactory;
beforeEach(async () => {
workspaceDataSourceService = {
getSchemaName: jest.fn().mockReturnValue('schemaName'),
executeRawQuery: jest.fn().mockResolvedValue([{ position: 1 }]),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
RecordPositionFactory,
{
provide: RecordPositionQueryFactory,
useValue: recordPositionQueryFactory,
},
{
provide: WorkspaceDataSourceService,
useValue: workspaceDataSourceService,
},
],
}).compile();
factory = module.get<RecordPositionFactory>(RecordPositionFactory);
});
it('should be defined', () => {
expect(factory).toBeDefined();
});
describe('create', () => {
const objectMetadata = { isCustom: false, nameSingular: 'company' };
const workspaceId = 'workspaceId';
it('should return the value when value is a number', async () => {
const value = 1;
const result = await factory.create({
value,
objectMetadata,
workspaceId,
});
expect(result).toEqual(value);
});
it('should return the existing position -1 when value is first', async () => {
const value = 'first';
const result = await factory.create({
value,
objectMetadata,
workspaceId,
});
expect(result).toEqual(0);
});
it('should return the existing position + 1 when value is last', async () => {
const value = 'last';
const result = await factory.create({
value,
objectMetadata,
workspaceId,
});
expect(result).toEqual(2);
});
});
});
@@ -1,10 +1,8 @@
import { QueryRunnerArgsFactory } from './query-runner-args.factory';
import { RecordPositionFactory } from './record-position.factory';
import { QueryResultGettersFactory } from './query-result-getters/query-result-getters.factory';
export const workspaceQueryRunnerFactories = [
QueryRunnerArgsFactory,
RecordPositionFactory,
QueryResultGettersFactory,
];
@@ -22,14 +22,13 @@ import {
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { lowercaseDomain } from 'src/engine/api/graphql/workspace-query-runner/utils/query-runner-links.util';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import {
RichTextV2Metadata,
richTextV2ValueSchema,
} from 'src/engine/metadata-modules/field-metadata/composite-types/rich-text-v2.composite-type';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { RecordPositionFactory } from './record-position.factory';
type ArgPositionBackfillInput = {
argIndex?: number;
shouldBackfillPosition: boolean;
@@ -37,7 +36,7 @@ type ArgPositionBackfillInput = {
@Injectable()
export class QueryRunnerArgsFactory {
constructor(private readonly recordPositionFactory: RecordPositionFactory) {}
constructor(private readonly recordPositionService: RecordPositionService) {}
async create(
args: ResolverArgs,
@@ -190,16 +189,18 @@ export class QueryRunnerArgsFactory {
case FieldMetadataType.POSITION: {
isFieldPositionPresent = true;
const newValue = await this.recordPositionFactory.create({
value,
workspaceId,
objectMetadata: {
isCustom: options.objectMetadataItemWithFieldMaps.isCustom,
nameSingular:
options.objectMetadataItemWithFieldMaps.nameSingular,
const newValue = await this.recordPositionService.buildRecordPosition(
{
value,
workspaceId,
objectMetadata: {
isCustom: options.objectMetadataItemWithFieldMaps.isCustom,
nameSingular:
options.objectMetadataItemWithFieldMaps.nameSingular,
},
index: argPositionBackfillInput.argIndex,
},
index: argPositionBackfillInput.argIndex,
});
);
return [key, newValue];
}
@@ -307,7 +308,7 @@ export class QueryRunnerArgsFactory {
...newArgEntries,
[
'position',
await this.recordPositionFactory.create({
await this.recordPositionService.buildRecordPosition({
value: 'first',
workspaceId,
objectMetadata: {
@@ -1,87 +0,0 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import {
RecordPositionQueryArgs,
RecordPositionQueryFactory,
RecordPositionQueryType,
} from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
export type RecordPositionFactoryCreateArgs = {
value: number | 'first' | 'last';
objectMetadata: { isCustom: boolean; nameSingular: string };
workspaceId: string;
index?: number;
};
@Injectable()
export class RecordPositionFactory {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly recordPositionQueryFactory: RecordPositionQueryFactory,
) {}
async create({
objectMetadata,
value,
workspaceId,
index = 0,
}: RecordPositionFactoryCreateArgs): Promise<number> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
if (typeof value === 'number') {
return value;
}
if (value === 'first') {
const recordWithMinPosition = await this.findRecordPosition(
{
recordPositionQueryType: RecordPositionQueryType.FIND_MIN_POSITION,
},
objectMetadata,
dataSourceSchema,
workspaceId,
);
return isDefined(recordWithMinPosition?.position)
? recordWithMinPosition.position - index - 1
: 1;
}
const recordWithMaxPosition = await this.findRecordPosition(
{
recordPositionQueryType: RecordPositionQueryType.FIND_MAX_POSITION,
},
objectMetadata,
dataSourceSchema,
workspaceId,
);
return isDefined(recordWithMaxPosition?.position)
? recordWithMaxPosition.position + index + 1
: 1;
}
private async findRecordPosition(
recordPositionQueryArgs: RecordPositionQueryArgs,
objectMetadata: { isCustom: boolean; nameSingular: string },
dataSourceSchema: string,
workspaceId: string,
) {
const [query, params] = this.recordPositionQueryFactory.create(
recordPositionQueryArgs,
objectMetadata,
dataSourceSchema,
);
const records = await this.workspaceDataSourceService.executeRawQuery(
query,
params,
workspaceId,
);
return records?.[0];
}
}
@@ -3,27 +3,21 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { FieldMetadataType } from 'twenty-shared/types';
import { RecordPositionQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
import { RecordPositionFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory';
import { RecordPositionBackfillService } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
describe('RecordPositionBackfillService', () => {
let recordPositionQueryFactory;
let recordPositionFactory;
let recordPositionService;
let objectMetadataRepository;
let workspaceDataSourceService;
let service: RecordPositionBackfillService;
beforeEach(async () => {
recordPositionQueryFactory = {
create: jest.fn().mockReturnValue(['query', []]),
};
recordPositionFactory = {
create: jest.fn().mockResolvedValue([
recordPositionService = {
buildRecordPosition: jest.fn().mockResolvedValue([
{
position: 1,
},
@@ -42,12 +36,8 @@ describe('RecordPositionBackfillService', () => {
providers: [
RecordPositionBackfillService,
{
provide: RecordPositionQueryFactory,
useValue: recordPositionQueryFactory,
},
{
provide: RecordPositionFactory,
useValue: recordPositionFactory,
provide: RecordPositionService,
useValue: recordPositionService,
},
{
provide: WorkspaceDataSourceService,
@@ -123,8 +113,7 @@ describe('RecordPositionBackfillService', () => {
]);
await service.backfill('workspaceId', false);
expect(workspaceDataSourceService.executeRawQuery).toHaveBeenCalledTimes(2);
expect(recordPositionFactory.create).toHaveBeenCalledTimes(1);
expect(recordPositionQueryFactory.create).toHaveBeenCalledTimes(2);
expect(recordPositionService.buildRecordPosition).toHaveBeenCalledTimes(1);
});
it('when dryRun is true, should not update position', async () => {
@@ -148,7 +137,6 @@ describe('RecordPositionBackfillService', () => {
]);
await service.backfill('workspaceId', true);
expect(workspaceDataSourceService.executeRawQuery).toHaveBeenCalledTimes(1);
expect(recordPositionFactory.create).toHaveBeenCalledTimes(1);
expect(recordPositionQueryFactory.create).toHaveBeenCalledTimes(1);
expect(recordPositionService.buildRecordPosition).toHaveBeenCalledTimes(1);
});
});
@@ -1,9 +1,8 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RecordPositionQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
import { RecordPositionFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory';
import { RecordPositionBackfillService } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@@ -12,11 +11,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
WorkspaceDataSourceModule,
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
],
providers: [
RecordPositionFactory,
RecordPositionQueryFactory,
RecordPositionBackfillService,
],
providers: [RecordPositionService, RecordPositionBackfillService],
exports: [RecordPositionBackfillService],
})
export class RecordPositionBackfillModule {}
@@ -2,14 +2,12 @@ import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'class-validator';
import { Repository } from 'typeorm';
import { FieldMetadataType } from 'twenty-shared/types';
import { Repository } from 'typeorm';
import {
RecordPositionQueryFactory,
RecordPositionQueryType,
} from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
import { RecordPositionFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { RecordPositionQueryType } from 'src/engine/core-modules/record-position/types/record-position-query.type';
import { buildRecordPositionQuery } from 'src/engine/core-modules/record-position/utils/build-record-position-query.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
@@ -19,9 +17,8 @@ export class RecordPositionBackfillService {
constructor(
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly recordPositionFactory: RecordPositionFactory,
private readonly recordPositionQueryFactory: RecordPositionQueryFactory,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly recordPositionService: RecordPositionService,
) {}
async backfill(workspaceId: string, dryRun: boolean) {
@@ -47,7 +44,7 @@ export class RecordPositionBackfillService {
for (const objectMetadata of objectMetadataCollection) {
const [recordsWithoutPositionQuery, recordsWithoutPositionQueryParams] =
this.recordPositionQueryFactory.create(
buildRecordPositionQuery(
{
recordPositionQueryType: RecordPositionQueryType.FIND_BY_POSITION,
positionValue: null,
@@ -73,7 +70,7 @@ export class RecordPositionBackfillService {
continue;
}
const position = await this.recordPositionFactory.create({
const position = await this.recordPositionService.buildRecordPosition({
objectMetadata: {
isCustom: objectMetadata.isCustom,
nameSingular: objectMetadata.nameSingular,
@@ -106,7 +103,7 @@ export class RecordPositionBackfillService {
continue;
}
const [query, params] = this.recordPositionQueryFactory.create(
const [query, params] = buildRecordPositionQuery(
{
recordPositionQueryType: RecordPositionQueryType.UPDATE_POSITION,
recordId: recordsWithoutPosition[recordIndex].id,
@@ -10,6 +10,7 @@ import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@@ -29,6 +30,7 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
TelemetryModule,
FileModule,
FeatureFlagModule,
RecordPositionModule,
],
providers: [
...workspaceQueryRunnerFactories,