refactor(core): Move some typeorm operators to repositories (no-changelog) (#8139)

Moving some persistence logic to repositories to reduce circular
dependencies.
This commit is contained in:
Iván Ovejero
2023-12-22 13:35:23 +01:00
committed by GitHub
parent ab74bade05
commit c6dd935895
6 changed files with 133 additions and 118 deletions

View File

@@ -1,5 +1,5 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import { DataSource, Repository } from 'typeorm'; import { DataSource, In, Repository } from 'typeorm';
import { TagEntity } from '../entities/TagEntity'; import { TagEntity } from '../entities/TagEntity';
@Service() @Service()
@@ -7,4 +7,11 @@ export class TagRepository extends Repository<TagEntity> {
constructor(dataSource: DataSource) { constructor(dataSource: DataSource) {
super(TagEntity, dataSource.manager); super(TagEntity, dataSource.manager);
} }
async findMany(tagIds: string[]) {
return this.find({
select: ['id', 'name'],
where: { id: In(tagIds) },
});
}
} }

View File

@@ -1,7 +1,22 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import { DataSource, Repository, type UpdateResult, type FindOptionsWhere } from 'typeorm'; import {
DataSource,
Repository,
In,
Like,
type UpdateResult,
type FindOptionsWhere,
type FindOptionsSelect,
type FindManyOptions,
type EntityManager,
type DeleteResult,
Not,
} from 'typeorm';
import type { ListQuery } from '@/requests';
import { isStringArray } from '@/utils';
import config from '@/config'; import config from '@/config';
import { WorkflowEntity } from '../entities/WorkflowEntity'; import { WorkflowEntity } from '../entities/WorkflowEntity';
import { SharedWorkflow } from '../entities/SharedWorkflow';
@Service() @Service()
export class WorkflowRepository extends Repository<WorkflowEntity> { export class WorkflowRepository extends Repository<WorkflowEntity> {
@@ -45,6 +60,29 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
return totalTriggerCount ?? 0; return totalTriggerCount ?? 0;
} }
async getSharings(
transaction: EntityManager,
workflowId: string,
relations = ['shared'],
): Promise<SharedWorkflow[]> {
const workflow = await transaction.findOne(WorkflowEntity, {
where: { id: workflowId },
relations,
});
return workflow?.shared ?? [];
}
async pruneSharings(
transaction: EntityManager,
workflowId: string,
userIds: string[],
): Promise<DeleteResult> {
return transaction.delete(SharedWorkflow, {
workflowId,
userId: Not(In(userIds)),
});
}
async updateWorkflowTriggerCount(id: string, triggerCount: number): Promise<UpdateResult> { async updateWorkflowTriggerCount(id: string, triggerCount: number): Promise<UpdateResult> {
const qb = this.createQueryBuilder('workflow'); const qb = this.createQueryBuilder('workflow');
return qb return qb
@@ -61,4 +99,77 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
.where('id = :id', { id }) .where('id = :id', { id })
.execute(); .execute();
} }
async getMany(sharedWorkflowIds: string[], options?: ListQuery.Options) {
if (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 };
const where: FindOptionsWhere<WorkflowEntity> = {
...options?.filter,
id: In(sharedWorkflowIds),
};
const reqTags = options?.filter?.tags;
if (isStringArray(reqTags)) {
where.tags = reqTags.map((tag) => ({ name: tag }));
}
type Select = FindOptionsSelect<WorkflowEntity> & { ownedBy?: true };
const select: Select = options?.select
? { ...options.select } // copy to enable field removal without affecting original
: {
name: true,
active: true,
createdAt: true,
updatedAt: true,
versionId: true,
shared: { userId: true, roleId: true },
};
delete select?.ownedBy; // remove non-entity field, handled after query
const relations: string[] = [];
const areTagsEnabled = !config.getEnv('workflowTagsDisabled');
const isDefaultSelect = options?.select === undefined;
const areTagsRequested = isDefaultSelect || options?.select?.tags === true;
const isOwnedByIncluded = isDefaultSelect || options?.select?.ownedBy === true;
if (areTagsEnabled && areTagsRequested) {
relations.push('tags');
select.tags = { id: true, name: true };
}
if (isOwnedByIncluded) relations.push('shared', 'shared.role', 'shared.user');
if (typeof where.name === 'string' && where.name !== '') {
where.name = Like(`%${where.name}%`);
}
const findManyOptions: FindManyOptions<WorkflowEntity> = {
select: { ...select, id: true },
where,
};
if (isDefaultSelect || options?.select?.updatedAt === true) {
findManyOptions.order = { updatedAt: 'ASC' };
}
if (relations.length > 0) {
findManyOptions.relations = relations;
}
if (options?.take) {
findManyOptions.skip = options.skip;
findManyOptions.take = options.take;
}
const [workflows, count] = (await this.findAndCount(findManyOptions)) as [
ListQuery.Workflow.Plain[] | ListQuery.Workflow.WithSharing[],
number,
];
return { workflows, count };
}
} }

View File

