diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/deep-partial-entity-with-nested-relation-fields.type.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/deep-partial-entity-with-nested-relation-fields.type.ts new file mode 100644 index 0000000000..3c495be996 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/deep-partial-entity-with-nested-relation-fields.type.ts @@ -0,0 +1,17 @@ +import { DeepPartial } from 'typeorm/common/DeepPartial'; + +import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; +import { + ConnectObject, + DisconnectObject, + EntityRelationFields, +} from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type'; + +export type DeepPartialWithNestedRelationFields = Omit< + DeepPartial, + EntityRelationFields +> & { + [K in keyof T]?: T[K] extends BaseWorkspaceEntity | null + ? DeepPartial | ConnectObject | DisconnectObject + : DeepPartial; +}; diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type.ts similarity index 95% rename from packages/twenty-server/src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type.ts rename to packages/twenty-server/src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type.ts index 15d1c1a1b5..2377b4986d 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type.ts @@ -17,7 +17,7 @@ export type DisconnectObject = { [RELATION_NESTED_QUERY_KEYWORDS.DISCONNECT]: true; }; -type EntityRelationFields = { +export type EntityRelationFields = { [K in keyof T]: T[K] extends BaseWorkspaceEntity | null ? K : never; }[keyof T]; diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/relation-nested-query-fields-by-entity-index.type.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/relation-nested-query-fields-by-entity-index.type.ts index c06ade3dc7..52a8b50018 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/relation-nested-query-fields-by-entity-index.type.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/relation-nested-query-fields-by-entity-index.type.ts @@ -1,7 +1,7 @@ import { ConnectObject, DisconnectObject, -} from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type'; +} from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type'; export type RelationConnectQueryFieldsByEntityIndex = { [entityIndex: string]: { [key: string]: ConnectObject }; diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts index e5aa0011b2..178e8c3cbe 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts @@ -40,7 +40,9 @@ import { PermissionsExceptionCode, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; -import { QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type'; +import { DeepPartialWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/deep-partial-entity-with-nested-relation-fields.type'; +import { QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type'; +import { RelationNestedQueries } from 'src/engine/twenty-orm/relation-nested-queries/relation-nested-queries'; import { OperationType, validateOperationIsPermittedOrThrow, @@ -976,7 +978,7 @@ export class WorkspaceEntityManager extends EntityManager { permissionOptions?: PermissionOptions, ): Promise; - override save>( + override save>( targetOrEntity: EntityTarget, entities: T[], options: SaveOptions & { @@ -985,14 +987,14 @@ export class WorkspaceEntityManager extends EntityManager { permissionOptions?: PermissionOptions, ): Promise; - override save>( + override save>( targetOrEntity: EntityTarget, entities: T[], options?: SaveOptions, permissionOptions?: PermissionOptions, ): Promise<(T & Entity)[]>; - override save>( + override save>( targetOrEntity: EntityTarget, entity: T, options: SaveOptions & { @@ -1001,7 +1003,7 @@ export class WorkspaceEntityManager extends EntityManager { permissionOptions?: PermissionOptions, ): Promise; - override save>( + override save>( targetOrEntity: EntityTarget, entity: T, options?: SaveOptions, @@ -1010,7 +1012,7 @@ export class WorkspaceEntityManager extends EntityManager { override async save< Entity extends ObjectLiteral, - T extends DeepPartial, + T extends DeepPartialWithNestedRelationFields, >( targetOrEntity: EntityTarget | Entity | Entity[], entityOrMaybeOptions: @@ -1062,6 +1064,30 @@ export class WorkspaceEntityManager extends EntityManager { target ?? (isEntityArray ? entity[0]?.constructor : entity.constructor); const entityArray = isEntityArray ? entity : [entity]; + + const relationNestedQueries = new RelationNestedQueries( + this.internalContext, + ); + + const relationNestedConfig = + relationNestedQueries.prepareNestedRelationQueries( + entityArray, + entityTarget, + ); + + const updatedEntities = isDefined(relationNestedConfig) + ? await relationNestedQueries.processRelationNestedQueries({ + entities: entityArray, + relationNestedConfig, + queryBuilder: this.createQueryBuilder( + undefined, + undefined, + undefined, + permissionOptions, + ), + }) + : entityArray; + const entityIds = entityArray.map((e) => (e as { id: string }).id); const beforeUpdate = await this.find( entityTarget, @@ -1086,7 +1112,7 @@ export class WorkspaceEntityManager extends EntityManager { ); const formattedEntityOrEntities = formatData( - entityArray, + updatedEntities, objectMetadataItem, ); diff --git a/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/relation-nested-queries.ts b/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/relation-nested-queries.ts index 4f76bf7e42..e6b5ac4cbe 100644 --- a/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/relation-nested-queries.ts +++ b/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/relation-nested-queries.ts @@ -1,11 +1,11 @@ import { isDefined } from 'class-validator'; import { RELATION_NESTED_QUERY_KEYWORDS } from 'twenty-shared/constants'; -import { EntityTarget, ObjectLiteral } from 'typeorm'; +import { EntityTarget, ObjectLiteral, SelectQueryBuilder } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; -import { QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type'; +import { QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type'; import { RelationConnectQueryConfig } from 'src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type'; import { RelationConnectQueryFieldsByEntityIndex, @@ -36,10 +36,9 @@ export class RelationNestedQueries { | QueryDeepPartialEntityWithNestedRelationFields[] | QueryDeepPartialEntityWithNestedRelationFields, target: EntityTarget, - ): [ - RelationConnectQueryConfig[], - RelationDisconnectQueryFieldsByEntityIndex, - ] { + ): + | [RelationConnectQueryConfig[], RelationDisconnectQueryFieldsByEntityIndex] + | null { const entitiesArray = Array.isArray(entities) ? entities : [entities]; const { @@ -53,7 +52,10 @@ export class RelationNestedQueries { relationConnectQueryFieldsByEntityIndex, ); - return [connectConfig, relationDisconnectQueryFieldsByEntityIndex]; + return connectConfig.length > 0 || + Object.keys(relationDisconnectQueryFieldsByEntityIndex).length > 0 + ? [connectConfig, relationDisconnectQueryFieldsByEntityIndex] + : null; } private prepareRelationConnect( @@ -90,7 +92,9 @@ export class RelationNestedQueries { RelationConnectQueryConfig[], RelationDisconnectQueryFieldsByEntityIndex, ]; - queryBuilder: WorkspaceSelectQueryBuilder; + queryBuilder: + | WorkspaceSelectQueryBuilder + | SelectQueryBuilder; }): Promise[]> { const entitiesArray = Array.isArray(entities) ? entities : [entities]; @@ -120,7 +124,9 @@ export class RelationNestedQueries { }: { entities: QueryDeepPartialEntityWithNestedRelationFields[]; relationConnectQueryConfigs: RelationConnectQueryConfig[]; - queryBuilder: WorkspaceSelectQueryBuilder; + queryBuilder: + | WorkspaceSelectQueryBuilder + | SelectQueryBuilder; }): Promise[]> { if (relationConnectQueryConfigs.length === 0) return entities; @@ -139,7 +145,9 @@ export class RelationNestedQueries { private async executeConnectQueries( relationConnectQueryConfigs: RelationConnectQueryConfig[], - queryBuilder: WorkspaceSelectQueryBuilder, + queryBuilder: + | WorkspaceSelectQueryBuilder + | SelectQueryBuilder, ): Promise<[RelationConnectQueryConfig, Record[]][]> { const allRecordsToConnectWithConfig: [ RelationConnectQueryConfig, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts index 7f82402061..061b18977c 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts @@ -1,4 +1,5 @@ import { ObjectRecordsPermissions } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { EntityTarget, InsertQueryBuilder, @@ -12,7 +13,7 @@ import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/works import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type'; +import { QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type'; import { RelationConnectQueryConfig } from 'src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type'; import { RelationDisconnectQueryFieldsByEntityIndex } from 'src/engine/twenty-orm/entity-manager/types/relation-nested-query-fields-by-entity-index.type'; import { @@ -38,10 +39,9 @@ export class WorkspaceInsertQueryBuilder< private authContext?: AuthContext; private featureFlagMap?: FeatureFlagMap; private relationNestedQueries: RelationNestedQueries; - private relationNestedConfig: [ - RelationConnectQueryConfig[], - RelationDisconnectQueryFieldsByEntityIndex, - ]; + private relationNestedConfig: + | [RelationConnectQueryConfig[], RelationDisconnectQueryFieldsByEntityIndex] + | null; constructor( queryBuilder: InsertQueryBuilder, @@ -115,24 +115,26 @@ export class WorkspaceInsertQueryBuilder< this.internalContext, ); - const nestedRelationQueryBuilder = new WorkspaceSelectQueryBuilder( - this as unknown as WorkspaceSelectQueryBuilder, - this.objectRecordsPermissions, - this.internalContext, - this.shouldBypassPermissionChecks, - this.authContext, - ); + if (isDefined(this.relationNestedConfig)) { + const nestedRelationQueryBuilder = new WorkspaceSelectQueryBuilder( + this as unknown as WorkspaceSelectQueryBuilder, + this.objectRecordsPermissions, + this.internalContext, + this.shouldBypassPermissionChecks, + this.authContext, + ); - const updatedValues = - await this.relationNestedQueries.processRelationNestedQueries({ - entities: this.expressionMap.valuesSet as - | QueryDeepPartialEntityWithNestedRelationFields - | QueryDeepPartialEntityWithNestedRelationFields[], - relationNestedConfig: this.relationNestedConfig, - queryBuilder: nestedRelationQueryBuilder, - }); + const updatedValues = + await this.relationNestedQueries.processRelationNestedQueries({ + entities: this.expressionMap.valuesSet as + | QueryDeepPartialEntityWithNestedRelationFields + | QueryDeepPartialEntityWithNestedRelationFields[], + relationNestedConfig: this.relationNestedConfig, + queryBuilder: nestedRelationQueryBuilder, + }); - this.expressionMap.valuesSet = updatedValues; + this.expressionMap.valuesSet = updatedValues; + } const result = await super.execute(); diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts index 757da9ebe7..1b0ee92913 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts @@ -1,4 +1,5 @@ import { ObjectRecordsPermissions } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { EntityTarget, ObjectLiteral, @@ -13,7 +14,7 @@ import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/works import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type'; +import { QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type'; import { RelationConnectQueryConfig } from 'src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type'; import { RelationDisconnectQueryFieldsByEntityIndex } from 'src/engine/twenty-orm/entity-manager/types/relation-nested-query-fields-by-entity-index.type'; import { @@ -38,10 +39,9 @@ export class WorkspaceUpdateQueryBuilder< private authContext?: AuthContext; private featureFlagMap?: FeatureFlagMap; private relationNestedQueries: RelationNestedQueries; - private relationNestedConfig: [ - RelationConnectQueryConfig[], - RelationDisconnectQueryFieldsByEntityIndex, - ]; + private relationNestedConfig: + | [RelationConnectQueryConfig[], RelationDisconnectQueryFieldsByEntityIndex] + | null; constructor( queryBuilder: UpdateQueryBuilder, @@ -115,17 +115,19 @@ export class WorkspaceUpdateQueryBuilder< this.authContext, ); - const updatedValues = - await this.relationNestedQueries.processRelationNestedQueries({ - entities: this.expressionMap.valuesSet as - | QueryDeepPartialEntityWithNestedRelationFields - | QueryDeepPartialEntityWithNestedRelationFields[], - relationNestedConfig: this.relationNestedConfig, - queryBuilder: nestedRelationQueryBuilder, - }); + if (isDefined(this.relationNestedConfig)) { + const updatedValues = + await this.relationNestedQueries.processRelationNestedQueries({ + entities: this.expressionMap.valuesSet as + | QueryDeepPartialEntityWithNestedRelationFields + | QueryDeepPartialEntityWithNestedRelationFields[], + relationNestedConfig: this.relationNestedConfig, + queryBuilder: nestedRelationQueryBuilder, + }); - this.expressionMap.valuesSet = - updatedValues.length === 1 ? updatedValues[0] : updatedValues; + this.expressionMap.valuesSet = + updatedValues.length === 1 ? updatedValues[0] : updatedValues; + } const formattedBefore = formatResult( before, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts index 081289305f..b3cb1ada77 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts @@ -27,7 +27,8 @@ import { PermissionsException, PermissionsExceptionCode, } from 'src/engine/metadata-modules/permissions/permissions.exception'; -import { QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type'; +import { DeepPartialWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/deep-partial-entity-with-nested-relation-fields.type'; +import { QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type'; import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; @@ -243,31 +244,31 @@ export class WorkspaceRepository< /** * SAVE METHODS */ - override save>( + override save>( entities: U[], options: SaveOptions & { reload: false }, entityManager?: WorkspaceEntityManager, ): Promise; - override save>( + override save>( entities: U[], options?: SaveOptions, entityManager?: WorkspaceEntityManager, ): Promise<(U & T)[]>; - override save>( + override save>( entity: U, options: SaveOptions & { reload: false }, entityManager?: WorkspaceEntityManager, ): Promise; - override save>( + override save>( entity: U, options?: SaveOptions, entityManager?: WorkspaceEntityManager, ): Promise; - override async save>( + override async save>( entityOrEntities: U | U[], options?: SaveOptions | (SaveOptions & { reload: false }), entityManager?: WorkspaceEntityManager, diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts index 5c7ba108c5..76453d9c1b 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts @@ -10,7 +10,7 @@ import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field- import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { ConnectObject } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type'; +import { ConnectObject } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type'; import { RelationConnectQueryConfig, UniqueConstraintCondition, diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/extract-nested-relation-fields-by-entity-index.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/extract-nested-relation-fields-by-entity-index.util.ts index 6fdf385504..8a24926c27 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/extract-nested-relation-fields-by-entity-index.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/extract-nested-relation-fields-by-entity-index.util.ts @@ -4,7 +4,7 @@ import { RELATION_NESTED_QUERY_KEYWORDS } from 'twenty-shared/constants'; import { ConnectObject, DisconnectObject, -} from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type'; +} from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type'; import { RelationConnectQueryFieldsByEntityIndex, RelationDisconnectQueryFieldsByEntityIndex, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts index 58317705be..cb26c1bb22 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts @@ -37,7 +37,6 @@ export class CleanerWorkspaceService { private readonly inactiveDaysBeforeDelete: number; private readonly inactiveDaysBeforeWarn: number; private readonly maxNumberOfWorkspacesDeletedPerExecution: number; - constructor( private readonly workspaceService: WorkspaceService, private readonly twentyConfigService: TwentyConfigService,