mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-14 16:44:07 -08:00
🔨 Refactor per feedback
This commit is contained in:
parent
18794942f2
commit
e293acb8b6
|
@ -74,20 +74,23 @@ export interface IWorkflowBase extends IWorkflowBaseWorkflow {
|
||||||
id?: number | string;
|
id?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITagBase {
|
export interface IWorkflowRequest extends Request {
|
||||||
name: string;
|
body: {
|
||||||
createdAt: Date;
|
tags?: string[];
|
||||||
updatedAt: Date;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITagDb extends ITagBase {
|
export interface ITagDb {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
|
name: string;
|
||||||
|
createdAt?: Date;
|
||||||
|
updatedAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Almost identical to editor-ui.Interfaces.ts
|
// Almost identical to editor-ui.Interfaces.ts
|
||||||
export interface IWorkflowDb extends IWorkflowBase {
|
export interface IWorkflowDb extends IWorkflowBase {
|
||||||
id: number | string;
|
id: number | string;
|
||||||
tags: Array<{ id: string | number; name: string }>;
|
tags: ITagDb[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkflowResponse extends IWorkflowBase {
|
export interface IWorkflowResponse extends IWorkflowBase {
|
||||||
|
|
|
@ -107,7 +107,7 @@ import * as querystring from 'querystring';
|
||||||
import * as Queue from '../src/Queue';
|
import * as Queue from '../src/Queue';
|
||||||
import { OptionsWithUrl } from 'request-promise-native';
|
import { OptionsWithUrl } from 'request-promise-native';
|
||||||
import { Registry } from 'prom-client';
|
import { Registry } from 'prom-client';
|
||||||
import { ITagBase, ITagDb, } from './Interfaces';
|
import { ITagDb, IWorkflowRequest } from './Interfaces';
|
||||||
|
|
||||||
import * as TagHelpers from './TagHelpers';
|
import * as TagHelpers from './TagHelpers';
|
||||||
|
|
||||||
|
@ -485,7 +485,7 @@ class App {
|
||||||
|
|
||||||
|
|
||||||
// Creates a new workflow
|
// Creates a new workflow
|
||||||
this.app.post(`/${this.restEndpoint}/workflows`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IWorkflowResponse> => {
|
this.app.post(`/${this.restEndpoint}/workflows`, ResponseHelper.send(async (req: IWorkflowRequest, res: express.Response): Promise<IWorkflowResponse> => {
|
||||||
|
|
||||||
const newWorkflowData = req.body as IWorkflowBase;
|
const newWorkflowData = req.body as IWorkflowBase;
|
||||||
|
|
||||||
|
@ -500,16 +500,21 @@ class App {
|
||||||
// Save the workflow in DB
|
// Save the workflow in DB
|
||||||
const result = await Db.collections.Workflow!.save(newWorkflowData);
|
const result = await Db.collections.Workflow!.save(newWorkflowData);
|
||||||
|
|
||||||
const { tags } = req.body as { tags: string | undefined };
|
const { tags } = req.body;
|
||||||
|
|
||||||
if (tags) {
|
const tagIds = tags;
|
||||||
const tagIds = tags.split(',');
|
const workflowId = result.id as string;
|
||||||
await TagHelpers.createRelations(result.id as string, tagIds);
|
|
||||||
const foundTags = await Db.collections.Tag!.find({
|
if (tagIds) {
|
||||||
|
await TagHelpers.validateTags(tagIds);
|
||||||
|
await TagHelpers.createRelations(workflowId, tagIds);
|
||||||
|
|
||||||
|
const found = await Db.collections.Tag!.find({
|
||||||
select: ['id', 'name'],
|
select: ['id', 'name'],
|
||||||
where: { id: In(tagIds) },
|
where: { id: In(tagIds) },
|
||||||
});
|
});
|
||||||
result.tags = foundTags.map(({ id, name }) => ({ id: id.toString(), name }));
|
|
||||||
|
result.tags = TagHelpers.stringifyId(found);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to response format in which the id is a string
|
// Convert to response format in which the id is a string
|
||||||
|
@ -561,7 +566,7 @@ class App {
|
||||||
const results = await Db.collections.Workflow!.find(findQuery);
|
const results = await Db.collections.Workflow!.find(findQuery);
|
||||||
results.forEach(workflow => {
|
results.forEach(workflow => {
|
||||||
if (workflow.tags) {
|
if (workflow.tags) {
|
||||||
workflow.tags = workflow.tags.map(({ id, name }) => ({ id: id.toString(), name }));
|
workflow.tags = TagHelpers.stringifyId(workflow.tags);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -591,9 +596,8 @@ class App {
|
||||||
|
|
||||||
|
|
||||||
// Updates an existing workflow
|
// Updates an existing workflow
|
||||||
this.app.patch(`/${this.restEndpoint}/workflows/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IWorkflowResponse> => {
|
this.app.patch(`/${this.restEndpoint}/workflows/:id`, ResponseHelper.send(async (req: IWorkflowRequest, res: express.Response): Promise<IWorkflowResponse> => {
|
||||||
|
const { tags } = req.body;
|
||||||
const { tags } = req.body as { tags: string | undefined };
|
|
||||||
const newWorkflowData = _.omit(req.body, ['tags']) as IWorkflowBase;
|
const newWorkflowData = _.omit(req.body, ['tags']) as IWorkflowBase;
|
||||||
|
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
|
@ -663,23 +667,22 @@ class App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tags) {
|
const workflowId = id;
|
||||||
await TagHelpers.deleteAllTagsForWorkflow(id);
|
const tagIds = tags;
|
||||||
|
|
||||||
const tagIds = tags.split(',');
|
if (tagIds) {
|
||||||
|
await TagHelpers.validateTags(tagIds);
|
||||||
|
await TagHelpers.validateNotRelated(workflowId, tagIds);
|
||||||
|
|
||||||
for (const tagId of tagIds) {
|
await TagHelpers.removeRelations(workflowId);
|
||||||
await TagHelpers.validateId(tagId);
|
await TagHelpers.createRelations(workflowId, tagIds);
|
||||||
}
|
|
||||||
|
|
||||||
await TagHelpers.createRelations(id, tagIds);
|
const found = await Db.collections.Tag!.find({
|
||||||
|
|
||||||
const foundTags = await Db.collections.Tag!.find({
|
|
||||||
select: ['id', 'name'],
|
select: ['id', 'name'],
|
||||||
where: { id: In(tagIds) },
|
where: { id: In(tagIds) },
|
||||||
});
|
});
|
||||||
responseData.tags = foundTags.map(({ id, name }) => ({ id: id.toString(), name }));
|
|
||||||
|
|
||||||
|
responseData.tags = TagHelpers.stringifyId(found);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to response format in which the id is a string
|
// Convert to response format in which the id is a string
|
||||||
|
@ -767,13 +770,13 @@ class App {
|
||||||
|
|
||||||
// Creates a tag
|
// Creates a tag
|
||||||
this.app.post(`/${this.restEndpoint}/tags`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<{ id: string, name: string }> => {
|
this.app.post(`/${this.restEndpoint}/tags`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<{ id: string, name: string }> => {
|
||||||
TagHelpers.validateRequestBody(req.body);
|
|
||||||
|
|
||||||
const { name } = req.body;
|
const { name } = req.body;
|
||||||
|
console.log('------------------------');
|
||||||
|
console.log(typeof name);
|
||||||
|
console.log('------------------------');
|
||||||
await TagHelpers.validateName(name);
|
await TagHelpers.validateName(name);
|
||||||
TagHelpers.validateLength(name);
|
|
||||||
|
|
||||||
const newTag: ITagBase = {
|
const newTag: Partial<ITagDb> = {
|
||||||
name,
|
name,
|
||||||
createdAt: this.getCurrentDate(),
|
createdAt: this.getCurrentDate(),
|
||||||
updatedAt: this.getCurrentDate(),
|
updatedAt: this.getCurrentDate(),
|
||||||
|
@ -787,21 +790,18 @@ class App {
|
||||||
// Deletes a tag
|
// Deletes a tag
|
||||||
this.app.delete(`/${this.restEndpoint}/tags/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<boolean> => {
|
this.app.delete(`/${this.restEndpoint}/tags/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<boolean> => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
await TagHelpers.validateId(id);
|
await TagHelpers.exists(id);
|
||||||
await Db.collections.Tag!.delete({ id });
|
await Db.collections.Tag!.delete({ id });
|
||||||
return true;
|
return true;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Updates an existing tag
|
// Updates a tag
|
||||||
this.app.patch(`/${this.restEndpoint}/tags/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<{ id: string, name: string }> => {
|
this.app.patch(`/${this.restEndpoint}/tags/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<{ id: string, name: string }> => {
|
||||||
TagHelpers.validateRequestBody(req.body);
|
|
||||||
|
|
||||||
const { name } = req.body;
|
const { name } = req.body;
|
||||||
await TagHelpers.validateName(name);
|
await TagHelpers.validateName(name);
|
||||||
TagHelpers.validateLength(name);
|
|
||||||
|
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
await TagHelpers.validateId(id);
|
await TagHelpers.exists(id);
|
||||||
|
|
||||||
const updatedTag: Partial<ITagDb> = {
|
const updatedTag: Partial<ITagDb> = {
|
||||||
name,
|
name,
|
||||||
|
|
|
@ -1,97 +1,36 @@
|
||||||
import {
|
import {
|
||||||
FindOneOptions,
|
|
||||||
getConnection,
|
getConnection,
|
||||||
In,
|
|
||||||
} from "typeorm";
|
} from "typeorm";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Db,
|
Db,
|
||||||
ITagDb,
|
ITagDb,
|
||||||
IWorkflowDb,
|
|
||||||
ResponseHelper,
|
ResponseHelper,
|
||||||
} from ".";
|
} from ".";
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
// validators
|
// utils
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate whether a tag ID exists so that it can be used for a workflow create or tag update operation.
|
* Type guard for string array.
|
||||||
*/
|
*/
|
||||||
export async function validateId(id: string): Promise<void> | never {
|
function isStringArray(tags: unknown[]): tags is string[] {
|
||||||
const findQuery = { where: { id } } as FindOneOptions;
|
return Array.isArray(tags) && !tags.some((value) => typeof value !== 'string');
|
||||||
const tag = await Db.collections.Tag!.findOne(findQuery);
|
|
||||||
|
|
||||||
if (!tag) {
|
|
||||||
throw new ResponseHelper.ResponseError(`Tag with ID ${id} does not exist.`, undefined, 400);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate whether a tag name has 1 to 24 characters.
|
* Stringify the ID in every `ITagDb` in an array.
|
||||||
|
* Side effect: Remove `createdAt` and `updatedAt` for a slimmer response.
|
||||||
*/
|
*/
|
||||||
export function validateLength(name: string): void | never {
|
export function stringifyId(tags: ITagDb[]) {
|
||||||
if (name.length <= 0 || name.length > 24) {
|
return tags.map(({ id, name }) => ({ id: id.toString(), name }));
|
||||||
throw new ResponseHelper.ResponseError('Tag name must be 1 to 24 characters long.', undefined, 400);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate whether a tag name exists so that it cannot be used for a tag create or tag update operation.
|
* Check if a workflow and a tag are related.
|
||||||
*/
|
*/
|
||||||
export async function validateName(name: string): Promise<void> | never {
|
async function checkRelated(workflowId: string, tagId: string): Promise<boolean> {
|
||||||
const findQuery = { where: { name } } as FindOneOptions;
|
|
||||||
const tag = await Db.collections.Tag!.findOne(findQuery);
|
|
||||||
|
|
||||||
if (tag) {
|
|
||||||
throw new ResponseHelper.ResponseError('Tag name already exists.', undefined, 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate that a tag and a workflow are not related so that a link can be created.
|
|
||||||
*/
|
|
||||||
export async function validateNoRelation(workflowId: number, tagId: number): Promise<void> | never {
|
|
||||||
const areRelated = await checkRelated(workflowId, tagId);
|
|
||||||
|
|
||||||
if (areRelated) {
|
|
||||||
throw new ResponseHelper.ResponseError(`Workflow ID ${workflowId} and tag ID ${tagId} are already related.`, undefined, 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate that a tag and a workflow are related so that their link can be deleted.
|
|
||||||
*/
|
|
||||||
export async function validateRelation(workflowId: number, tagId: number): Promise<void> | never {
|
|
||||||
const areRelated = await checkRelated(workflowId, tagId);
|
|
||||||
|
|
||||||
if (!areRelated) {
|
|
||||||
throw new ResponseHelper.ResponseError(`Workflow ID ${workflowId} and tag ID ${tagId} are not related.`, undefined, 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate whether the request body for a create/update operation has a `name` property.
|
|
||||||
*/
|
|
||||||
export function validateRequestBody({ name }: { name: string | undefined }): void | never {
|
|
||||||
if (!name) {
|
|
||||||
throw new ResponseHelper.ResponseError(`Property 'name' missing from request body.`, undefined, 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------------
|
|
||||||
// queries
|
|
||||||
// ----------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a workflow and a tag are related. Used only in validators.
|
|
||||||
*/
|
|
||||||
async function checkRelated(
|
|
||||||
workflowId: number,
|
|
||||||
tagId: number
|
|
||||||
): Promise<boolean> {
|
|
||||||
const result = await getConnection().createQueryBuilder()
|
const result = await getConnection().createQueryBuilder()
|
||||||
.select()
|
.select()
|
||||||
.from('workflows_tags', 'workflows_tags')
|
.from('workflows_tags', 'workflows_tags')
|
||||||
|
@ -102,8 +41,88 @@ async function checkRelated(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve all existing tags, whether linked to a workflow or not,
|
* Check whether a tag ID exists in the `tag_entity` table.
|
||||||
* including how many workflows each tag is linked to.
|
*
|
||||||
|
* Used for creating a workflow or updating a tag.
|
||||||
|
*/
|
||||||
|
export async function exists(id: string): Promise<void> | never {
|
||||||
|
const tag = await Db.collections.Tag!.findOne({ where: { id }});
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
throw new ResponseHelper.ResponseError(`Tag with ID ${id} does not exist.`, undefined, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// validators
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate whether every tag ID is a string array and exists in the `tag_entity` table.
|
||||||
|
*
|
||||||
|
* Used for creating a workflow or updating a tag.
|
||||||
|
*/
|
||||||
|
export async function validateTags(tags: unknown[]): Promise<void> | never {
|
||||||
|
if (!isStringArray(tags)) {
|
||||||
|
throw new ResponseHelper.ResponseError(`The tags property is not an array of strings.`, undefined, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tagId of tags) {
|
||||||
|
await exists(tagId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate whether a tag name
|
||||||
|
* - is present in the request body,
|
||||||
|
* - is a string,
|
||||||
|
* - is 1 to 24 characters long, and
|
||||||
|
* - does not exist already.
|
||||||
|
*
|
||||||
|
* Used for creating or updating a tag.
|
||||||
|
*/
|
||||||
|
export async function validateName(name: unknown): Promise<void> | never {
|
||||||
|
if (name === undefined) {
|
||||||
|
throw new ResponseHelper.ResponseError(`Property 'name' missing from request body.`, undefined, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof name !== 'string') {
|
||||||
|
throw new ResponseHelper.ResponseError(`Property 'name' must be a string.`, undefined, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.length <= 0 || name.length > 24) {
|
||||||
|
throw new ResponseHelper.ResponseError('Tag name must be 1 to 24 characters long.', undefined, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tag = await Db.collections.Tag!.findOne({ where: { name } });
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
throw new ResponseHelper.ResponseError('Tag name already exists.', undefined, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that the provided tags are related to a workflow.
|
||||||
|
*
|
||||||
|
* Used for relating the tags and the workflow.
|
||||||
|
*/
|
||||||
|
export async function validateNotRelated(workflowId: string, tags: string[]): Promise<void> | never {
|
||||||
|
tags.forEach(async tagId => {
|
||||||
|
const areRelated = await checkRelated(workflowId, tagId);
|
||||||
|
|
||||||
|
if (areRelated) {
|
||||||
|
throw new ResponseHelper.ResponseError(`Workflow ID ${workflowId} and tag ID ${tagId} are already related.`, undefined, 400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// queries
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all existing tags, whether related to a workflow or not,
|
||||||
|
* including how many workflows each tag is related to.
|
||||||
*/
|
*/
|
||||||
export async function getAllTagsWithUsageCount(): Promise<Array<{
|
export async function getAllTagsWithUsageCount(): Promise<Array<{
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -156,7 +175,7 @@ export async function createRelations(workflowId: string, tagIds: string[]) {
|
||||||
/**
|
/**
|
||||||
* Remove all tags for a workflow during a tag update operation.
|
* Remove all tags for a workflow during a tag update operation.
|
||||||
*/
|
*/
|
||||||
export async function deleteAllTagsForWorkflow(workflowId: string) {
|
export async function removeRelations(workflowId: string) {
|
||||||
await getConnection().createQueryBuilder()
|
await getConnection().createQueryBuilder()
|
||||||
.delete()
|
.delete()
|
||||||
.from('workflows_tags')
|
.from('workflows_tags')
|
||||||
|
|
Loading…
Reference in a new issue