Skip to content

Commit

Permalink
refactor(core): Move typeorm operators from various sources into re…
Browse files Browse the repository at this point in the history
…positories (no-changelog) (#8174)

Follow-up to: #8165
  • Loading branch information
ivov authored Dec 28, 2023
1 parent 405e267 commit e418d42
Show file tree
Hide file tree
Showing 17 changed files with 185 additions and 209 deletions.
3 changes: 0 additions & 3 deletions packages/cli/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import type { WorkflowExecute } from 'n8n-core';

import type PCancelable from 'p-cancelable';
import type { FindOperator } from 'typeorm';

import type { ChildProcess } from 'child_process';

Expand Down Expand Up @@ -606,8 +605,6 @@ export interface IWorkflowStatisticsDataLoaded {
dataLoaded: boolean;
}

export type WhereClause = Record<string, { [key: string]: string | FindOperator<unknown> }>;

// ----------------------------------
// community nodes
// ----------------------------------
Expand Down
34 changes: 0 additions & 34 deletions packages/cli/src/UserManagement/UserManagementHelper.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import { In } from 'typeorm';
import { Container } from 'typedi';
import type { Scope } from '@n8n/permissions';

import type { WhereClause } from '@/Interfaces';
import type { User } from '@db/entities/User';
import { License } from '@/License';

export function isSharingEnabled(): boolean {
Expand All @@ -29,32 +24,3 @@ export function rightDiff<T1, T2>(
return acc;
}, []);
}

/**
* Build a `where` clause for a TypeORM entity search,
* checking for member access if the user is not an owner.
*/
export function whereClause({
user,
entityType,
globalScope,
entityId = '',
roles = [],
}: {
user: User;
entityType: 'workflow' | 'credentials';
globalScope: Scope;
entityId?: string;
roles?: string[];
}): WhereClause {
const where: WhereClause = entityId ? { [entityType]: { id: entityId } } : {};

if (!user.hasGlobalScope(globalScope)) {
where.user = { id: user.id };
if (roles?.length) {
where.role = { name: In(roles) };
}
}

return where;
}
2 changes: 1 addition & 1 deletion packages/cli/src/WorkflowHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promi

