refactor(core): Continue moving typeorm operators to repositories (no-changelog) (#8186)

Follow-up to: #8163
This commit is contained in:
Iván Ovejero 2024-01-02 17:53:24 +01:00 committed by GitHub
parent 0ca2759d75
commit 40c1eeeddd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 341 additions and 354 deletions

View file

@ -18,16 +18,16 @@ import {
setWorkflowAsActive,
setWorkflowAsInactive,
updateWorkflow,
getSharedWorkflows,
createWorkflow,
getWorkflowIdsViaTags,
parseTagNames,
getWorkflowsAndCount,
} from './workflows.service';
import { WorkflowService } from '@/workflows/workflow.service';
import { InternalHooks } from '@/InternalHooks';
import { RoleService } from '@/services/role.service';
import { WorkflowHistoryService } from '@/workflows/workflowHistory/workflowHistory.service.ee';
import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflow.repository';
import { TagRepository } from '@/databases/repositories/tag.repository';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
export = {
createWorkflow: [
@ -106,17 +106,24 @@ export = {
if (['owner', 'admin'].includes(req.user.globalRole.name)) {
if (tags) {
const workflowIds = await getWorkflowIdsViaTags(parseTagNames(tags));
const workflowIds = await Container.get(TagRepository).getWorkflowIdsViaTags(
parseTagNames(tags),
);
where.id = In(workflowIds);
}
} else {
const options: { workflowIds?: string[] } = {};
if (tags) {
options.workflowIds = await getWorkflowIdsViaTags(parseTagNames(tags));
options.workflowIds = await Container.get(TagRepository).getWorkflowIdsViaTags(
parseTagNames(tags),
);
}
const sharedWorkflows = await getSharedWorkflows(req.user, options);
const sharedWorkflows = await Container.get(SharedWorkflowRepository).getSharedWorkflows(
req.user,
options,
);
if (!sharedWorkflows.length) {
return res.status(200).json({
@ -129,7 +136,7 @@ export = {
where.id = In(workflowsIds);
}
const [workflows, count] = await getWorkflowsAndCount({
const [workflows, count] = await Container.get(WorkflowRepository).findAndCount({
skip: offset,
take: limit,
where,

View file

@ -1,14 +1,9 @@
import type { FindManyOptions, UpdateResult } from 'typeorm';
import { In } from 'typeorm';
import intersection from 'lodash/intersection';
import * as Db from '@/Db';
import type { User } from '@db/entities/User';
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import type { Role } from '@db/entities/Role';
import config from '@/config';
import { TagService } from '@/services/tag.service';
import Container from 'typedi';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
@ -39,43 +34,12 @@ export async function getSharedWorkflow(
});
}
export async function getSharedWorkflows(
user: User,
options: {
relations?: string[];
workflowIds?: string[];
},
): Promise<SharedWorkflow[]> {
return Container.get(SharedWorkflowRepository).find({
where: {
...(!['owner', 'admin'].includes(user.globalRole.name) && { userId: user.id }),
...(options.workflowIds && { workflowId: In(options.workflowIds) }),
},
...(options.relations && { relations: options.relations }),
});
}
export async function getWorkflowById(id: string): Promise<WorkflowEntity | null> {
return Container.get(WorkflowRepository).findOne({
where: { id },
});
}
/**
* Returns the workflow IDs that have certain tags.
* Intersection! e.g. workflow needs to have all provided tags.
*/
export async function getWorkflowIdsViaTags(tags: string[]): Promise<string[]> {
const dbTags = await Container.get(TagService).findMany({
where: { name: In(tags) },
relations: ['workflows'],
});
const workflowIdsPerTag = dbTags.map((tag) => tag.workflows.map((workflow) => workflow.id));
return intersection(...workflowIdsPerTag);
}
export async function createWorkflow(
workflow: WorkflowEntity,
user: User,
@ -98,14 +62,14 @@ export async function createWorkflow(
});
}
export async function setWorkflowAsActive(workflow: WorkflowEntity): Promise<UpdateResult> {
return Container.get(WorkflowRepository).update(workflow.id, {
export async function setWorkflowAsActive(workflow: WorkflowEntity) {
await Container.get(WorkflowRepository).update(workflow.id, {
active: true,
updatedAt: new Date(),
});
}
export async function setWorkflowAsInactive(workflow: WorkflowEntity): Promise<UpdateResult> {
export async function setWorkflowAsInactive(workflow: WorkflowEntity) {
return Container.get(WorkflowRepository).update(workflow.id, {
active: false,
updatedAt: new Date(),
@ -116,16 +80,7 @@ export async function deleteWorkflow(workflow: WorkflowEntity): Promise<Workflow
return Container.get(WorkflowRepository).remove(workflow);
}
export async function getWorkflowsAndCount(
options: FindManyOptions<WorkflowEntity>,
): Promise<[WorkflowEntity[], number]> {
return Container.get(WorkflowRepository).findAndCount(options);
}
export async function updateWorkflow(
workflowId: string,
updateData: WorkflowEntity,
): Promise<UpdateResult> {
export async function updateWorkflow(workflowId: string, updateData: WorkflowEntity) {
return Container.get(WorkflowRepository).update(workflowId, updateData);
}

View file

@ -1,9 +1,6 @@
import type { INode, Workflow } from 'n8n-workflow';
import { NodeOperationError, WorkflowOperationError } from 'n8n-workflow';
import type { FindOptionsWhere } from 'typeorm';
import { In } from 'typeorm';
import config from '@/config';
import type { SharedCredentials } from '@db/entities/SharedCredentials';
import { isSharingEnabled } from './UserManagementHelper';
import { OwnershipService } from '@/services/ownership.service';
import Container from 'typedi';
@ -48,17 +45,12 @@ export class PermissionChecker {
workflowUserIds = workflowSharings.map((s) => s.userId);
}
const credentialsWhere: FindOptionsWhere<SharedCredentials> = { userId: In(workflowUserIds) };
const roleId = await Container.get(RoleService).findCredentialOwnerRoleId();
if (!isSharingEnabled()) {
const role = await Container.get(RoleService).findCredentialOwnerRole();
// If credential sharing is not enabled, get only credentials owned by this user
credentialsWhere.roleId = role.id;
}
const credentialSharings = await Container.get(SharedCredentialsRepository).find({
where: credentialsWhere,
});
const credentialSharings = await Container.get(SharedCredentialsRepository).findSharings(
workflowUserIds,
roleId,
);
const accessibleCredIds = credentialSharings.map((s) => s.credentialsId);

View file

@ -10,6 +10,7 @@ import type { IActiveWorkflowUsersChanged } from '../Interfaces';
import type { OnPushMessageEvent } from '@/push/types';
import { CollaborationState } from '@/collaboration/collaboration.state';
import { TIME } from '@/constants';
import { UserRepository } from '@/databases/repositories/user.repository';
/**
* After how many minutes of inactivity a user should be removed
@ -28,6 +29,7 @@ export class CollaborationService {
private readonly push: Push,
private readonly state: CollaborationState,
private readonly userService: UserService,
private readonly userRepository: UserRepository,
) {
if (!push.isBidirectional) {
logger.warn(
@ -89,7 +91,10 @@ export class CollaborationService {
if (workflowUserIds.length === 0) {
return;
}
const users = await this.userService.getByIds(this.userService.getManager(), workflowUserIds);
const users = await this.userRepository.getByIds(
this.userService.getManager(),
workflowUserIds,
);
const msgData: IActiveWorkflowUsersChanged = {
workflowId,

View file

@ -1,7 +1,6 @@
import { flags } from '@oclif/command';
import fs from 'fs';
import path from 'path';
import type { FindOptionsWhere } from 'typeorm';
import { Credentials } from 'n8n-core';
import type { ICredentialsDb, ICredentialsDecryptedDb } from '@/Interfaces';
import { BaseCommand } from '../BaseCommand';
@ -107,13 +106,9 @@ export class ExportCredentialsCommand extends BaseCommand {
}
}
const findQuery: FindOptionsWhere<ICredentialsDb> = {};
if (flags.id) {
findQuery.id = flags.id;
}
const credentials: ICredentialsDb[] =
await Container.get(CredentialsRepository).findBy(findQuery);
const credentials: ICredentialsDb[] = await Container.get(CredentialsRepository).findBy(
flags.id ? { id: flags.id } : {},
);
if (flags.decrypted) {
for (let i = 0; i < credentials.length; i++) {

View file

@ -1,8 +1,6 @@
import { flags } from '@oclif/command';
import fs from 'fs';
import path from 'path';
import type { FindOptionsWhere } from 'typeorm';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { BaseCommand } from '../BaseCommand';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import Container from 'typedi';
@ -101,13 +99,8 @@ export class ExportWorkflowsCommand extends BaseCommand {
}
}
const findQuery: FindOptionsWhere<WorkflowEntity> = {};
if (flags.id) {
findQuery.id = flags.id;
}
const workflows = await Container.get(WorkflowRepository).find({
where: findQuery,
where: flags.id ? { id: flags.id } : {},
relations: ['tags'],
});

View file

@ -1,6 +1,5 @@
import Container from 'typedi';
import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants';
import { In } from 'typeorm';
import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository';
import { AuthProviderSyncHistoryRepository } from '@db/repositories/authProviderSyncHistory.repository';
import { SettingsRepository } from '@db/repositories/settings.repository';
@ -17,7 +16,7 @@ export class Reset extends BaseCommand {
});
await Container.get(AuthProviderSyncHistoryRepository).delete({ providerType: 'ldap' });
await Container.get(AuthIdentityRepository).delete({ providerType: 'ldap' });
await Container.get(UserRepository).delete({ id: In(ldapIdentities.map((i) => i.userId)) });
await Container.get(UserRepository).deleteMany(ldapIdentities.map((i) => i.userId));
await Container.get(SettingsRepository).delete({ key: LDAP_FEATURE_NAME });
await Container.get(SettingsRepository).insert({
key: LDAP_FEATURE_NAME,

View file

@ -1,6 +1,4 @@
import { flags } from '@oclif/command';
import type { FindOptionsWhere } from 'typeorm';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { BaseCommand } from '../BaseCommand';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import Container from 'typedi';
@ -32,12 +30,13 @@ export class ListWorkflowCommand extends BaseCommand {
this.error('The --active flag has to be passed using true or false');
}
const findQuery: FindOptionsWhere<WorkflowEntity> = {};
if (flags.active !== undefined) {
findQuery.active = flags.active === 'true';
}
const workflowRepository = Container.get(WorkflowRepository);
const workflows =
flags.active !== undefined
? await workflowRepository.findByActiveState(flags.active === 'true')
: await workflowRepository.find();
const workflows = await Container.get(WorkflowRepository).findBy(findQuery);
if (flags.onlyId) {
workflows.forEach((workflow) => this.logger.info(workflow.id));
} else {

View file

@ -1,7 +1,4 @@
import { flags } from '@oclif/command';
import type { FindOptionsWhere } from 'typeorm';
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { BaseCommand } from '../BaseCommand';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import Container from 'typedi';
@ -43,7 +40,6 @@ export class UpdateWorkflowCommand extends BaseCommand {
return;
}
const updateQuery: QueryDeepPartialEntity<WorkflowEntity> = {};
if (flags.active === undefined) {
console.info('No update flag like "--active=true" has been set!');
return;
@ -54,18 +50,16 @@ export class UpdateWorkflowCommand extends BaseCommand {
return;
}
updateQuery.active = flags.active === 'true';
const newState = flags.active === 'true';
const findQuery: FindOptionsWhere<WorkflowEntity> = {};
if (flags.id) {
this.logger.info(`Deactivating workflow with ID: ${flags.id}`);
findQuery.id = flags.id;
await Container.get(WorkflowRepository).updateActiveState(flags.id, newState);
} else {
this.logger.info('Deactivating all workflows');
findQuery.active = true;
await Container.get(WorkflowRepository).deactivateAll();
}
await Container.get(WorkflowRepository).update(findQuery, updateQuery);
this.logger.info('Done');
}

View file

@ -138,7 +138,7 @@ export class AuthController {
}
try {
user = await this.userService.findOneOrFail({ where: {} });
user = await this.userRepository.findOneOrFail({ where: {}, relations: ['globalRole'] });
} catch (error) {
throw new InternalServerError(
'No users found in database - did you wipe the users table? Create at least one user.',

View file

@ -163,7 +163,7 @@ export class InvitationController {
invitee.lastName = lastName;
invitee.password = await this.passwordUtility.hash(validPassword);
const updatedUser = await this.userService.save(invitee);
const updatedUser = await this.userRepository.save(invitee);
await issueCookie(res, updatedUser);

View file

@ -20,6 +20,7 @@ import { Logger } from '@/Logger';
import { ExternalHooks } from '@/ExternalHooks';
import { InternalHooks } from '@/InternalHooks';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { UserRepository } from '@/databases/repositories/user.repository';
@Authorized()
@RestController('/me')
@ -30,6 +31,7 @@ export class MeController {
private readonly internalHooks: InternalHooks,
private readonly userService: UserService,
private readonly passwordUtility: PasswordUtility,
private readonly userRepository: UserRepository,
) {}
/**
@ -76,7 +78,10 @@ export class MeController {
await this.externalHooks.run('user.profile.beforeUpdate', [userId, currentEmail, payload]);
await this.userService.update(userId, payload);
const user = await this.userService.findOneOrFail({ where: { id: userId } });
const user = await this.userRepository.findOneOrFail({
where: { id: userId },
relations: ['globalRole'],
});
this.logger.info('User updated successfully', { userId });
@ -132,7 +137,7 @@ export class MeController {
req.user.password = await this.passwordUtility.hash(validPassword);
const user = await this.userService.save(req.user);
const user = await this.userRepository.save(req.user);
this.logger.info('Password updated successfully', { userId: user.id });
await issueCookie(res, user);
@ -164,7 +169,7 @@ export class MeController {
throw new BadRequestError('Personalization answers are mandatory');
}
await this.userService.save({
await this.userRepository.save({
id: req.user.id,
// @ts-ignore
personalizationAnswers,
@ -227,9 +232,10 @@ export class MeController {
await this.userService.updateSettings(id, payload);
const user = await this.userService.findOneOrFail({
const user = await this.userRepository.findOneOrFail({
select: ['settings'],
where: { id },
relations: ['globalRole'],
});
return user.settings;

View file

@ -13,6 +13,7 @@ import { UserService } from '@/services/user.service';
import { Logger } from '@/Logger';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { InternalHooks } from '@/InternalHooks';
import { UserRepository } from '@/databases/repositories/user.repository';
@Authorized(['global', 'owner'])
@RestController('/owner')
@ -24,6 +25,7 @@ export class OwnerController {
private readonly userService: UserService,
private readonly passwordUtility: PasswordUtility,
private readonly postHog: PostHogClient,
private readonly userRepository: UserRepository,
) {}
/**
@ -85,7 +87,7 @@ export class OwnerController {
await validateEntity(owner);
owner = await this.userService.save(owner);
owner = await this.userRepository.save(owner);
this.logger.info('Owner was set up successfully', { userId });

View file

@ -1,6 +1,5 @@
import { Response } from 'express';
import { rateLimit } from 'express-rate-limit';
import { IsNull, Not } from 'typeorm';
import validator from 'validator';
import { Get, Post, RestController } from '@/decorators';
@ -23,6 +22,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { UnprocessableRequestError } from '@/errors/response-errors/unprocessable.error';
import { UserRepository } from '@/databases/repositories/user.repository';
const throttle = rateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
@ -42,6 +42,7 @@ export class PasswordResetController {
private readonly urlService: UrlService,
private readonly license: License,
private readonly passwordUtility: PasswordUtility,
private readonly userRepository: UserRepository,
) {}
/**
@ -78,13 +79,7 @@ export class PasswordResetController {
}
// User should just be able to reset password if one is already present
const user = await this.userService.findOne({
where: {
email,
password: Not(IsNull()),
},
relations: ['authIdentities', 'globalRole'],
});
const user = await this.userRepository.findNonShellUser(email);
if (!user?.isOwner && !this.license.isWithinUsersLimit()) {
this.logger.debug(

View file

@ -1,5 +1,4 @@
import type { FindManyOptions } from 'typeorm';
import { In, Not } from 'typeorm';
import { In } from 'typeorm';
import { User } from '@db/entities/User';
import { SharedCredentials } from '@db/entities/SharedCredentials';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
@ -21,6 +20,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { License } from '@/License';
import { ExternalHooks } from '@/ExternalHooks';
import { InternalHooks } from '@/InternalHooks';
import { UserRepository } from '@/databases/repositories/user.repository';
@Authorized()
@RestController('/users')
@ -35,6 +35,7 @@ export class UsersController {
private readonly roleService: RoleService,
private readonly userService: UserService,
private readonly license: License,
private readonly userRepository: UserRepository,
) {}
static ERROR_MESSAGES = {
@ -49,44 +50,6 @@ export class UsersController {
},
} as const;
private async toFindManyOptions(listQueryOptions?: ListQuery.Options) {
const findManyOptions: FindManyOptions<User> = {};
if (!listQueryOptions) {
findManyOptions.relations = ['globalRole', 'authIdentities'];
return findManyOptions;
}
const { filter, select, take, skip } = listQueryOptions;
if (select) findManyOptions.select = select;
if (take) findManyOptions.take = take;
if (skip) findManyOptions.skip = skip;
if (take && !select) {
findManyOptions.relations = ['globalRole', 'authIdentities'];
}
if (take && select && !select?.id) {
findManyOptions.select = { ...findManyOptions.select, id: true }; // pagination requires id
}
if (filter) {
const { isOwner, ...otherFilters } = filter;
findManyOptions.where = otherFilters;
if (isOwner !== undefined) {
const ownerRole = await this.roleService.findGlobalOwnerRole();
findManyOptions.relations = ['globalRole'];
findManyOptions.where.globalRole = { id: isOwner ? ownerRole.id : Not(ownerRole.id) };
}
}
return findManyOptions;
}
private removeSupplementaryFields(
publicUsers: Array<Partial<PublicUser>>,
listQueryOptions: ListQuery.Options,
@ -122,9 +85,14 @@ export class UsersController {
async listUsers(req: ListQuery.Request) {
const { listQueryOptions } = req;
const findManyOptions = await this.toFindManyOptions(listQueryOptions);
const globalOwner = await this.roleService.findGlobalOwnerRole();
const users = await this.userService.findMany(findManyOptions);
const findManyOptions = await this.userRepository.toFindManyOptions(
listQueryOptions,
globalOwner.id,
);
const users = await this.userRepository.find(findManyOptions);
const publicUsers: Array<Partial<PublicUser>> = await Promise.all(
users.map(async (u) =>
@ -140,8 +108,9 @@ export class UsersController {
@Get('/:id/password-reset-link')
@RequireGlobalScope('user:resetPassword')
async getUserPasswordResetLink(req: UserRequest.PasswordResetLink) {
const user = await this.userService.findOneOrFail({
const user = await this.userRepository.findOneOrFail({
where: { id: req.params.id },
relations: ['globalRole'],
});
if (!user) {
throw new NotFoundError('User not found');
@ -160,9 +129,10 @@ export class UsersController {
await this.userService.updateSettings(id, payload);
const user = await this.userService.findOneOrFail({
const user = await this.userRepository.findOneOrFail({
select: ['settings'],
where: { id },
relations: ['globalRole'],
});
return user.settings;
@ -192,10 +162,9 @@ export class UsersController {
);
}
const users = await this.userService.findMany({
where: { id: In([transferId, idToDelete]) },
relations: ['globalRole'],
});
const userIds = transferId ? [transferId, idToDelete] : [idToDelete];
const users = await this.userRepository.findManybyIds(userIds);
if (!users.length || (transferId && users.length !== 2)) {
throw new NotFoundError(
@ -354,8 +323,9 @@ export class UsersController {
throw new UnauthorizedError(NO_USER_TO_OWNER);
}
const targetUser = await this.userService.findOne({
const targetUser = await this.userRepository.findOne({
where: { id: req.params.id },
relations: ['globalRole'],
});
if (targetUser === null) {

View file

@ -2,11 +2,11 @@ import type { EntityManager, FindOptionsWhere } from 'typeorm';
import { CredentialsEntity } from '@db/entities/CredentialsEntity';
import type { SharedCredentials } from '@db/entities/SharedCredentials';
import type { User } from '@db/entities/User';
import { UserService } from '@/services/user.service';
import { CredentialsService, type CredentialsGetSharedOptions } from './credentials.service';
import { RoleService } from '@/services/role.service';
import Container from 'typedi';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import { UserRepository } from '@/databases/repositories/user.repository';
export class EECredentialsService extends CredentialsService {
static async isOwned(
@ -66,7 +66,7 @@ export class EECredentialsService extends CredentialsService {
credential: CredentialsEntity,
shareWithIds: string[],
): Promise<SharedCredentials[]> {
const users = await Container.get(UserService).getByIds(transaction, shareWithIds);
const users = await Container.get(UserRepository).getByIds(transaction, shareWithIds);
const role = await Container.get(RoleService).findCredentialUserRole();
const newSharedCredentials = users

View file

@ -1,4 +1,5 @@
import { Service } from 'typedi';
import type { FindOptionsWhere } from 'typeorm';
import { DataSource, In, Not, Repository } from 'typeorm';
import { SharedCredentials } from '../entities/SharedCredentials';
import type { User } from '../entities/User';
@ -50,4 +51,13 @@ export class SharedCredentialsRepository extends Repository<SharedCredentials> {
return sharings.map((s) => s.credentialsId);
}
async findSharings(userIds: string[], roleId?: string) {
const where: FindOptionsWhere<SharedCredentials> = { userId: In(userIds) };
// If credential sharing is not enabled, get only credentials owned by this user
if (roleId) where.roleId = roleId;
return this.find({ where });
}
}

View file

@ -1,9 +1,11 @@
import { Service } from 'typedi';
import { DataSource, type FindOptionsWhere, Repository, In, Not } from 'typeorm';
import { DataSource, Repository, In, Not } from 'typeorm';
import type { EntityManager, FindOptionsWhere } from 'typeorm';
import { SharedWorkflow } from '../entities/SharedWorkflow';
import { type User } from '../entities/User';
import type { Scope } from '@n8n/permissions';
import type { Role } from '../entities/Role';
import type { WorkflowEntity } from '../entities/WorkflowEntity';
@Service()
export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
@ -72,4 +74,55 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
async makeOwnerOfAllWorkflows(user: User, role: Role) {
return this.update({ userId: Not(user.id), roleId: role.id }, { user });
}
async getSharing(
user: User,
workflowId: string,
options: { allowGlobalScope: true; globalScope: Scope } | { allowGlobalScope: false },
relations: string[] = ['workflow'],
): Promise<SharedWorkflow | null> {
const where: FindOptionsWhere<SharedWorkflow> = { workflowId };
// Omit user from where if the requesting user has relevant
// global workflow permissions. This allows the user to
// access workflows they don't own.
if (!options.allowGlobalScope || !user.hasGlobalScope(options.globalScope)) {
where.userId = user.id;
}
return this.findOne({ where, relations });
}
async getSharedWorkflows(
user: User,
options: {
relations?: string[];
workflowIds?: string[];
},
): Promise<SharedWorkflow[]> {
return this.find({
where: {
...(!['owner', 'admin'].includes(user.globalRole.name) && { userId: user.id }),
...(options.workflowIds && { workflowId: In(options.workflowIds) }),
},
...(options.relations && { relations: options.relations }),
});
}
async share(transaction: EntityManager, workflow: WorkflowEntity, users: User[], roleId: string) {
const newSharedWorkflows = users.reduce<SharedWorkflow[]>((acc, user) => {
if (user.isPending) {
return acc;
}
const entity: Partial<SharedWorkflow> = {
workflowId: workflow.id,
userId: user.id,
roleId,
};
acc.push(this.create(entity));
return acc;
}, []);
return transaction.save(newSharedWorkflows);
}
}

View file

@ -1,6 +1,9 @@
import { Service } from 'typedi';
import type { EntityManager } from 'typeorm';
import { DataSource, In, Repository } from 'typeorm';
import { TagEntity } from '../entities/TagEntity';
import type { WorkflowEntity } from '../entities/WorkflowEntity';
import intersection from 'lodash/intersection';
@Service()
export class TagRepository extends Repository<TagEntity> {
@ -14,4 +17,57 @@ export class TagRepository extends Repository<TagEntity> {
where: { id: In(tagIds) },
});
}
/**
* Set tags on workflow to import while ensuring all tags exist in the database,
* either by matching incoming to existing tags or by creating them first.
*/
async setTags(tx: EntityManager, dbTags: TagEntity[], workflow: WorkflowEntity) {
if (!workflow?.tags?.length) return;
for (let i = 0; i < workflow.tags.length; i++) {
const importTag = workflow.tags[i];
if (!importTag.name) continue;
const identicalMatch = dbTags.find(
(dbTag) =>
dbTag.id === importTag.id &&
dbTag.createdAt &&
importTag.createdAt &&
dbTag.createdAt.getTime() === new Date(importTag.createdAt).getTime(),
);
if (identicalMatch) {
workflow.tags[i] = identicalMatch;
continue;
}
const nameMatch = dbTags.find((dbTag) => dbTag.name === importTag.name);
if (nameMatch) {
workflow.tags[i] = nameMatch;
continue;
}
const tagEntity = this.create(importTag);
workflow.tags[i] = await tx.save<TagEntity>(tagEntity);
}
}
/**
* Returns the workflow IDs that have certain tags.
* Intersection! e.g. workflow needs to have all provided tags.
*/
async getWorkflowIdsViaTags(tags: string[]): Promise<string[]> {
const dbTags = await this.find({
where: { name: In(tags) },
relations: ['workflows'],
});
const workflowIdsPerTag = dbTags.map((tag) => tag.workflows.map((workflow) => workflow.id));
return intersection(...workflowIdsPerTag);
}
}

View file

@ -1,6 +1,8 @@
import { Service } from 'typedi';
import { DataSource, In, Not, Repository } from 'typeorm';
import type { EntityManager, FindManyOptions } from 'typeorm';
import { DataSource, In, IsNull, Not, Repository } from 'typeorm';
import { User } from '../entities/User';
import type { ListQuery } from '@/requests';
@Service()
export class UserRepository extends Repository<User> {
@ -18,4 +20,68 @@ export class UserRepository extends Repository<User> {
async deleteAllExcept(user: User) {
await this.delete({ id: Not(user.id) });
}
async getByIds(transaction: EntityManager, ids: string[]) {
return transaction.find(User, { where: { id: In(ids) } });
}
async findManyByEmail(emails: string[]) {
return this.find({
where: { email: In(emails) },
relations: ['globalRole'],
select: ['email', 'password', 'id'],
});
}
async deleteMany(userIds: string[]) {
return this.delete({ id: In(userIds) });
}
async findNonShellUser(email: string) {
return this.findOne({
where: {
email,
password: Not(IsNull()),
},
relations: ['authIdentities', 'globalRole'],
});
}
async toFindManyOptions(listQueryOptions?: ListQuery.Options, globalOwnerRoleId?: string) {
const findManyOptions: FindManyOptions<User> = {};
if (!listQueryOptions) {
findManyOptions.relations = ['globalRole', 'authIdentities'];
return findManyOptions;
}
const { filter, select, take, skip } = listQueryOptions;
if (select) findManyOptions.select = select;
if (take) findManyOptions.take = take;
if (skip) findManyOptions.skip = skip;
if (take && !select) {
findManyOptions.relations = ['globalRole', 'authIdentities'];
}
if (take && select && !select?.id) {
findManyOptions.select = { ...findManyOptions.select, id: true }; // pagination requires id
}
if (filter) {
const { isOwner, ...otherFilters } = filter;
findManyOptions.where = otherFilters;
if (isOwner !== undefined && globalOwnerRoleId) {
findManyOptions.relations = ['globalRole'];
findManyOptions.where.globalRole = {
id: isOwner ? globalOwnerRoleId : Not(globalOwnerRoleId),
};
}
}
return findManyOptions;
}
}

View file

@ -198,4 +198,16 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
.innerJoin(WebhookEntity, 'webhook_entity', 'workflow.id = webhook_entity.workflowId')
.execute() as Promise<Array<{ id: string; name: string }>>;
}
async updateActiveState(workflowId: string, newState: boolean) {
return this.update({ id: workflowId }, { active: newState });
}
async deactivateAll() {
return this.update({ active: true }, { active: false });
}
async findByActiveState(activeState: boolean) {
return this.findBy({ active: activeState });
}
}

View file

@ -5,7 +5,6 @@ import { generateNanoId } from '@db/utils/generators';
import { canCreateNewVariable } from './environmentHelpers';
import { CacheService } from '@/services/cache.service';
import { VariablesRepository } from '@db/repositories/variables.repository';
import type { DeepPartial } from 'typeorm';
import { VariableCountLimitReachedError } from '@/errors/variable-count-limit-reached.error';
import { VariableValidationError } from '@/errors/variable-validation.error';
@ -23,9 +22,7 @@ export class VariablesService {
return Container.get(VariablesService).findAll();
},
});
return (variables as Array<DeepPartial<Variables>>).map((v) =>
this.variablesRepository.create(v),
);
return (variables as Array<Partial<Variables>>).map((v) => this.variablesRepository.create(v));
}
async getCount(): Promise<number> {
@ -38,7 +35,7 @@ export class VariablesService {
if (!foundVariable) {
return null;
}
return this.variablesRepository.create(foundVariable as DeepPartial<Variables>);
return this.variablesRepository.create(foundVariable as Partial<Variables>);
}
async delete(id: string): Promise<void> {

View file

@ -1,6 +1,5 @@
import { v4 as uuid } from 'uuid';
import { Container } from 'typedi';
import type { DeleteResult, InsertResult } from 'typeorm';
import type { INodeCredentials, MessageEventBusDestinationOptions } from 'n8n-workflow';
import { MessageEventBusDestinationTypeNames } from 'n8n-workflow';
import { Logger } from '@/Logger';
@ -92,7 +91,7 @@ export abstract class MessageEventBusDestination implements MessageEventBusDesti
id: this.getId(),
destination: this.serialize(),
};
const dbResult: InsertResult = await Container.get(EventDestinationsRepository).upsert(data, {
const dbResult = await Container.get(EventDestinationsRepository).upsert(data, {
skipUpdateIfNoValuesChanged: true,
conflictPaths: ['id'],
});
@ -103,7 +102,7 @@ export abstract class MessageEventBusDestination implements MessageEventBusDesti
return MessageEventBusDestination.deleteFromDb(this.getId());
}
static async deleteFromDb(id: string): Promise<DeleteResult> {
static async deleteFromDb(id: string) {
const dbResult = await Container.get(EventDestinationsRepository).delete({ id });
return dbResult;
}

View file

@ -16,7 +16,6 @@ import type {
import { MessageEventBusDestinationTypeNames } from 'n8n-workflow';
import { RestController, Get, Post, Delete, Authorized, RequireGlobalScope } from '@/decorators';
import type { MessageEventBusDestination } from './MessageEventBusDestination/MessageEventBusDestination.ee';
import type { DeleteResult } from 'typeorm';
import { AuthenticatedRequest } from '@/requests';
import { logStreamingLicensedMiddleware } from './middleware/logStreamingEnabled.middleware.ee';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@ -123,7 +122,7 @@ export class EventBusControllerEE {
@Delete('/destination', { middlewares: [logStreamingLicensedMiddleware] })
@RequireGlobalScope('eventBusDestination:delete')
async deleteDestination(req: AuthenticatedRequest): Promise<DeleteResult | undefined> {
async deleteDestination(req: AuthenticatedRequest) {
if (isWithIdString(req.query)) {
return eventBus.removeDestination(req.query.id);
} else {

View file

@ -1,7 +1,6 @@
import { Service } from 'typedi';
import { v4 as uuid } from 'uuid';
import { type INode, type INodeCredentialsDetails } from 'n8n-workflow';
import type { EntityManager } from 'typeorm';
import { Logger } from '@/Logger';
import * as Db from '@/Db';
@ -72,7 +71,7 @@ export class ImportService {
if (!workflow.tags?.length) continue;
await this.setTags(tx, workflow);
await this.tagRepository.setTags(tx, this.dbTags, workflow);
for (const tag of workflow.tags) {
await tx.upsert(WorkflowTagMapping, { tagId: tag.id, workflowId }, [
@ -112,42 +111,4 @@ export class ImportService {
node.credentials[type] = nodeCredential;
}
}
/**
* Set tags on workflow to import while ensuring all tags exist in the database,
* either by matching incoming to existing tags or by creating them first.
*/
private async setTags(tx: EntityManager, workflow: WorkflowEntity) {
if (!workflow?.tags?.length) return;
for (let i = 0; i < workflow.tags.length; i++) {
const importTag = workflow.tags[i];
if (!importTag.name) continue;
const identicalMatch = this.dbTags.find(
(dbTag) =>
dbTag.id === importTag.id &&
dbTag.createdAt &&
importTag.createdAt &&
dbTag.createdAt.getTime() === new Date(importTag.createdAt).getTime(),
);
if (identicalMatch) {
workflow.tags[i] = identicalMatch;
continue;
}
const nameMatch = this.dbTags.find((dbTag) => dbTag.name === importTag.name);
if (nameMatch) {
workflow.tags[i] = nameMatch;
continue;
}
const tagEntity = this.tagRepository.create(importTag);
workflow.tags[i] = await tx.save<TagEntity>(tagEntity);
}
}
}

View file

@ -4,6 +4,7 @@ import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.reposi
import { CacheService } from './cache.service';
import type { RoleNames, RoleScopes } from '@db/entities/Role';
import { InvalidRoleError } from '@/errors/invalid-role.error';
import { isSharingEnabled } from '@/UserManagement/UserManagementHelper';
@Service()
export class RoleService {
@ -100,4 +101,8 @@ export class RoleService {
})
.then((shared) => shared?.role);
}
async findCredentialOwnerRoleId() {
return isSharingEnabled() ? undefined : (await this.findCredentialOwnerRole()).id;
}
}

View file

@ -3,8 +3,6 @@ import { Service } from 'typedi';
import { validateEntity } from '@/GenericHelpers';
import type { ITagWithCountDb } from '@/Interfaces';
import type { TagEntity } from '@db/entities/TagEntity';
import type { FindManyOptions, FindOneOptions } from 'typeorm';
import type { UpsertOptions } from 'typeorm/repository/UpsertOptions';
import { ExternalHooks } from '@/ExternalHooks';
type GetAllResult<T> = T extends { withUsageCount: true } ? ITagWithCountDb[] : TagEntity[];
@ -46,18 +44,6 @@ export class TagService {
return deleteResult;
}
async findOne(options: FindOneOptions<TagEntity>) {
return this.tagRepository.findOne(options);
}
async findMany(options: FindManyOptions<TagEntity>) {
return this.tagRepository.find(options);
}
async upsert(tag: TagEntity, options: UpsertOptions<TagEntity>) {
return this.tagRepository.upsert(tag, options);
}
async getAll<T extends { withUsageCount: boolean }>(options?: T): Promise<GetAllResult<T>> {
if (options?.withUsageCount) {
const allTags = await this.tagRepository.find({

View file

@ -1,6 +1,4 @@
import { Container, Service } from 'typedi';
import type { EntityManager, FindManyOptions, FindOneOptions, FindOptionsWhere } from 'typeorm';
import { In } from 'typeorm';
import { User } from '@db/entities/User';
import type { IUserSettings } from 'n8n-workflow';
import { UserRepository } from '@db/repositories/user.repository';
@ -29,38 +27,10 @@ export class UserService {
private readonly urlService: UrlService,
) {}
async findOne(options: FindOneOptions<User>) {
return this.userRepository.findOne({ relations: ['globalRole'], ...options });
}
async findOneOrFail(options: FindOneOptions<User>) {
return this.userRepository.findOneOrFail({ relations: ['globalRole'], ...options });
}
async findMany(options: FindManyOptions<User>) {
return this.userRepository.find(options);
}
async findOneBy(options: FindOptionsWhere<User>) {
return this.userRepository.findOneBy(options);
}
create(data: Partial<User>) {
return this.userRepository.create(data);
}
async save(user: Partial<User>) {
return this.userRepository.save(user);
}
async update(userId: string, data: Partial<User>) {
return this.userRepository.update(userId, data);
}
async getByIds(transaction: EntityManager, ids: string[]) {
return transaction.find(User, { where: { id: In(ids) } });
}
getManager() {
return this.userRepository.manager;
}
@ -257,12 +227,9 @@ export class UserService {
async inviteUsers(owner: User, attributes: Array<{ email: string; role: 'member' | 'admin' }>) {
const memberRole = await this.roleService.findGlobalMemberRole();
const adminRole = await this.roleService.findGlobalAdminRole();
const emails = attributes.map(({ email }) => email);
const existingUsers = await this.findMany({
where: { email: In(attributes.map(({ email }) => email)) },
relations: ['globalRole'],
select: ['email', 'password', 'id'],
});
const existingUsers = await this.userRepository.findManyByEmail(emails);
const existUsersEmails = existingUsers.map((user) => user.email);

View file

@ -3,7 +3,6 @@ import { Service } from 'typedi';
import { CacheService } from './cache.service';
import type { WebhookEntity } from '@db/entities/WebhookEntity';
import type { IHttpRequestMethods } from 'n8n-workflow';
import type { DeepPartial } from 'typeorm';
type Method = NonNullable<IHttpRequestMethods>;
@ -97,7 +96,7 @@ export class WebhookService {
return this.webhookRepository.insert(webhook);
}
createWebhook(data: DeepPartial<WebhookEntity>) {
createWebhook(data: Partial<WebhookEntity>) {
return this.webhookRepository.create(data);
}

View file

@ -1,17 +1,12 @@
import type { EntityManager } from 'typeorm';
import * as WorkflowHelpers from '@/WorkflowHelpers';
import type { SharedWorkflow } from '@db/entities/SharedWorkflow';
import type { User } from '@db/entities/User';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { UserService } from '@/services/user.service';
import { WorkflowService } from './workflow.service';
import type {
CredentialUsedByWorkflow,
WorkflowWithSharingsAndCredentials,
} from './workflows.types';
import { CredentialsService } from '@/credentials/credentials.service';
import { ApplicationError, NodeOperationError } from 'n8n-workflow';
import { RoleService } from '@/services/role.service';
import { Service } from 'typedi';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
@ -19,23 +14,25 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { CredentialsRepository } from '@/databases/repositories/credentials.repository';
import { RoleService } from '@/services/role.service';
import type { EntityManager } from 'typeorm';
import { UserRepository } from '@/databases/repositories/user.repository';
@Service()
export class EnterpriseWorkflowService {
constructor(
private readonly workflowService: WorkflowService,
private readonly userService: UserService,
private readonly roleService: RoleService,
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly workflowRepository: WorkflowRepository,
private readonly credentialsRepository: CredentialsRepository,
private readonly userRepository: UserRepository,
private readonly roleService: RoleService,
) {}
async isOwned(
user: User,
workflowId: string,
): Promise<{ ownsWorkflow: boolean; workflow?: WorkflowEntity }> {
const sharing = await this.workflowService.getSharing(
const sharing = await this.sharedWorkflowRepository.getSharing(
user,
workflowId,
{ allowGlobalScope: false },
@ -49,28 +46,11 @@ export class EnterpriseWorkflowService {
return { ownsWorkflow: true, workflow };
}
async share(
transaction: EntityManager,
workflow: WorkflowEntity,
shareWithIds: string[],
): Promise<SharedWorkflow[]> {
const users = await this.userService.getByIds(transaction, shareWithIds);
async share(transaction: EntityManager, workflow: WorkflowEntity, shareWithIds: string[]) {
const users = await this.userRepository.getByIds(transaction, shareWithIds);
const role = await this.roleService.findWorkflowEditorRole();
const newSharedWorkflows = users.reduce<SharedWorkflow[]>((acc, user) => {
if (user.isPending) {
return acc;
}
const entity: Partial<SharedWorkflow> = {
workflowId: workflow.id,
userId: user.id,
roleId: role?.id,
};
acc.push(this.sharedWorkflowRepository.create(entity));
return acc;
}, []);
return transaction.save(newSharedWorkflows);
await this.sharedWorkflowRepository.share(transaction, workflow, users, role.id);
}
addOwnerAndSharings(workflow: WorkflowWithSharingsAndCredentials): void {

View file

@ -1,14 +1,12 @@
import Container, { Service } from 'typedi';
import type { INode, IPinData } from 'n8n-workflow';
import { NodeApiError, Workflow } from 'n8n-workflow';
import type { FindOptionsWhere } from 'typeorm';
import pick from 'lodash/pick';
import omit from 'lodash/omit';
import { v4 as uuid } from 'uuid';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import * as WorkflowHelpers from '@/WorkflowHelpers';
import config from '@/config';
import type { SharedWorkflow } from '@db/entities/SharedWorkflow';
import type { User } from '@db/entities/User';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { validateEntity } from '@/GenericHelpers';
@ -25,7 +23,6 @@ import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { OwnershipService } from '@/services/ownership.service';
import { WorkflowHistoryService } from './workflowHistory/workflowHistory.service.ee';
import { BinaryDataService } from 'n8n-core';
import type { Scope } from '@n8n/permissions';
import { Logger } from '@/Logger';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
@ -34,10 +31,6 @@ import { ExecutionRepository } from '@db/repositories/execution.repository';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
export type WorkflowsGetSharedOptions =
| { allowGlobalScope: true; globalScope: Scope }
| { allowGlobalScope: false };
@Service()
export class WorkflowService {
constructor(
@ -57,24 +50,6 @@ export class WorkflowService {
private readonly activeWorkflowRunner: ActiveWorkflowRunner,
) {}
async getSharing(
user: User,
workflowId: string,
options: WorkflowsGetSharedOptions,
relations: string[] = ['workflow'],
): Promise<SharedWorkflow | null> {
const where: FindOptionsWhere<SharedWorkflow> = { workflowId };
// Omit user from where if the requesting user has relevant
// global workflow permissions. This allows the user to
// access workflows they don't own.
if (!options.allowGlobalScope || !user.hasGlobalScope(options.globalScope)) {
where.userId = user.id;
}
return this.sharedWorkflowRepository.findOne({ where, relations });
}
/**
* Find the pinned trigger to execute the workflow from, if any.
*

View file

@ -386,10 +386,14 @@ workflowsController.put(
workflow = undefined;
// Allow owners/admins to share
if (req.user.hasGlobalScope('workflow:share')) {
const sharedRes = await Container.get(WorkflowService).getSharing(req.user, workflowId, {
allowGlobalScope: true,
globalScope: 'workflow:share',
});
const sharedRes = await Container.get(SharedWorkflowRepository).getSharing(
req.user,
workflowId,
{
allowGlobalScope: true,
globalScope: 'workflow:share',
},
);
workflow = sharedRes?.workflow;
}
if (!workflow) {

View file

@ -8,30 +8,31 @@ import type {
WorkflowClosedMessage,
WorkflowOpenedMessage,
} from '@/collaboration/collaboration.message';
import type { UserRepository } from '@/databases/repositories/user.repository';
import { mock } from 'jest-mock-extended';
describe('CollaborationService', () => {
let collaborationService: CollaborationService;
let mockLogger: Logger;
let mockUserService: jest.Mocked<UserService>;
let mockUserRepository: jest.Mocked<UserRepository>;
let state: CollaborationState;
let push: Push;
beforeEach(() => {
mockLogger = {
warn: jest.fn(),
error: jest.fn(),
} as unknown as jest.Mocked<Logger>;
mockUserService = {
getByIds: jest.fn(),
getManager: jest.fn(),
} as unknown as jest.Mocked<UserService>;
push = {
on: jest.fn(),
sendToUsers: jest.fn(),
} as unknown as Push;
mockLogger = mock<Logger>();
mockUserService = mock<UserService>();
mockUserRepository = mock<UserRepository>();
push = mock<Push>();
state = new CollaborationState();
collaborationService = new CollaborationService(mockLogger, push, state, mockUserService);
collaborationService = new CollaborationService(
mockLogger,
push,
state,
mockUserService,
mockUserRepository,
);
});
describe('workflow opened message', () => {
@ -61,7 +62,7 @@ describe('CollaborationService', () => {
describe('user is not yet active', () => {
it('updates state correctly', async () => {
mockUserService.getByIds.mockResolvedValueOnce([{ id: userId } as User]);
mockUserRepository.getByIds.mockResolvedValueOnce([{ id: userId } as User]);
await collaborationService.handleUserMessage(userId, message);
expect(state.getActiveWorkflowUsers(workflowId)).toEqual([
@ -73,7 +74,7 @@ describe('CollaborationService', () => {
});
it('sends active workflow users changed message', async () => {
mockUserService.getByIds.mockResolvedValueOnce([{ id: userId } as User]);
mockUserRepository.getByIds.mockResolvedValueOnce([{ id: userId } as User]);
await collaborationService.handleUserMessage(userId, message);
expectActiveUsersChangedMessage([userId]);
@ -86,7 +87,7 @@ describe('CollaborationService', () => {
});
it('updates state correctly', async () => {
mockUserService.getByIds.mockResolvedValueOnce([{ id: userId } as User]);
mockUserRepository.getByIds.mockResolvedValueOnce([{ id: userId } as User]);
await collaborationService.handleUserMessage(userId, message);
expect(state.getActiveWorkflowUsers(workflowId)).toEqual([
@ -98,7 +99,7 @@ describe('CollaborationService', () => {
});
it('sends active workflow users changed message', async () => {
mockUserService.getByIds.mockResolvedValueOnce([{ id: userId } as User]);
mockUserRepository.getByIds.mockResolvedValueOnce([{ id: userId } as User]);
await collaborationService.handleUserMessage(userId, message);
expectActiveUsersChangedMessage([userId]);

View file

@ -14,11 +14,13 @@ import { License } from '@/License';
import { badPasswords } from '../shared/testData';
import { mockInstance } from '../../shared/mocking';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { UserRepository } from '@/databases/repositories/user.repository';
describe('MeController', () => {
const externalHooks = mockInstance(ExternalHooks);
const internalHooks = mockInstance(InternalHooks);
const userService = mockInstance(UserService);
const userRepository = mockInstance(UserRepository);
mockInstance(License).isWithinUsersLimit.mockReturnValue(true);
const controller = Container.get(MeController);
@ -47,7 +49,7 @@ describe('MeController', () => {
const reqBody = { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' };
const req = mock<MeRequest.UserUpdate>({ user, body: reqBody });
const res = mock<Response>();
userService.findOneOrFail.mockResolvedValue(user);
userRepository.findOneOrFail.mockResolvedValue(user);
jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token');
userService.toPublic.mockResolvedValue({} as unknown as PublicUser);
@ -82,7 +84,7 @@ describe('MeController', () => {
const reqBody = { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' };
const req = mock<MeRequest.UserUpdate>({ user, body: reqBody });
const res = mock<Response>();
userService.findOneOrFail.mockResolvedValue(user);
userRepository.findOneOrFail.mockResolvedValue(user);
jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token');
// Add invalid data to the request payload
@ -166,7 +168,7 @@ describe('MeController', () => {
body: { currentPassword: 'old_password', newPassword: 'NewPassword123' },
});
const res = mock<Response>();
userService.save.calledWith(req.user).mockResolvedValue(req.user);
userRepository.save.calledWith(req.user).mockResolvedValue(req.user);
jest.spyOn(jwt, 'sign').mockImplementation(() => 'new-signed-token');
await controller.updatePassword(req, res);

View file

@ -16,11 +16,13 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { PasswordUtility } from '@/services/password.utility';
import Container from 'typedi';
import type { InternalHooks } from '@/InternalHooks';
import { UserRepository } from '@/databases/repositories/user.repository';
describe('OwnerController', () => {
const configGetSpy = jest.spyOn(config, 'getEnv');
const internalHooks = mock<InternalHooks>();
const userService = mockInstance(UserService);
const userRepository = mockInstance(UserRepository);
const settingsRepository = mock<SettingsRepository>();
mockInstance(License).isWithinUsersLimit.mockReturnValue(true);
const controller = new OwnerController(
@ -30,6 +32,7 @@ describe('OwnerController', () => {
userService,
Container.get(PasswordUtility),
mock(),
userRepository,
);
describe('setupOwner', () => {
@ -87,12 +90,12 @@ describe('OwnerController', () => {
});
const res = mock<Response>();
configGetSpy.mockReturnValue(false);
userService.save.calledWith(anyObject()).mockResolvedValue(user);
userRepository.save.calledWith(anyObject()).mockResolvedValue(user);
jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token');
await controller.setupOwner(req, res);
expect(userService.save).toHaveBeenCalledWith(user);
expect(userRepository.save).toHaveBeenCalledWith(user);
const cookieOptions = captor<CookieOptions>();
expect(res.cookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME, 'signed-token', cookieOptions);