refactor(core): Move tag collection into repository (no-changelog) (#6860)

* refactor(core): Move tag collection into repository

* Fix tests

* Address feedback

* Fix missing spot
This commit is contained in:
Iván Ovejero
2023-08-08 14:08:56 +02:00
committed by GitHub
parent 8de28fe4d0
commit 11440bfd3c
15 changed files with 59 additions and 61 deletions

View File

@@ -35,7 +35,6 @@ import {
SettingsRepository, SettingsRepository,
SharedCredentialsRepository, SharedCredentialsRepository,
SharedWorkflowRepository, SharedWorkflowRepository,
TagRepository,
UserRepository, UserRepository,
VariablesRepository, VariablesRepository,
WorkflowRepository, WorkflowRepository,
@@ -177,7 +176,6 @@ export async function init(testConnectionOptions?: ConnectionOptions): Promise<v
collections.InstalledPackages = Container.get(InstalledPackagesRepository); collections.InstalledPackages = Container.get(InstalledPackagesRepository);
collections.SharedCredentials = Container.get(SharedCredentialsRepository); collections.SharedCredentials = Container.get(SharedCredentialsRepository);
collections.SharedWorkflow = Container.get(SharedWorkflowRepository); collections.SharedWorkflow = Container.get(SharedWorkflowRepository);
collections.Tag = Container.get(TagRepository);
collections.Variables = Container.get(VariablesRepository); collections.Variables = Container.get(VariablesRepository);
collections.WorkflowStatistics = Container.get(WorkflowStatisticsRepository); collections.WorkflowStatistics = Container.get(WorkflowStatisticsRepository);
collections.WorkflowTagMapping = Container.get(WorkflowTagMappingRepository); collections.WorkflowTagMapping = Container.get(WorkflowTagMappingRepository);

View File

@@ -54,7 +54,6 @@ import type {
SettingsRepository, SettingsRepository,
SharedCredentialsRepository, SharedCredentialsRepository,
SharedWorkflowRepository, SharedWorkflowRepository,
TagRepository,
UserRepository, UserRepository,
VariablesRepository, VariablesRepository,
WorkflowRepository, WorkflowRepository,
@@ -100,7 +99,6 @@ export interface IDatabaseCollections extends Record<string, Repository<any>> {
Settings: SettingsRepository; Settings: SettingsRepository;
SharedCredentials: SharedCredentialsRepository; SharedCredentials: SharedCredentialsRepository;
SharedWorkflow: SharedWorkflowRepository; SharedWorkflow: SharedWorkflowRepository;
Tag: TagRepository;
User: UserRepository; User: UserRepository;
Variables: VariablesRepository; Variables: VariablesRepository;
Workflow: WorkflowRepository; Workflow: WorkflowRepository;

View File

@@ -8,6 +8,8 @@ import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { SharedWorkflow } from '@db/entities/SharedWorkflow'; import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
import config from '@/config'; import config from '@/config';
import { TagRepository } from '@/databases/repositories';
import Container from 'typedi';
function insertIf(condition: boolean, elements: string[]): string[] { function insertIf(condition: boolean, elements: string[]): string[] {
return condition ? elements : []; return condition ? elements : [];
@@ -62,7 +64,7 @@ export async function getWorkflowById(id: string): Promise<WorkflowEntity | null
* Intersection! e.g. workflow needs to have all provided tags. * Intersection! e.g. workflow needs to have all provided tags.
*/ */
export async function getWorkflowIdsViaTags(tags: string[]): Promise<string[]> { export async function getWorkflowIdsViaTags(tags: string[]): Promise<string[]> {
const dbTags = await Db.collections.Tag.find({ const dbTags = await Container.get(TagRepository).find({
where: { name: In(tags) }, where: { name: In(tags) },
relations: ['workflows'], relations: ['workflows'],
}); });

View File

@@ -482,7 +482,7 @@ export class Server extends AbstractServer {
logger, logger,
jwtService, jwtService,
}), }),
new TagsController({ config, repositories, externalHooks }), Container.get(TagsController),
new TranslationController(config, this.credentialTypes), new TranslationController(config, this.credentialTypes),
new UsersController({ new UsersController({
config, config,

View File

@@ -1,6 +1,8 @@
import type { EntityManager } from 'typeorm'; import type { EntityManager } from 'typeorm';
import { TagEntity } from '@db/entities/TagEntity'; import type { TagEntity } from '@db/entities/TagEntity';
import type { ITagToImport, IWorkflowToImport } from '@/Interfaces'; import type { ITagToImport, IWorkflowToImport } from '@/Interfaces';
import { TagRepository } from './databases/repositories';
import Container from 'typedi';
// ---------------------------------- // ----------------------------------
// utils // utils
@@ -26,8 +28,7 @@ export function sortByRequestOrder(
// ---------------------------------- // ----------------------------------
const createTag = async (transactionManager: EntityManager, name: string): Promise<TagEntity> => { const createTag = async (transactionManager: EntityManager, name: string): Promise<TagEntity> => {
const tag = new TagEntity(); const tag = Container.get(TagRepository).create({ name: name.trim() });
tag.name = name;
return transactionManager.save<TagEntity>(tag); return transactionManager.save<TagEntity>(tag);
}; };

View File

@@ -18,6 +18,7 @@ import { replaceInvalidCredentials } from '@/WorkflowHelpers';
import { BaseCommand, UM_FIX_INSTRUCTION } from '../BaseCommand'; import { BaseCommand, UM_FIX_INSTRUCTION } from '../BaseCommand';
import { generateNanoId } from '@db/utils/generators'; import { generateNanoId } from '@db/utils/generators';
import { RoleService } from '@/services/role.service'; import { RoleService } from '@/services/role.service';
import { TagRepository } from '@/databases/repositories';
function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IWorkflowToImport[] { function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IWorkflowToImport[] {
if (!Array.isArray(workflows)) { if (!Array.isArray(workflows)) {
@@ -92,7 +93,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner(); const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner();
const credentials = await Db.collections.Credentials.find(); const credentials = await Db.collections.Credentials.find();
const tags = await Db.collections.Tag.find(); const tags = await Container.get(TagRepository).find();
let totalImported = 0; let totalImported = 0;

View File

@@ -1,35 +1,25 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import type { Config } from '@/config'; import config from '@/config';
import { Authorized, Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators'; import { Authorized, Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators';
import type { IDatabaseCollections, IExternalHooksClass, ITagWithCountDb } from '@/Interfaces'; import { type ITagWithCountDb } from '@/Interfaces';
import { TagEntity } from '@db/entities/TagEntity'; import type { TagEntity } from '@db/entities/TagEntity';
import type { TagRepository } from '@db/repositories'; import { TagRepository } from '@db/repositories';
import { validateEntity } from '@/GenericHelpers'; import { validateEntity } from '@/GenericHelpers';
import { BadRequestError } from '@/ResponseHelper'; import { BadRequestError } from '@/ResponseHelper';
import { TagsRequest } from '@/requests'; import { TagsRequest } from '@/requests';
import { Service } from 'typedi';
import { ExternalHooks } from '@/ExternalHooks';
@Authorized() @Authorized()
@RestController('/tags') @RestController('/tags')
@Service()
export class TagsController { export class TagsController {
private config: Config; private config = config;
private externalHooks: IExternalHooksClass; constructor(
private tagsRepository: TagRepository,
private tagsRepository: TagRepository; private externalHooks: ExternalHooks,
) {}
constructor({
config,
externalHooks,
repositories,
}: {
config: Config;
externalHooks: IExternalHooksClass;
repositories: Pick<IDatabaseCollections, 'Tag'>;
}) {
this.config = config;
this.externalHooks = externalHooks;
this.tagsRepository = repositories.Tag;
}
// TODO: move this into a new decorator `@IfEnabled('workflowTagsDisabled')` // TODO: move this into a new decorator `@IfEnabled('workflowTagsDisabled')`
@Middleware() @Middleware()
@@ -63,8 +53,7 @@ export class TagsController {
// Creates a tag // Creates a tag
@Post('/') @Post('/')
async createTag(req: TagsRequest.Create): Promise<TagEntity> { async createTag(req: TagsRequest.Create): Promise<TagEntity> {
const newTag = new TagEntity(); const newTag = this.tagsRepository.create({ name: req.body.name.trim() });
newTag.name = req.body.name.trim();
await this.externalHooks.run('tag.beforeCreate', [newTag]); await this.externalHooks.run('tag.beforeCreate', [newTag]);
await validateEntity(newTag); await validateEntity(newTag);
@@ -77,12 +66,7 @@ export class TagsController {
// Updates a tag // Updates a tag
@Patch('/:id(\\w+)') @Patch('/:id(\\w+)')
async updateTag(req: TagsRequest.Update): Promise<TagEntity> { async updateTag(req: TagsRequest.Update): Promise<TagEntity> {
const { name } = req.body; const newTag = this.tagsRepository.create({ id: req.params.id, name: req.body.name.trim() });
const { id } = req.params;
const newTag = new TagEntity();
newTag.id = id;
newTag.name = name.trim();
await this.externalHooks.run('tag.beforeUpdate', [newTag]); await this.externalHooks.run('tag.beforeUpdate', [newTag]);
await validateEntity(newTag); await validateEntity(newTag);

View File

@@ -1,6 +1,5 @@
import Container, { Service } from 'typedi'; import Container, { Service } from 'typedi';
import path from 'path'; import path from 'path';
import * as Db from '@/Db';
import { import {
getTagsPath, getTagsPath,
getTrackingInformationFromPostPushResult, getTrackingInformationFromPostPushResult,
@@ -39,6 +38,7 @@ import type { Variables } from '@db/entities/Variables';
import type { SourceControlWorkflowVersionId } from './types/sourceControlWorkflowVersionId'; import type { SourceControlWorkflowVersionId } from './types/sourceControlWorkflowVersionId';
import type { ExportableCredential } from './types/exportableCredential'; import type { ExportableCredential } from './types/exportableCredential';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { TagRepository } from '@/databases/repositories';
@Service() @Service()
export class SourceControlService { export class SourceControlService {
private sshKeyName: string; private sshKeyName: string;
@@ -52,6 +52,7 @@ export class SourceControlService {
private sourceControlPreferencesService: SourceControlPreferencesService, private sourceControlPreferencesService: SourceControlPreferencesService,
private sourceControlExportService: SourceControlExportService, private sourceControlExportService: SourceControlExportService,
private sourceControlImportService: SourceControlImportService, private sourceControlImportService: SourceControlImportService,
private tagRepository: TagRepository,
) { ) {
const userFolder = UserSettings.getUserN8nFolderPath(); const userFolder = UserSettings.getUserN8nFolderPath();
this.sshFolder = path.join(userFolder, SOURCE_CONTROL_SSH_FOLDER); this.sshFolder = path.join(userFolder, SOURCE_CONTROL_SSH_FOLDER);
@@ -682,7 +683,7 @@ export class SourceControlService {
options: SourceControlGetStatus, options: SourceControlGetStatus,
sourceControlledFiles: SourceControlledFile[], sourceControlledFiles: SourceControlledFile[],
) { ) {
const lastUpdatedTag = await Db.collections.Tag.find({ const lastUpdatedTag = await this.tagRepository.find({
order: { updatedAt: 'DESC' }, order: { updatedAt: 'DESC' },
take: 1, take: 1,
select: ['updatedAt'], select: ['updatedAt'],

View File

@@ -26,6 +26,7 @@ import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { In } from 'typeorm'; import { In } from 'typeorm';
import type { SourceControlledFile } from './types/sourceControlledFile'; import type { SourceControlledFile } from './types/sourceControlledFile';
import { VariablesService } from '../variables/variables.service'; import { VariablesService } from '../variables/variables.service';
import { TagRepository } from '@/databases/repositories';
@Service() @Service()
export class SourceControlExportService { export class SourceControlExportService {
@@ -35,7 +36,10 @@ export class SourceControlExportService {
private credentialExportFolder: string; private credentialExportFolder: string;
constructor(private readonly variablesService: VariablesService) { constructor(
private readonly variablesService: VariablesService,
private readonly tagRepository: TagRepository,
) {
const userFolder = UserSettings.getUserN8nFolderPath(); const userFolder = UserSettings.getUserN8nFolderPath();
this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER); this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
this.workflowExportFolder = path.join(this.gitFolder, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER); this.workflowExportFolder = path.join(this.gitFolder, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER);
@@ -167,7 +171,7 @@ export class SourceControlExportService {
async exportTagsToWorkFolder(): Promise<ExportResult> { async exportTagsToWorkFolder(): Promise<ExportResult> {
try { try {
sourceControlFoldersExistCheck([this.gitFolder]); sourceControlFoldersExistCheck([this.gitFolder]);
const tags = await Db.collections.Tag.find(); const tags = await this.tagRepository.find();
// do not export empty tags // do not export empty tags
if (tags.length === 0) { if (tags.length === 0) {
return { return {

View File

@@ -27,6 +27,7 @@ import { getCredentialExportPath, getWorkflowExportPath } from './sourceControlH
import type { SourceControlledFile } from './types/sourceControlledFile'; import type { SourceControlledFile } from './types/sourceControlledFile';
import { RoleService } from '@/services/role.service'; import { RoleService } from '@/services/role.service';
import { VariablesService } from '../variables/variables.service'; import { VariablesService } from '../variables/variables.service';
import { TagRepository } from '@/databases/repositories';
@Service() @Service()
export class SourceControlImportService { export class SourceControlImportService {
@@ -39,6 +40,7 @@ export class SourceControlImportService {
constructor( constructor(
private readonly variablesService: VariablesService, private readonly variablesService: VariablesService,
private readonly activeWorkflowRunner: ActiveWorkflowRunner, private readonly activeWorkflowRunner: ActiveWorkflowRunner,
private readonly tagRepository: TagRepository,
) { ) {
const userFolder = UserSettings.getUserN8nFolderPath(); const userFolder = UserSettings.getUserN8nFolderPath();
this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER); this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
@@ -265,7 +267,7 @@ export class SourceControlImportService {
tags: TagEntity[]; tags: TagEntity[];
mappings: WorkflowTagMapping[]; mappings: WorkflowTagMapping[];
}> { }> {
const localTags = await Db.collections.Tag.find({ const localTags = await this.tagRepository.find({
select: ['id', 'name'], select: ['id', 'name'],
}); });
const localMappings = await Db.collections.WorkflowTagMapping.find({ const localMappings = await Db.collections.WorkflowTagMapping.find({
@@ -481,7 +483,7 @@ export class SourceControlImportService {
await Promise.all( await Promise.all(
mappedTags.tags.map(async (tag) => { mappedTags.tags.map(async (tag) => {
const findByName = await Db.collections.Tag.findOne({ const findByName = await this.tagRepository.findOne({
where: { name: tag.name }, where: { name: tag.name },
select: ['id'], select: ['id'],
}); });
@@ -490,7 +492,7 @@ export class SourceControlImportService {
`A tag with the name <strong>${tag.name}</strong> already exists locally.<br />Please either rename the local tag, or the remote one with the id <strong>${tag.id}</strong> in the tags.json file.`, `A tag with the name <strong>${tag.name}</strong> already exists locally.<br />Please either rename the local tag, or the remote one with the id <strong>${tag.id}</strong> in the tags.json file.`,
); );
} }
await Db.collections.Tag.upsert( await this.tagRepository.upsert(
{ {
...tag, ...tag,
}, },

View File

@@ -20,6 +20,7 @@ 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';
import { TagRepository } from '@/databases/repositories';
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
export const EEWorkflowController = express.Router(); export const EEWorkflowController = express.Router();
@@ -134,7 +135,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 Db.collections.Tag.find({ newWorkflow.tags = await Container.get(TagRepository).find({
select: ['id', 'name'], select: ['id', 'name'],
where: { where: {
id: In(tagIds), id: In(tagIds),

View File

@@ -24,6 +24,7 @@ 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';
import { TagRepository } from '@/databases/repositories';
export const workflowsController = express.Router(); export const workflowsController = express.Router();
@@ -62,7 +63,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 Db.collections.Tag.find({ newWorkflow.tags = await Container.get(TagRepository).find({
select: ['id', 'name'], select: ['id', 'name'],
where: { where: {
id: In(tagIds), id: In(tagIds),

View File

@@ -35,6 +35,8 @@ import type { ExecutionData } from '@db/entities/ExecutionData';
import { generateNanoId } from '@db/utils/generators'; import { generateNanoId } from '@db/utils/generators';
import { RoleService } from '@/services/role.service'; import { RoleService } from '@/services/role.service';
import { VariablesService } from '@/environments/variables/variables.service'; import { VariablesService } from '@/environments/variables/variables.service';
import { TagRepository } from '@/databases/repositories';
import { separate } from '@/utils';
export type TestDBType = 'postgres' | 'mysql'; export type TestDBType = 'postgres' | 'mysql';
@@ -113,7 +115,13 @@ export async function terminate() {
* Truncate specific DB tables in a test DB. * Truncate specific DB tables in a test DB.
*/ */
export async function truncate(collections: CollectionName[]) { export async function truncate(collections: CollectionName[]) {
for (const collection of collections) { const [tag, rest] = separate(collections, (c) => c === 'Tag');
if (tag) {
await Container.get(TagRepository).delete({});
}
for (const collection of rest) {
await Db.collections[collection].delete({}); await Db.collections[collection].delete({});
} }
} }
@@ -384,7 +392,7 @@ export async function createWaitingExecution(workflow: WorkflowEntity) {
export async function createTag(attributes: Partial<TagEntity> = {}) { export async function createTag(attributes: Partial<TagEntity> = {}) {
const { name } = attributes; const { name } = attributes;
return Db.collections.Tag.save({ return Container.get(TagRepository).save({
id: generateNanoId(), id: generateNanoId(),
name: name ?? randomName(), name: name ?? randomName(),
...attributes, ...attributes,

View File

@@ -272,11 +272,7 @@ export const setupTestServer = ({
); );
break; break;
case 'tags': case 'tags':
registerController( registerController(app, config, Container.get(TagsController));
app,
config,
new TagsController({ config, externalHooks, repositories }),
);
break; break;
} }
} }

View File

@@ -1,7 +1,8 @@
import * as Db from '@/Db';
import * as utils from './shared/utils/'; import * as utils from './shared/utils/';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
import type { SuperAgentTest } from 'supertest'; import type { SuperAgentTest } from 'supertest';
import { TagRepository } from '@/databases/repositories';
import Container from 'typedi';
let authOwnerAgent: SuperAgentTest; let authOwnerAgent: SuperAgentTest;
const testServer = utils.setupTestServer({ endpointGroups: ['tags'] }); const testServer = utils.setupTestServer({ endpointGroups: ['tags'] });
@@ -21,18 +22,18 @@ describe('POST /tags', () => {
const resp = await authOwnerAgent.post('/tags').send({ name: 'test' }); const resp = await authOwnerAgent.post('/tags').send({ name: 'test' });
expect(resp.statusCode).toBe(200); expect(resp.statusCode).toBe(200);
const dbTag = await Db.collections.Tag.findBy({ name: 'test' }); const dbTag = await Container.get(TagRepository).findBy({ name: 'test' });
expect(dbTag.length === 1); expect(dbTag.length === 1);
}); });
test('should not create duplicate tag', async () => { test('should not create duplicate tag', async () => {
const newTag = Db.collections.Tag.create({ name: 'test' }); const newTag = Container.get(TagRepository).create({ name: 'test' });
await Db.collections.Tag.save(newTag); await Container.get(TagRepository).save(newTag);
const resp = await authOwnerAgent.post('/tags').send({ name: 'test' }); const resp = await authOwnerAgent.post('/tags').send({ name: 'test' });
expect(resp.status).toBe(500); expect(resp.status).toBe(500);
const dbTag = await Db.collections.Tag.findBy({ name: 'test' }); const dbTag = await Container.get(TagRepository).findBy({ name: 'test' });
expect(dbTag.length).toBe(1); expect(dbTag.length).toBe(1);
}); });
}); });