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

View File

@ -3,8 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
import { ServerBlockNoteEditor } from '@blocknote/server-util';
import chalk from 'chalk';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { FieldMetadataType } from 'twenty-shared/types';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandOptions,

View File

@ -1,81 +0,0 @@
import {
RecordPositionQueryFactory,
RecordPositionQueryType,
} from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
describe('RecordPositionQueryFactory', () => {
const objectMetadataItem = {
isCustom: false,
nameSingular: 'company',
};
const dataSourceSchema = 'workspace_test';
const factory: RecordPositionQueryFactory = new RecordPositionQueryFactory();
it('should be defined', () => {
expect(factory).toBeDefined();
});
describe('create', () => {
it('should return query and params for FIND_BY_POSITION', async () => {
const positionValue = 1;
const queryType = RecordPositionQueryType.FIND_BY_POSITION;
const [query, params] = factory.create(
{ positionValue, recordPositionQueryType: queryType },
objectMetadataItem,
dataSourceSchema,
);
expect(query).toEqual(
`SELECT id, position FROM ${dataSourceSchema}."${objectMetadataItem.nameSingular}"
WHERE "position" = $1`,
);
expect(params).toEqual([positionValue]);
});
it('should return query and params for FIND_MIN_POSITION', async () => {
const queryType = RecordPositionQueryType.FIND_MIN_POSITION;
const [query, params] = factory.create(
{ recordPositionQueryType: queryType },
objectMetadataItem,
dataSourceSchema,
);
expect(query).toEqual(
`SELECT MIN(position) as position FROM ${dataSourceSchema}."${objectMetadataItem.nameSingular}"`,
);
expect(params).toEqual([]);
});
it('should return query and params for FIND_MAX_POSITION', async () => {
const queryType = RecordPositionQueryType.FIND_MAX_POSITION;
const [query, params] = factory.create(
{ recordPositionQueryType: queryType },
objectMetadataItem,
dataSourceSchema,
);
expect(query).toEqual(
`SELECT MAX(position) as position FROM ${dataSourceSchema}."${objectMetadataItem.nameSingular}"`,
);
expect(params).toEqual([]);
});
it('should return query and params for UPDATE_POSITION', async () => {
const positionValue = 1;
const recordId = '1';
const queryType = RecordPositionQueryType.UPDATE_POSITION;
const [query, params] = factory.create(
{ positionValue, recordId, recordPositionQueryType: queryType },
objectMetadataItem,
dataSourceSchema,
);
expect(query).toEqual(
`UPDATE ${dataSourceSchema}."${objectMetadataItem.nameSingular}"
SET "position" = $1
WHERE "id" = $2`,
);
expect(params).toEqual([positionValue, recordId]);
});
});
});

View File

@ -1,8 +1,5 @@
import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory';
import { RecordPositionQueryFactory } from './record-position-query.factory';
export const workspaceQueryBuilderFactories = [
RecordPositionQueryFactory,
ForeignDataWrapperServerQueryFactory,
];

View File

