Connect/Disconnect - Add Disconnect logic + Migration to query builders (insert/update) (#13271)
Context : Large PR with 600+ test files. Enable connect and disconnect logic in createMany (upsert true) / updateOne / updateMany resolvers - Add disconnect logic - Gather disconnect and connect logic -> called relation nested queries - Move logic to query builder (insert and update one) with a preparation step in .set/.values and an execution step in .execute - Add integration tests Test : - Test API call on updateMany, updateOne, createMany (upsert:true) with connect/disconnect
This commit is contained in:
+249
@@ -0,0 +1,249 @@
|
||||
import { isDefined } from 'class-validator';
|
||||
import { RELATION_NESTED_QUERY_KEYWORDS } from 'twenty-shared/constants';
|
||||
import { EntityTarget, ObjectLiteral } 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 { RelationConnectQueryConfig } from 'src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type';
|
||||
import {
|
||||
RelationConnectQueryFieldsByEntityIndex,
|
||||
RelationDisconnectQueryFieldsByEntityIndex,
|
||||
} from 'src/engine/twenty-orm/entity-manager/types/relation-nested-query-fields-by-entity-index.type';
|
||||
import {
|
||||
TwentyORMException,
|
||||
TwentyORMExceptionCode,
|
||||
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
|
||||
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
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 { extractNestedRelationFieldsByEntityIndex } from 'src/engine/twenty-orm/utils/extract-nested-relation-fields-by-entity-index.util';
|
||||
import { getAssociatedRelationFieldName } from 'src/engine/twenty-orm/utils/get-associated-relation-field-name.util';
|
||||
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';
|
||||
|
||||
export class RelationNestedQueries {
|
||||
private readonly internalContext: WorkspaceInternalContext;
|
||||
|
||||
constructor(internalContext: WorkspaceInternalContext) {
|
||||
this.internalContext = internalContext;
|
||||
}
|
||||
|
||||
prepareNestedRelationQueries<Entity extends ObjectLiteral>(
|
||||
entities:
|
||||
| QueryDeepPartialEntityWithNestedRelationFields<Entity>[]
|
||||
| QueryDeepPartialEntityWithNestedRelationFields<Entity>,
|
||||
target: EntityTarget<Entity>,
|
||||
): [
|
||||
RelationConnectQueryConfig[],
|
||||
RelationDisconnectQueryFieldsByEntityIndex,
|
||||
] {
|
||||
const entitiesArray = Array.isArray(entities) ? entities : [entities];
|
||||
|
||||
const {
|
||||
relationConnectQueryFieldsByEntityIndex,
|
||||
relationDisconnectQueryFieldsByEntityIndex,
|
||||
} = extractNestedRelationFieldsByEntityIndex(entitiesArray);
|
||||
|
||||
const connectConfig = this.prepareRelationConnect(
|
||||
entitiesArray,
|
||||
target,
|
||||
relationConnectQueryFieldsByEntityIndex,
|
||||
);
|
||||
|
||||
return [connectConfig, relationDisconnectQueryFieldsByEntityIndex];
|
||||
}
|
||||
|
||||
private prepareRelationConnect<Entity extends ObjectLiteral>(
|
||||
entities: QueryDeepPartialEntityWithNestedRelationFields<Entity>[],
|
||||
target: EntityTarget<Entity>,
|
||||
relationConnectQueryFieldsByEntityIndex: RelationConnectQueryFieldsByEntityIndex,
|
||||
) {
|
||||
const objectMetadata = getObjectMetadataFromEntityTarget(
|
||||
target,
|
||||
this.internalContext,
|
||||
);
|
||||
|
||||
const objectMetadataMap = this.internalContext.objectMetadataMaps;
|
||||
|
||||
const relationConnectQueryConfigs = computeRelationConnectQueryConfigs(
|
||||
entities,
|
||||
objectMetadata,
|
||||
objectMetadataMap,
|
||||
relationConnectQueryFieldsByEntityIndex,
|
||||
);
|
||||
|
||||
return relationConnectQueryConfigs;
|
||||
}
|
||||
|
||||
async processRelationNestedQueries<Entity extends ObjectLiteral>({
|
||||
entities,
|
||||
relationNestedConfig,
|
||||
queryBuilder,
|
||||
}: {
|
||||
entities:
|
||||
| QueryDeepPartialEntityWithNestedRelationFields<Entity>[]
|
||||
| QueryDeepPartialEntityWithNestedRelationFields<Entity>;
|
||||
relationNestedConfig: [
|
||||
RelationConnectQueryConfig[],
|
||||
RelationDisconnectQueryFieldsByEntityIndex,
|
||||
];
|
||||
queryBuilder: WorkspaceSelectQueryBuilder<Entity>;
|
||||
}): Promise<QueryDeepPartialEntity<Entity>[]> {
|
||||
const entitiesArray = Array.isArray(entities) ? entities : [entities];
|
||||
|
||||
const [
|
||||
relationConnectQueryConfigs,
|
||||
relationDisconnectQueryFieldsByEntityIndex,
|
||||
] = relationNestedConfig;
|
||||
|
||||
const updatedEntitiesWithDisconnect = this.processRelationDisconnect({
|
||||
entities: entitiesArray,
|
||||
relationDisconnectQueryFieldsByEntityIndex,
|
||||
});
|
||||
|
||||
const updatedEntitiesWithConnect = await this.processRelationConnect({
|
||||
entities: updatedEntitiesWithDisconnect,
|
||||
relationConnectQueryConfigs,
|
||||
queryBuilder,
|
||||
});
|
||||
|
||||
return updatedEntitiesWithConnect;
|
||||
}
|
||||
|
||||
private async processRelationConnect<Entity extends ObjectLiteral>({
|
||||
entities,
|
||||
relationConnectQueryConfigs,
|
||||
queryBuilder,
|
||||
}: {
|
||||
entities: QueryDeepPartialEntityWithNestedRelationFields<Entity>[];
|
||||
relationConnectQueryConfigs: RelationConnectQueryConfig[];
|
||||
queryBuilder: WorkspaceSelectQueryBuilder<Entity>;
|
||||
}): Promise<QueryDeepPartialEntity<Entity>[]> {
|
||||
if (relationConnectQueryConfigs.length === 0) return entities;
|
||||
|
||||
const recordsToConnectWithConfig = await this.executeConnectQueries(
|
||||
relationConnectQueryConfigs,
|
||||
queryBuilder,
|
||||
);
|
||||
|
||||
const updatedEntities = this.updateEntitiesWithRecordToConnectId<Entity>(
|
||||
entities,
|
||||
recordsToConnectWithConfig,
|
||||
);
|
||||
|
||||
return updatedEntities;
|
||||
}
|
||||
|
||||
private async executeConnectQueries<Entity extends ObjectLiteral>(
|
||||
relationConnectQueryConfigs: RelationConnectQueryConfig[],
|
||||
queryBuilder: WorkspaceSelectQueryBuilder<Entity>,
|
||||
): Promise<[RelationConnectQueryConfig, Record<string, unknown>[]][]> {
|
||||
const allRecordsToConnectWithConfig: [
|
||||
RelationConnectQueryConfig,
|
||||
Record<string, unknown>[],
|
||||
][] = [];
|
||||
|
||||
for (const connectQueryConfig of relationConnectQueryConfigs) {
|
||||
const { clause, parameters } = createSqlWhereTupleInClause(
|
||||
connectQueryConfig.recordToConnectConditions,
|
||||
connectQueryConfig.targetObjectName,
|
||||
);
|
||||
|
||||
queryBuilder.expressionMap.aliases = [];
|
||||
queryBuilder.expressionMap.mainAlias = undefined;
|
||||
|
||||
const recordsToConnect = await queryBuilder
|
||||
.select(getRecordToConnectFields(connectQueryConfig))
|
||||
.where(clause, parameters)
|
||||
.from(
|
||||
connectQueryConfig.targetObjectName,
|
||||
connectQueryConfig.targetObjectName,
|
||||
)
|
||||
.getRawMany();
|
||||
|
||||
allRecordsToConnectWithConfig.push([
|
||||
connectQueryConfig,
|
||||
recordsToConnect,
|
||||
]);
|
||||
}
|
||||
|
||||
return allRecordsToConnectWithConfig;
|
||||
}
|
||||
|
||||
private updateEntitiesWithRecordToConnectId<Entity extends ObjectLiteral>(
|
||||
entities: QueryDeepPartialEntityWithNestedRelationFields<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;
|
||||
});
|
||||
}
|
||||
|
||||
private processRelationDisconnect<Entity extends ObjectLiteral>({
|
||||
entities,
|
||||
relationDisconnectQueryFieldsByEntityIndex,
|
||||
}: {
|
||||
entities: QueryDeepPartialEntityWithNestedRelationFields<Entity>[];
|
||||
relationDisconnectQueryFieldsByEntityIndex: RelationDisconnectQueryFieldsByEntityIndex;
|
||||
}): QueryDeepPartialEntityWithNestedRelationFields<Entity>[] {
|
||||
return entities.map((entity, index) => {
|
||||
const nestedRelationDisconnectFields =
|
||||
relationDisconnectQueryFieldsByEntityIndex[index];
|
||||
|
||||
if (!isDefined(nestedRelationDisconnectFields)) return entity;
|
||||
|
||||
for (const [disconnectFieldName, disconnectObject] of Object.entries(
|
||||
nestedRelationDisconnectFields ?? {},
|
||||
)) {
|
||||
entity = {
|
||||
...entity,
|
||||
[disconnectFieldName]: null,
|
||||
...(disconnectObject[RELATION_NESTED_QUERY_KEYWORDS.DISCONNECT] ===
|
||||
true
|
||||
? { [getAssociatedRelationFieldName(disconnectFieldName)]: null }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user