From 023d071103130509bc7edb4703b4c3fb00f894a5 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 1 Apr 2025 11:50:43 +0200 Subject: [PATCH] Set record position on workflow creation (#11308) - Migrate record position factory to core-modules - set position on record creation --- ...migrate-rich-text-content-patch.command.ts | 2 +- .../record-position-query.factory.spec.ts | 81 ------------ .../factories/factories.ts | 3 - .../record-position-query.factory.ts | 121 ------------------ .../workspace-query-builder.module.ts | 3 +- .../query-runner-args.factory.spec.ts | 28 ++-- .../workspace-query-runner/factories/index.ts | 2 - .../factories/query-runner-args.factory.ts | 27 ++-- .../record-position-backfill-service.spec.ts | 28 ++-- .../record-position-backfill-module.ts | 9 +- .../record-position-backfill-service.ts | 19 ++- .../workspace-query-runner.module.ts | 2 + .../record-position/record-position.module.ts | 12 ++ .../record-position.service.spec.ts} | 27 ++-- .../services/record-position.service.ts} | 52 ++++---- .../types/record-position-query.type.ts | 31 +++++ .../build-record-position-query.util.spec.ts | 72 +++++++++++ .../utils/build-record-position-query.util.ts | 90 +++++++++++++ .../create-record.workflow-action.ts | 9 ++ .../record-crud/record-crud-action.module.ts | 2 + 20 files changed, 303 insertions(+), 317 deletions(-) delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/__tests__/record-position-query.factory.spec.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory.ts create mode 100644 packages/twenty-server/src/engine/core-modules/record-position/record-position.module.ts rename packages/twenty-server/src/engine/{api/graphql/workspace-query-runner/factories/__tests__/record-position.factory.spec.ts => core-modules/record-position/services/__tests__/record-position.service.spec.ts} (64%) rename packages/twenty-server/src/engine/{api/graphql/workspace-query-runner/factories/record-position.factory.ts => core-modules/record-position/services/record-position.service.ts} (64%) create mode 100644 packages/twenty-server/src/engine/core-modules/record-position/types/record-position-query.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/record-position/utils/__tests__/build-record-position-query.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/record-position/utils/build-record-position-query.util.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command.ts index dc81c5ac9..1bc5d1a14 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command.ts @@ -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, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/__tests__/record-position-query.factory.spec.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/__tests__/record-position-query.factory.spec.ts deleted file mode 100644 index 106447ccf..000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/__tests__/record-position-query.factory.spec.ts +++ /dev/null @@ -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]); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts index ca9778a33..021c642d1 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts @@ -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, ]; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory.ts deleted file mode 100644 index 5d9f4f249..000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory.ts +++ /dev/null @@ -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], - ]; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module.ts index 4dbb652bd..c7f5a01aa 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module.ts @@ -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 {} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts index e359c0f01..ce257fd59 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts @@ -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 }], diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/index.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/index.ts index a82d328d6..8fe5dd226 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/index.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/index.ts @@ -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, ]; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts index a0bb822cc..e807f2113 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts @@ -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: { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/__tests__/record-position-backfill-service.spec.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/__tests__/record-position-backfill-service.spec.ts index ec825588b..a6e929240 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/__tests__/record-position-backfill-service.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/__tests__/record-position-backfill-service.spec.ts @@ -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); }); }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-module.ts index a0748a0c9..08aaa8c6c 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-module.ts @@ -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 {} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service.ts index 8d9370c49..e98b47d27 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service.ts @@ -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, - 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, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts index 9b162c07c..abe786b41 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts @@ -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, diff --git a/packages/twenty-server/src/engine/core-modules/record-position/record-position.module.ts b/packages/twenty-server/src/engine/core-modules/record-position/record-position.module.ts new file mode 100644 index 000000000..4ada1d5c7 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/record-position/record-position.module.ts @@ -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 {} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/record-position.factory.spec.ts b/packages/twenty-server/src/engine/core-modules/record-position/services/__tests__/record-position.service.spec.ts similarity index 64% rename from packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/record-position.factory.spec.ts rename to packages/twenty-server/src/engine/core-modules/record-position/services/__tests__/record-position.service.spec.ts index a6728b8fb..b33cf2e2e 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/record-position.factory.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/record-position/services/__tests__/record-position.service.spec.ts @@ -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); + service = module.get(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, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/record-position.factory.ts b/packages/twenty-server/src/engine/core-modules/record-position/services/record-position.service.ts similarity index 64% rename from packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/record-position.factory.ts rename to packages/twenty-server/src/engine/core-modules/record-position/services/record-position.service.ts index b3ae41d79..d775a707f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/record-position.factory.ts +++ b/packages/twenty-server/src/engine/core-modules/record-position/services/record-position.service.ts @@ -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 { + }: RecordPositionServiceCreateArgs): Promise { 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, diff --git a/packages/twenty-server/src/engine/core-modules/record-position/types/record-position-query.type.ts b/packages/twenty-server/src/engine/core-modules/record-position/types/record-position-query.type.ts new file mode 100644 index 000000000..3e9ca8134 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/record-position/types/record-position-query.type.ts @@ -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; diff --git a/packages/twenty-server/src/engine/core-modules/record-position/utils/__tests__/build-record-position-query.util.spec.ts b/packages/twenty-server/src/engine/core-modules/record-position/utils/__tests__/build-record-position-query.util.spec.ts new file mode 100644 index 000000000..224e5b60d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/record-position/utils/__tests__/build-record-position-query.util.spec.ts @@ -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]); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/record-position/utils/build-record-position-query.util.ts b/packages/twenty-server/src/engine/core-modules/record-position/utils/build-record-position-query.util.ts new file mode 100644 index 000000000..b26022d03 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/record-position/utils/build-record-position-query.util.ts @@ -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], + ]; +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts index 06489ed06..f010bf23d 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts @@ -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, 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', diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/record-crud-action.module.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/record-crud-action.module.ts index 01532f0dc..0ba218ba1 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/record-crud-action.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/record-crud-action.module.ts @@ -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,