refactor: Consolidate WorkflowService.getMany() (no-changelog) (#6892)

In scope:

- Consolidate `WorkflowService.getMany()`.
- Support non-entity field `ownedBy` for `select`.
- Support `tags` for `filter`.
- Move `addOwnerId` to `OwnershipService`.
- Remove unneeded check for `filter.id`.
- Simplify DTO validation for `filter` and `select`.
- Expand tests for `GET /workflows`.

Workflow list query DTOs:

```
filter → name, active, tags
select → id, name, active, tags, createdAt, updatedAt, versionId, ownedBy
```

Out of scope:

- Migrate `shared_workflow.roleId` and `shared_credential.roleId` to
string IDs.
- Refactor `WorkflowHelpers.getSharedWorkflowIds()`.
This commit is contained in:
Iván Ovejero
2023-08-22 13:19:37 +02:00
committed by GitHub
parent f32e993227
commit 2cfa6d344e
20 changed files with 686 additions and 318 deletions

View File

@@ -6,7 +6,7 @@ import * as WorkflowHelpers from '@/WorkflowHelpers';
import config from '@/config';
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { validateEntity } from '@/GenericHelpers';
import type { ListQueryRequest, WorkflowRequest } from '@/requests';
import type { ListQuery, WorkflowRequest } from '@/requests';
import { isSharingEnabled, rightDiff } from '@/UserManagement/UserManagementHelper';
import { EEWorkflowsService as EEWorkflows } from './workflows.services.ee';
import { ExternalHooks } from '@/ExternalHooks';
@@ -205,18 +205,14 @@ EEWorkflowController.post(
EEWorkflowController.get(
'/',
listQueryMiddleware,
async (req: ListQueryRequest, res: express.Response) => {
async (req: ListQuery.Request, res: express.Response) => {
try {
const [workflows, count] = await EEWorkflows.getMany(req.user, req.listQueryOptions);
const sharedWorkflowIds = await WorkflowHelpers.getSharedWorkflowIds(req.user);
let data;
if (req.listQueryOptions?.select) {
data = workflows;
} else {
const role = await Container.get(RoleService).findWorkflowOwnerRole();
data = workflows.map((w) => EEWorkflows.addOwnerId(w, role));
}
const { workflows: data, count } = await EEWorkflows.getMany(
sharedWorkflowIds,
req.listQueryOptions,
);
res.json({ count, data });
} catch (maybeError) {

View File

@@ -14,7 +14,7 @@ import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { validateEntity } from '@/GenericHelpers';
import { ExternalHooks } from '@/ExternalHooks';
import { getLogger } from '@/Logger';
import type { ListQueryRequest, WorkflowRequest } from '@/requests';
import type { ListQuery, WorkflowRequest } from '@/requests';
import { isBelowOnboardingThreshold } from '@/WorkflowHelpers';
import { EEWorkflowController } from './workflows.controller.ee';
import { WorkflowsService } from './workflows.services';
@@ -118,9 +118,14 @@ workflowsController.post(
workflowsController.get(
'/',
listQueryMiddleware,
async (req: ListQueryRequest, res: express.Response) => {
async (req: ListQuery.Request, res: express.Response) => {
try {
const [data, count] = await WorkflowsService.getMany(req.user, req.listQueryOptions);
const sharedWorkflowIds = await WorkflowHelpers.getSharedWorkflowIds(req.user, ['owner']);
const { workflows: data, count } = await WorkflowsService.getMany(
sharedWorkflowIds,
req.listQueryOptions,
);
res.json({ count, data });
} catch (maybeError) {

View File

@@ -5,7 +5,6 @@ import * as ResponseHelper from '@/ResponseHelper';
import * as WorkflowHelpers from '@/WorkflowHelpers';
import type { ICredentialsDb } from '@/Interfaces';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { UserService } from '@/user/user.service';
@@ -13,20 +12,13 @@ import { WorkflowsService } from './workflows.services';
import type {
CredentialUsedByWorkflow,
WorkflowWithSharingsAndCredentials,
WorkflowForList,
} from './workflows.types';
import { EECredentialsService as EECredentials } from '@/credentials/credentials.service.ee';
import { getSharedWorkflowIds } from '@/WorkflowHelpers';
import { NodeOperationError } from 'n8n-workflow';
import { RoleService } from '@/services/role.service';
import Container from 'typedi';
export class EEWorkflowsService extends WorkflowsService {
static async getWorkflowIdsForUser(user: User) {
// Get all workflows regardless of role
return getSharedWorkflowIds(user);
}
static async isOwned(
user: User,
workflowId: string,
@@ -88,14 +80,6 @@ export class EEWorkflowsService extends WorkflowsService {
return transaction.save(newSharedWorkflows);
}
static addOwnerId(workflow: WorkflowForList, workflowOwnerRole: Role) {
const ownerId = workflow.shared?.find(({ roleId }) => String(roleId) === workflowOwnerRole.id)
?.userId;
workflow.ownedBy = ownerId ? { id: ownerId } : null;
delete workflow.shared;
return workflow;
}
static addOwnerAndSharings(workflow: WorkflowWithSharingsAndCredentials): void {
workflow.ownedBy = null;
workflow.sharedWith = [];

View File

@@ -11,23 +11,23 @@ import * as ResponseHelper from '@/ResponseHelper';
import * as WorkflowHelpers from '@/WorkflowHelpers';
import config from '@/config';
import type { SharedWorkflow } from '@db/entities/SharedWorkflow';
import type { RoleNames } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { validateEntity } from '@/GenericHelpers';
import { ExternalHooks } from '@/ExternalHooks';
import { type WorkflowRequest, type ListQuery, hasSharing } from '@/requests';
import { TagService } from '@/services/tag.service';
import type { ListQueryOptions, WorkflowRequest } from '@/requests';
import type { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces';
import { NodeTypes } from '@/NodeTypes';
import { WorkflowRunner } from '@/WorkflowRunner';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import { TestWebhooks } from '@/TestWebhooks';
import { getSharedWorkflowIds } from '@/WorkflowHelpers';
import { isSharingEnabled, whereClause } from '@/UserManagement/UserManagementHelper';
import { whereClause } from '@/UserManagement/UserManagementHelper';
import { InternalHooks } from '@/InternalHooks';
import type { WorkflowForList } from './workflows.types';
import { WorkflowRepository } from '@/databases/repositories';
import { RoleService } from '@/services/role.service';
import { OwnershipService } from '@/services/ownership.service';
import { isStringArray } from '@/utils';
export class WorkflowsService {
static async getSharing(
@@ -94,75 +94,85 @@ export class WorkflowsService {
return Db.collections.Workflow.findOne({ where: workflow, relations: options?.relations });
}
// Warning: this function is overridden by EE to disregard role list.
static async getWorkflowIdsForUser(user: User, roles?: RoleNames[]): Promise<string[]> {
return getSharedWorkflowIds(user, roles);
}
static async getMany(sharedWorkflowIds: string[], options?: ListQuery.Options) {
if (sharedWorkflowIds.length === 0) return { workflows: [], count: 0 };
static async getMany(
user: User,
options?: ListQueryOptions,
): Promise<[WorkflowForList[], number]> {
const sharedWorkflowIds = await this.getWorkflowIdsForUser(user, ['owner']);
if (sharedWorkflowIds.length === 0) {
// return early since without shared workflows there can be no hits
// (note: getSharedWorkflowIds() returns _all_ workflow ids for global owners)
return [[], 0];
}
const filter = options?.filter ?? {};
// safeguard against querying ids not shared with the user
const workflowId = filter?.id?.toString();
if (workflowId !== undefined && !sharedWorkflowIds.includes(workflowId)) {
LoggerProxy.verbose(`User ${user.id} attempted to query non-shared workflow ${workflowId}`);
return [[], 0];
}
const DEFAULT_SELECT: FindOptionsSelect<WorkflowEntity> = {
id: true,
name: true,
active: true,
createdAt: true,
updatedAt: true,
const where: FindOptionsWhere<WorkflowEntity> = {
...options?.filter,
id: In(sharedWorkflowIds),
};
const select: FindOptionsSelect<WorkflowEntity> = options?.select ?? DEFAULT_SELECT;
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 (isDefaultSelect && !config.getEnv('workflowTagsDisabled')) {
if (areTagsEnabled && areTagsRequested) {
relations.push('tags');
select.tags = { id: true, name: true };
}
if (isDefaultSelect && isSharingEnabled()) {
relations.push('shared');
select.shared = { userId: true, roleId: true };
select.versionId = true;
}
if (isOwnedByIncluded) relations.push('shared');
filter.id = In(sharedWorkflowIds);
if (typeof filter.name === 'string' && filter.name !== '') {
filter.name = Like(`%${filter.name}%`);
if (typeof where.name === 'string' && where.name !== '') {
where.name = Like(`%${where.name}%`);
}
const findManyOptions: FindManyOptions<WorkflowEntity> = {
select,
relations,
where: filter,
order: { updatedAt: 'ASC' },
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;
}
return Container.get(WorkflowRepository).findAndCount(findManyOptions);
const [workflows, count] = (await Container.get(WorkflowRepository).findAndCount(
findManyOptions,
)) as [ListQuery.Workflow.Plain[] | ListQuery.Workflow.WithSharing[], number];
if (!hasSharing(workflows)) return { workflows, count };
const workflowOwnerRole = await Container.get(RoleService).findWorkflowOwnerRole();
return {
workflows: workflows.map((w) =>
Container.get(OwnershipService).addOwnedBy(w, workflowOwnerRole),
),
count,
};
}
static async update(

View File

@@ -9,12 +9,6 @@ export interface WorkflowWithSharingsAndCredentials extends Omit<WorkflowEntity,
shared?: SharedWorkflow[];
}
export interface WorkflowForList
extends Omit<WorkflowEntity, 'ownedBy' | 'nodes' | 'connections' | 'shared' | 'settings'> {
ownedBy?: Pick<IUser, 'id'> | null;
shared?: SharedWorkflow[];
}
export interface CredentialUsedByWorkflow {
id: string;
name: string;