@ -1,121 +0,0 @@
import { Injectable } from '@nestjs/common';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
export enum RecordPositionQueryType {
FIND_MIN_POSITION = 'FIND_MIN_POSITION',
FIND_MAX_POSITION = 'FIND_MAX_POSITION',
FIND_BY_POSITION = 'FIND_BY_POSITION',
UPDATE_POSITION = 'UPDATE_POSITION',
}
type FindByPositionQueryArgs = {
positionValue: number | null;
recordPositionQueryType: RecordPositionQueryType.FIND_BY_POSITION;
};
type FindMinPositionQueryArgs = {
recordPositionQueryType: RecordPositionQueryType.FIND_MIN_POSITION;
};
type FindMaxPositionQueryArgs = {
recordPositionQueryType: RecordPositionQueryType.FIND_MAX_POSITION;
};
type UpdatePositionQueryArgs = {
recordId: string;
positionValue: number;
recordPositionQueryType: RecordPositionQueryType.UPDATE_POSITION;
};
type RecordPositionQuery = string;
type RecordPositionQueryParams = any[];
export type RecordPositionQueryArgs =
| FindByPositionQueryArgs
| FindMinPositionQueryArgs
| FindMaxPositionQueryArgs
| UpdatePositionQueryArgs;
@Injectable()
export class RecordPositionQueryFactory {
create(
recordPositionQueryArgs: RecordPositionQueryArgs,
objectMetadata: { isCustom: boolean; nameSingular: string },
dataSourceSchema: string,
): [RecordPositionQuery, RecordPositionQueryParams] {
const tableName = computeTableName(
objectMetadata.nameSingular,
objectMetadata.isCustom,
);
switch (recordPositionQueryArgs.recordPositionQueryType) {
case RecordPositionQueryType.FIND_BY_POSITION:
return this.buildFindByPositionQuery(
recordPositionQueryArgs satisfies FindByPositionQueryArgs,
tableName,
dataSourceSchema,
);
case RecordPositionQueryType.FIND_MIN_POSITION:
return this.buildFindMinPositionQuery(tableName, dataSourceSchema);
case RecordPositionQueryType.FIND_MAX_POSITION:
return this.buildFindMaxPositionQuery(tableName, dataSourceSchema);
case RecordPositionQueryType.UPDATE_POSITION:
return this.buildUpdatePositionQuery(
recordPositionQueryArgs satisfies UpdatePositionQueryArgs,
tableName,
dataSourceSchema,
);
default:
throw new Error('Invalid RecordPositionQueryType');
}
}
private buildFindByPositionQuery(
{ positionValue }: FindByPositionQueryArgs,
name: string,
dataSourceSchema: string,
): [RecordPositionQuery, RecordPositionQueryParams] {
const positionStringParam = positionValue ? '= $1' : 'IS NULL';
return [
`SELECT id, position FROM ${dataSourceSchema}."${name}"
WHERE "position" ${positionStringParam}`,
positionValue ? [positionValue] : [],
];
}
private buildFindMaxPositionQuery(
name: string,
dataSourceSchema: string,
): [RecordPositionQuery, RecordPositionQueryParams] {
return [
`SELECT MAX(position) as position FROM ${dataSourceSchema}."${name}"`,
[],
];
}
private buildFindMinPositionQuery(
name: string,
dataSourceSchema: string,
): [RecordPositionQuery, RecordPositionQueryParams] {
return [
`SELECT MIN(position) as position FROM ${dataSourceSchema}."${name}"`,
[],
];
}
private buildUpdatePositionQuery(
{ recordId, positionValue }: UpdatePositionQueryArgs,
name: string,
dataSourceSchema: string,
): [RecordPositionQuery, RecordPositionQueryParams] {
return [
`UPDATE ${dataSourceSchema}."${name}"
SET "position" = $1
WHERE "id" = $2`,
[positionValue, recordId],
];
}
}

View File

@ -1,6 +1,5 @@
import { Module } from '@nestjs/common';
import { RecordPositionQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { workspaceQueryBuilderFactories } from './factories/factories';
@ -8,6 +7,6 @@ import { workspaceQueryBuilderFactories } from './factories/factories';
@Module({
imports: [ObjectMetadataModule],
providers: [...workspaceQueryBuilderFactories],
exports: [RecordPositionQueryFactory],
exports: [],
})
export class WorkspaceQueryBuilderModule {}

View File

@ -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 }],

View File

@ -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,
];

View File

@ -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: {

View File

@ -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);
});
});

View File

@ -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 {}

View File

@ -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,

View File

@ -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,

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { RecordPositionService } from './services/record-position.service';
@Module({
imports: [WorkspaceDataSourceModule],
providers: [RecordPositionService],
exports: [RecordPositionService],
})
export class RecordPositionModule {}

View File

