Connect/Disconnect in .save (#13444)

This commit is contained in:
Etienne
2025-07-28 20:28:41 +02:00
committed by GitHub
parent 2a82a33fff
commit c186b78f67
11 changed files with 119 additions and 64 deletions
@@ -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<T> = Omit<
DeepPartial<T>,
EntityRelationFields<T>
> & {
[K in keyof T]?: T[K] extends BaseWorkspaceEntity | null
? DeepPartial<T[K]> | ConnectObject | DisconnectObject
: DeepPartial<T[K]>;
};
@@ -17,7 +17,7 @@ export type DisconnectObject = {
[RELATION_NESTED_QUERY_KEYWORDS.DISCONNECT]: true;
};
type EntityRelationFields<T> = {
export type EntityRelationFields<T> = {
[K in keyof T]: T[K] extends BaseWorkspaceEntity | null ? K : never;
}[keyof T];
@@ -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 };
@@ -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<Entity>;
override save<Entity, T extends DeepPartial<Entity>>(
override save<Entity, T extends DeepPartialWithNestedRelationFields<Entity>>(
targetOrEntity: EntityTarget<Entity>,
entities: T[],
options: SaveOptions & {
@@ -985,14 +987,14 @@ export class WorkspaceEntityManager extends EntityManager {
permissionOptions?: PermissionOptions,
): Promise<T[]>;
override save<Entity, T extends DeepPartial<Entity>>(
override save<Entity, T extends DeepPartialWithNestedRelationFields<Entity>>(
targetOrEntity: EntityTarget<Entity>,
entities: T[],
options?: SaveOptions,
permissionOptions?: PermissionOptions,
): Promise<(T & Entity)[]>;
override save<Entity, T extends DeepPartial<Entity>>(
override save<Entity, T extends DeepPartialWithNestedRelationFields<Entity>>(
targetOrEntity: EntityTarget<Entity>,
entity: T,
options: SaveOptions & {
@@ -1001,7 +1003,7 @@ export class WorkspaceEntityManager extends EntityManager {
permissionOptions?: PermissionOptions,
): Promise<T>;
override save<Entity, T extends DeepPartial<Entity>>(
override save<Entity, T extends DeepPartialWithNestedRelationFields<Entity>>(
targetOrEntity: EntityTarget<Entity>,
entity: T,
options?: SaveOptions,
@@ -1010,7 +1012,7 @@ export class WorkspaceEntityManager extends EntityManager {
override async save<
Entity extends ObjectLiteral,
T extends DeepPartial<Entity>,
T extends DeepPartialWithNestedRelationFields<Entity>,
>(
targetOrEntity: EntityTarget<Entity> | 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,
);
@@ -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<Entity>[]
| QueryDeepPartialEntityWithNestedRelationFields<Entity>,
target: EntityTarget<Entity>,
): [
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<Entity extends ObjectLiteral>(
@@ -90,7 +92,9 @@ export class RelationNestedQueries {
RelationConnectQueryConfig[],
RelationDisconnectQueryFieldsByEntityIndex,
];
queryBuilder: WorkspaceSelectQueryBuilder<Entity>;
queryBuilder:
| WorkspaceSelectQueryBuilder<Entity>
| SelectQueryBuilder<Entity>;
}): Promise<QueryDeepPartialEntity<Entity>[]> {
const entitiesArray = Array.isArray(entities) ? entities : [entities];
@@ -120,7 +124,9 @@ export class RelationNestedQueries {
}: {
entities: QueryDeepPartialEntityWithNestedRelationFields<Entity>[];
relationConnectQueryConfigs: RelationConnectQueryConfig[];
queryBuilder: WorkspaceSelectQueryBuilder<Entity>;
queryBuilder:
| WorkspaceSelectQueryBuilder<Entity>
| SelectQueryBuilder<Entity>;
}): Promise<QueryDeepPartialEntity<Entity>[]> {
if (relationConnectQueryConfigs.length === 0) return entities;
@@ -139,7 +145,9 @@ export class RelationNestedQueries {
private async executeConnectQueries<Entity extends ObjectLiteral>(
relationConnectQueryConfigs: RelationConnectQueryConfig[],
queryBuilder: WorkspaceSelectQueryBuilder<Entity>,
queryBuilder:
| WorkspaceSelectQueryBuilder<Entity>
| SelectQueryBuilder<Entity>,
): Promise<[RelationConnectQueryConfig, Record<string, unknown>[]][]> {
const allRecordsToConnectWithConfig: [
RelationConnectQueryConfig,
@@ -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<T>,
@@ -115,24 +115,26 @@ export class WorkspaceInsertQueryBuilder<
this.internalContext,
);
const nestedRelationQueryBuilder = new WorkspaceSelectQueryBuilder(
this as unknown as WorkspaceSelectQueryBuilder<T>,
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
);
if (isDefined(this.relationNestedConfig)) {
const nestedRelationQueryBuilder = new WorkspaceSelectQueryBuilder(
this as unknown as WorkspaceSelectQueryBuilder<T>,
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
);
const updatedValues =
await this.relationNestedQueries.processRelationNestedQueries({
entities: this.expressionMap.valuesSet as
| QueryDeepPartialEntityWithNestedRelationFields<T>
| QueryDeepPartialEntityWithNestedRelationFields<T>[],
relationNestedConfig: this.relationNestedConfig,
queryBuilder: nestedRelationQueryBuilder,
});
const updatedValues =
await this.relationNestedQueries.processRelationNestedQueries({
entities: this.expressionMap.valuesSet as
| QueryDeepPartialEntityWithNestedRelationFields<T>
| QueryDeepPartialEntityWithNestedRelationFields<T>[],
relationNestedConfig: this.relationNestedConfig,
queryBuilder: nestedRelationQueryBuilder,
});
this.expressionMap.valuesSet = updatedValues;
this.expressionMap.valuesSet = updatedValues;
}
const result = await super.execute();
@@ -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<T>,
@@ -115,17 +115,19 @@ export class WorkspaceUpdateQueryBuilder<
this.authContext,
);
const updatedValues =
await this.relationNestedQueries.processRelationNestedQueries({
entities: this.expressionMap.valuesSet as
| QueryDeepPartialEntityWithNestedRelationFields<T>
| QueryDeepPartialEntityWithNestedRelationFields<T>[],
relationNestedConfig: this.relationNestedConfig,
queryBuilder: nestedRelationQueryBuilder,
});
if (isDefined(this.relationNestedConfig)) {
const updatedValues =
await this.relationNestedQueries.processRelationNestedQueries({
entities: this.expressionMap.valuesSet as
| QueryDeepPartialEntityWithNestedRelationFields<T>
| QueryDeepPartialEntityWithNestedRelationFields<T>[],
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<T[]>(
before,
@@ -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<U extends DeepPartial<T>>(
override save<U extends DeepPartialWithNestedRelationFields<T>>(
entities: U[],
options: SaveOptions & { reload: false },
entityManager?: WorkspaceEntityManager,
): Promise<T[]>;
override save<U extends DeepPartial<T>>(
override save<U extends DeepPartialWithNestedRelationFields<T>>(
entities: U[],
options?: SaveOptions,
entityManager?: WorkspaceEntityManager,
): Promise<(U & T)[]>;
override save<U extends DeepPartial<T>>(
override save<U extends DeepPartialWithNestedRelationFields<T>>(
entity: U,
options: SaveOptions & { reload: false },
entityManager?: WorkspaceEntityManager,
): Promise<T>;
override save<U extends DeepPartial<T>>(
override save<U extends DeepPartialWithNestedRelationFields<T>>(
entity: U,
options?: SaveOptions,
entityManager?: WorkspaceEntityManager,
): Promise<U & T>;
override async save<U extends DeepPartial<T>>(
override async save<U extends DeepPartialWithNestedRelationFields<T>>(
entityOrEntities: U | U[],
options?: SaveOptions | (SaveOptions & { reload: false }),
entityManager?: WorkspaceEntityManager,
@@ -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,
@@ -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,
@@ -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,