@@ -1,9 +1,8 @@
import type { DeleteResult, EntityManager } from 'typeorm'; import type { EntityManager } from 'typeorm';
import { In, Not } from 'typeorm';
import * as WorkflowHelpers from '@/WorkflowHelpers'; import * as WorkflowHelpers from '@/WorkflowHelpers';
import { SharedWorkflow } from '@db/entities/SharedWorkflow'; import type { SharedWorkflow } from '@db/entities/SharedWorkflow';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { UserService } from '@/services/user.service'; import { UserService } from '@/services/user.service';
import { WorkflowService } from './workflow.service'; import { WorkflowService } from './workflow.service';
import type { import type {
@@ -48,29 +47,6 @@ export class EnterpriseWorkflowService {
return { ownsWorkflow: true, workflow }; return { ownsWorkflow: true, workflow };
} }
async getSharings(
transaction: EntityManager,
workflowId: string,
relations = ['shared'],
): Promise<SharedWorkflow[]> {
const workflow = await transaction.findOne(WorkflowEntity, {
where: { id: workflowId },
relations,
});
return workflow?.shared ?? [];
}
async pruneSharings(
transaction: EntityManager,
workflowId: string,
userIds: string[],
): Promise<DeleteResult> {
return transaction.delete(SharedWorkflow, {
workflowId,
userId: Not(In(userIds)),
});
}
async share( async share(
transaction: EntityManager, transaction: EntityManager,
workflow: WorkflowEntity, workflow: WorkflowEntity,

View File

@@ -1,8 +1,7 @@
import Container, { Service } from 'typedi'; import Container, { Service } from 'typedi';
import type { INode, IPinData } from 'n8n-workflow'; import type { INode, IPinData } from 'n8n-workflow';
import { NodeApiError, Workflow } from 'n8n-workflow'; import { NodeApiError, Workflow } from 'n8n-workflow';
import type { FindManyOptions, FindOptionsSelect, FindOptionsWhere } from 'typeorm'; import type { FindOptionsWhere } from 'typeorm';
import { In, Like } from 'typeorm';
import pick from 'lodash/pick'; import pick from 'lodash/pick';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
@@ -14,7 +13,7 @@ import type { User } from '@db/entities/User';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { validateEntity } from '@/GenericHelpers'; import { validateEntity } from '@/GenericHelpers';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import { type WorkflowRequest, type ListQuery, hasSharing } from '@/requests'; import { type WorkflowRequest, hasSharing, type ListQuery } from '@/requests';
import { TagService } from '@/services/tag.service'; import { TagService } from '@/services/tag.service';
import type { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces'; import type { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
@@ -25,7 +24,6 @@ import { whereClause } from '@/UserManagement/UserManagementHelper';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { OwnershipService } from '@/services/ownership.service'; import { OwnershipService } from '@/services/ownership.service';
import { isStringArray } from '@/utils';
import { WorkflowHistoryService } from './workflowHistory/workflowHistory.service.ee'; import { WorkflowHistoryService } from './workflowHistory/workflowHistory.service.ee';
import { BinaryDataService } from 'n8n-core'; import { BinaryDataService } from 'n8n-core';
import type { Scope } from '@n8n/permissions'; import type { Scope } from '@n8n/permissions';
@@ -121,74 +119,7 @@ export class WorkflowService {
} }
async getMany(sharedWorkflowIds: string[], options?: ListQuery.Options) { async getMany(sharedWorkflowIds: string[], options?: ListQuery.Options) {
if (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 }; const { workflows, count } = await this.workflowRepository.getMany(sharedWorkflowIds, options);
const where: FindOptionsWhere<WorkflowEntity> = {
...options?.filter,
id: In(sharedWorkflowIds),
};
const reqTags = options?.filter?.tags;
if (isStringArray(reqTags)) {
where.tags = reqTags.map((tag) => ({ name: tag }));
}
type Select = FindOptionsSelect<WorkflowEntity> & { ownedBy?: true };
const select: Select = options?.select
? { ...options.select } // copy to enable field removal without affecting original
: {
name: true,
active: true,
createdAt: true,
updatedAt: true,
versionId: true,
shared: { userId: true, roleId: true },
};
delete select?.ownedBy; // remove non-entity field, handled after query
const relations: string[] = [];
const areTagsEnabled = !config.getEnv('workflowTagsDisabled');
const isDefaultSelect = options?.select === undefined;
const areTagsRequested = isDefaultSelect || options?.select?.tags === true;
const isOwnedByIncluded = isDefaultSelect || options?.select?.ownedBy === true;
if (areTagsEnabled && areTagsRequested) {
relations.push('tags');
select.tags = { id: true, name: true };
}
if (isOwnedByIncluded) relations.push('shared', 'shared.role', 'shared.user');
if (typeof where.name === 'string' && where.name !== '') {
where.name = Like(`%${where.name}%`);
}
const findManyOptions: FindManyOptions<WorkflowEntity> = {
select: { ...select, id: true },
where,
};
if (isDefaultSelect || options?.select?.updatedAt === true) {
findManyOptions.order = { updatedAt: 'ASC' };
}
if (relations.length > 0) {
findManyOptions.relations = relations;
}
if (options?.take) {
findManyOptions.skip = options.skip;
findManyOptions.take = options.take;
}
const [workflows, count] = (await this.workflowRepository.findAndCount(findManyOptions)) as [
ListQuery.Workflow.Plain[] | ListQuery.Workflow.WithSharing[],
number,
];
return hasSharing(workflows) return hasSharing(workflows)
? { ? {

View File

@@ -14,7 +14,6 @@ import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { CredentialsService } from '../credentials/credentials.service'; import { CredentialsService } from '../credentials/credentials.service';
import type { IExecutionPushResponse } from '@/Interfaces'; import type { IExecutionPushResponse } from '@/Interfaces';
import * as GenericHelpers from '@/GenericHelpers'; import * as GenericHelpers from '@/GenericHelpers';
import { In } from 'typeorm';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { RoleService } from '@/services/role.service'; import { RoleService } from '@/services/role.service';
@@ -29,6 +28,7 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { WorkflowService } from './workflow.service'; import { WorkflowService } from './workflow.service';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { TagRepository } from '@/databases/repositories/tag.repository';
export const EEWorkflowController = express.Router(); export const EEWorkflowController = express.Router();
@@ -81,7 +81,7 @@ EEWorkflowController.put(
} }
const ownerIds = ( const ownerIds = (
await Container.get(EnterpriseWorkflowService).getSharings( await Container.get(WorkflowRepository).getSharings(
Db.getConnection().createEntityManager(), Db.getConnection().createEntityManager(),
workflowId, workflowId,
['shared', 'shared.role'], ['shared', 'shared.role'],
@@ -93,12 +93,12 @@ EEWorkflowController.put(
let newShareeIds: string[] = []; let newShareeIds: string[] = [];
await Db.transaction(async (trx) => { await Db.transaction(async (trx) => {
// remove all sharings that are not supposed to exist anymore // remove all sharings that are not supposed to exist anymore
await Container.get(EnterpriseWorkflowService).pruneSharings(trx, workflowId, [ await Container.get(WorkflowRepository).pruneSharings(trx, workflowId, [
...ownerIds, ...ownerIds,
...shareWithIds, ...shareWithIds,
]); ]);
const sharings = await Container.get(EnterpriseWorkflowService).getSharings(trx, workflowId); const sharings = await Container.get(WorkflowRepository).getSharings(trx, workflowId);
// extract the new sharings that need to be added // extract the new sharings that need to be added
newShareeIds = rightDiff( newShareeIds = rightDiff(
@@ -169,12 +169,7 @@ EEWorkflowController.post(
const { tags: tagIds } = req.body; const { tags: tagIds } = req.body;
if (tagIds?.length && !config.getEnv('workflowTagsDisabled')) { if (tagIds?.length && !config.getEnv('workflowTagsDisabled')) {
newWorkflow.tags = await Container.get(TagService).findMany({ newWorkflow.tags = await Container.get(TagRepository).findMany(tagIds);
select: ['id', 'name'],
where: {
id: In(tagIds),
},
});
} }
await WorkflowHelpers.replaceInvalidCredentials(newWorkflow); await WorkflowHelpers.replaceInvalidCredentials(newWorkflow);

View File

@@ -17,7 +17,6 @@ import { isBelowOnboardingThreshold } from '@/WorkflowHelpers';
import { EEWorkflowController } from './workflows.controller.ee'; import { EEWorkflowController } from './workflows.controller.ee';
import { WorkflowService } from './workflow.service'; import { WorkflowService } from './workflow.service';
import { whereClause } from '@/UserManagement/UserManagementHelper'; import { whereClause } from '@/UserManagement/UserManagementHelper';
import { In } from 'typeorm';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { RoleService } from '@/services/role.service'; import { RoleService } from '@/services/role.service';
@@ -31,6 +30,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { NamingService } from '@/services/naming.service'; import { NamingService } from '@/services/naming.service';
import { TagRepository } from '@/databases/repositories/tag.repository';
export const workflowsController = express.Router(); export const workflowsController = express.Router();
workflowsController.use('/', EEWorkflowController); workflowsController.use('/', EEWorkflowController);
@@ -56,12 +56,7 @@ workflowsController.post(
const { tags: tagIds } = req.body; const { tags: tagIds } = req.body;
if (tagIds?.length && !config.getEnv('workflowTagsDisabled')) { if (tagIds?.length && !config.getEnv('workflowTagsDisabled')) {
newWorkflow.tags = await Container.get(TagService).findMany({ newWorkflow.tags = await Container.get(TagRepository).findMany(tagIds);
select: ['id', 'name'],
where: {
id: In(tagIds),
},
});
} }
await WorkflowHelpers.replaceInvalidCredentials(newWorkflow); await WorkflowHelpers.replaceInvalidCredentials(newWorkflow);