@ -1,17 +1,12 @@
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 { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
describe('RecordPositionFactory', () => {
const recordPositionQueryFactory = {
create: jest.fn().mockReturnValue(['query', []]),
};
describe('RecordPositionService', () => {
let workspaceDataSourceService;
let factory: RecordPositionFactory;
let service: RecordPositionService;
beforeEach(async () => {
workspaceDataSourceService = {
@ -20,11 +15,7 @@ describe('RecordPositionFactory', () => {
};
const module: TestingModule = await Test.createTestingModule({
providers: [
RecordPositionFactory,
{
provide: RecordPositionQueryFactory,
useValue: recordPositionQueryFactory,
},
RecordPositionService,
{
provide: WorkspaceDataSourceService,
useValue: workspaceDataSourceService,
@ -32,11 +23,11 @@ describe('RecordPositionFactory', () => {
],
}).compile();
factory = module.get<RecordPositionFactory>(RecordPositionFactory);
service = module.get<RecordPositionService>(RecordPositionService);
});
it('should be defined', () => {
expect(factory).toBeDefined();
expect(service).toBeDefined();
});
describe('create', () => {
@ -46,7 +37,7 @@ describe('RecordPositionFactory', () => {
it('should return the value when value is a number', async () => {
const value = 1;
const result = await factory.create({
const result = await service.buildRecordPosition({
value,
objectMetadata,
workspaceId,
@ -57,7 +48,7 @@ describe('RecordPositionFactory', () => {
it('should return the existing position -1 when value is first', async () => {
const value = 'first';
const result = await factory.create({
const result = await service.buildRecordPosition({
value,
objectMetadata,
workspaceId,
@ -68,7 +59,7 @@ describe('RecordPositionFactory', () => {
it('should return the existing position + 1 when value is last', async () => {
const value = 'last';
const result = await factory.create({
const result = await service.buildRecordPosition({
value,
objectMetadata,
workspaceId,

View File

@ -4,30 +4,30 @@ import { isDefined } from 'class-validator';
import {
RecordPositionQueryArgs,
RecordPositionQueryFactory,
RecordPositionQueryType,
} from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
} 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 { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
export type RecordPositionFactoryCreateArgs = {
export type RecordPositionServiceCreateArgs = {
value: number | 'first' | 'last';
objectMetadata: { isCustom: boolean; nameSingular: string };
workspaceId: string;
index?: number;
};
@Injectable()
export class RecordPositionFactory {
export class RecordPositionService {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly recordPositionQueryFactory: RecordPositionQueryFactory,
) {}
async create({
async buildRecordPosition({
objectMetadata,
value,
workspaceId,
index = 0,
}: RecordPositionFactoryCreateArgs): Promise<number> {
}: RecordPositionServiceCreateArgs): Promise<number> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
@ -36,41 +36,43 @@ export class RecordPositionFactory {
}
if (value === 'first') {
const recordWithMinPosition = await this.findRecordPosition(
{
recordPositionQueryType: RecordPositionQueryType.FIND_MIN_POSITION,
},
objectMetadata,
dataSourceSchema,
workspaceId,
);
const recordWithMinPosition =
await this.createAndExecuteRecordPositionQuery(
{
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,
);
const recordWithMaxPosition =
await this.createAndExecuteRecordPositionQuery(
{
recordPositionQueryType: RecordPositionQueryType.FIND_MAX_POSITION,
},
objectMetadata,
dataSourceSchema,
workspaceId,
);
return isDefined(recordWithMaxPosition?.position)
? recordWithMaxPosition.position + index + 1
: 1;
}
private async findRecordPosition(
private async createAndExecuteRecordPositionQuery(
recordPositionQueryArgs: RecordPositionQueryArgs,
objectMetadata: { isCustom: boolean; nameSingular: string },
dataSourceSchema: string,
workspaceId: string,
) {
const [query, params] = this.recordPositionQueryFactory.create(
const [query, params] = buildRecordPositionQuery(
recordPositionQueryArgs,
objectMetadata,
dataSourceSchema,

View File

@ -0,0 +1,31 @@
export type FindByPositionQueryArgs = {
positionValue: number | null;
recordPositionQueryType: RecordPositionQueryType.FIND_BY_POSITION;
};
export type FindMinPositionQueryArgs = {
recordPositionQueryType: RecordPositionQueryType.FIND_MIN_POSITION;
};
export type FindMaxPositionQueryArgs = {
recordPositionQueryType: RecordPositionQueryType.FIND_MAX_POSITION;
};
export type UpdatePositionQueryArgs = {
recordId: string;
positionValue: number;
recordPositionQueryType: RecordPositionQueryType.UPDATE_POSITION;
};
export enum RecordPositionQueryType {
FIND_MIN_POSITION = 'FIND_MIN_POSITION',
FIND_MAX_POSITION = 'FIND_MAX_POSITION',
FIND_BY_POSITION = 'FIND_BY_POSITION',
UPDATE_POSITION = 'UPDATE_POSITION',
}
export type RecordPositionQueryArgs =
| FindByPositionQueryArgs
| FindMinPositionQueryArgs
| FindMaxPositionQueryArgs
| UpdatePositionQueryArgs;

View File

@ -0,0 +1,72 @@
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';
describe('buildRecordPositionQuery', () => {
const objectMetadataItem = {
isCustom: false,
nameSingular: 'company',
};
const dataSourceSchema = 'workspace_test';
it('should return query and params for FIND_BY_POSITION', async () => {
const positionValue = 1;
const queryType = RecordPositionQueryType.FIND_BY_POSITION;
const [query, params] = buildRecordPositionQuery(
{ positionValue, recordPositionQueryType: queryType },
objectMetadataItem,
dataSourceSchema,
);
expect(query).toEqual(
`SELECT id, position FROM ${dataSourceSchema}."${objectMetadataItem.nameSingular}"
WHERE "position" = $1`,
);
expect(params).toEqual([positionValue]);
});
it('should return query and params for FIND_MIN_POSITION', async () => {
const queryType = RecordPositionQueryType.FIND_MIN_POSITION;
const [query, params] = buildRecordPositionQuery(
{ recordPositionQueryType: queryType },
objectMetadataItem,
dataSourceSchema,
);
expect(query).toEqual(
`SELECT MIN(position) as position FROM ${dataSourceSchema}."${objectMetadataItem.nameSingular}"`,
);
expect(params).toEqual([]);
});
it('should return query and params for FIND_MAX_POSITION', async () => {
const queryType = RecordPositionQueryType.FIND_MAX_POSITION;
const [query, params] = buildRecordPositionQuery(
{ recordPositionQueryType: queryType },
objectMetadataItem,
dataSourceSchema,
);
expect(query).toEqual(
`SELECT MAX(position) as position FROM ${dataSourceSchema}."${objectMetadataItem.nameSingular}"`,
);
expect(params).toEqual([]);
});
it('should return query and params for UPDATE_POSITION', async () => {
const positionValue = 1;
const recordId = '1';
const queryType = RecordPositionQueryType.UPDATE_POSITION;
const [query, params] = buildRecordPositionQuery(
{ positionValue, recordId, recordPositionQueryType: queryType },
objectMetadataItem,
dataSourceSchema,
);
expect(query).toEqual(
`UPDATE ${dataSourceSchema}."${objectMetadataItem.nameSingular}"
SET "position" = $1
WHERE "id" = $2`,
);
expect(params).toEqual([positionValue, recordId]);
});
});

View File

@ -0,0 +1,90 @@
import {
FindByPositionQueryArgs,
RecordPositionQueryArgs,
RecordPositionQueryType,
UpdatePositionQueryArgs,
} from 'src/engine/core-modules/record-position/types/record-position-query.type';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
type RecordPositionQuery = string;
type RecordPositionQueryParams = any[];
export const buildRecordPositionQuery = (
recordPositionQueryArgs: RecordPositionQueryArgs,
objectMetadata: { isCustom: boolean; nameSingular: string },
dataSourceSchema: string,
): [RecordPositionQuery, RecordPositionQueryParams] => {
const tableName = computeTableName(
objectMetadata.nameSingular,
objectMetadata.isCustom,
);
switch (recordPositionQueryArgs.recordPositionQueryType) {
case RecordPositionQueryType.FIND_BY_POSITION:
return buildFindByPositionQuery(
recordPositionQueryArgs satisfies FindByPositionQueryArgs,
tableName,
dataSourceSchema,
);
case RecordPositionQueryType.FIND_MIN_POSITION:
return buildFindMinPositionQuery(tableName, dataSourceSchema);
case RecordPositionQueryType.FIND_MAX_POSITION:
return buildFindMaxPositionQuery(tableName, dataSourceSchema);
case RecordPositionQueryType.UPDATE_POSITION:
return buildUpdatePositionQuery(
recordPositionQueryArgs satisfies UpdatePositionQueryArgs,
tableName,
dataSourceSchema,
);
default:
throw new Error('Invalid RecordPositionQueryType');
}
};
const buildFindByPositionQuery = (
{ positionValue }: FindByPositionQueryArgs,
name: string,
dataSourceSchema: string,
): [RecordPositionQuery, RecordPositionQueryParams] => {
const positionStringParam = positionValue ? '= $1' : 'IS NULL';
return [
`SELECT id, position FROM ${dataSourceSchema}."${name}"
WHERE "position" ${positionStringParam}`,
positionValue ? [positionValue] : [],
];
};
const buildFindMaxPositionQuery = (
name: string,
dataSourceSchema: string,
): [RecordPositionQuery, RecordPositionQueryParams] => {
return [
`SELECT MAX(position) as position FROM ${dataSourceSchema}."${name}"`,
[],
];
};
const buildFindMinPositionQuery = (
name: string,
dataSourceSchema: string,
): [RecordPositionQuery, RecordPositionQueryParams] => {
return [
`SELECT MIN(position) as position FROM ${dataSourceSchema}."${name}"`,
[],
];
};
const buildUpdatePositionQuery = (
{ recordId, positionValue }: UpdatePositionQueryArgs,
name: string,
dataSourceSchema: string,
): [RecordPositionQuery, RecordPositionQueryParams] => {
return [
`UPDATE ${dataSourceSchema}."${name}"
SET "position" = $1
WHERE "id" = $2`,
[positionValue, recordId],
];
};

View File

@ -6,6 +6,7 @@ import { Repository } from 'typeorm';
import { WorkflowExecutor } from 'src/modules/workflow/workflow-executor/interfaces/workflow-executor.interface';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
@ -33,6 +34,7 @@ export class CreateRecordWorkflowAction implements WorkflowExecutor {
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
private readonly recordPositionService: RecordPositionService,
) {}
async execute({
@ -80,8 +82,15 @@ export class CreateRecordWorkflowAction implements WorkflowExecutor {
);
}
const position = await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata,
workspaceId,
});
const objectRecord = await repository.save({
...workflowActionInput.objectRecord,
position,
createdBy: {
source: FieldActorSource.WORKFLOW,
name: 'Workflow',

View File

@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@ -16,6 +17,7 @@ import { UpdateRecordWorkflowAction } from 'src/modules/workflow/workflow-execut
WorkspaceCacheStorageModule,
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
FeatureFlagModule,
RecordPositionModule,
],
providers: [
ScopedWorkspaceContextFactory,