Connect logic in Workspace Entity Manager (#13078)

Large PR, sorry for that. Don't hesitate to reach me to have full
context (env. 500lines for integration and unit tests)

- Add connect logic in Workspace Entity Manager
- Update QueryDeepPartialEntity type to enable dev to use connect
- Add integration test on createOne / createMany
- Add unit test to cover main utils
- Remove feature flag on connect

closes https://github.com/twentyhq/core-team-issues/issues/1148
closes https://github.com/twentyhq/core-team-issues/issues/1147
This commit is contained in:
Etienne
2025-07-09 14:16:28 +02:00
committed by GitHub
parent a95ca10f29
commit fce33004bc
27 changed files with 1293 additions and 80 deletions

View File

@ -37,12 +37,22 @@ import {
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { QueryDeepPartialEntityWithRelationConnect } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type';
import { RelationConnectQueryConfig } from 'src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type';
import {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import {
OperationType,
validateOperationIsPermittedOrThrow,
} from 'src/engine/twenty-orm/repository/permissions.utils';
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { computeRelationConnectQueryConfigs } from 'src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util';
import { createSqlWhereTupleInClause } from 'src/engine/twenty-orm/utils/create-sql-where-tuple-in-clause.utils';
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
import { getRecordToConnectFields } from 'src/engine/twenty-orm/utils/get-record-to-connect-fields.util';
type PermissionOptions = {
shouldBypassPermissionChecks?: boolean;
@ -165,11 +175,21 @@ export class WorkspaceEntityManager extends EntityManager {
);
}
override insert<Entity extends ObjectLiteral>(
override async insert<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
entity: QueryDeepPartialEntity<Entity> | QueryDeepPartialEntity<Entity>[],
entity:
| QueryDeepPartialEntityWithRelationConnect<Entity>
| QueryDeepPartialEntityWithRelationConnect<Entity>[],
permissionOptions?: PermissionOptions,
): Promise<InsertResult> {
const entityArray = Array.isArray(entity) ? entity : [entity];
const connectedEntities = await this.processRelationConnect<Entity>(
entityArray,
target,
permissionOptions,
);
return this.createQueryBuilder(
undefined,
undefined,
@ -178,7 +198,7 @@ export class WorkspaceEntityManager extends EntityManager {
)
.insert()
.into(target)
.values(entity)
.values(connectedEntities)
.execute();
}
@ -1321,4 +1341,118 @@ export class WorkspaceEntityManager extends EntityManager {
PermissionsExceptionCode.RAW_SQL_NOT_ALLOWED,
);
}
private async processRelationConnect<Entity extends ObjectLiteral>(
entities: QueryDeepPartialEntityWithRelationConnect<Entity>[],
target: EntityTarget<Entity>,
permissionOptions?: PermissionOptions,
): Promise<QueryDeepPartialEntity<Entity>[]> {
const objectMetadata = getObjectMetadataFromEntityTarget(
target,
this.internalContext,
);
const objectMetadataMap = this.internalContext.objectMetadataMaps;
const relationConnectQueryConfigs = computeRelationConnectQueryConfigs(
entities,
objectMetadata,
objectMetadataMap,
);
if (!isDefined(relationConnectQueryConfigs)) return entities;
const recordsToConnectWithConfig = await this.executeConnectQueries(
relationConnectQueryConfigs,
permissionOptions,
);
const updatedEntities = this.updateEntitiesWithRecordToConnectId<Entity>(
entities,
recordsToConnectWithConfig,
);
return updatedEntities;
}
private async executeConnectQueries(
relationConnectQueryConfigs: Record<string, RelationConnectQueryConfig>,
permissionOptions?: PermissionOptions,
): Promise<[RelationConnectQueryConfig, Record<string, unknown>[]][]> {
const AllRecordsToConnectWithConfig: [
RelationConnectQueryConfig,
Record<string, unknown>[],
][] = [];
for (const connectQueryConfig of Object.values(
relationConnectQueryConfigs,
)) {
const { clause, parameters } = createSqlWhereTupleInClause(
connectQueryConfig.recordToConnectConditions,
connectQueryConfig.targetObjectName,
);
const recordsToConnect = await this.createQueryBuilder(
connectQueryConfig.targetObjectName,
connectQueryConfig.targetObjectName,
undefined,
permissionOptions,
)
.select(getRecordToConnectFields(connectQueryConfig))
.where(clause, parameters)
.getRawMany();
AllRecordsToConnectWithConfig.push([
connectQueryConfig,
recordsToConnect,
]);
}
return AllRecordsToConnectWithConfig;
}
private updateEntitiesWithRecordToConnectId<Entity extends ObjectLiteral>(
entities: QueryDeepPartialEntityWithRelationConnect<Entity>[],
recordsToConnectWithConfig: [
RelationConnectQueryConfig,
Record<string, unknown>[],
][],
): QueryDeepPartialEntity<Entity>[] {
return entities.map((entity, index) => {
for (const [
connectQueryConfig,
recordsToConnect,
] of recordsToConnectWithConfig) {
if (
isDefined(
connectQueryConfig.recordToConnectConditionByEntityIndex[index],
)
) {
const recordToConnect = recordsToConnect.filter((record) =>
connectQueryConfig.recordToConnectConditionByEntityIndex[
index
].every(([field, value]) => record[field] === value),
);
if (recordToConnect.length !== 1) {
const recordToConnectTotal = recordToConnect.length;
const connectFieldName = connectQueryConfig.connectFieldName;
throw new TwentyORMException(
`Expected 1 record to connect to ${connectFieldName}, but found ${recordToConnectTotal}.`,
TwentyORMExceptionCode.CONNECT_RECORD_NOT_FOUND,
);
}
entity = {
...entity,
[connectQueryConfig.relationFieldName]: recordToConnect[0]['id'],
[connectQueryConfig.connectFieldName]: null,
};
}
}
return entity;
});
}
}