/**
* Get the IDs of the workflows that have been shared with the user.
* Returns all IDs if user has the 'workflow:read' scope (see `whereClause`)
* Returns all IDs if user has the 'workflow:read' scope.
*/
export async function getSharedWorkflowIds(user: User, roles?: RoleNames[]): Promise<string[]> {
const where: FindOptionsWhere<SharedWorkflow> = {};
Expand Down
21 changes: 8 additions & 13 deletions packages/cli/src/commands/user-management/reset.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Container } from 'typedi';
import { Not } from 'typeorm';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import { User } from '@db/entities/User';
import { CredentialsRepository } from '@db/repositories/credentials.repository';
Expand All @@ -25,20 +24,16 @@ export class Reset extends BaseCommand {
async run(): Promise<void> {
const owner = await this.getInstanceOwner();

const ownerWorkflowRole = await Container.get(RoleService).findWorkflowOwnerRole();
const ownerCredentialRole = await Container.get(RoleService).findCredentialOwnerRole();
const workflowOwnerRole = await Container.get(RoleService).findWorkflowOwnerRole();
const credentialOwnerRole = await Container.get(RoleService).findCredentialOwnerRole();

await Container.get(SharedWorkflowRepository).update(
{ userId: Not(owner.id), roleId: ownerWorkflowRole.id },
{ user: owner },
await Container.get(SharedWorkflowRepository).makeOwnerOfAllWorkflows(owner, workflowOwnerRole);
await Container.get(SharedCredentialsRepository).makeOwnerOfAllCredentials(
owner,
credentialOwnerRole,
);

await Container.get(SharedCredentialsRepository).update(
{ userId: Not(owner.id), roleId: ownerCredentialRole.id },
{ user: owner },
);

await Container.get(UserRepository).delete({ id: Not(owner.id) });
await Container.get(UserRepository).deleteAllExcept(owner);
await Container.get(UserRepository).save(Object.assign(owner, defaultUserProps));

const danglingCredentials: CredentialsEntity[] = await Container.get(CredentialsRepository)
Expand All @@ -50,7 +45,7 @@ export class Reset extends BaseCommand {
Container.get(SharedCredentialsRepository).create({
credentials,
user: owner,
role: ownerCredentialRole,
role: credentialOwnerRole,
}),
);
await Container.get(SharedCredentialsRepository).save(newSharedCredentials);
Expand Down
15 changes: 4 additions & 11 deletions packages/cli/src/controllers/debug.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { Get, RestController } from '@/decorators';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { In } from 'typeorm';
import { WebhookEntity } from '@/databases/entities/WebhookEntity';

@RestController('/debug')
export class DebugController {
Expand All @@ -17,16 +15,11 @@ export class DebugController {
async getMultiMainSetupDetails() {
const leaderKey = await this.multiMainSetup.fetchLeaderKey();

const triggersAndPollers = await this.workflowRepository.find({
select: ['id', 'name'],
where: { id: In(this.activeWorkflowRunner.allActiveInMemory()) },
});
const triggersAndPollers = await this.workflowRepository.findIn(
this.activeWorkflowRunner.allActiveInMemory(),
);

const webhooks = (await this.workflowRepository
.createQueryBuilder('workflow')
.select('DISTINCT workflow.id, workflow.name')
.innerJoin(WebhookEntity, 'webhook_entity', 'workflow.id = webhook_entity.workflowId')
.execute()) as Array<{ id: string; name: string }>;
const webhooks = await this.workflowRepository.findWebhookBasedActiveWorkflows();

const activationErrors = await this.activeWorkflowRunner.getAllWorkflowActivationErrors();

Expand Down
14 changes: 3 additions & 11 deletions packages/cli/src/controllers/workflowStatistics.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { StatisticsNames } from '@db/entities/WorkflowStatistics';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { WorkflowStatisticsRepository } from '@db/repositories/workflowStatistics.repository';
import { ExecutionRequest } from '@/requests';
import { whereClause } from '@/UserManagement/UserManagementHelper';
import type { IWorkflowStatisticsDataLoaded } from '@/Interfaces';
import { Logger } from '@/Logger';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
Expand Down Expand Up @@ -33,17 +32,10 @@ export class WorkflowStatisticsController {
async hasWorkflowAccess(req: ExecutionRequest.Get, res: Response, next: NextFunction) {
const { user } = req;
const workflowId = req.params.id;
const allowed = await this.sharedWorkflowRepository.exist({
relations: ['workflow'],
where: whereClause({
user,
globalScope: 'workflow:read',
entityType: 'workflow',
entityId: workflowId,
}),
});

if (allowed) {
const hasAccess = await this.sharedWorkflowRepository.hasAccess(workflowId, user);

if (hasAccess) {
next();
} else {
this.logger.verbose('User attempted to read a workflow without permissions', {
Expand Down
81 changes: 9 additions & 72 deletions packages/cli/src/credentials/credentials.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import type {
} from 'n8n-workflow';
import { CREDENTIAL_EMPTY_VALUE, deepCopy, NodeHelpers } from 'n8n-workflow';
import { Container } from 'typedi';
import type { FindManyOptions, FindOptionsWhere } from 'typeorm';
import { In, Like } from 'typeorm';
import type { FindOptionsWhere } from 'typeorm';

import type { Scope } from '@n8n/permissions';

Expand Down Expand Up @@ -42,97 +41,35 @@ export class CredentialsService {
});
}

private static toFindManyOptions(listQueryOptions?: ListQuery.Options) {
const findManyOptions: FindManyOptions<CredentialsEntity> = {};

type Select = Array<keyof CredentialsEntity>;

const defaultRelations = ['shared', 'shared.role', 'shared.user'];
const defaultSelect: Select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'];

if (!listQueryOptions) return { select: defaultSelect, relations: defaultRelations };

const { filter, select, take, skip } = listQueryOptions;

if (typeof filter?.name === 'string' && filter?.name !== '') {
filter.name = Like(`%${filter.name}%`);
}

if (typeof filter?.type === 'string' && filter?.type !== '') {
filter.type = Like(`%${filter.type}%`);
}

if (filter) findManyOptions.where = filter;
if (select) findManyOptions.select = select;
if (take) findManyOptions.take = take;
if (skip) findManyOptions.skip = skip;

if (take && select && !select?.id) {
findManyOptions.select = { ...findManyOptions.select, id: true }; // pagination requires id
}

if (!findManyOptions.select) {
findManyOptions.select = defaultSelect;
findManyOptions.relations = defaultRelations;
}

return findManyOptions;
}

static async getMany(
user: User,
options: { listQueryOptions?: ListQuery.Options; onlyOwn?: boolean } = {},
) {
const findManyOptions = this.toFindManyOptions(options.listQueryOptions);

const returnAll = user.hasGlobalScope('credential:list') && !options.onlyOwn;
const isDefaultSelect = !options.listQueryOptions?.select;

if (returnAll) {
const credentials = await Container.get(CredentialsRepository).find(findManyOptions);
const credentials = await Container.get(CredentialsRepository).findMany(
options.listQueryOptions,
);

return isDefaultSelect
? credentials.map((c) => Container.get(OwnershipService).addOwnedByAndSharedWith(c))
: credentials;
}

const ids = await this.getAccessibleCredentials(user.id);
const ids = await Container.get(SharedCredentialsRepository).getAccessibleCredentials(user.id);

const credentials = await Container.get(CredentialsRepository).find({
...findManyOptions,
where: { ...findManyOptions.where, id: In(ids) }, // only accessible credentials
});
const credentials = await Container.get(CredentialsRepository).findMany(
options.listQueryOptions,
ids, // only accessible credentials
);

return isDefaultSelect
? credentials.map((c) => Container.get(OwnershipService).addOwnedByAndSharedWith(c))
: credentials;
}

/**
* Get the IDs of all credentials owned by or shared with a user.
*/
private static async getAccessibleCredentials(userId: string) {
const sharings = await Container.get(SharedCredentialsRepository).find({
relations: ['role'],
where: {
userId,
role: { name: In(['owner', 'user']), scope: 'credential' },
},
});

return sharings.map((s) => s.credentialsId);
}

static async getManyByIds(ids: string[], { withSharings } = { withSharings: false }) {
const options: FindManyOptions<CredentialsEntity> = { where: { id: In(ids) } };

if (withSharings) {
options.relations = ['shared', 'shared.user', 'shared.role'];
}

return Container.get(CredentialsRepository).find(options);
}

/**
* Retrieve the sharing that matches a user and a credential.
*/
Expand Down
70 changes: 60 additions & 10 deletions packages/cli/src/databases/repositories/credentials.repository.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
import { Service } from 'typedi';
import {
DataSource,
In,
Not,
Repository,
type DeleteResult,
type EntityManager,
type FindOptionsWhere,
Like,
} from 'typeorm';
import { DataSource, In, Not, Repository, Like } from 'typeorm';
import type { FindManyOptions, DeleteResult, EntityManager, FindOptionsWhere } from 'typeorm';
import { CredentialsEntity } from '../entities/CredentialsEntity';
import { SharedCredentials } from '../entities/SharedCredentials';
import type { ListQuery } from '@/requests';

@Service()
export class CredentialsRepository extends Repository<CredentialsEntity> {
Expand All @@ -36,4 +29,61 @@ export class CredentialsRepository extends Repository<CredentialsEntity> {
where: { name: Like(`${credentialName}%`) },
});
}

async findMany(listQueryOptions?: ListQuery.Options, credentialIds?: string[]) {
const findManyOptions = this.toFindManyOptions(listQueryOptions);

if (credentialIds) {
findManyOptions.where = { ...findManyOptions.where, id: In(credentialIds) };
}

return this.find(findManyOptions);
}

private toFindManyOptions(listQueryOptions?: ListQuery.Options) {
const findManyOptions: FindManyOptions<CredentialsEntity> = {};

type Select = Array<keyof CredentialsEntity>;

const defaultRelations = ['shared', 'shared.role', 'shared.user'];
const defaultSelect: Select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'];

if (!listQueryOptions) return { select: defaultSelect, relations: defaultRelations };

const { filter, select, take, skip } = listQueryOptions;

if (typeof filter?.name === 'string' && filter?.name !== '') {
filter.name = Like(`%${filter.name}%`);
}

if (typeof filter?.type === 'string' && filter?.type !== '') {
filter.type = Like(`%${filter.type}%`);
}

if (filter) findManyOptions.where = filter;
if (select) findManyOptions.select = select;
if (take) findManyOptions.take = take;
if (skip) findManyOptions.skip = skip;

if (take && select && !select?.id) {
findManyOptions.select = { ...findManyOptions.select, id: true }; // pagination requires id
}

if (!findManyOptions.select) {
findManyOptions.select = defaultSelect;
findManyOptions.relations = defaultRelations;
}

return findManyOptions;
}

async getManyByIds(ids: string[], { withSharings } = { withSharings: false }) {
const findManyOptions: FindManyOptions<CredentialsEntity> = { where: { id: In(ids) } };

if (withSharings) {
findManyOptions.relations = ['shared', 'shared.user', 'shared.role'];
}

return this.find(findManyOptions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -187,17 +187,17 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
where?: FindOptionsWhere<ExecutionEntity>;
},
): Promise<IExecutionFlattedDb | IExecutionResponse | IExecutionBase | undefined> {
const whereClause: FindOneOptions<ExecutionEntity> = {
const findOptions: FindOneOptions<ExecutionEntity> = {
where: {
id,
...options?.where,
},
};
if (options?.includeData) {
whereClause.relations = ['executionData'];
findOptions.relations = ['executionData'];
}

const execution = await this.findOne(whereClause);
const execution = await this.findOne(findOptions);

if (!execution) {
return undefined;
Expand Down
Loading

0 comments on commit e418d42

Please sign in to comment.