refactor(core): Move remaining tags logic to service (no-changelog) (#6920)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Iván Ovejero 2023-08-22 12:24:43 +02:00 committed by GitHub
parent 9e3e298aca
commit 9b9b891e68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 192 additions and 168 deletions

View file

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

View file

@ -1,94 +0,0 @@
import type { EntityManager } from 'typeorm';
import type { TagEntity } from '@db/entities/TagEntity';
import type { ITagToImport, IWorkflowToImport } from '@/Interfaces';
import { TagRepository } from './databases/repositories';
import Container from 'typedi';
// ----------------------------------
// utils
// ----------------------------------
/**
* Sort tags based on the order of the tag IDs in the request.
*/
export function sortByRequestOrder(
tags: TagEntity[],
{ requestOrder }: { requestOrder: string[] },
) {
const tagMap = tags.reduce<Record<string, TagEntity>>((acc, tag) => {
acc[tag.id] = tag;
return acc;
}, {});
return requestOrder.map((tagId) => tagMap[tagId]);
}
// ----------------------------------
// mutations
// ----------------------------------
const createTag = async (transactionManager: EntityManager, name: string): Promise<TagEntity> => {
const tag = Container.get(TagRepository).create({ name: name.trim() });
return transactionManager.save<TagEntity>(tag);
};
const findOrCreateTag = async (
transactionManager: EntityManager,
importTag: ITagToImport,
tagsEntities: TagEntity[],
): Promise<TagEntity> => {
// Assume tag is identical if createdAt date is the same to preserve a changed tag name
const identicalMatch = tagsEntities.find(
(existingTag) =>
existingTag.id === importTag.id &&
existingTag.createdAt &&
importTag.createdAt &&
existingTag.createdAt.getTime() === new Date(importTag.createdAt).getTime(),
);
if (identicalMatch) {
return identicalMatch;
}
const nameMatch = tagsEntities.find((existingTag) => existingTag.name === importTag.name);
if (nameMatch) {
return nameMatch;
}
const created = await createTag(transactionManager, importTag.name);
tagsEntities.push(created);
return created;
};
const hasTags = (workflow: IWorkflowToImport) =>
'tags' in workflow && Array.isArray(workflow.tags) && workflow.tags.length > 0;
/**
* Set tag IDs to use existing tags, creates a new tag if no matching tag could be found
*/
export async function setTagsForImport(
transactionManager: EntityManager,
workflow: IWorkflowToImport,
tags: TagEntity[],
): Promise<void> {
if (!hasTags(workflow)) {
return;
}
const workflowTags = workflow.tags;
const tagLookupPromises = [];
for (let i = 0; i < workflowTags.length; i++) {
if (workflowTags[i]?.name) {
const lookupPromise = findOrCreateTag(transactionManager, workflowTags[i], tags).then(
(tag) => {
workflowTags[i] = {
id: tag.id,
name: tag.name,
};
},
);
tagLookupPromises.push(lookupPromise);
}
}
await Promise.all(tagLookupPromises);
}

View file

@ -11,14 +11,13 @@ import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { setTagsForImport } from '@/TagHelpers';
import { disableAutoGeneratedIds } from '@db/utils/commandHelpers';
import type { ICredentialsDb, IWorkflowToImport } from '@/Interfaces';
import { replaceInvalidCredentials } from '@/WorkflowHelpers';
import { BaseCommand, UM_FIX_INSTRUCTION } from '../BaseCommand';
import { generateNanoId } from '@db/utils/generators';
import { RoleService } from '@/services/role.service';
import { TagRepository } from '@/databases/repositories';
import { TagService } from '@/services/tag.service';
function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IWorkflowToImport[] {
if (!Array.isArray(workflows)) {
@ -66,6 +65,8 @@ export class ImportWorkflowsCommand extends BaseCommand {
private transactionManager: EntityManager;
private tagService = Container.get(TagService);
async init() {
disableAutoGeneratedIds(WorkflowEntity);
await super.init();
@ -93,7 +94,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner();
const credentials = await Db.collections.Credentials.find();
const tags = await Container.get(TagRepository).find();
const tags = await this.tagService.getAll();
let totalImported = 0;
@ -133,7 +134,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
}
if (Object.prototype.hasOwnProperty.call(workflow, 'tags')) {
await setTagsForImport(transactionManager, workflow, tags);
await this.tagService.setTagsForImport(transactionManager, workflow, tags);
}
if (workflow.active) {
@ -183,7 +184,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
}
}
if (Object.prototype.hasOwnProperty.call(workflow, 'tags')) {
await setTagsForImport(transactionManager, workflow, tags);
await this.tagService.setTagsForImport(transactionManager, workflow, tags);
}
if (workflow.active) {
this.logger.info(

View file

@ -1,14 +1,10 @@
import { Request, Response, NextFunction } from 'express';
import config from '@/config';
import { Authorized, Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators';
import { type ITagWithCountDb } from '@/Interfaces';
import type { TagEntity } from '@db/entities/TagEntity';
import { TagRepository } from '@db/repositories';
import { validateEntity } from '@/GenericHelpers';
import { TagService } from '@/services/tag.service';
import { BadRequestError } from '@/ResponseHelper';
import { TagsRequest } from '@/requests';
import { Service } from 'typedi';
import { ExternalHooks } from '@/ExternalHooks';
@Authorized()
@RestController('/tags')
@ -16,10 +12,7 @@ import { ExternalHooks } from '@/ExternalHooks';
export class TagsController {
private config = config;
constructor(
private tagsRepository: TagRepository,
private externalHooks: ExternalHooks,
) {}
constructor(private tagService: TagService) {}
// TODO: move this into a new decorator `@IfEnabled('workflowTagsDisabled')`
@Middleware()
@ -29,61 +22,32 @@ export class TagsController {
next();
}
// Retrieves all tags, with or without usage count
@Get('/')
async getAll(req: TagsRequest.GetAll): Promise<TagEntity[] | ITagWithCountDb[]> {
const { withUsageCount } = req.query;
if (withUsageCount === 'true') {
return this.tagsRepository
.find({
select: ['id', 'name', 'createdAt', 'updatedAt'],
relations: ['workflowMappings'],
})
.then((tags) =>
tags.map(({ workflowMappings, ...rest }) => ({
...rest,
usageCount: workflowMappings.length,
})),
);
}
return this.tagsRepository.find({ select: ['id', 'name', 'createdAt', 'updatedAt'] });
async getAll(req: TagsRequest.GetAll) {
return this.tagService.getAll({ withUsageCount: req.query.withUsageCount === 'true' });
}
// Creates a tag
@Post('/')
async createTag(req: TagsRequest.Create): Promise<TagEntity> {
const newTag = this.tagsRepository.create({ name: req.body.name.trim() });
async createTag(req: TagsRequest.Create) {
const tag = this.tagService.toEntity({ name: req.body.name });
await this.externalHooks.run('tag.beforeCreate', [newTag]);
await validateEntity(newTag);
const tag = await this.tagsRepository.save(newTag);
await this.externalHooks.run('tag.afterCreate', [tag]);
return tag;
return this.tagService.save(tag, 'create');
}
// Updates a tag
@Patch('/:id(\\w+)')
async updateTag(req: TagsRequest.Update): Promise<TagEntity> {
const newTag = this.tagsRepository.create({ id: req.params.id, name: req.body.name.trim() });
async updateTag(req: TagsRequest.Update) {
const newTag = this.tagService.toEntity({ id: req.params.id, name: req.body.name.trim() });
await this.externalHooks.run('tag.beforeUpdate', [newTag]);
await validateEntity(newTag);
const tag = await this.tagsRepository.save(newTag);
await this.externalHooks.run('tag.afterUpdate', [tag]);
return tag;
return this.tagService.save(newTag, 'update');
}
@Authorized(['global', 'owner'])
@Delete('/:id(\\w+)')
async deleteTag(req: TagsRequest.Delete) {
const { id } = req.params;
await this.externalHooks.run('tag.beforeDelete', [id]);
await this.tagsRepository.delete({ id });
await this.externalHooks.run('tag.afterDelete', [id]);
await this.tagService.delete(id);
return true;
}
}

View file

@ -39,6 +39,7 @@ import type { SourceControlWorkflowVersionId } from './types/sourceControlWorkfl
import type { ExportableCredential } from './types/exportableCredential';
import { InternalHooks } from '@/InternalHooks';
import { TagRepository } from '@/databases/repositories';
@Service()
export class SourceControlService {
private sshKeyName: string;

View file

@ -492,15 +492,12 @@ 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.`,
);
}
await this.tagRepository.upsert(
{
...tag,
},
{
skipUpdateIfNoValuesChanged: true,
conflictPaths: { id: true },
},
);
const tagCopy = this.tagRepository.create(tag);
await this.tagRepository.upsert(tagCopy, {
skipUpdateIfNoValuesChanged: true,
conflictPaths: { id: true },
});
}),
);

View file

@ -0,0 +1,157 @@
import { TagRepository } from '@/databases/repositories';
import { Service } from 'typedi';
import { validateEntity } from '@/GenericHelpers';
import type { ITagToImport, ITagWithCountDb, IWorkflowToImport } from '@/Interfaces';
import type { TagEntity } from '@/databases/entities/TagEntity';
import type { EntityManager, FindManyOptions, FindOneOptions } from 'typeorm';
import type { UpsertOptions } from 'typeorm/repository/UpsertOptions';
import { ExternalHooks } from '@/ExternalHooks';
type GetAllResult<T> = T extends { withUsageCount: true } ? ITagWithCountDb[] : TagEntity[];
@Service()
export class TagService {
constructor(
private externalHooks: ExternalHooks,
private tagRepository: TagRepository,
) {}
toEntity(attrs: { name: string; id?: string }) {
attrs.name = attrs.name.trim();
return this.tagRepository.create(attrs);
}
async save(tag: TagEntity, actionKind: 'create' | 'update') {
await validateEntity(tag);
const action = actionKind[0].toUpperCase() + actionKind.slice(1);
await this.externalHooks.run(`tag.before${action}`, [tag]);
const savedTag = this.tagRepository.save(tag);
await this.externalHooks.run(`tag.after${action}`, [tag]);
return savedTag;
}
async delete(id: string) {
await this.externalHooks.run('tag.beforeDelete', [id]);
const deleteResult = this.tagRepository.delete(id);
await this.externalHooks.run('tag.afterDelete', [id]);
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({
select: ['id', 'name', 'createdAt', 'updatedAt'],
relations: ['workflowMappings'],
});
return allTags.map(({ workflowMappings, ...rest }) => {
return {
...rest,
usageCount: workflowMappings.length,
} as ITagWithCountDb;
}) as GetAllResult<T>;
}
return this.tagRepository.find({
select: ['id', 'name', 'createdAt', 'updatedAt'],
}) as Promise<GetAllResult<T>>;
}
/**
* Sort tags based on the order of the tag IDs in the request.
*/
sortByRequestOrder(tags: TagEntity[], { requestOrder }: { requestOrder: string[] }) {
const tagMap = tags.reduce<Record<string, TagEntity>>((acc, tag) => {
acc[tag.id] = tag;
return acc;
}, {});
return requestOrder.map((tagId) => tagMap[tagId]);
}
/**
* Set tag IDs to use existing tags, creates a new tag if no matching tag could be found
*/
async setTagsForImport(
transactionManager: EntityManager,
workflow: IWorkflowToImport,
tags: TagEntity[],
) {
if (!this.hasTags(workflow)) return;
const workflowTags = workflow.tags;
const tagLookupPromises = [];
for (let i = 0; i < workflowTags.length; i++) {
if (workflowTags[i]?.name) {
const lookupPromise = this.findOrCreateTag(transactionManager, workflowTags[i], tags).then(
(tag) => {
workflowTags[i] = {
id: tag.id,
name: tag.name,
};
},
);
tagLookupPromises.push(lookupPromise);
}
}
await Promise.all(tagLookupPromises);
}
private hasTags(workflow: IWorkflowToImport) {
return 'tags' in workflow && Array.isArray(workflow.tags) && workflow.tags.length > 0;
}
private async findOrCreateTag(
transactionManager: EntityManager,
importTag: ITagToImport,
tagsEntities: TagEntity[],
) {
// Assume tag is identical if createdAt date is the same to preserve a changed tag name
const identicalMatch = tagsEntities.find(
(existingTag) =>
existingTag.id === importTag.id &&
existingTag.createdAt &&
importTag.createdAt &&
existingTag.createdAt.getTime() === new Date(importTag.createdAt).getTime(),
);
if (identicalMatch) {
return identicalMatch;
}
const nameMatch = tagsEntities.find((existingTag) => existingTag.name === importTag.name);
if (nameMatch) {
return nameMatch;
}
const created = await this.txCreateTag(transactionManager, importTag.name);
tagsEntities.push(created);
return created;
}
private async txCreateTag(transactionManager: EntityManager, name: string) {
const tag = this.tagRepository.create({ name: name.trim() });
return transactionManager.save<TagEntity>(tag);
}
}

View file

@ -12,7 +12,6 @@ import { EEWorkflowsService as EEWorkflows } from './workflows.services.ee';
import { ExternalHooks } from '@/ExternalHooks';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { LoggerProxy } from 'n8n-workflow';
import * as TagHelpers from '@/TagHelpers';
import { EECredentialsService as EECredentials } from '../credentials/credentials.service.ee';
import type { IExecutionPushResponse } from '@/Interfaces';
import * as GenericHelpers from '@/GenericHelpers';
@ -22,7 +21,7 @@ import { InternalHooks } from '@/InternalHooks';
import { RoleService } from '@/services/role.service';
import * as utils from '@/utils';
import { listQueryMiddleware } from '@/middlewares';
import { TagRepository } from '@/databases/repositories';
import { TagService } from '@/services/tag.service';
// eslint-disable-next-line @typescript-eslint/naming-convention
export const EEWorkflowController = express.Router();
@ -137,7 +136,7 @@ EEWorkflowController.post(
const { tags: tagIds } = req.body;
if (tagIds?.length && !config.getEnv('workflowTagsDisabled')) {
newWorkflow.tags = await Container.get(TagRepository).find({
newWorkflow.tags = await Container.get(TagService).findMany({
select: ['id', 'name'],
where: {
id: In(tagIds),
@ -188,7 +187,7 @@ EEWorkflowController.post(
}
if (tagIds && !config.getEnv('workflowTagsDisabled') && savedWorkflow.tags) {
savedWorkflow.tags = TagHelpers.sortByRequestOrder(savedWorkflow.tags, {
savedWorkflow.tags = Container.get(TagService).sortByRequestOrder(savedWorkflow.tags, {
requestOrder: tagIds,
});
}

View file

@ -9,7 +9,6 @@ import * as ResponseHelper from '@/ResponseHelper';
import * as WorkflowHelpers from '@/WorkflowHelpers';
import type { IWorkflowResponse, IExecutionPushResponse } from '@/Interfaces';
import config from '@/config';
import * as TagHelpers from '@/TagHelpers';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { validateEntity } from '@/GenericHelpers';
@ -26,7 +25,7 @@ import { InternalHooks } from '@/InternalHooks';
import { RoleService } from '@/services/role.service';
import * as utils from '@/utils';
import { listQueryMiddleware } from '@/middlewares';
import { TagRepository } from '@/databases/repositories';
import { TagService } from '@/services/tag.service';
export const workflowsController = express.Router();
@ -65,7 +64,7 @@ workflowsController.post(
const { tags: tagIds } = req.body;
if (tagIds?.length && !config.getEnv('workflowTagsDisabled')) {
newWorkflow.tags = await Container.get(TagRepository).find({
newWorkflow.tags = await Container.get(TagService).findMany({
select: ['id', 'name'],
where: {
id: In(tagIds),
@ -101,7 +100,7 @@ workflowsController.post(
}
if (tagIds && !config.getEnv('workflowTagsDisabled') && savedWorkflow.tags) {
savedWorkflow.tags = TagHelpers.sortByRequestOrder(savedWorkflow.tags, {
savedWorkflow.tags = Container.get(TagService).sortByRequestOrder(savedWorkflow.tags, {
requestOrder: tagIds,
});
}

View file

@ -16,7 +16,7 @@ import type { User } from '@db/entities/User';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { validateEntity } from '@/GenericHelpers';
import { ExternalHooks } from '@/ExternalHooks';
import * as TagHelpers from '@/TagHelpers';
import { TagService } from '@/services/tag.service';
import type { ListQueryOptions, WorkflowRequest } from '@/requests';
import type { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces';
import { NodeTypes } from '@/NodeTypes';
@ -299,7 +299,7 @@ export class WorkflowsService {
}
if (updatedWorkflow.tags?.length && tagIds?.length) {
updatedWorkflow.tags = TagHelpers.sortByRequestOrder(updatedWorkflow.tags, {
updatedWorkflow.tags = Container.get(TagService).sortByRequestOrder(updatedWorkflow.tags, {
requestOrder: tagIds,
});
}