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
1459 lines
43 KiB
TypeScript
1459 lines
43 KiB
TypeScript
import { Entity } from '@microsoft/microsoft-graph-types';
|
|
import { ObjectRecordsPermissions } from 'twenty-shared/types';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import {
|
|
DeleteResult,
|
|
EntityManager,
|
|
EntityTarget,
|
|
FindManyOptions,
|
|
FindOneOptions,
|
|
FindOptionsWhere,
|
|
InsertResult,
|
|
ObjectId,
|
|
ObjectLiteral,
|
|
QueryRunner,
|
|
RemoveOptions,
|
|
Repository,
|
|
SaveOptions,
|
|
SelectQueryBuilder,
|
|
TypeORMError,
|
|
UpdateResult,
|
|
} from 'typeorm';
|
|
import { DeepPartial } from 'typeorm/common/DeepPartial';
|
|
import { PickKeysByType } from 'typeorm/common/PickKeysByType';
|
|
import { EntityNotFoundError } from 'typeorm/error/EntityNotFoundError';
|
|
import { FindOptionsUtils } from 'typeorm/find-options/FindOptionsUtils';
|
|
import { EntityPersistExecutor } from 'typeorm/persistence/EntityPersistExecutor';
|
|
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
|
import { PlainObjectToDatabaseEntityTransformer } from 'typeorm/query-builder/transformer/PlainObjectToDatabaseEntityTransformer';
|
|
import { UpsertOptions } from 'typeorm/repository/UpsertOptions';
|
|
import { InstanceChecker } from 'typeorm/util/InstanceChecker';
|
|
|
|
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';
|
|
|
|
import {
|
|
PermissionsException,
|
|
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;
|
|
objectRecordsPermissions?: ObjectRecordsPermissions;
|
|
};
|
|
|
|
export class WorkspaceEntityManager extends EntityManager {
|
|
private readonly internalContext: WorkspaceInternalContext;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
readonly repositories: Map<string, Repository<any>>;
|
|
declare connection: WorkspaceDataSource;
|
|
|
|
constructor(
|
|
internalContext: WorkspaceInternalContext,
|
|
connection: WorkspaceDataSource,
|
|
queryRunner?: QueryRunner,
|
|
) {
|
|
super(connection, queryRunner);
|
|
this.internalContext = internalContext;
|
|
this.repositories = new Map();
|
|
}
|
|
|
|
getFeatureFlagMap(): FeatureFlagMap {
|
|
return this.connection.featureFlagMap;
|
|
}
|
|
|
|
override getRepository<Entity extends ObjectLiteral>(
|
|
target: EntityTarget<Entity>,
|
|
permissionOptions?: {
|
|
shouldBypassPermissionChecks?: boolean;
|
|
roleId?: string;
|
|
},
|
|
): WorkspaceRepository<Entity> {
|
|
const dataSource = this.connection;
|
|
|
|
const repositoryKey = this.getRepositoryKey({
|
|
target,
|
|
dataSource,
|
|
roleId: permissionOptions?.roleId,
|
|
shouldBypassPermissionChecks:
|
|
permissionOptions?.shouldBypassPermissionChecks ?? false,
|
|
});
|
|
const repoFromMap = this.repositories.get(repositoryKey);
|
|
|
|
if (repoFromMap) {
|
|
return repoFromMap as WorkspaceRepository<Entity>;
|
|
}
|
|
|
|
let objectPermissions = {};
|
|
|
|
if (permissionOptions?.roleId) {
|
|
const objectPermissionsByRoleId = dataSource.permissionsPerRoleId;
|
|
|
|
if (!isDefined(objectPermissionsByRoleId?.[permissionOptions.roleId])) {
|
|
throw new PermissionsException(
|
|
`No permissions found for role in datasource (missing ${
|
|
!isDefined(objectPermissionsByRoleId)
|
|
? 'objectPermissionsByRoleId object'
|
|
: `roleId in objectPermissionsByRoleId object (${permissionOptions.roleId})`
|
|
})`,
|
|
PermissionsExceptionCode.NO_PERMISSIONS_FOUND_IN_DATASOURCE,
|
|
);
|
|
} else {
|
|
objectPermissions = objectPermissionsByRoleId[permissionOptions.roleId];
|
|
}
|
|
}
|
|
|
|
const newRepository = new WorkspaceRepository<Entity>(
|
|
this.internalContext,
|
|
target,
|
|
this,
|
|
dataSource.featureFlagMap,
|
|
this.queryRunner,
|
|
objectPermissions,
|
|
permissionOptions?.shouldBypassPermissionChecks,
|
|
);
|
|
|
|
this.repositories.set(repositoryKey, newRepository);
|
|
|
|
return newRepository;
|
|
}
|
|
|
|
override createQueryBuilder<Entity extends ObjectLiteral>(
|
|
entityClassOrQueryRunner?: EntityTarget<Entity> | QueryRunner,
|
|
alias?: string,
|
|
queryRunner?: QueryRunner,
|
|
options: {
|
|
shouldBypassPermissionChecks?: boolean;
|
|
objectRecordsPermissions?: ObjectRecordsPermissions;
|
|
} = {
|
|
shouldBypassPermissionChecks: false,
|
|
objectRecordsPermissions: {},
|
|
},
|
|
): SelectQueryBuilder<Entity> | WorkspaceSelectQueryBuilder<Entity> {
|
|
let queryBuilder: SelectQueryBuilder<Entity>;
|
|
|
|
if (alias) {
|
|
queryBuilder = this.connection.createQueryBuilder(
|
|
entityClassOrQueryRunner as EntityTarget<Entity>,
|
|
alias as string,
|
|
queryRunner as QueryRunner | undefined,
|
|
{
|
|
calledByWorkspaceEntityManager: true,
|
|
},
|
|
);
|
|
} else {
|
|
queryBuilder = this.connection.createQueryBuilder(
|
|
entityClassOrQueryRunner as QueryRunner,
|
|
{
|
|
calledByWorkspaceEntityManager: true,
|
|
},
|
|
);
|
|
}
|
|
|
|
return new WorkspaceSelectQueryBuilder(
|
|
queryBuilder,
|
|
options?.objectRecordsPermissions ?? {},
|
|
this.internalContext,
|
|
options?.shouldBypassPermissionChecks ?? false,
|
|
);
|
|
}
|
|
|
|
override async insert<Entity extends ObjectLiteral>(
|
|
target: EntityTarget<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,
|
|
undefined,
|
|
permissionOptions,
|
|
)
|
|
.insert()
|
|
.into(target)
|
|
.values(connectedEntities)
|
|
.execute();
|
|
}
|
|
|
|
override upsert<Entity extends ObjectLiteral>(
|
|
target: EntityTarget<Entity>,
|
|
entityOrEntities:
|
|
| QueryDeepPartialEntity<Entity>
|
|
| QueryDeepPartialEntity<Entity>[],
|
|
conflictPathsOrOptions: string[] | UpsertOptions<Entity>,
|
|
permissionOptions?: {
|
|
shouldBypassPermissionChecks?: boolean;
|
|
objectRecordsPermissions?: ObjectRecordsPermissions;
|
|
},
|
|
): Promise<InsertResult> {
|
|
const metadata = this.connection.getMetadata(target);
|
|
let options;
|
|
|
|
if (Array.isArray(conflictPathsOrOptions)) {
|
|
options = {
|
|
conflictPaths: conflictPathsOrOptions,
|
|
};
|
|
} else {
|
|
options = conflictPathsOrOptions;
|
|
}
|
|
let entities: QueryDeepPartialEntity<Entity>[];
|
|
|
|
if (!Array.isArray(entityOrEntities)) {
|
|
entities = [entityOrEntities];
|
|
} else {
|
|
entities = entityOrEntities;
|
|
}
|
|
const conflictColumns = metadata.mapPropertyPathsToColumns(
|
|
Array.isArray(options.conflictPaths)
|
|
? options.conflictPaths
|
|
: Object.keys(options.conflictPaths),
|
|
);
|
|
const overwriteColumns = metadata.columns.filter(
|
|
(col) =>
|
|
!conflictColumns.includes(col) &&
|
|
entities.some(
|
|
(entity) => typeof col.getEntityValue(entity) !== 'undefined',
|
|
),
|
|
);
|
|
|
|
return this.createQueryBuilder(
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
permissionOptions,
|
|
)
|
|
.insert()
|
|
.into(target)
|
|
.values(entities)
|
|
.orUpdate(
|
|
[...conflictColumns, ...overwriteColumns].map(
|
|
(col) => col.databaseName,
|
|
),
|
|
conflictColumns.map((col) => col.databaseName),
|
|
{
|
|
skipUpdateIfNoValuesChanged: options.skipUpdateIfNoValuesChanged,
|
|
indexPredicate: options.indexPredicate,
|
|
upsertType:
|
|
options.upsertType ||
|
|
this.connection.driver.supportedUpsertTypes[0],
|
|
},
|
|
)
|
|
.execute();
|
|
}
|
|
|
|
override update<Entity extends ObjectLiteral>(
|
|
target: EntityTarget<Entity>,
|
|
criteria:
|
|
| string
|
|
| string[]
|
|
| number
|
|
| number[]
|
|
| Date
|
|
| Date[]
|
|
| ObjectId
|
|
| ObjectId[]
|
|
| unknown,
|
|
partialEntity: QueryDeepPartialEntity<Entity>,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<UpdateResult> {
|
|
if (
|
|
criteria === undefined ||
|
|
criteria === null ||
|
|
criteria === '' ||
|
|
(Array.isArray(criteria) && criteria.length === 0)
|
|
) {
|
|
return Promise.reject(
|
|
new TypeORMError(
|
|
`Empty criteria(s) are not allowed for the update method.`,
|
|
),
|
|
);
|
|
}
|
|
if (
|
|
typeof criteria === 'string' ||
|
|
typeof criteria === 'number' ||
|
|
criteria instanceof Date ||
|
|
Array.isArray(criteria)
|
|
) {
|
|
return this.createQueryBuilder(
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
permissionOptions,
|
|
)
|
|
.update(target)
|
|
.set(partialEntity)
|
|
.whereInIds(criteria)
|
|
.execute();
|
|
} else {
|
|
return this.createQueryBuilder(
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
permissionOptions,
|
|
)
|
|
.update(target)
|
|
.set(partialEntity)
|
|
.where(criteria)
|
|
.execute();
|
|
}
|
|
}
|
|
|
|
override increment<Entity extends ObjectLiteral>(
|
|
target: EntityTarget<Entity>,
|
|
criteria: object,
|
|
propertyPath: string,
|
|
value: number | string,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<UpdateResult> {
|
|
const metadata = this.connection.getMetadata(target);
|
|
const column = metadata.findColumnWithPropertyPath(propertyPath);
|
|
|
|
if (!column)
|
|
throw new TypeORMError(
|
|
`Column ${propertyPath} was not found in ${metadata.targetName} entity.`,
|
|
);
|
|
if (isNaN(Number(value)))
|
|
throw new TypeORMError(`Value "${value}" is not a number.`);
|
|
// convert possible embeded path "social.likes" into object { social: { like: () => value } }
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const values = propertyPath.split('.').reduceRight<any>(
|
|
(value, key) => ({ [key]: value }),
|
|
() => this.connection.driver.escape(column.databaseName) + ' + ' + value,
|
|
);
|
|
|
|
return this.createQueryBuilder(
|
|
target,
|
|
'entity',
|
|
undefined,
|
|
permissionOptions,
|
|
)
|
|
.update(target as QueryDeepPartialEntity<Entity>)
|
|
.set(values)
|
|
.where(criteria)
|
|
.execute();
|
|
}
|
|
|
|
private getRepositoryKey({
|
|
target,
|
|
dataSource,
|
|
roleId,
|
|
shouldBypassPermissionChecks,
|
|
}: {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
target: EntityTarget<unknown>;
|
|
dataSource: WorkspaceDataSource;
|
|
shouldBypassPermissionChecks: boolean;
|
|
roleId?: string;
|
|
}) {
|
|
const repositoryPrefix = dataSource.getMetadata(target).name;
|
|
const roleIdSuffix = roleId ? `_${roleId}` : '';
|
|
const rolesPermissionsVersionSuffix = dataSource.rolesPermissionsVersion
|
|
? `_${dataSource.rolesPermissionsVersion}`
|
|
: '';
|
|
const featureFlagMapVersionSuffix = dataSource.featureFlagMapVersion
|
|
? `_${dataSource.featureFlagMapVersion}`
|
|
: '';
|
|
|
|
return shouldBypassPermissionChecks
|
|
? `${repositoryPrefix}_bypass${featureFlagMapVersionSuffix}`
|
|
: `${repositoryPrefix}${roleIdSuffix}${rolesPermissionsVersionSuffix}${featureFlagMapVersionSuffix}`;
|
|
}
|
|
|
|
validatePermissions<Entity extends ObjectLiteral>(
|
|
target: EntityTarget<Entity> | Entity,
|
|
operationType: OperationType,
|
|
permissionOptions?: {
|
|
shouldBypassPermissionChecks?: boolean;
|
|
objectRecordsPermissions?: ObjectRecordsPermissions;
|
|
},
|
|
): void {
|
|
if (permissionOptions?.shouldBypassPermissionChecks === true) {
|
|
return;
|
|
}
|
|
|
|
const entityName =
|
|
typeof target === 'function' || typeof target === 'string'
|
|
? this.extractTargetNameSingularFromEntityTarget(target)
|
|
: this.extractTargetNameSingularFromEntity(target);
|
|
|
|
validateOperationIsPermittedOrThrow({
|
|
entityName,
|
|
operationType,
|
|
objectRecordsPermissions:
|
|
permissionOptions?.objectRecordsPermissions ?? {},
|
|
objectMetadataMaps: this.internalContext.objectMetadataMaps,
|
|
});
|
|
}
|
|
|
|
private extractTargetNameSingularFromEntityTarget(
|
|
target: EntityTarget<unknown>,
|
|
): string {
|
|
return this.connection.getMetadata(target).name;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
private extractTargetNameSingularFromEntity(entity: any): string {
|
|
return this.connection.getMetadata(entity.constructor).name;
|
|
}
|
|
|
|
override find<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
options?: FindManyOptions<Entity>,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity[]> {
|
|
const metadata = this.connection.getMetadata(entityClass);
|
|
|
|
return this.createQueryBuilder(
|
|
entityClass,
|
|
FindOptionsUtils.extractFindManyOptionsAlias(options) || metadata.name,
|
|
this.queryRunner,
|
|
permissionOptions,
|
|
)
|
|
.setFindOptions(options || {})
|
|
.getMany();
|
|
}
|
|
|
|
override findBy<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity[]> {
|
|
const metadata = this.connection.getMetadata(entityClass);
|
|
|
|
return this.createQueryBuilder(
|
|
entityClass,
|
|
metadata.name,
|
|
this.queryRunner,
|
|
permissionOptions,
|
|
)
|
|
.setFindOptions({ where: where })
|
|
.getMany();
|
|
}
|
|
|
|
override findOne<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
options: FindOneOptions<Entity>,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity | null> {
|
|
const metadata = this.connection.getMetadata(entityClass);
|
|
// prepare alias for built query
|
|
let alias = metadata.name;
|
|
|
|
if (options && options.join) {
|
|
alias = options.join.alias;
|
|
}
|
|
if (!options.where) {
|
|
throw new Error(
|
|
`You must provide selection conditions in order to find a single row.`,
|
|
);
|
|
}
|
|
|
|
// create query builder and apply find options
|
|
return this.createQueryBuilder(
|
|
entityClass,
|
|
alias,
|
|
this.queryRunner,
|
|
permissionOptions,
|
|
)
|
|
.setFindOptions({
|
|
...options,
|
|
take: 1,
|
|
})
|
|
.getOne();
|
|
}
|
|
|
|
override findOneBy<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity | null> {
|
|
const metadata = this.connection.getMetadata(entityClass);
|
|
|
|
// create query builder and apply find options
|
|
return this.createQueryBuilder(
|
|
entityClass,
|
|
metadata.name,
|
|
this.queryRunner,
|
|
permissionOptions,
|
|
)
|
|
.setFindOptions({
|
|
where,
|
|
take: 1,
|
|
})
|
|
.getOne();
|
|
}
|
|
|
|
override findAndCount<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
options?: FindManyOptions<Entity>,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<[Entity[], number]> {
|
|
const metadata = this.connection.getMetadata(entityClass);
|
|
|
|
return this.createQueryBuilder(
|
|
entityClass,
|
|
FindOptionsUtils.extractFindManyOptionsAlias(options) || metadata.name,
|
|
this.queryRunner,
|
|
permissionOptions,
|
|
)
|
|
.setFindOptions(options || {})
|
|
.getManyAndCount();
|
|
}
|
|
|
|
override findAndCountBy<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<[Entity[], number]> {
|
|
const metadata = this.connection.getMetadata(entityClass);
|
|
|
|
return this.createQueryBuilder(
|
|
entityClass,
|
|
metadata.name,
|
|
this.queryRunner,
|
|
permissionOptions,
|
|
)
|
|
.setFindOptions({ where })
|
|
.getManyAndCount();
|
|
}
|
|
|
|
override findOneOrFail<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
options: FindOneOptions<Entity>,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity> {
|
|
return this.findOne(entityClass, options, permissionOptions).then(
|
|
(value) => {
|
|
if (value === null) {
|
|
return Promise.reject(new EntityNotFoundError(entityClass, options));
|
|
}
|
|
|
|
return Promise.resolve(value);
|
|
},
|
|
);
|
|
}
|
|
|
|
override findOneByOrFail<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity> {
|
|
return this.findOneBy(entityClass, where, permissionOptions).then(
|
|
(value) => {
|
|
if (value === null) {
|
|
return Promise.reject(new EntityNotFoundError(entityClass, where));
|
|
}
|
|
|
|
return Promise.resolve(value);
|
|
},
|
|
);
|
|
}
|
|
|
|
override delete<Entity extends ObjectLiteral>(
|
|
targetOrEntity: EntityTarget<Entity>,
|
|
criteria: unknown,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<DeleteResult> {
|
|
if (
|
|
criteria === undefined ||
|
|
criteria === null ||
|
|
criteria === '' ||
|
|
(Array.isArray(criteria) && criteria.length === 0)
|
|
) {
|
|
return Promise.reject(
|
|
new TypeORMError(
|
|
`Empty criteria(s) are not allowed for the delete method.`,
|
|
),
|
|
);
|
|
}
|
|
if (
|
|
typeof criteria === 'string' ||
|
|
typeof criteria === 'number' ||
|
|
criteria instanceof Date ||
|
|
Array.isArray(criteria)
|
|
) {
|
|
return this.createQueryBuilder(
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
permissionOptions,
|
|
)
|
|
.delete()
|
|
.from(targetOrEntity)
|
|
.whereInIds(criteria)
|
|
.execute();
|
|
} else {
|
|
return this.createQueryBuilder(
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
permissionOptions,
|
|
)
|
|
.delete()
|
|
.from(targetOrEntity)
|
|
.where(criteria)
|
|
.execute();
|
|
}
|
|
}
|
|
|
|
override softDelete<Entity extends ObjectLiteral>(
|
|
targetOrEntity: EntityTarget<Entity>,
|
|
criteria: unknown,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<UpdateResult> {
|
|
// if user passed empty criteria or empty list of criterias, then throw an error
|
|
if (
|
|
criteria === undefined ||
|
|
criteria === null ||
|
|
criteria === '' ||
|
|
(Array.isArray(criteria) && criteria.length === 0)
|
|
) {
|
|
return Promise.reject(
|
|
new TypeORMError(
|
|
`Empty criteria(s) are not allowed for the softDelete method.`,
|
|
),
|
|
);
|
|
}
|
|
if (
|
|
typeof criteria === 'string' ||
|
|
typeof criteria === 'number' ||
|
|
criteria instanceof Date ||
|
|
Array.isArray(criteria)
|
|
) {
|
|
return this.createQueryBuilder(
|
|
undefined,
|
|
undefined,
|
|
this.queryRunner,
|
|
permissionOptions,
|
|
)
|
|
.softDelete()
|
|
.from(targetOrEntity)
|
|
.whereInIds(criteria)
|
|
.execute();
|
|
} else {
|
|
return this.createQueryBuilder(
|
|
undefined,
|
|
undefined,
|
|
this.queryRunner,
|
|
permissionOptions,
|
|
)
|
|
.softDelete()
|
|
.from(targetOrEntity)
|
|
.where(criteria)
|
|
.execute();
|
|
}
|
|
}
|
|
|
|
override restore<Entity extends ObjectLiteral>(
|
|
targetOrEntity: EntityTarget<Entity>,
|
|
criteria: unknown,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<UpdateResult> {
|
|
// if user passed empty criteria or empty list of criterias, then throw an error
|
|
if (
|
|
criteria === undefined ||
|
|
criteria === null ||
|
|
criteria === '' ||
|
|
(Array.isArray(criteria) && criteria.length === 0)
|
|
) {
|
|
return Promise.reject(
|
|
new TypeORMError(
|
|
`Empty criteria(s) are not allowed for the restore method.`,
|
|
),
|
|
);
|
|
}
|
|
if (
|
|
typeof criteria === 'string' ||
|
|
typeof criteria === 'number' ||
|
|
criteria instanceof Date ||
|
|
Array.isArray(criteria)
|
|
) {
|
|
return this.createQueryBuilder(
|
|
undefined,
|
|
undefined,
|
|
this.queryRunner,
|
|
permissionOptions,
|
|
)
|
|
.restore()
|
|
.from(targetOrEntity)
|
|
.whereInIds(criteria)
|
|
.execute();
|
|
} else {
|
|
return this.createQueryBuilder(
|
|
undefined,
|
|
undefined,
|
|
this.queryRunner,
|
|
permissionOptions,
|
|
)
|
|
.restore()
|
|
.from(targetOrEntity)
|
|
.where(criteria)
|
|
.execute();
|
|
}
|
|
}
|
|
|
|
override exists<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
options?: FindManyOptions<Entity>,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<boolean> {
|
|
const metadata = this.connection.getMetadata(entityClass);
|
|
|
|
return this.createQueryBuilder(
|
|
entityClass,
|
|
FindOptionsUtils.extractFindManyOptionsAlias(options) || metadata.name,
|
|
this.queryRunner,
|
|
permissionOptions,
|
|
)
|
|
.setFindOptions(options || {})
|
|
.select('1')
|
|
.limit(1)
|
|
.getRawOne()
|
|
.then((result) => isDefined(result));
|
|
}
|
|
|
|
override existsBy<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<boolean> {
|
|
const metadata = this.connection.getMetadata(entityClass);
|
|
|
|
return this.createQueryBuilder(
|
|
entityClass,
|
|
metadata.name,
|
|
this.queryRunner,
|
|
permissionOptions,
|
|
)
|
|
.setFindOptions({ where })
|
|
.select('1')
|
|
.limit(1)
|
|
.getRawOne()
|
|
.then((result) => isDefined(result));
|
|
}
|
|
|
|
override count<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
options?: FindManyOptions<Entity>,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<number> {
|
|
const metadata = this.connection.getMetadata(entityClass);
|
|
|
|
return this.createQueryBuilder(
|
|
entityClass,
|
|
FindOptionsUtils.extractFindManyOptionsAlias(options) || metadata.name,
|
|
this.queryRunner,
|
|
permissionOptions,
|
|
)
|
|
.setFindOptions(options || {})
|
|
.getCount();
|
|
}
|
|
|
|
override countBy<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<number> {
|
|
const metadata = this.connection.getMetadata(entityClass);
|
|
|
|
return this.createQueryBuilder(
|
|
entityClass,
|
|
metadata.name,
|
|
this.queryRunner,
|
|
permissionOptions,
|
|
)
|
|
.setFindOptions({ where })
|
|
.getCount();
|
|
}
|
|
|
|
async callAggregateFunCustom(
|
|
entityClass: EntityTarget<Entity>,
|
|
fnName: string,
|
|
columnName: string,
|
|
where = {},
|
|
permissionOptions?: PermissionOptions,
|
|
) {
|
|
const metadata = this.connection.getMetadata(entityClass);
|
|
const column = metadata.columns.find(
|
|
(item) => item.propertyPath === columnName,
|
|
);
|
|
|
|
if (!column) {
|
|
throw new TypeORMError(
|
|
`Column "${columnName}" was not found in table "${metadata.name}"`,
|
|
);
|
|
}
|
|
const result = await this.createQueryBuilder(
|
|
entityClass,
|
|
metadata.name,
|
|
this.queryRunner,
|
|
permissionOptions,
|
|
)
|
|
.setFindOptions({ where })
|
|
.select(
|
|
`${fnName}(${this.connection.driver.escape(column.databaseName)})`,
|
|
fnName,
|
|
)
|
|
.getRawOne();
|
|
|
|
return result[fnName] === null ? null : parseFloat(result[fnName]);
|
|
}
|
|
|
|
override sum<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
columnName: PickKeysByType<Entity, number>,
|
|
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<number | null> {
|
|
return this.callAggregateFunCustom(
|
|
entityClass,
|
|
'SUM',
|
|
columnName,
|
|
where,
|
|
permissionOptions,
|
|
);
|
|
}
|
|
|
|
override average<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
columnName: PickKeysByType<Entity, number>,
|
|
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<number | null> {
|
|
return this.callAggregateFunCustom(
|
|
entityClass,
|
|
'AVG',
|
|
columnName,
|
|
where,
|
|
permissionOptions,
|
|
);
|
|
}
|
|
|
|
override minimum<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
columnName: PickKeysByType<Entity, number>,
|
|
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<number | null> {
|
|
return this.callAggregateFunCustom(
|
|
entityClass,
|
|
'MIN',
|
|
columnName,
|
|
where,
|
|
permissionOptions,
|
|
);
|
|
}
|
|
|
|
override maximum<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
columnName: PickKeysByType<Entity, number>,
|
|
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<number | null> {
|
|
return this.callAggregateFunCustom(
|
|
entityClass,
|
|
'MAX',
|
|
columnName,
|
|
where,
|
|
permissionOptions,
|
|
);
|
|
}
|
|
|
|
override clear<Entity>(
|
|
entityClass: EntityTarget<Entity>,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<void> {
|
|
this.validatePermissions(entityClass, 'delete', permissionOptions);
|
|
|
|
return super.clear(entityClass);
|
|
}
|
|
|
|
override async preload<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
entityLike: DeepPartial<Entity>,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity | undefined> {
|
|
const managerWithPermissionOptions = Object.assign(
|
|
Object.create(Object.getPrototypeOf(this)),
|
|
this,
|
|
{
|
|
findByIds: (entityClass: EntityTarget<Entity>, ids: string[]) => {
|
|
return this.findByIds(entityClass, ids, permissionOptions);
|
|
},
|
|
},
|
|
);
|
|
|
|
const metadata = this.connection.getMetadata(entityClass);
|
|
const plainObjectToDatabaseEntityTransformer =
|
|
new PlainObjectToDatabaseEntityTransformer(managerWithPermissionOptions);
|
|
const transformedEntity =
|
|
await plainObjectToDatabaseEntityTransformer.transform(
|
|
entityLike,
|
|
metadata,
|
|
);
|
|
|
|
if (transformedEntity)
|
|
return this.merge(entityClass, transformedEntity, entityLike) as Entity;
|
|
|
|
return undefined;
|
|
}
|
|
|
|
override decrement<Entity extends ObjectLiteral>(
|
|
target: EntityTarget<Entity>,
|
|
criteria: object,
|
|
propertyPath: string,
|
|
value: number | string,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<UpdateResult> {
|
|
const metadata = this.connection.getMetadata(target);
|
|
const column = metadata.findColumnWithPropertyPath(propertyPath);
|
|
|
|
if (!column)
|
|
throw new TypeORMError(
|
|
`Column ${propertyPath} was not found in ${metadata.targetName} entity.`,
|
|
);
|
|
if (isNaN(Number(value)))
|
|
throw new TypeORMError(`Value "${value}" is not a number.`);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const values = propertyPath.split('.').reduceRight<any>(
|
|
(value, key) => ({ [key]: value }),
|
|
() => this.connection.driver.escape(column.databaseName) + ' - ' + value,
|
|
);
|
|
|
|
return this.createQueryBuilder(
|
|
target,
|
|
'entity',
|
|
undefined,
|
|
permissionOptions,
|
|
)
|
|
.update(target as QueryDeepPartialEntity<Entity>)
|
|
.set(values)
|
|
.where(criteria)
|
|
.execute();
|
|
}
|
|
|
|
override async findByIds<Entity extends ObjectLiteral>(
|
|
entityClass: EntityTarget<Entity>,
|
|
ids: string[],
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity[]> {
|
|
if (!ids.length) return Promise.resolve([]);
|
|
const metadata = this.connection.getMetadata(entityClass);
|
|
|
|
return this.createQueryBuilder(
|
|
entityClass,
|
|
metadata.name,
|
|
undefined,
|
|
permissionOptions,
|
|
)
|
|
.andWhereInIds(ids)
|
|
.getMany();
|
|
}
|
|
|
|
/**
|
|
* Functions duplicated from EntityManager but with a queryRunner that will bypass permissions
|
|
* because permissions cannot be passed on to the call to createQueryBuilder() done in SubjectExecutor called by EntityPersistExecutor
|
|
* queryBuilder checks are replaced by validatePermissions()
|
|
*/
|
|
|
|
override save<Entity extends ObjectLiteral>(
|
|
entities: Entity[],
|
|
options?: SaveOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity[]>;
|
|
|
|
override save<Entity extends ObjectLiteral>(
|
|
entity: Entity,
|
|
options?: SaveOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity>;
|
|
|
|
override save<Entity, T extends DeepPartial<Entity>>(
|
|
targetOrEntity: EntityTarget<Entity>,
|
|
entities: T[],
|
|
options: SaveOptions & {
|
|
reload: false;
|
|
},
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<T[]>;
|
|
|
|
override save<Entity, T extends DeepPartial<Entity>>(
|
|
targetOrEntity: EntityTarget<Entity>,
|
|
entities: T[],
|
|
options?: SaveOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<(T & Entity)[]>;
|
|
|
|
override save<Entity, T extends DeepPartial<Entity>>(
|
|
targetOrEntity: EntityTarget<Entity>,
|
|
entity: T,
|
|
options: SaveOptions & {
|
|
reload: false;
|
|
},
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<T>;
|
|
|
|
override save<Entity, T extends DeepPartial<Entity>>(
|
|
targetOrEntity: EntityTarget<Entity>,
|
|
entity: T,
|
|
options?: SaveOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<T>;
|
|
|
|
override async save<
|
|
Entity extends ObjectLiteral,
|
|
T extends DeepPartial<Entity>,
|
|
>(
|
|
targetOrEntity: EntityTarget<Entity> | Entity | Entity[],
|
|
entityOrMaybeOptions:
|
|
| T
|
|
| T[]
|
|
| SaveOptions
|
|
| (SaveOptions & { reload: false }),
|
|
maybeOptionsOrMaybePermissionOptions?:
|
|
| PermissionOptions
|
|
| SaveOptions
|
|
| (SaveOptions & { reload: false }),
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<(T & Entity) | (T & Entity)[] | Entity | Entity[]> {
|
|
const permissionOptionsFromArgs =
|
|
maybeOptionsOrMaybePermissionOptions &&
|
|
('shouldBypassPermissionChecks' in maybeOptionsOrMaybePermissionOptions ||
|
|
'objectRecordsPermissions' in maybeOptionsOrMaybePermissionOptions)
|
|
? maybeOptionsOrMaybePermissionOptions
|
|
: permissionOptions;
|
|
|
|
this.validatePermissions(
|
|
targetOrEntity,
|
|
'update',
|
|
permissionOptionsFromArgs,
|
|
);
|
|
|
|
let target =
|
|
arguments.length > 1 &&
|
|
(typeof targetOrEntity === 'function' ||
|
|
InstanceChecker.isEntitySchema(targetOrEntity) ||
|
|
typeof targetOrEntity === 'string')
|
|
? targetOrEntity
|
|
: undefined;
|
|
const entity = target ? entityOrMaybeOptions : targetOrEntity;
|
|
const options = target
|
|
? maybeOptionsOrMaybePermissionOptions
|
|
: entityOrMaybeOptions;
|
|
|
|
if (InstanceChecker.isEntitySchema(target)) target = target.options.name;
|
|
if (Array.isArray(entity) && entity.length === 0)
|
|
return Promise.resolve(entity as Entity[]);
|
|
|
|
const queryRunnerForEntityPersistExecutor =
|
|
this.connection.createQueryRunnerForEntityPersistExecutor();
|
|
|
|
return new EntityPersistExecutor(
|
|
this.connection,
|
|
queryRunnerForEntityPersistExecutor,
|
|
'save',
|
|
target,
|
|
entity as ObjectLiteral,
|
|
options as SaveOptions | (SaveOptions & { reload: false }),
|
|
)
|
|
.execute()
|
|
.then(() => entity as Entity)
|
|
.finally(() => queryRunnerForEntityPersistExecutor.release());
|
|
}
|
|
|
|
override remove<Entity>(
|
|
entity: Entity,
|
|
options?: RemoveOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity>;
|
|
|
|
override remove<Entity>(
|
|
targetOrEntity: EntityTarget<Entity>,
|
|
entity: Entity,
|
|
options?: RemoveOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity>;
|
|
|
|
override remove<Entity>(
|
|
entity: Entity[],
|
|
options?: RemoveOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity>;
|
|
|
|
override remove<Entity>(
|
|
targetOrEntity: EntityTarget<Entity>,
|
|
entity: Entity[],
|
|
options?: RemoveOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity[]>;
|
|
|
|
override async remove<Entity extends ObjectLiteral>(
|
|
targetOrEntity: EntityTarget<Entity> | Entity[] | Entity,
|
|
entityOrMaybeOptions: Entity | Entity[] | RemoveOptions,
|
|
maybeOptionsOrMaybePermissionOptions?: RemoveOptions | PermissionOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity | Entity[]> {
|
|
const permissionOptionsFromArgs =
|
|
maybeOptionsOrMaybePermissionOptions &&
|
|
('shouldBypassPermissionChecks' in maybeOptionsOrMaybePermissionOptions ||
|
|
'objectRecordsPermissions' in maybeOptionsOrMaybePermissionOptions)
|
|
? (maybeOptionsOrMaybePermissionOptions as PermissionOptions)
|
|
: permissionOptions;
|
|
|
|
this.validatePermissions(
|
|
targetOrEntity,
|
|
'delete',
|
|
permissionOptionsFromArgs,
|
|
);
|
|
|
|
const target =
|
|
arguments.length > 1 &&
|
|
(typeof targetOrEntity === 'function' ||
|
|
InstanceChecker.isEntitySchema(targetOrEntity) ||
|
|
typeof targetOrEntity === 'string')
|
|
? targetOrEntity
|
|
: undefined;
|
|
const entity = target ? entityOrMaybeOptions : targetOrEntity;
|
|
const options = target
|
|
? maybeOptionsOrMaybePermissionOptions
|
|
: entityOrMaybeOptions;
|
|
|
|
if (Array.isArray(entity) && entity.length === 0)
|
|
return Promise.resolve(entity);
|
|
|
|
const queryRunnerForEntityPersistExecutor =
|
|
this.connection.createQueryRunnerForEntityPersistExecutor();
|
|
|
|
return new EntityPersistExecutor(
|
|
this.connection,
|
|
queryRunnerForEntityPersistExecutor,
|
|
'remove',
|
|
target as string | undefined,
|
|
entity as ObjectLiteral,
|
|
options as RemoveOptions,
|
|
)
|
|
.execute()
|
|
.then(() => entity as Entity | Entity[])
|
|
.finally(() => queryRunnerForEntityPersistExecutor.release());
|
|
}
|
|
|
|
override softRemove<Entity extends ObjectLiteral>(
|
|
entities: Entity[],
|
|
options?: SaveOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity[]>;
|
|
|
|
override softRemove<Entity extends ObjectLiteral>(
|
|
entities: Entity,
|
|
options?: SaveOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity>;
|
|
|
|
override softRemove<
|
|
Entity extends ObjectLiteral,
|
|
T extends DeepPartial<Entity>,
|
|
>(
|
|
targetOrEntity: EntityTarget<Entity>,
|
|
entities: T[],
|
|
options?: SaveOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<T[]>;
|
|
|
|
override softRemove<
|
|
Entity extends ObjectLiteral,
|
|
T extends DeepPartial<Entity>,
|
|
>(
|
|
targetOrEntity: EntityTarget<Entity>,
|
|
entities: T,
|
|
options?: SaveOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<T>;
|
|
|
|
override async softRemove<
|
|
Entity extends ObjectLiteral,
|
|
T extends DeepPartial<Entity>,
|
|
>(
|
|
targetOrEntityOrEntities: Entity | Entity[] | EntityTarget<Entity>,
|
|
entitiesOrMaybeOptions: T | T[] | SaveOptions,
|
|
maybeOptionsOrMaybePermissionOptions?: SaveOptions | PermissionOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity | Entity[] | T | T[]> {
|
|
const permissionOptionsFromArgs =
|
|
maybeOptionsOrMaybePermissionOptions &&
|
|
('shouldBypassPermissionChecks' in maybeOptionsOrMaybePermissionOptions ||
|
|
'objectRecordsPermissions' in maybeOptionsOrMaybePermissionOptions)
|
|
? (maybeOptionsOrMaybePermissionOptions as PermissionOptions)
|
|
: permissionOptions;
|
|
|
|
this.validatePermissions(
|
|
targetOrEntityOrEntities,
|
|
'soft-delete',
|
|
permissionOptionsFromArgs,
|
|
);
|
|
|
|
let target =
|
|
arguments.length > 1 &&
|
|
(typeof targetOrEntityOrEntities === 'function' ||
|
|
InstanceChecker.isEntitySchema(targetOrEntityOrEntities) ||
|
|
typeof targetOrEntityOrEntities === 'string')
|
|
? targetOrEntityOrEntities
|
|
: undefined;
|
|
const entity = target ? entitiesOrMaybeOptions : targetOrEntityOrEntities;
|
|
const options = target
|
|
? maybeOptionsOrMaybePermissionOptions
|
|
: entitiesOrMaybeOptions;
|
|
|
|
if (InstanceChecker.isEntitySchema(target)) target = target.options.name;
|
|
if (Array.isArray(entity) && entity.length === 0)
|
|
return Promise.resolve(entity);
|
|
|
|
const queryRunnerForEntityPersistExecutor =
|
|
this.connection.createQueryRunnerForEntityPersistExecutor();
|
|
|
|
return new EntityPersistExecutor(
|
|
this.connection,
|
|
queryRunnerForEntityPersistExecutor,
|
|
'soft-remove',
|
|
target,
|
|
entity as ObjectLiteral,
|
|
options as SaveOptions,
|
|
)
|
|
.execute()
|
|
.then(() => entity as Entity)
|
|
.finally(() => queryRunnerForEntityPersistExecutor.release());
|
|
}
|
|
|
|
override recover<Entity>(
|
|
entities: Entity[],
|
|
options?: SaveOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity[]>;
|
|
|
|
override recover<Entity>(
|
|
entity: Entity,
|
|
options?: SaveOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity>;
|
|
|
|
override recover<Entity, T extends DeepPartial<Entity>>(
|
|
targetOrEntity: EntityTarget<Entity>,
|
|
entities: T[],
|
|
options?: SaveOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<T[]>;
|
|
|
|
override recover<Entity, T extends DeepPartial<Entity>>(
|
|
targetOrEntity: EntityTarget<Entity>,
|
|
entity: T,
|
|
options?: SaveOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<T>;
|
|
|
|
override async recover<
|
|
Entity extends ObjectLiteral,
|
|
T extends DeepPartial<Entity>,
|
|
>(
|
|
targetOrEntityOrEntities: EntityTarget<Entity> | Entity | Entity[],
|
|
entityOrEntitiesOrMaybeOptions: T | T[] | SaveOptions,
|
|
maybeOptionsOrMaybePermissionOptions?: SaveOptions | PermissionOptions,
|
|
permissionOptions?: PermissionOptions,
|
|
): Promise<Entity | Entity[] | T | T[]> {
|
|
const permissionOptionsFromArgs =
|
|
maybeOptionsOrMaybePermissionOptions &&
|
|
('shouldBypassPermissionChecks' in maybeOptionsOrMaybePermissionOptions ||
|
|
'objectRecordsPermissions' in maybeOptionsOrMaybePermissionOptions)
|
|
? (maybeOptionsOrMaybePermissionOptions as PermissionOptions)
|
|
: permissionOptions;
|
|
|
|
this.validatePermissions(
|
|
targetOrEntityOrEntities,
|
|
'restore',
|
|
permissionOptionsFromArgs,
|
|
);
|
|
|
|
let target =
|
|
arguments.length > 1 &&
|
|
(typeof targetOrEntityOrEntities === 'function' ||
|
|
InstanceChecker.isEntitySchema(targetOrEntityOrEntities) ||
|
|
typeof targetOrEntityOrEntities === 'string')
|
|
? targetOrEntityOrEntities
|
|
: undefined;
|
|
const entity = target
|
|
? entityOrEntitiesOrMaybeOptions
|
|
: targetOrEntityOrEntities;
|
|
const options = target
|
|
? maybeOptionsOrMaybePermissionOptions
|
|
: entityOrEntitiesOrMaybeOptions;
|
|
|
|
if (InstanceChecker.isEntitySchema(target)) target = target.options.name;
|
|
if (Array.isArray(entity) && entity.length === 0)
|
|
return Promise.resolve(entity);
|
|
|
|
const queryRunnerForEntityPersistExecutor =
|
|
this.connection.createQueryRunnerForEntityPersistExecutor();
|
|
|
|
return new EntityPersistExecutor(
|
|
this.connection,
|
|
queryRunnerForEntityPersistExecutor,
|
|
'recover',
|
|
target,
|
|
entity as ObjectLiteral,
|
|
options as SaveOptions,
|
|
)
|
|
.execute()
|
|
.then(() => entity as Entity)
|
|
.finally(() => queryRunnerForEntityPersistExecutor.release());
|
|
}
|
|
|
|
// Forbidden methods
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
override query<T = any>(_query: string, _parameters?: any[]): Promise<T> {
|
|
throw new PermissionsException(
|
|
'Method not allowed.',
|
|
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;
|
|
});
|
|
}
|
|
}
|