Files
twenty/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts
T
Charles Bochet 9d57bc39e5 Migrate from ESLint to OxLint (#18443)
## Summary

Fully replaces ESLint with OxLint across the entire monorepo:

- **Replaced all ESLint configs** (`eslint.config.mjs`) with OxLint
configs (`.oxlintrc.json`) for every package: `twenty-front`,
`twenty-server`, `twenty-emails`, `twenty-ui`, `twenty-shared`,
`twenty-sdk`, `twenty-zapier`, `twenty-docs`, `twenty-website`,
`twenty-apps/*`, `create-twenty-app`
- **Migrated custom lint rules** from ESLint plugin format to OxLint JS
plugin system (`@oxlint/plugins`), including
`styled-components-prefixed-with-styled`, `no-hardcoded-colors`,
`sort-css-properties-alphabetically`,
`graphql-resolvers-should-be-guarded`,
`rest-api-methods-should-be-guarded`, `max-consts-per-file`, and
Jotai-related rules
- **Migrated custom rule tests** from ESLint `RuleTester` + Jest to
`oxlint/plugins-dev` `RuleTester` + Vitest
- **Removed all ESLint dependencies** from `package.json` files and
regenerated lockfiles
- **Updated Nx targets** (`lint`, `lint:diff-with-main`, `fmt`) in
`nx.json` and per-project `project.json` to use `oxlint` commands with
proper `dependsOn` for plugin builds
- **Updated CI workflows** (`.github/workflows/ci-*.yaml`) — no more
ESLint executor
- **Updated IDE setup**: replaced `dbaeumer.vscode-eslint` with
`oxc.oxc-vscode` extension, configured `source.fixAll.oxc` and
format-on-save with Prettier
- **Replaced all `eslint-disable` comments** with `oxlint-disable`
equivalents across the codebase
- **Updated docs** (`twenty-docs`) to reference OxLint instead of ESLint
- **Renamed** `twenty-eslint-rules` package to `twenty-oxlint-rules`

### Temporarily disabled rules (tracked in `OXLINT_MIGRATION_TODO.md`)

| Rule | Package | Violations | Auto-fixable |
|------|---------|-----------|-------------|
| `twenty/sort-css-properties-alphabetically` | twenty-front | 578 | Yes
|
| `typescript/consistent-type-imports` | twenty-server | 3814 | Yes |
| `twenty/max-consts-per-file` | twenty-server | 94 | No |

### Dropped plugins (no OxLint equivalent)

`eslint-plugin-project-structure`, `lingui/*`, `@stylistic/*`,
`import/order`, `prefer-arrow/prefer-arrow-functions`,
`eslint-plugin-mdx`, `@next/eslint-plugin-next`,
`eslint-plugin-storybook`, `eslint-plugin-react-refresh`. Partial
coverage for `jsx-a11y` and `unused-imports`.

### Additional fixes (pre-existing issues exposed by merge)

- Fixed `EmailThreadPreview.tsx` broken import from main rename
(`useOpenEmailThreadInSidePanel`)
- Restored truthiness guard in `getActivityTargetObjectRecords.ts`
- Fixed `AgentTurnResolver` return types to match entity (virtual
`fileMediaType`/`fileUrl` are resolved via `@ResolveField()`)

## Test plan

- [x] `npx nx lint twenty-front` passes
- [x] `npx nx lint twenty-server` passes
- [x] `npx nx lint twenty-docs` passes
- [x] Custom oxlint rules validated with Vitest: `npx nx test
twenty-oxlint-rules`
- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx typecheck twenty-server` passes
- [x] CI workflows trigger correctly with `dependsOn:
["twenty-oxlint-rules:build"]`
- [x] IDE linting works with `oxc.oxc-vscode` extension
2026-03-06 01:03:50 +01:00

1841 lines
56 KiB
TypeScript

import isEmpty from 'lodash.isempty';
import {
type ObjectsPermissions,
type ObjectsPermissionsByRoleId,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
type DeleteResult,
EntityManager,
type EntityTarget,
type FindManyOptions,
type FindOneOptions,
type FindOptionsWhere,
In,
type InsertResult,
type ObjectId,
type ObjectLiteral,
type QueryRunner,
type RemoveOptions,
type Repository,
type SaveOptions,
type SelectQueryBuilder,
TypeORMError,
type UpdateResult,
} from 'typeorm';
import { type DeepPartial } from 'typeorm/common/DeepPartial';
import { type 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 { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { PlainObjectToDatabaseEntityTransformer } from 'typeorm/query-builder/transformer/PlainObjectToDatabaseEntityTransformer';
import { type UpsertOptions } from 'typeorm/repository/UpsertOptions';
import { InstanceChecker } from 'typeorm/util/InstanceChecker';
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
PermissionsException,
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { type BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { type DeepPartialWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/deep-partial-entity-with-nested-relation-fields.type';
import { type QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type';
import { getEntityTarget } from 'src/engine/twenty-orm/entity-manager/utils/get-entity-target';
import { computeTwentyORMException } from 'src/engine/twenty-orm/error-handling/compute-twenty-orm-exception';
import { FilesFieldSync } from 'src/engine/twenty-orm/field-operations/files-field-sync/files-field-sync';
import { RelationNestedQueries } from 'src/engine/twenty-orm/field-operations/relation-nested-queries/relation-nested-queries';
import { type GlobalWorkspaceDataSource } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource';
import {
type 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 { getWorkspaceContext } from 'src/engine/twenty-orm/storage/orm-workspace-context.storage';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
import { type WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
type PermissionOptions = {
shouldBypassPermissionChecks?: boolean;
objectRecordsPermissions?: ObjectsPermissions;
};
export class WorkspaceEntityManager extends EntityManager {
// oxlint-disable-next-line @typescripttypescript/no-explicit-any
readonly repositories: Map<string, Repository<any>>;
declare connection: GlobalWorkspaceDataSource;
constructor(
connection: GlobalWorkspaceDataSource,
queryRunner?: QueryRunner,
) {
super(connection, queryRunner);
this.repositories = new Map();
}
private get eventEmitterService(): WorkspaceEventEmitter {
return this.connection.eventEmitterService;
}
get authContext(): WorkspaceAuthContext {
const context = getWorkspaceContext();
return context.authContext;
}
get internalContext(): WorkspaceInternalContext {
const context = getWorkspaceContext();
return {
workspaceId: context.authContext.workspace.id,
flatObjectMetadataMaps: context.flatObjectMetadataMaps,
flatFieldMetadataMaps: context.flatFieldMetadataMaps,
flatIndexMaps: context.flatIndexMaps,
flatRowLevelPermissionPredicateMaps:
context.flatRowLevelPermissionPredicateMaps,
flatRowLevelPermissionPredicateGroupMaps:
context.flatRowLevelPermissionPredicateGroupMaps,
objectIdByNameSingular: context.objectIdByNameSingular,
featureFlagsMap: context.featureFlagsMap,
userWorkspaceRoleMap: context.userWorkspaceRoleMap,
eventEmitterService: this.eventEmitterService,
coreDataSource: this.connection.coreDataSource,
};
}
getFeatureFlagMap(): FeatureFlagMap {
return this.connection.featureFlagMap;
}
private getPermissionsForRole(
roleId: string,
permissionsPerRoleId: ObjectsPermissionsByRoleId,
): ObjectsPermissions {
if (!isDefined(permissionsPerRoleId?.[roleId])) {
throw new PermissionsException(
`No permissions found for role in datasource (roleId: ${roleId})`,
PermissionsExceptionCode.NO_PERMISSIONS_FOUND_IN_DATASOURCE,
);
}
return permissionsPerRoleId[roleId];
}
override getRepository<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
rolePermissionConfig?: RolePermissionConfig,
authContext?: WorkspaceAuthContext,
): WorkspaceRepository<Entity> {
const dataSource = this.connection;
let objectPermissions = {};
let shouldBypassPermissionChecks = false;
const objectPermissionsByRoleId = dataSource.permissionsPerRoleId;
if (
rolePermissionConfig &&
'shouldBypassPermissionChecks' in rolePermissionConfig
) {
shouldBypassPermissionChecks =
rolePermissionConfig.shouldBypassPermissionChecks;
}
if (rolePermissionConfig && 'unionOf' in rolePermissionConfig) {
if (rolePermissionConfig.unionOf.length === 1) {
objectPermissions = this.getPermissionsForRole(
rolePermissionConfig.unionOf[0],
objectPermissionsByRoleId,
);
} else {
// TODO: Implement union logic for combining permissions across multiple roles
throw new Error(
'Union permission logic for multiple roles not yet implemented',
);
}
}
if (rolePermissionConfig && 'intersectionOf' in rolePermissionConfig) {
const allRolePermissions = rolePermissionConfig.intersectionOf.map(
(roleId: string) =>
this.getPermissionsForRole(roleId, objectPermissionsByRoleId),
);
objectPermissions = computePermissionIntersection(allRolePermissions);
}
const newRepository = new WorkspaceRepository<Entity>(
target,
this,
dataSource.featureFlagMap,
this.queryRunner,
objectPermissions,
shouldBypassPermissionChecks,
authContext,
);
return newRepository;
}
override createQueryBuilder<Entity extends ObjectLiteral>(
entityClassOrQueryRunner?: EntityTarget<Entity> | QueryRunner,
alias?: string,
queryRunner?: QueryRunner,
options: {
shouldBypassPermissionChecks?: boolean;
objectRecordsPermissions?: ObjectsPermissions;
} = {
shouldBypassPermissionChecks: false,
objectRecordsPermissions: {},
},
): 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,
this.authContext,
this.getFeatureFlagMap(),
);
}
override async insert<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
entity:
| QueryDeepPartialEntityWithNestedRelationFields<Entity>
| QueryDeepPartialEntityWithNestedRelationFields<Entity>[],
selectedColumns: string[] | '*' = '*',
permissionOptions?: PermissionOptions,
authContext?: WorkspaceAuthContext,
): Promise<InsertResult> {
const metadata = this.connection.getMetadata(target);
return this.createQueryBuilder(
target,
metadata.name,
undefined,
permissionOptions,
)
.insert()
.setWorkspaceAuthContext(authContext ?? ({} as WorkspaceAuthContext))
.values(entity)
.returning(selectedColumns)
.execute();
}
override upsert<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
entityOrEntities:
| QueryDeepPartialEntityWithNestedRelationFields<Entity>
| QueryDeepPartialEntityWithNestedRelationFields<Entity>[],
conflictPathsOrOptions: string[] | UpsertOptions<Entity>,
permissionOptions?: {
shouldBypassPermissionChecks?: boolean;
objectRecordsPermissions?: ObjectsPermissions;
},
selectedColumns: string[] | '*' = '*',
): Promise<InsertResult> {
const metadata = this.connection.getMetadata(target);
let options;
if (Array.isArray(conflictPathsOrOptions)) {
options = {
conflictPaths: conflictPathsOrOptions,
};
} else {
options = conflictPathsOrOptions;
}
let entities: QueryDeepPartialEntityWithNestedRelationFields<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',
),
);
const overwrites = [...conflictColumns, ...overwriteColumns].map(
(col) => col.databaseName,
);
const conflictTargets = conflictColumns.map((col) => col.databaseName);
const upsertOptions = {
skipUpdateIfNoValuesChanged: options.skipUpdateIfNoValuesChanged,
indexPredicate: options.indexPredicate,
upsertType:
options.upsertType || this.connection.driver.supportedUpsertTypes[0],
};
const queryBuilder = this.createQueryBuilder(
undefined,
undefined,
undefined,
permissionOptions,
)
.insert()
.into(target)
.values(entities)
.orUpdate(overwrites, conflictTargets, upsertOptions)
.returning(selectedColumns);
return queryBuilder.execute();
}
override update<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
criteria:
| string
| string[]
| number
| number[]
| Date
| Date[]
| ObjectId
| ObjectId[]
| unknown,
partialEntity: QueryDeepPartialEntity<Entity>,
permissionOptions?: PermissionOptions,
selectedColumns: string[] | '*' = '*',
): Promise<UpdateResult> {
const metadata = this.connection.getMetadata(target);
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(
target,
metadata.name,
undefined,
permissionOptions,
)
.update()
.set(partialEntity)
.whereInIds(criteria)
.returning(selectedColumns)
.execute();
} else {
return this.createQueryBuilder(
target,
metadata.name,
undefined,
permissionOptions,
)
.update()
.set(partialEntity)
.where(criteria)
.returning(selectedColumns)
.execute();
}
}
public updateMany<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
inputs: {
criteria: string;
partialEntity: QueryDeepPartialEntity<Entity>;
}[],
permissionOptions?: PermissionOptions,
selectedColumns: string[] | '*' = '*',
): Promise<UpdateResult> {
const metadata = this.connection.getMetadata(target);
return this.createQueryBuilder(
target,
metadata.name,
undefined,
permissionOptions,
)
.update()
.setManyInputs(inputs)
.returning(selectedColumns ?? [])
.execute();
}
override increment<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
criteria: object,
propertyPath: string,
value: number | string,
permissionOptions?: PermissionOptions,
selectedColumns: string[] | '*' = '*',
): 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 } }
// oxlint-disable-next-line @typescripttypescript/no-explicit-any
const values = propertyPath.split('.').reduceRight<any>(
(value, key) => ({ [key]: value }),
() => this.connection.driver.escape(column.databaseName) + ' + ' + value,
);
return this.update(
target,
criteria,
values,
permissionOptions,
selectedColumns,
);
}
validatePermissions<Entity extends ObjectLiteral>({
target,
operationType,
permissionOptions,
selectedColumns,
updatedColumns = [],
}: {
target: EntityTarget<Entity> | Entity;
operationType: OperationType;
permissionOptions?: {
shouldBypassPermissionChecks?: boolean;
objectRecordsPermissions?: ObjectsPermissions;
};
selectedColumns: string[];
updatedColumns?: string[];
}): void {
if (permissionOptions?.shouldBypassPermissionChecks === true) {
return;
}
const entityName =
typeof target === 'function' || typeof target === 'string'
? this.extractTargetNameSingularFromEntityTarget(target)
: this.extractTargetNameSingularFromEntity(target);
validateOperationIsPermittedOrThrow({
entityName,
operationType,
objectsPermissions: permissionOptions?.objectRecordsPermissions ?? {},
flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
objectIdByNameSingular: this.internalContext.objectIdByNameSingular,
selectedColumns,
allFieldsSelected: false,
updatedColumns,
});
}
private extractTargetNameSingularFromEntityTarget(
target: EntityTarget<ObjectLiteral>,
): string {
return this.connection.getMetadata(target).name;
}
// oxlint-disable-next-line @typescripttypescript/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,
selectedColumns: string[] | '*' = '*',
): 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)
.returning(selectedColumns)
.execute();
} else {
return this.createQueryBuilder(
undefined,
undefined,
undefined,
permissionOptions,
)
.delete()
.from(targetOrEntity)
.where(criteria)
.returning(selectedColumns)
.execute();
}
}
override softDelete<Entity extends ObjectLiteral>(
targetOrEntity: EntityTarget<Entity>,
criteria: unknown,
permissionOptions?: PermissionOptions,
selectedColumns: string[] | '*' = '*',
): 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)
.returning(selectedColumns)
.execute();
} else {
return this.createQueryBuilder(
undefined,
undefined,
this.queryRunner,
permissionOptions,
)
.softDelete()
.from(targetOrEntity)
.where(criteria)
.returning(selectedColumns)
.execute();
}
}
override restore<Entity extends ObjectLiteral>(
targetOrEntity: EntityTarget<Entity>,
criteria: unknown,
permissionOptions?: PermissionOptions,
selectedColumns: string[] | '*' = '*',
): 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)
.returning(selectedColumns)
.execute();
} else {
return this.createQueryBuilder(
undefined,
undefined,
this.queryRunner,
permissionOptions,
)
.restore()
.from(targetOrEntity)
.where(criteria)
.returning(selectedColumns)
.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('id')
.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('id')
.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<Entity extends ObjectLiteral>(
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({
target: entityClass,
operationType: 'delete',
permissionOptions,
selectedColumns: [], // TODO
});
return super.clear(entityClass);
}
override async preload<Entity extends ObjectLiteral>(
entityClass: EntityTarget<Entity>,
entityLike: DeepPartial<Entity>,
permissionOptions?: PermissionOptions,
): Promise<Entity | undefined> {
const objectMetadataItem = getObjectMetadataFromEntityTarget(
entityClass,
this.internalContext,
);
const formattedEntityLike = formatData(
entityLike,
objectMetadataItem,
this.internalContext.flatFieldMetadataMaps,
);
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(
formattedEntityLike,
metadata,
);
if (transformedEntity)
return this.merge(
entityClass,
transformedEntity,
formattedEntityLike,
) as Entity;
return undefined;
}
override decrement<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
criteria: object,
propertyPath: string,
value: number | string,
permissionOptions?: PermissionOptions,
selectedColumns: string[] | '*' = '*',
): 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.`);
// oxlint-disable-next-line @typescripttypescript/no-explicit-any
const values = propertyPath.split('.').reduceRight<any>(
(value, key) => ({ [key]: value }),
() => this.connection.driver.escape(column.databaseName) + ' - ' + value,
);
return this.update(
target,
criteria,
values,
permissionOptions,
selectedColumns,
);
}
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 DeepPartialWithNestedRelationFields<Entity>>(
targetOrEntity: EntityTarget<Entity>,
entities: T[],
options: SaveOptions & {
reload: false;
},
permissionOptions?: PermissionOptions,
): Promise<T[]>;
override save<Entity, T extends DeepPartialWithNestedRelationFields<Entity>>(
targetOrEntity: EntityTarget<Entity>,
entities: T[],
options?: SaveOptions,
permissionOptions?: PermissionOptions,
): Promise<(T & Entity)[]>;
override save<Entity, T extends DeepPartialWithNestedRelationFields<Entity>>(
targetOrEntity: EntityTarget<Entity>,
entity: T,
options: SaveOptions & {
reload: false;
},
permissionOptions?: PermissionOptions,
): Promise<T>;
override save<Entity, T extends DeepPartialWithNestedRelationFields<Entity>>(
targetOrEntity: EntityTarget<Entity>,
entity: T,
options?: SaveOptions,
permissionOptions?: PermissionOptions,
): Promise<T>;
override async save<
Entity extends ObjectLiteral,
T extends DeepPartialWithNestedRelationFields<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[]> {
try {
const permissionOptionsFromArgs =
maybeOptionsOrMaybePermissionOptions &&
('shouldBypassPermissionChecks' in
maybeOptionsOrMaybePermissionOptions ||
'objectRecordsPermissions' in maybeOptionsOrMaybePermissionOptions)
? maybeOptionsOrMaybePermissionOptions
: permissionOptions;
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();
const isEntityArray = Array.isArray(entity);
const entityTarget =
target ?? (isEntityArray ? entity[0]?.constructor : entity.constructor);
const entityArray = isEntityArray ? entity : [entity];
const relationNestedQueries = new RelationNestedQueries(
this.internalContext,
);
const relationNestedConfig =
relationNestedQueries.prepareNestedRelationQueries(
entityArray,
entityTarget,
);
const entityWithConnectedRelations = isDefined(relationNestedConfig)
? await relationNestedQueries.processRelationNestedQueries({
entities: entityArray,
relationNestedConfig,
queryBuilder: this.createQueryBuilder(
undefined,
undefined,
undefined,
permissionOptions,
),
})
: entityArray;
const entityIds = entityArray
.map((entity) => (entity as { id: string }).id)
.filter(isDefined);
const beforeUpdate = await this.find(
entityTarget,
{
where: { id: In(entityIds) },
},
{ shouldBypassPermissionChecks: true }, // Bypass as this is for event emission
);
const beforeUpdateMapById = beforeUpdate.reduce(
(acc, e: ObjectLiteral) => {
acc[e.id] = e;
return acc;
},
{} as Record<string, ObjectLiteral>,
);
const filesFieldSync = new FilesFieldSync(this.internalContext);
let filesFieldDiffByEntityIndex = null;
let filesFieldFileIds = null;
filesFieldDiffByEntityIndex =
filesFieldSync.computeFilesFieldDiffBeforeUpsert(
entityWithConnectedRelations,
entityTarget,
beforeUpdateMapById,
);
if (isDefined(filesFieldDiffByEntityIndex)) {
const result = await filesFieldSync.enrichFilesFields({
entities: entityWithConnectedRelations,
filesFieldDiffByEntityIndex,
workspaceId: this.internalContext.workspaceId,
target: entityTarget,
});
filesFieldFileIds = result.fileIds;
entityWithConnectedRelations.splice(
0,
entityWithConnectedRelations.length,
...result.entities,
);
}
const objectMetadataItem = getObjectMetadataFromEntityTarget(
entityTarget,
this.internalContext,
);
const formattedEntityOrEntities = formatData(
entityWithConnectedRelations,
objectMetadataItem,
this.internalContext.flatFieldMetadataMaps,
);
const updatedColumns = formattedEntityOrEntities
.map((e) => Object.keys(e))
.flat();
this.validatePermissions({
target: targetOrEntity,
operationType: 'update',
permissionOptions: permissionOptionsFromArgs,
selectedColumns: [],
updatedColumns,
});
const result = await new EntityPersistExecutor(
this.connection,
queryRunnerForEntityPersistExecutor,
'save',
target,
formattedEntityOrEntities as ObjectLiteral[],
options as SaveOptions | (SaveOptions & { reload: false }),
)
.execute()
.then(() => formattedEntityOrEntities as Entity[])
.finally(() => queryRunnerForEntityPersistExecutor.release());
if (isDefined(filesFieldFileIds)) {
await filesFieldSync.updateFileEntityRecords(filesFieldFileIds);
}
const resultArray = Array.isArray(result) ? result : [result];
let formattedResult = formatResult<Entity[]>(
resultArray,
objectMetadataItem,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
const updatedEntities = formattedResult.filter(
(entity) => beforeUpdateMapById[entity.id],
);
const createdEntities = formattedResult.filter(
(entity) => !beforeUpdateMapById[entity.id],
);
this.internalContext.eventEmitterService.emitDatabaseBatchEvent(
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.UPDATED,
objectMetadataItem,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
recordsAfter: updatedEntities,
recordsBefore: updatedEntities.map(
(entity) => beforeUpdateMapById[entity.id],
),
}),
);
this.internalContext.eventEmitterService.emitDatabaseBatchEvent(
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.CREATED,
objectMetadataItem,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
recordsAfter: createdEntities,
}),
);
const permissionCheckApplies =
permissionOptionsFromArgs?.shouldBypassPermissionChecks !== true &&
objectMetadataItem.isSystem !== true;
if (permissionCheckApplies) {
formattedResult = this.getFormattedResultWithoutNonReadableFields({
formattedResult,
objectMetadataItem,
permissionOptionsFromArgs,
});
}
return isEntityArray ? formattedResult : formattedResult[0];
} catch (error) {
const objectMetadataItem = getObjectMetadataFromEntityTarget(
getEntityTarget(targetOrEntity, entityOrMaybeOptions),
this.internalContext,
);
throw await computeTwentyORMException(
error,
objectMetadataItem,
this,
this.internalContext,
);
}
}
private getFormattedResultWithoutNonReadableFields<
Entity extends ObjectLiteral,
>({
formattedResult,
objectMetadataItem,
permissionOptionsFromArgs,
}: {
formattedResult: Entity[];
objectMetadataItem: FlatObjectMetadata;
permissionOptionsFromArgs: PermissionOptions | undefined;
}): Entity[] {
if (permissionOptionsFromArgs?.shouldBypassPermissionChecks === true) {
return formattedResult;
}
const restrictedFields =
permissionOptionsFromArgs?.objectRecordsPermissions?.[
objectMetadataItem.id
].restrictedFields;
if (!restrictedFields) {
throw new InternalServerError('Restricted fields not found');
}
if (isEmpty(restrictedFields)) {
return formattedResult;
}
const restrictedFieldNames = new Set(
Object.entries(restrictedFields)
.filter(([_, fieldPermissions]) => fieldPermissions.canRead === false)
.map(([fieldMetadataId]) => {
const fieldMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: fieldMetadataId,
flatEntityMaps: this.internalContext.flatFieldMetadataMaps,
});
if (!isDefined(fieldMetadata)) {
throw new InternalServerError(
`Field metadata not found for field ${fieldMetadataId}`,
);
}
return fieldMetadata.name;
}),
);
const filteredResult = formattedResult.map((individualFormattedResult) => {
return Object.fromEntries(
Object.entries(individualFormattedResult).filter(
([key]) => !restrictedFieldNames.has(key),
),
);
});
return filteredResult as 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 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({
target: targetOrEntity,
operationType: 'delete',
permissionOptions: permissionOptionsFromArgs,
selectedColumns: [], // TODO
});
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;
const isEntityArray = Array.isArray(entity);
if (isEntityArray && entity.length === 0) return Promise.resolve(entity);
const queryRunnerForEntityPersistExecutor =
this.connection.createQueryRunnerForEntityPersistExecutor();
const entityTarget =
target ?? (isEntityArray ? entity[0]?.constructor : entity.constructor);
const objectMetadataItem = getObjectMetadataFromEntityTarget(
entityTarget,
this.internalContext,
);
const formattedEntity = formatData(
entity,
objectMetadataItem,
this.internalContext.flatFieldMetadataMaps,
);
const result = new EntityPersistExecutor(
this.connection,
queryRunnerForEntityPersistExecutor,
'remove',
target as string | undefined,
formattedEntity as ObjectLiteral,
options as RemoveOptions,
)
.execute()
.then(() => formattedEntity as Entity | Entity[])
.finally(() => queryRunnerForEntityPersistExecutor.release());
const formattedResult = formatResult<Entity[]>(
result,
objectMetadataItem,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
const recordsBefore = Array.isArray(formattedResult)
? formattedResult
: [formattedResult];
this.internalContext.eventEmitterService.emitDatabaseBatchEvent(
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.DESTROYED,
objectMetadataItem,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
recordsBefore,
}),
);
return isEntityArray ? formattedResult : formattedResult[0];
}
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({
target: targetOrEntityOrEntities,
operationType: 'soft-delete',
permissionOptions: permissionOptionsFromArgs,
selectedColumns: [], // TODO
});
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();
const isEntityArray = Array.isArray(entity);
const entityTarget =
target ?? (isEntityArray ? entity[0]?.constructor : entity.constructor);
const entityArray = isEntityArray ? entity : [entity];
const entityIds = entityArray
.map((entity) => (entity as { id: string }).id)
.filter(isDefined);
const recordsBeforeFindResult = await this.find<BaseWorkspaceEntity>(
entityTarget,
{
where: { id: In(entityIds) },
},
{ shouldBypassPermissionChecks: true }, // Bypass as this is for event emission
);
const beforeUpdateMapById = recordsBeforeFindResult.reduce(
(acc, e: BaseWorkspaceEntity) => {
acc[e.id] = e;
return acc;
},
{} as Record<string, BaseWorkspaceEntity>,
);
const objectMetadataItem = getObjectMetadataFromEntityTarget(
entityTarget,
this.internalContext,
);
const formattedEntity = formatData(
entity,
objectMetadataItem,
this.internalContext.flatFieldMetadataMaps,
);
const result = new EntityPersistExecutor(
this.connection,
queryRunnerForEntityPersistExecutor,
'soft-remove',
target,
formattedEntity as ObjectLiteral,
options as SaveOptions,
)
.execute()
.then(() => formattedEntity as Entity)
.finally(() => queryRunnerForEntityPersistExecutor.release());
const formattedResult = formatResult<Entity[]>(
result,
objectMetadataItem,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
const recordsAfter = Array.isArray(formattedResult)
? formattedResult
: [formattedResult];
const recordsBefore = recordsAfter.map<Entity>(
(record) => beforeUpdateMapById[record.id] as unknown as Entity,
);
this.internalContext.eventEmitterService.emitDatabaseBatchEvent(
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.DELETED,
objectMetadataItem,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
recordsAfter,
recordsBefore,
}),
);
return isEntityArray ? formattedResult : formattedResult[0];
}
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({
target: targetOrEntityOrEntities,
operationType: 'restore',
permissionOptions: permissionOptionsFromArgs,
selectedColumns: [], // TODO
});
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;
const isEntityArray = Array.isArray(entity);
if (isEntityArray && entity.length === 0) return Promise.resolve(entity);
const queryRunnerForEntityPersistExecutor =
this.connection.createQueryRunnerForEntityPersistExecutor();
const entityTarget =
target ?? (isEntityArray ? entity[0]?.constructor : entity.constructor);
const entityArray = isEntityArray ? entity : [entity];
const entityIds = entityArray
.map((entity) => (entity as { id: string }).id)
.filter(isDefined);
const recordsBeforeFindResult = await this.find<BaseWorkspaceEntity>(
entityTarget,
{
where: { id: In(entityIds) },
},
{ shouldBypassPermissionChecks: true }, // Bypass as this is for event emission
);
const beforeUpdateMapById = recordsBeforeFindResult.reduce(
(acc, e: BaseWorkspaceEntity) => {
acc[e.id] = e;
return acc;
},
{} as Record<string, BaseWorkspaceEntity>,
);
const objectMetadataItem = getObjectMetadataFromEntityTarget(
entityTarget,
this.internalContext,
);
const formattedEntity = formatData(
entity,
objectMetadataItem,
this.internalContext.flatFieldMetadataMaps,
);
const result = new EntityPersistExecutor(
this.connection,
queryRunnerForEntityPersistExecutor,
'recover',
target,
formattedEntity as ObjectLiteral,
options as SaveOptions,
)
.execute()
.then(() => formattedEntity as Entity)
.finally(() => queryRunnerForEntityPersistExecutor.release());
const formattedResult = formatResult<Entity[]>(
result,
objectMetadataItem,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
const recordsAfter = Array.isArray(formattedResult)
? formattedResult
: [formattedResult];
const recordsBefore = recordsAfter.map<Entity>(
(record) => beforeUpdateMapById[record.id] as unknown as Entity,
);
this.internalContext.eventEmitterService.emitDatabaseBatchEvent(
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.RESTORED,
objectMetadataItem,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
recordsAfter,
recordsBefore,
}),
);
return isEntityArray ? formattedResult : formattedResult[0];
}
// Forbidden methods
// oxlint-disable-next-line @typescripttypescript/no-explicit-any
override query<T = any>(_query: string, _parameters?: any[]): Promise<T> {
throw new PermissionsException(
'Method not allowed.',
PermissionsExceptionCode.RAW_SQL_NOT_ALLOWED,
);
}
}