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:
@ -1,3 +1,4 @@
|
||||
import { RELATION_NESTED_QUERY_KEYWORDS } from 'twenty-shared/constants';
|
||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
@ -7,20 +8,24 @@ export type ConnectWhereValue = string | Record<string, string>;
|
||||
export type ConnectWhere = Record<string, ConnectWhereValue>;
|
||||
|
||||
export type ConnectObject = {
|
||||
connect: {
|
||||
where: ConnectWhere;
|
||||
[RELATION_NESTED_QUERY_KEYWORDS.CONNECT]: {
|
||||
[RELATION_NESTED_QUERY_KEYWORDS.CONNECT_WHERE]: ConnectWhere;
|
||||
};
|
||||
};
|
||||
|
||||
export type DisconnectObject = {
|
||||
[RELATION_NESTED_QUERY_KEYWORDS.DISCONNECT]: true;
|
||||
};
|
||||
|
||||
type EntityRelationFields<T> = {
|
||||
[K in keyof T]: T[K] extends BaseWorkspaceEntity | null ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
export type QueryDeepPartialEntityWithRelationConnect<T> = Omit<
|
||||
export type QueryDeepPartialEntityWithNestedRelationFields<T> = Omit<
|
||||
QueryDeepPartialEntity<T>,
|
||||
EntityRelationFields<T>
|
||||
> & {
|
||||
[K in keyof T]?: T[K] extends BaseWorkspaceEntity | null
|
||||
? T[K] | ConnectObject
|
||||
? T[K] | ConnectObject | DisconnectObject
|
||||
: T[K];
|
||||
};
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
import {
|
||||
ConnectObject,
|
||||
DisconnectObject,
|
||||
} from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type';
|
||||
|
||||
export type RelationConnectQueryFieldsByEntityIndex = {
|
||||
[entityIndex: string]: { [key: string]: ConnectObject };
|
||||
};
|
||||
|
||||
export type RelationDisconnectQueryFieldsByEntityIndex = {
|
||||
[entityIndex: string]: {
|
||||
[key: string]: DisconnectObject;
|
||||
};
|
||||
};
|
||||
@ -40,24 +40,16 @@ 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 { QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type';
|
||||
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 { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.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';
|
||||
|
||||
type PermissionOptions = {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
@ -172,19 +164,11 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
override async insert<Entity extends ObjectLiteral>(
|
||||
target: EntityTarget<Entity>,
|
||||
entity:
|
||||
| QueryDeepPartialEntityWithRelationConnect<Entity>
|
||||
| QueryDeepPartialEntityWithRelationConnect<Entity>[],
|
||||
| QueryDeepPartialEntityWithNestedRelationFields<Entity>
|
||||
| QueryDeepPartialEntityWithNestedRelationFields<Entity>[],
|
||||
selectedColumns: string[] = [],
|
||||
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,
|
||||
@ -193,7 +177,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
)
|
||||
.insert()
|
||||
.into(target)
|
||||
.values(connectedEntities)
|
||||
.values(entity)
|
||||
.returning(selectedColumns)
|
||||
.execute();
|
||||
}
|
||||
@ -1484,118 +1468,4 @@ 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,6 @@ import {
|
||||
InsertResult,
|
||||
ObjectLiteral,
|
||||
} from 'typeorm';
|
||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
|
||||
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||
@ -13,10 +12,14 @@ 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 { 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 {
|
||||
TwentyORMException,
|
||||
TwentyORMExceptionCode,
|
||||
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
|
||||
import { RelationNestedQueries } from 'src/engine/twenty-orm/relation-nested-queries/relation-nested-queries';
|
||||
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils';
|
||||
import { WorkspaceDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-delete-query-builder';
|
||||
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
@ -34,6 +37,11 @@ export class WorkspaceInsertQueryBuilder<
|
||||
private internalContext: WorkspaceInternalContext;
|
||||
private authContext?: AuthContext;
|
||||
private featureFlagMap?: FeatureFlagMap;
|
||||
private relationNestedQueries: RelationNestedQueries;
|
||||
private relationNestedConfig: [
|
||||
RelationConnectQueryConfig[],
|
||||
RelationDisconnectQueryFieldsByEntityIndex,
|
||||
];
|
||||
|
||||
constructor(
|
||||
queryBuilder: InsertQueryBuilder<T>,
|
||||
@ -49,6 +57,9 @@ export class WorkspaceInsertQueryBuilder<
|
||||
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
|
||||
this.authContext = authContext;
|
||||
this.featureFlagMap = featureFlagMap;
|
||||
this.relationNestedQueries = new RelationNestedQueries(
|
||||
this.internalContext,
|
||||
);
|
||||
}
|
||||
|
||||
override clone(): this {
|
||||
@ -65,10 +76,18 @@ export class WorkspaceInsertQueryBuilder<
|
||||
}
|
||||
|
||||
override values(
|
||||
values: QueryDeepPartialEntity<T> | QueryDeepPartialEntity<T>[],
|
||||
values:
|
||||
| QueryDeepPartialEntityWithNestedRelationFields<T>
|
||||
| QueryDeepPartialEntityWithNestedRelationFields<T>[],
|
||||
): this {
|
||||
const mainAliasTarget = this.getMainAliasTarget();
|
||||
|
||||
this.relationNestedConfig =
|
||||
this.relationNestedQueries.prepareNestedRelationQueries(
|
||||
values,
|
||||
mainAliasTarget,
|
||||
);
|
||||
|
||||
const objectMetadata = getObjectMetadataFromEntityTarget(
|
||||
mainAliasTarget,
|
||||
this.internalContext,
|
||||
@ -96,6 +115,25 @@ export class WorkspaceInsertQueryBuilder<
|
||||
this.internalContext,
|
||||
);
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
this.expressionMap.valuesSet = updatedValues;
|
||||
|
||||
const result = await super.execute();
|
||||
|
||||
const formattedResult = formatResult<T[]>(
|
||||
|
||||
@ -13,10 +13,14 @@ 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 { 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 {
|
||||
TwentyORMException,
|
||||
TwentyORMExceptionCode,
|
||||
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
|
||||
import { RelationNestedQueries } from 'src/engine/twenty-orm/relation-nested-queries/relation-nested-queries';
|
||||
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils';
|
||||
import { WorkspaceDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-delete-query-builder';
|
||||
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
@ -33,6 +37,12 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
private internalContext: WorkspaceInternalContext;
|
||||
private authContext?: AuthContext;
|
||||
private featureFlagMap?: FeatureFlagMap;
|
||||
private relationNestedQueries: RelationNestedQueries;
|
||||
private relationNestedConfig: [
|
||||
RelationConnectQueryConfig[],
|
||||
RelationDisconnectQueryFieldsByEntityIndex,
|
||||
];
|
||||
|
||||
constructor(
|
||||
queryBuilder: UpdateQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectRecordsPermissions,
|
||||
@ -47,13 +57,16 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
|
||||
this.authContext = authContext;
|
||||
this.featureFlagMap = featureFlagMap;
|
||||
this.relationNestedQueries = new RelationNestedQueries(
|
||||
this.internalContext,
|
||||
);
|
||||
}
|
||||
|
||||
override clone(): this {
|
||||
const clonedQueryBuilder = super.clone();
|
||||
|
||||
return new WorkspaceUpdateQueryBuilder(
|
||||
clonedQueryBuilder,
|
||||
clonedQueryBuilder as UpdateQueryBuilder<T>,
|
||||
this.objectRecordsPermissions,
|
||||
this.internalContext,
|
||||
this.shouldBypassPermissionChecks,
|
||||
@ -94,6 +107,26 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
|
||||
const before = await eventSelectQueryBuilder.getMany();
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
this.expressionMap.valuesSet =
|
||||
updatedValues.length === 1 ? updatedValues[0] : updatedValues;
|
||||
|
||||
const formattedBefore = formatResult<T[]>(
|
||||
before,
|
||||
objectMetadata,
|
||||
@ -131,7 +164,21 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
};
|
||||
}
|
||||
|
||||
override set(_values: QueryDeepPartialEntity<T>): this {
|
||||
override set(
|
||||
_values:
|
||||
| QueryDeepPartialEntityWithNestedRelationFields<T>
|
||||
| QueryDeepPartialEntityWithNestedRelationFields<T>[],
|
||||
): this;
|
||||
|
||||
override set(_values: QueryDeepPartialEntity<T>): this;
|
||||
|
||||
override set(
|
||||
_values:
|
||||
| QueryDeepPartialEntityWithNestedRelationFields<T>
|
||||
| QueryDeepPartialEntityWithNestedRelationFields<T>[]
|
||||
| QueryDeepPartialEntity<T>
|
||||
| QueryDeepPartialEntity<T>[],
|
||||
): this {
|
||||
const mainAliasTarget = this.getMainAliasTarget();
|
||||
|
||||
const objectMetadata = getObjectMetadataFromEntityTarget(
|
||||
@ -139,9 +186,19 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
this.internalContext,
|
||||
);
|
||||
|
||||
const extendedValues = _values as
|
||||
| QueryDeepPartialEntityWithNestedRelationFields<T>
|
||||
| QueryDeepPartialEntityWithNestedRelationFields<T>[];
|
||||
|
||||
this.relationNestedConfig =
|
||||
this.relationNestedQueries.prepareNestedRelationQueries(
|
||||
extendedValues,
|
||||
mainAliasTarget,
|
||||
);
|
||||
|
||||
const formattedUpdateSet = formatData(_values, objectMetadata);
|
||||
|
||||
return super.set(formattedUpdateSet);
|
||||
return super.set(formattedUpdateSet as QueryDeepPartialEntity<T>);
|
||||
}
|
||||
|
||||
override select(): WorkspaceSelectQueryBuilder<T> {
|
||||
|
||||
@ -27,7 +27,7 @@ import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { QueryDeepPartialEntityWithRelationConnect } 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-relation-connect.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';
|
||||
@ -536,8 +536,8 @@ export class WorkspaceRepository<
|
||||
*/
|
||||
override async insert(
|
||||
entity:
|
||||
| QueryDeepPartialEntityWithRelationConnect<T>
|
||||
| QueryDeepPartialEntityWithRelationConnect<T>[],
|
||||
| QueryDeepPartialEntityWithNestedRelationFields<T>
|
||||
| QueryDeepPartialEntityWithNestedRelationFields<T>[],
|
||||
entityManager?: WorkspaceEntityManager,
|
||||
selectedColumns?: string[],
|
||||
): Promise<InsertResult> {
|
||||
|
||||
@ -160,9 +160,10 @@ describe('computeRelationConnectQueryConfigs', () => {
|
||||
peopleEntityInputs,
|
||||
personMetadata,
|
||||
objectMetadataMaps,
|
||||
{},
|
||||
);
|
||||
|
||||
expect(result).toEqual({});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should throw an error if a connect field is not a relation field', () => {
|
||||
@ -176,11 +177,18 @@ describe('computeRelationConnectQueryConfigs', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const relationConnectQueryFieldsByEntityIndex = {
|
||||
'0': {
|
||||
name: { connect: { where: { name: { lastName: 'Doe' } } } },
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
computeRelationConnectQueryConfigs(
|
||||
peopleEntityInputs,
|
||||
personMetadata,
|
||||
objectMetadataMaps,
|
||||
relationConnectQueryFieldsByEntityIndex,
|
||||
);
|
||||
}).toThrow('Connect is not allowed for name on person');
|
||||
});
|
||||
@ -195,11 +203,18 @@ describe('computeRelationConnectQueryConfigs', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const relationConnectQueryFieldsByEntityIndex = {
|
||||
'0': {
|
||||
'company-related-to-1': { connect: { where: { name: 'company1' } } },
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
computeRelationConnectQueryConfigs(
|
||||
peopleEntityInputs,
|
||||
personMetadata,
|
||||
objectMetadataMaps,
|
||||
relationConnectQueryFieldsByEntityIndex,
|
||||
);
|
||||
}).toThrow(
|
||||
"Missing required fields: at least one unique constraint have to be fully populated for 'company-related-to-1'.",
|
||||
@ -222,11 +237,26 @@ describe('computeRelationConnectQueryConfigs', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const relationConnectQueryFieldsByEntityIndex = {
|
||||
'0': {
|
||||
'company-related-to-1': {
|
||||
connect: {
|
||||
where: {
|
||||
domainName: { primaryLinkUrl: 'company1.com' },
|
||||
id: '1',
|
||||
address: 'company1 address',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
computeRelationConnectQueryConfigs(
|
||||
peopleEntityInputs,
|
||||
personMetadata,
|
||||
objectMetadataMaps,
|
||||
relationConnectQueryFieldsByEntityIndex,
|
||||
);
|
||||
}).toThrow(
|
||||
"Field address is not a unique constraint field for 'company-related-to-1'.",
|
||||
@ -255,11 +285,27 @@ describe('computeRelationConnectQueryConfigs', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const relationConnectQueryFieldsByEntityIndex = {
|
||||
'0': {
|
||||
'company-related-to-1': {
|
||||
connect: {
|
||||
where: {
|
||||
domainName: { primaryLinkUrl: 'company1.com' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'1': {
|
||||
'company-related-to-1': { connect: { where: { id: '2' } } },
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
computeRelationConnectQueryConfigs(
|
||||
peopleEntityInputs,
|
||||
personMetadata,
|
||||
objectMetadataMaps,
|
||||
relationConnectQueryFieldsByEntityIndex,
|
||||
);
|
||||
}).toThrow(
|
||||
'Expected the same constraint fields to be used consistently across all operations for company-related-to-1.',
|
||||
@ -298,14 +344,42 @@ describe('computeRelationConnectQueryConfigs', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const relationConnectQueryFieldsByEntityIndex = {
|
||||
'0': {
|
||||
'company-related-to-1': {
|
||||
connect: {
|
||||
where: { domainName: { primaryLinkUrl: 'company.com' } },
|
||||
},
|
||||
},
|
||||
'company-related-to-2': {
|
||||
connect: {
|
||||
where: { id: '1' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'1': {
|
||||
'company-related-to-1': {
|
||||
connect: {
|
||||
where: { domainName: { primaryLinkUrl: 'other-company.com' } },
|
||||
},
|
||||
},
|
||||
'company-related-to-2': {
|
||||
connect: {
|
||||
where: { id: '2' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = computeRelationConnectQueryConfigs(
|
||||
peopleEntityInputs,
|
||||
personMetadata,
|
||||
objectMetadataMaps,
|
||||
relationConnectQueryFieldsByEntityIndex,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
'company-related-to-1': {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
connectFieldName: 'company-related-to-1',
|
||||
recordToConnectConditions: [
|
||||
[['domainNamePrimaryLinkUrl', 'company.com']],
|
||||
@ -326,7 +400,7 @@ describe('computeRelationConnectQueryConfigs', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
'company-related-to-2': {
|
||||
{
|
||||
connectFieldName: 'company-related-to-2',
|
||||
recordToConnectConditions: [[['id', '1']], [['id', '2']]],
|
||||
recordToConnectConditionByEntityIndex: {
|
||||
@ -344,6 +418,6 @@ describe('computeRelationConnectQueryConfigs', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -17,7 +17,7 @@ describe('createSqlWhereTupleInClause', () => {
|
||||
const result = createSqlWhereTupleInClause(conditions, tableName);
|
||||
|
||||
expect(result.clause).toBe(
|
||||
'(table_name.field1, table_name.field2) IN ((:value0_0, :value0_1), (:value1_0, :value1_1))',
|
||||
'("table_name"."field1", "table_name"."field2") IN ((:value0_0, :value0_1), (:value1_0, :value1_1))',
|
||||
);
|
||||
expect(result.parameters).toEqual({
|
||||
value0_0: 'value1',
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
RelationConnectQueryConfig,
|
||||
UniqueConstraintCondition,
|
||||
} from 'src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type';
|
||||
import { RelationConnectQueryFieldsByEntityIndex } from 'src/engine/twenty-orm/entity-manager/types/relation-nested-query-fields-by-entity-index.type';
|
||||
import {
|
||||
TwentyORMException,
|
||||
TwentyORMExceptionCode,
|
||||
@ -28,19 +29,19 @@ export const computeRelationConnectQueryConfigs = (
|
||||
entities: Record<string, unknown>[],
|
||||
objectMetadata: ObjectMetadataItemWithFieldMaps,
|
||||
objectMetadataMap: ObjectMetadataMaps,
|
||||
relationConnectQueryFieldsByEntityIndex: RelationConnectQueryFieldsByEntityIndex,
|
||||
) => {
|
||||
const allConnectQueryConfigs: Record<string, RelationConnectQueryConfig> = {};
|
||||
|
||||
for (const [entityIndex, entity] of entities.entries()) {
|
||||
const connectFields = extractConnectFields(entity);
|
||||
const nestedRelationConnectFields =
|
||||
relationConnectQueryFieldsByEntityIndex[entityIndex];
|
||||
|
||||
if (connectFields.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const connectField of connectFields) {
|
||||
const [connectFieldName, connectObject] = Object.entries(connectField)[0];
|
||||
if (!isDefined(nestedRelationConnectFields)) continue;
|
||||
|
||||
for (const [connectFieldName, connectObject] of Object.entries(
|
||||
nestedRelationConnectFields,
|
||||
)) {
|
||||
const {
|
||||
recordToConnectCondition,
|
||||
uniqueConstraintFields,
|
||||
@ -78,7 +79,7 @@ export const computeRelationConnectQueryConfigs = (
|
||||
}
|
||||
}
|
||||
|
||||
return allConnectQueryConfigs;
|
||||
return Object.values(allConnectQueryConfigs);
|
||||
};
|
||||
|
||||
const updateConnectQueryConfigs = (
|
||||
@ -177,63 +178,6 @@ const computeRecordToConnectCondition = (
|
||||
};
|
||||
};
|
||||
|
||||
const extractConnectFields = (
|
||||
entity: Record<string, unknown>,
|
||||
): { [connectFieldName: string]: ConnectObject }[] => {
|
||||
const connectFields: { [entityKey: string]: ConnectObject }[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(entity)) {
|
||||
if (hasRelationConnect(value)) {
|
||||
connectFields.push({ [key]: value });
|
||||
}
|
||||
}
|
||||
|
||||
return connectFields;
|
||||
};
|
||||
|
||||
const hasRelationConnect = (value: unknown): value is ConnectObject => {
|
||||
if (!isDefined(value) || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const obj = value as Record<string, unknown>;
|
||||
|
||||
if (!isDefined(obj.connect) || typeof obj.connect !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const connect = obj.connect as Record<string, unknown>;
|
||||
|
||||
if (!isDefined(connect.where) || typeof connect.where !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const where = connect.where as Record<string, unknown>;
|
||||
|
||||
const whereKeys = Object.keys(where);
|
||||
|
||||
if (whereKeys.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return whereKeys.every((key) => {
|
||||
const whereValue = where[key];
|
||||
|
||||
if (typeof whereValue === 'string') {
|
||||
return true;
|
||||
}
|
||||
if (whereValue && typeof whereValue === 'object') {
|
||||
const subObj = whereValue as Record<string, unknown>;
|
||||
|
||||
return Object.values(subObj).every(
|
||||
(subValue) => typeof subValue === 'string',
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const checkUniqueConstraintFullyPopulated = (
|
||||
objectMetadata: ObjectMetadataItemWithFieldMaps,
|
||||
connectObject: ConnectObject,
|
||||
|
||||
@ -5,7 +5,7 @@ export const createSqlWhereTupleInClause = (
|
||||
const fieldNames = conditions[0].map(([field, _]) => field);
|
||||
|
||||
const tupleClause = fieldNames
|
||||
.map((field) => `${tableName}.${field}`)
|
||||
.map((field) => `"${tableName}"."${field}"`)
|
||||
.join(', ');
|
||||
const valuePlaceholders = conditions
|
||||
.map((_, index) => {
|
||||
|
||||
@ -0,0 +1,135 @@
|
||||
import { isDefined } from 'class-validator';
|
||||
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';
|
||||
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';
|
||||
|
||||
const hasRelationConnect = (value: unknown): value is ConnectObject => {
|
||||
if (!isDefined(value) || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const obj = value as Record<string, unknown>;
|
||||
|
||||
if (
|
||||
!isDefined(obj[RELATION_NESTED_QUERY_KEYWORDS.CONNECT]) ||
|
||||
typeof obj[RELATION_NESTED_QUERY_KEYWORDS.CONNECT] !== 'object'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const connect = obj[RELATION_NESTED_QUERY_KEYWORDS.CONNECT] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
if (
|
||||
!isDefined(connect[RELATION_NESTED_QUERY_KEYWORDS.CONNECT_WHERE]) ||
|
||||
typeof connect[RELATION_NESTED_QUERY_KEYWORDS.CONNECT_WHERE] !== 'object'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const where = connect[RELATION_NESTED_QUERY_KEYWORDS.CONNECT_WHERE] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
const whereKeys = Object.keys(where);
|
||||
|
||||
if (whereKeys.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return whereKeys.every((key) => {
|
||||
const whereValue = where[key];
|
||||
|
||||
if (typeof whereValue === 'string') {
|
||||
return true;
|
||||
}
|
||||
if (whereValue && typeof whereValue === 'object') {
|
||||
const subObj = whereValue as Record<string, unknown>;
|
||||
|
||||
return Object.values(subObj).every(
|
||||
(subValue) => typeof subValue === 'string',
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const hasRelationDisconnect = (value: unknown): value is DisconnectObject => {
|
||||
if (!isDefined(value) || typeof value !== 'object') return false;
|
||||
|
||||
const obj = value as Record<string, unknown>;
|
||||
|
||||
if (
|
||||
!isDefined(obj[RELATION_NESTED_QUERY_KEYWORDS.DISCONNECT]) ||
|
||||
typeof obj[RELATION_NESTED_QUERY_KEYWORDS.DISCONNECT] !== 'boolean'
|
||||
)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const extractNestedRelationFieldsByEntityIndex = (
|
||||
entities: Record<string, unknown>[],
|
||||
): {
|
||||
relationConnectQueryFieldsByEntityIndex: RelationConnectQueryFieldsByEntityIndex;
|
||||
relationDisconnectQueryFieldsByEntityIndex: RelationDisconnectQueryFieldsByEntityIndex;
|
||||
} => {
|
||||
const relationConnectQueryFieldsByEntityIndex: RelationConnectQueryFieldsByEntityIndex =
|
||||
{};
|
||||
const relationDisconnectQueryFieldsByEntityIndex: RelationDisconnectQueryFieldsByEntityIndex =
|
||||
{};
|
||||
|
||||
for (const [entityIndex, entity] of Object.entries(entities)) {
|
||||
for (const [key, value] of Object.entries(entity)) {
|
||||
const hasConnect = hasRelationConnect(value);
|
||||
const hasDisconnect = hasRelationDisconnect(value);
|
||||
|
||||
if (hasConnect && hasDisconnect) {
|
||||
throw new TwentyORMException(
|
||||
`Cannot have both connect and disconnect for the same field on ${entity.key}.`,
|
||||
TwentyORMExceptionCode.CONNECT_NOT_ALLOWED,
|
||||
);
|
||||
}
|
||||
|
||||
const relationConnectQueryFields =
|
||||
relationConnectQueryFieldsByEntityIndex?.[entityIndex] || {};
|
||||
|
||||
if (hasConnect) {
|
||||
relationConnectQueryFieldsByEntityIndex[entityIndex] = {
|
||||
...relationConnectQueryFields,
|
||||
[key]: value,
|
||||
};
|
||||
}
|
||||
|
||||
const relationDisconnectQueryFields =
|
||||
relationDisconnectQueryFieldsByEntityIndex?.[entityIndex] || {};
|
||||
|
||||
if (hasDisconnect) {
|
||||
relationDisconnectQueryFieldsByEntityIndex[entityIndex] = {
|
||||
...relationDisconnectQueryFields,
|
||||
[key]: value,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
relationConnectQueryFieldsByEntityIndex,
|
||||
relationDisconnectQueryFieldsByEntityIndex,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user