mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-10 06:34:05 -08:00
feat(API): Add tag support to public API (#8588)
Co-authored-by: Jesús Burgers <jesus.burgers@chakray.co.uk> Co-authored-by: Jesús Burgers <43568066+jburgers-chakray@users.noreply.github.com>
This commit is contained in:
parent
64b10d7f5c
commit
a743a40376
|
@ -5,6 +5,8 @@ import type { User } from '@db/entities/User';
|
|||
|
||||
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||
|
||||
import type { TagEntity } from '@db/entities/TagEntity';
|
||||
|
||||
import type { UserManagementMailer } from '@/UserManagement/email';
|
||||
|
||||
import type { Risk } from '@/security-audit/types';
|
||||
|
@ -57,6 +59,24 @@ export declare namespace ExecutionRequest {
|
|||
type Delete = Get;
|
||||
}
|
||||
|
||||
export declare namespace TagRequest {
|
||||
type GetAll = AuthenticatedRequest<
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
offset?: number;
|
||||
}
|
||||
>;
|
||||
|
||||
type Create = AuthenticatedRequest<{}, {}, TagEntity>;
|
||||
type Get = AuthenticatedRequest<{ id: string }>;
|
||||
type Delete = Get;
|
||||
type Update = AuthenticatedRequest<{ id: string }, {}, TagEntity>;
|
||||
}
|
||||
|
||||
export declare namespace CredentialTypeRequest {
|
||||
type Get = AuthenticatedRequest<{ credentialTypeName: string }, {}, {}, {}>;
|
||||
}
|
||||
|
@ -74,6 +94,7 @@ export declare namespace WorkflowRequest {
|
|||
offset?: number;
|
||||
workflowId?: number;
|
||||
active: boolean;
|
||||
name?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -82,6 +103,8 @@ export declare namespace WorkflowRequest {
|
|||
type Delete = Get;
|
||||
type Update = AuthenticatedRequest<{ id: string }, {}, WorkflowEntity, {}>;
|
||||
type Activate = Get;
|
||||
type GetTags = Get;
|
||||
type UpdateTags = AuthenticatedRequest<{ id: string }, {}, TagEntity[]>;
|
||||
}
|
||||
|
||||
export declare namespace UserRequest {
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
get:
|
||||
x-eov-operation-id: getTag
|
||||
x-eov-operation-handler: v1/handlers/tags/tags.handler
|
||||
tags:
|
||||
- Tags
|
||||
summary: Retrieves a tag
|
||||
description: Retrieves a tag.
|
||||
parameters:
|
||||
- $ref: '../schemas/parameters/tagId.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/tag.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
delete:
|
||||
x-eov-operation-id: deleteTag
|
||||
x-eov-operation-handler: v1/handlers/tags/tags.handler
|
||||
tags:
|
||||
- Tags
|
||||
summary: Delete a tag
|
||||
description: Deletes a tag.
|
||||
parameters:
|
||||
- $ref: '../schemas/parameters/tagId.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/tag.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'403':
|
||||
$ref: '../../../../shared/spec/responses/forbidden.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
put:
|
||||
x-eov-operation-id: updateTag
|
||||
x-eov-operation-handler: v1/handlers/tags/tags.handler
|
||||
tags:
|
||||
- Tags
|
||||
summary: Update a tag
|
||||
description: Update a tag.
|
||||
parameters:
|
||||
- $ref: '../schemas/parameters/tagId.yml'
|
||||
requestBody:
|
||||
description: Updated tag object.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/tag.yml'
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Tag object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/tag.yml'
|
||||
'400':
|
||||
$ref: '../../../../shared/spec/responses/badRequest.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
'409':
|
||||
$ref: '../../../../shared/spec/responses/conflict.yml'
|
|
@ -0,0 +1,46 @@
|
|||
post:
|
||||
x-eov-operation-id: createTag
|
||||
x-eov-operation-handler: v1/handlers/tags/tags.handler
|
||||
tags:
|
||||
- Tags
|
||||
summary: Create a tag
|
||||
description: Create a tag in your instance.
|
||||
requestBody:
|
||||
description: Created tag object.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/tag.yml'
|
||||
required: true
|
||||
responses:
|
||||
'201':
|
||||
description: A tag object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/tag.yml'
|
||||
'400':
|
||||
$ref: '../../../../shared/spec/responses/badRequest.yml'
|
||||
'409':
|
||||
$ref: '../../../../shared/spec/responses/conflict.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
get:
|
||||
x-eov-operation-id: getTags
|
||||
x-eov-operation-handler: v1/handlers/tags/tags.handler
|
||||
tags:
|
||||
- Tags
|
||||
summary: Retrieve all tags
|
||||
description: Retrieve all tags from your instance.
|
||||
parameters:
|
||||
- $ref: '../../../../shared/spec/parameters/limit.yml'
|
||||
- $ref: '../../../../shared/spec/parameters/cursor.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/tagList.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
|
@ -0,0 +1,6 @@
|
|||
name: id
|
||||
in: path
|
||||
description: The ID of the tag.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
|
@ -1,8 +1,12 @@
|
|||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
example: 12
|
||||
type: string
|
||||
readOnly: true
|
||||
example: 2tUt1wbLX592XDdX
|
||||
name:
|
||||
type: string
|
||||
example: Production
|
|
@ -0,0 +1,11 @@
|
|||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: './tag.yml'
|
||||
nextCursor:
|
||||
type: string
|
||||
description: Paginate through tags by setting the cursor parameter to a nextCursor attribute returned by a previous request. Default value fetches the first "page" of the collection.
|
||||
nullable: true
|
||||
example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA
|
103
packages/cli/src/PublicApi/v1/handlers/tags/tags.handler.ts
Normal file
103
packages/cli/src/PublicApi/v1/handlers/tags/tags.handler.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
import type express from 'express';
|
||||
|
||||
import type { TagEntity } from '@db/entities/TagEntity';
|
||||
import { authorize, validCursor } from '../../shared/middlewares/global.middleware';
|
||||
import type { TagRequest } from '../../../types';
|
||||
import { encodeNextCursor } from '../../shared/services/pagination.service';
|
||||
|
||||
import { Container } from 'typedi';
|
||||
import type { FindManyOptions } from '@n8n/typeorm';
|
||||
import { TagRepository } from '@db/repositories/tag.repository';
|
||||
import { TagService } from '@/services/tag.service';
|
||||
|
||||
export = {
|
||||
createTag: [
|
||||
authorize(['global:owner', 'global:admin', 'global:member']),
|
||||
async (req: TagRequest.Create, res: express.Response): Promise<express.Response> => {
|
||||
const { name } = req.body;
|
||||
|
||||
const newTag = Container.get(TagService).toEntity({ name: name.trim() });
|
||||
|
||||
try {
|
||||
const createdTag = await Container.get(TagService).save(newTag, 'create');
|
||||
return res.status(201).json(createdTag);
|
||||
} catch (error) {
|
||||
return res.status(409).json({ message: 'Tag already exists' });
|
||||
}
|
||||
},
|
||||
],
|
||||
updateTag: [
|
||||
authorize(['global:owner', 'global:admin', 'global:member']),
|
||||
async (req: TagRequest.Update, res: express.Response): Promise<express.Response> => {
|
||||
const { id } = req.params;
|
||||
const { name } = req.body;
|
||||
|
||||
try {
|
||||
await Container.get(TagService).getById(id);
|
||||
} catch (error) {
|
||||
return res.status(404).json({ message: 'Not Found' });
|
||||
}
|
||||
|
||||
const updateTag = Container.get(TagService).toEntity({ id, name: name.trim() });
|
||||
|
||||
try {
|
||||
const updatedTag = await Container.get(TagService).save(updateTag, 'update');
|
||||
return res.json(updatedTag);
|
||||
} catch (error) {
|
||||
return res.status(409).json({ message: 'Tag already exists' });
|
||||
}
|
||||
},
|
||||
],
|
||||
deleteTag: [
|
||||
authorize(['global:owner', 'global:admin']),
|
||||
async (req: TagRequest.Delete, res: express.Response): Promise<express.Response> => {
|
||||
const { id } = req.params;
|
||||
|
||||
let tag;
|
||||
try {
|
||||
tag = await Container.get(TagService).getById(id);
|
||||
} catch (error) {
|
||||
return res.status(404).json({ message: 'Not Found' });
|
||||
}
|
||||
|
||||
await Container.get(TagService).delete(id);
|
||||
return res.json(tag);
|
||||
},
|
||||
],
|
||||
getTags: [
|
||||
authorize(['global:owner', 'global:admin', 'global:member']),
|
||||
validCursor,
|
||||
async (req: TagRequest.GetAll, res: express.Response): Promise<express.Response> => {
|
||||
const { offset = 0, limit = 100 } = req.query;
|
||||
|
||||
const query: FindManyOptions<TagEntity> = {
|
||||
skip: offset,
|
||||
take: limit,
|
||||
};
|
||||
|
||||
const [tags, count] = await Container.get(TagRepository).findAndCount(query);
|
||||
|
||||
return res.json({
|
||||
data: tags,
|
||||
nextCursor: encodeNextCursor({
|
||||
offset,
|
||||
limit,
|
||||
numberOfTotalRecords: count,
|
||||
}),
|
||||
});
|
||||
},
|
||||
],
|
||||
getTag: [
|
||||
authorize(['global:owner', 'global:admin', 'global:member']),
|
||||
async (req: TagRequest.Get, res: express.Response): Promise<express.Response> => {
|
||||
const { id } = req.params;
|
||||
|
||||
try {
|
||||
const tag = await Container.get(TagService).getById(id);
|
||||
return res.json(tag);
|
||||
} catch (error) {
|
||||
return res.status(404).json({ message: 'Not Found' });
|
||||
}
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
get:
|
||||
x-eov-operation-id: getWorkflowTags
|
||||
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
|
||||
tags:
|
||||
- Workflow
|
||||
summary: Get workflow tags
|
||||
description: Get workflow tags.
|
||||
parameters:
|
||||
- $ref: '../schemas/parameters/workflowId.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: List of tags
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/workflowTags.yml'
|
||||
'400':
|
||||
$ref: '../../../../shared/spec/responses/badRequest.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
put:
|
||||
x-eov-operation-id: updateWorkflowTags
|
||||
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
|
||||
tags:
|
||||
- Workflow
|
||||
summary: Update tags of a workflow
|
||||
description: Update tags of a workflow.
|
||||
parameters:
|
||||
- $ref: '../schemas/parameters/workflowId.yml'
|
||||
requestBody:
|
||||
description: List of tags
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/tagIds.yml'
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: List of tags after add the tag
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/workflowTags.yml'
|
||||
'400':
|
||||
$ref: '../../../../shared/spec/responses/badRequest.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
|
@ -44,6 +44,14 @@ get:
|
|||
schema:
|
||||
type: string
|
||||
example: test,production
|
||||
- name: name
|
||||
in: query
|
||||
required: false
|
||||
explode: false
|
||||
allowReserved: true
|
||||
schema:
|
||||
type: string
|
||||
example: My Workflow
|
||||
- $ref: '../../../../shared/spec/parameters/limit.yml'
|
||||
- $ref: '../../../../shared/spec/parameters/cursor.yml'
|
||||
responses:
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
type: array
|
||||
items:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- id
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: 2tUt1wbLX592XDdX
|
|
@ -45,5 +45,5 @@ properties:
|
|||
tags:
|
||||
type: array
|
||||
items:
|
||||
$ref: './tag.yml'
|
||||
$ref: '../../../tags/spec/schemas/tag.yml'
|
||||
readOnly: true
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
type: array
|
||||
items:
|
||||
$ref: '../../../tags/spec/schemas/tag.yml'
|
|
@ -1,7 +1,8 @@
|
|||
import type express from 'express';
|
||||
|
||||
import { Container } from 'typedi';
|
||||
import type { FindOptionsWhere } from '@n8n/typeorm';
|
||||
import { In } from '@n8n/typeorm';
|
||||
import { In, Like, QueryFailedError } from '@n8n/typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
||||
|
@ -20,6 +21,8 @@ import {
|
|||
updateWorkflow,
|
||||
createWorkflow,
|
||||
parseTagNames,
|
||||
getWorkflowTags,
|
||||
updateTags,
|
||||
} from './workflows.service';
|
||||
import { WorkflowService } from '@/workflows/workflow.service';
|
||||
import { InternalHooks } from '@/InternalHooks';
|
||||
|
@ -95,10 +98,11 @@ export = {
|
|||
authorize(['global:owner', 'global:admin', 'global:member']),
|
||||
validCursor,
|
||||
async (req: WorkflowRequest.GetAll, res: express.Response): Promise<express.Response> => {
|
||||
const { offset = 0, limit = 100, active = undefined, tags = undefined } = req.query;
|
||||
const { offset = 0, limit = 100, active, tags, name } = req.query;
|
||||
|
||||
const where: FindOptionsWhere<WorkflowEntity> = {
|
||||
...(active !== undefined && { active }),
|
||||
...(name !== undefined && { name: Like('%' + name.trim() + '%') }),
|
||||
};
|
||||
|
||||
if (['global:owner', 'global:admin'].includes(req.user.role)) {
|
||||
|
@ -280,4 +284,59 @@ export = {
|
|||
return res.json(sharedWorkflow.workflow);
|
||||
},
|
||||
],
|
||||
getWorkflowTags: [
|
||||
authorize(['global:owner', 'global:admin', 'global:member']),
|
||||
async (req: WorkflowRequest.GetTags, res: express.Response): Promise<express.Response> => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (config.getEnv('workflowTagsDisabled')) {
|
||||
return res.status(400).json({ message: 'Workflow Tags Disabled' });
|
||||
}
|
||||
|
||||
const sharedWorkflow = await getSharedWorkflow(req.user, id);
|
||||
|
||||
if (!sharedWorkflow) {
|
||||
// user trying to access a workflow he does not own
|
||||
// or workflow does not exist
|
||||
return res.status(404).json({ message: 'Not Found' });
|
||||
}
|
||||
|
||||
const tags = await getWorkflowTags(id);
|
||||
|
||||
return res.json(tags);
|
||||
},
|
||||
],
|
||||
updateWorkflowTags: [
|
||||
authorize(['global:owner', 'global:admin', 'global:member']),
|
||||
async (req: WorkflowRequest.UpdateTags, res: express.Response): Promise<express.Response> => {
|
||||
const { id } = req.params;
|
||||
const newTags = req.body.map((newTag) => newTag.id);
|
||||
|
||||
if (config.getEnv('workflowTagsDisabled')) {
|
||||
return res.status(400).json({ message: 'Workflow Tags Disabled' });
|
||||
}
|
||||
|
||||
const sharedWorkflow = await getSharedWorkflow(req.user, id);
|
||||
|
||||
if (!sharedWorkflow) {
|
||||
// user trying to access a workflow he does not own
|
||||
// or workflow does not exist
|
||||
return res.status(404).json({ message: 'Not Found' });
|
||||
}
|
||||
|
||||
let tags;
|
||||
try {
|
||||
await updateTags(id, newTags);
|
||||
tags = await getWorkflowTags(id);
|
||||
} catch (error) {
|
||||
if (error instanceof QueryFailedError && error.message.includes('SQLITE_CONSTRAINT')) {
|
||||
return res.status(404).json({ message: 'Some tags not found' });
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return res.json(tags);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -2,10 +2,13 @@ import { Container } from 'typedi';
|
|||
import * as Db from '@/Db';
|
||||
import type { User } from '@db/entities/User';
|
||||
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||
import { WorkflowTagMapping } from '@db/entities/WorkflowTagMapping';
|
||||
import { SharedWorkflow, type WorkflowSharingRole } from '@db/entities/SharedWorkflow';
|
||||
import config from '@/config';
|
||||
import { WorkflowRepository } from '@db/repositories/workflow.repository';
|
||||
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
||||
import { WorkflowTagMappingRepository } from '@db/repositories/workflowTagMapping.repository';
|
||||
import { TagRepository } from '@db/repositories/tag.repository';
|
||||
|
||||
function insertIf(condition: boolean, elements: string[]): string[] {
|
||||
return condition ? elements : [];
|
||||
|
@ -86,3 +89,29 @@ export async function updateWorkflow(workflowId: string, updateData: WorkflowEnt
|
|||
export function parseTagNames(tags: string): string[] {
|
||||
return tags.split(',').map((tag) => tag.trim());
|
||||
}
|
||||
|
||||
export async function getWorkflowTags(workflowId: string) {
|
||||
return await Container.get(TagRepository).find({
|
||||
select: ['id', 'name', 'createdAt', 'updatedAt'],
|
||||
where: {
|
||||
workflowMappings: {
|
||||
...(workflowId && { workflowId }),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateTags(workflowId: string, newTags: string[]): Promise<any> {
|
||||
await Db.transaction(async (transactionManager) => {
|
||||
const oldTags = await Container.get(WorkflowTagMappingRepository).findBy({
|
||||
workflowId,
|
||||
});
|
||||
if (oldTags.length > 0) {
|
||||
await transactionManager.delete(WorkflowTagMapping, oldTags);
|
||||
}
|
||||
await transactionManager.insert(
|
||||
WorkflowTagMapping,
|
||||
newTags.map((tagId) => ({ tagId, workflowId })),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -26,6 +26,8 @@ tags:
|
|||
description: Operations about workflows
|
||||
- name: Credential
|
||||
description: Operations about credentials
|
||||
- name: Tags
|
||||
description: Operations about tags
|
||||
- name: SourceControl
|
||||
description: Operations about source control
|
||||
|
||||
|
@ -42,6 +44,10 @@ paths:
|
|||
$ref: './handlers/executions/spec/paths/executions.yml'
|
||||
/executions/{id}:
|
||||
$ref: './handlers/executions/spec/paths/executions.id.yml'
|
||||
/tags:
|
||||
$ref: './handlers/tags/spec/paths/tags.yml'
|
||||
/tags/{id}:
|
||||
$ref: './handlers/tags/spec/paths/tags.id.yml'
|
||||
/workflows:
|
||||
$ref: './handlers/workflows/spec/paths/workflows.yml'
|
||||
/workflows/{id}:
|
||||
|
@ -50,6 +56,8 @@ paths:
|
|||
$ref: './handlers/workflows/spec/paths/workflows.id.activate.yml'
|
||||
/workflows/{id}/deactivate:
|
||||
$ref: './handlers/workflows/spec/paths/workflows.id.deactivate.yml'
|
||||
/workflows/{id}/tags:
|
||||
$ref: './handlers/workflows/spec/paths/workflows.id.tags.yml'
|
||||
/users:
|
||||
$ref: './handlers/users/spec/paths/users.yml'
|
||||
/users/{id}:
|
||||
|
|
|
@ -6,6 +6,8 @@ ExecutionId:
|
|||
$ref: '../../../handlers/executions/spec/schemas/parameters/executionId.yml'
|
||||
WorkflowId:
|
||||
$ref: '../../../handlers/workflows/spec/schemas/parameters/workflowId.yml'
|
||||
TagId:
|
||||
$ref: '../../../handlers/tags/spec/schemas/parameters/tagId.yml'
|
||||
IncludeData:
|
||||
$ref: '../../../handlers/executions/spec/schemas/parameters/includeData.yml'
|
||||
UserIdentifier:
|
||||
|
|
|
@ -6,3 +6,5 @@ BadRequest:
|
|||
$ref: './badRequest.yml'
|
||||
Conflict:
|
||||
$ref: './conflict.yml'
|
||||
Forbidden:
|
||||
$ref: './forbidden.yml'
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
description: Forbidden
|
|
@ -7,7 +7,7 @@ Execution:
|
|||
Node:
|
||||
$ref: './../../../handlers/workflows/spec/schemas/node.yml'
|
||||
Tag:
|
||||
$ref: './../../../handlers/workflows/spec/schemas/tag.yml'
|
||||
$ref: './../../../handlers/tags/spec/schemas/tag.yml'
|
||||
Workflow:
|
||||
$ref: './../../../handlers/workflows/spec/schemas/workflow.yml'
|
||||
WorkflowSettings:
|
||||
|
|
|
@ -64,6 +64,12 @@ export class TagService {
|
|||
}) as Promise<GetAllResult<T>>);
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
return await this.tagRepository.findOneOrFail({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort tags based on the order of the tag IDs in the request.
|
||||
*/
|
||||
|
|
337
packages/cli/test/integration/publicApi/tags.test.ts
Normal file
337
packages/cli/test/integration/publicApi/tags.test.ts
Normal file
|
@ -0,0 +1,337 @@
|
|||
import type { SuperAgentTest } from 'supertest';
|
||||
import Container from 'typedi';
|
||||
import type { User } from '@db/entities/User';
|
||||
import { TagRepository } from '@db/repositories/tag.repository';
|
||||
|
||||
import { randomApiKey } from '../shared/random';
|
||||
import * as utils from '../shared/utils/';
|
||||
import * as testDb from '../shared/testDb';
|
||||
import { createUser } from '../shared/db/users';
|
||||
import { createTag } from '../shared/db/tags';
|
||||
|
||||
let owner: User;
|
||||
let member: User;
|
||||
let authOwnerAgent: SuperAgentTest;
|
||||
let authMemberAgent: SuperAgentTest;
|
||||
|
||||
const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] });
|
||||
|
||||
beforeAll(async () => {
|
||||
owner = await createUser({
|
||||
role: 'global:owner',
|
||||
apiKey: randomApiKey(),
|
||||
});
|
||||
|
||||
member = await createUser({
|
||||
role: 'global:member',
|
||||
apiKey: randomApiKey(),
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['Tag']);
|
||||
|
||||
authOwnerAgent = testServer.publicApiAgentFor(owner);
|
||||
authMemberAgent = testServer.publicApiAgentFor(member);
|
||||
});
|
||||
|
||||
const testWithAPIKey =
|
||||
(method: 'get' | 'post' | 'put' | 'delete', url: string, apiKey: string | null) => async () => {
|
||||
void authOwnerAgent.set({ 'X-N8N-API-KEY': apiKey });
|
||||
const response = await authOwnerAgent[method](url);
|
||||
expect(response.statusCode).toBe(401);
|
||||
};
|
||||
|
||||
describe('GET /tags', () => {
|
||||
test('should fail due to missing API Key', testWithAPIKey('get', '/tags', null));
|
||||
|
||||
test('should fail due to invalid API Key', testWithAPIKey('get', '/tags', 'abcXYZ'));
|
||||
|
||||
test('should return all tags', async () => {
|
||||
await Promise.all([createTag({}), createTag({}), createTag({})]);
|
||||
|
||||
const response = await authMemberAgent.get('/tags');
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.data.length).toBe(3);
|
||||
expect(response.body.nextCursor).toBeNull();
|
||||
|
||||
for (const tag of response.body.data) {
|
||||
const { id, name, createdAt, updatedAt } = tag;
|
||||
|
||||
expect(id).toBeDefined();
|
||||
expect(name).toBeDefined();
|
||||
expect(createdAt).toBeDefined();
|
||||
expect(updatedAt).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
test('should return all tags with pagination', async () => {
|
||||
await Promise.all([createTag({}), createTag({}), createTag({})]);
|
||||
|
||||
const response = await authMemberAgent.get('/tags?limit=1');
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.data.length).toBe(1);
|
||||
expect(response.body.nextCursor).not.toBeNull();
|
||||
|
||||
const response2 = await authMemberAgent.get(`/tags?limit=1&cursor=${response.body.nextCursor}`);
|
||||
|
||||
expect(response2.statusCode).toBe(200);
|
||||
expect(response2.body.data.length).toBe(1);
|
||||
expect(response2.body.nextCursor).not.toBeNull();
|
||||
expect(response2.body.nextCursor).not.toBe(response.body.nextCursor);
|
||||
|
||||
const responses = [...response.body.data, ...response2.body.data];
|
||||
|
||||
for (const tag of responses) {
|
||||
const { id, name, createdAt, updatedAt } = tag;
|
||||
|
||||
expect(id).toBeDefined();
|
||||
expect(name).toBeDefined();
|
||||
expect(createdAt).toBeDefined();
|
||||
expect(updatedAt).toBeDefined();
|
||||
}
|
||||
|
||||
// check that we really received a different result
|
||||
expect(response.body.data[0].id).not.toBe(response2.body.data[0].id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /tags/:id', () => {
|
||||
test('should fail due to missing API Key', testWithAPIKey('get', '/tags/gZqmqiGAuo1dHT7q', null));
|
||||
|
||||
test(
|
||||
'should fail due to invalid API Key',
|
||||
testWithAPIKey('get', '/tags/gZqmqiGAuo1dHT7q', 'abcXYZ'),
|
||||
);
|
||||
|
||||
test('should fail due to non-existing tag', async () => {
|
||||
const response = await authOwnerAgent.get('/tags/gZqmqiGAuo1dHT7q');
|
||||
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test('should retrieve tag', async () => {
|
||||
// create tag
|
||||
const tag = await createTag({});
|
||||
|
||||
const response = await authMemberAgent.get(`/tags/${tag.id}`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const { id, name, createdAt, updatedAt } = response.body;
|
||||
|
||||
expect(id).toEqual(tag.id);
|
||||
expect(name).toEqual(tag.name);
|
||||
expect(createdAt).toEqual(tag.createdAt.toISOString());
|
||||
expect(updatedAt).toEqual(tag.updatedAt.toISOString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /tags/:id', () => {
|
||||
test(
|
||||
'should fail due to missing API Key',
|
||||
testWithAPIKey('delete', '/tags/gZqmqiGAuo1dHT7q', null),
|
||||
);
|
||||
|
||||
test(
|
||||
'should fail due to invalid API Key',
|
||||
testWithAPIKey('delete', '/tags/gZqmqiGAuo1dHT7q', 'abcXYZ'),
|
||||
);
|
||||
|
||||
test('should fail due to non-existing tag', async () => {
|
||||
const response = await authOwnerAgent.delete('/tags/gZqmqiGAuo1dHT7q');
|
||||
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test('owner should delete the tag', async () => {
|
||||
// create tag
|
||||
const tag = await createTag({});
|
||||
|
||||
const response = await authOwnerAgent.delete(`/tags/${tag.id}`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const { id, name, createdAt, updatedAt } = response.body;
|
||||
|
||||
expect(id).toEqual(tag.id);
|
||||
expect(name).toEqual(tag.name);
|
||||
expect(createdAt).toEqual(tag.createdAt.toISOString());
|
||||
expect(updatedAt).toEqual(tag.updatedAt.toISOString());
|
||||
|
||||
// make sure the tag actually deleted from the db
|
||||
const deletedTag = await Container.get(TagRepository).findOneBy({
|
||||
id: tag.id,
|
||||
});
|
||||
|
||||
expect(deletedTag).toBeNull();
|
||||
});
|
||||
|
||||
test('non-owner should not delete tag', async () => {
|
||||
// create tag
|
||||
const tag = await createTag({});
|
||||
|
||||
const response = await authMemberAgent.delete(`/tags/${tag.id}`);
|
||||
|
||||
expect(response.statusCode).toBe(403);
|
||||
|
||||
const { message } = response.body;
|
||||
|
||||
expect(message).toEqual('Forbidden');
|
||||
|
||||
// make sure the tag was not deleted from the db
|
||||
const notDeletedTag = await Container.get(TagRepository).findOneBy({
|
||||
id: tag.id,
|
||||
});
|
||||
|
||||
expect(notDeletedTag).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /tags', () => {
|
||||
test('should fail due to missing API Key', testWithAPIKey('post', '/tags', null));
|
||||
|
||||
test('should fail due to invalid API Key', testWithAPIKey('post', '/tags', 'abcXYZ'));
|
||||
|
||||
test('should fail due to invalid body', async () => {
|
||||
const response = await authOwnerAgent.post('/tags').send({});
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('should create tag', async () => {
|
||||
const payload = {
|
||||
name: 'Tag 1',
|
||||
};
|
||||
|
||||
const response = await authMemberAgent.post('/tags').send(payload);
|
||||
|
||||
expect(response.statusCode).toBe(201);
|
||||
|
||||
const { id, name, createdAt, updatedAt } = response.body;
|
||||
|
||||
expect(id).toBeDefined();
|
||||
expect(name).toBe(payload.name);
|
||||
expect(createdAt).toBeDefined();
|
||||
expect(updatedAt).toEqual(createdAt);
|
||||
|
||||
// check if created tag in DB
|
||||
const tag = await Container.get(TagRepository).findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(tag?.name).toBe(name);
|
||||
expect(tag?.createdAt.toISOString()).toEqual(createdAt);
|
||||
expect(tag?.updatedAt.toISOString()).toEqual(updatedAt);
|
||||
});
|
||||
|
||||
test('should not create tag if tag with same name exists', async () => {
|
||||
const tag = {
|
||||
name: 'Tag 1',
|
||||
};
|
||||
|
||||
// create tag
|
||||
await createTag(tag);
|
||||
|
||||
const response = await authMemberAgent.post('/tags').send(tag);
|
||||
|
||||
expect(response.statusCode).toBe(409);
|
||||
|
||||
const { message } = response.body;
|
||||
|
||||
expect(message).toBe('Tag already exists');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /tags/:id', () => {
|
||||
test('should fail due to missing API Key', testWithAPIKey('put', '/tags/gZqmqiGAuo1dHT7q', null));
|
||||
|
||||
test(
|
||||
'should fail due to invalid API Key',
|
||||
testWithAPIKey('put', '/tags/gZqmqiGAuo1dHT7q', 'abcXYZ'),
|
||||
);
|
||||
|
||||
test('should fail due to non-existing tag', async () => {
|
||||
const response = await authOwnerAgent.put('/tags/gZqmqiGAuo1dHT7q').send({
|
||||
name: 'testing',
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test('should fail due to invalid body', async () => {
|
||||
const response = await authOwnerAgent.put('/tags/gZqmqiGAuo1dHT7q').send({});
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('should update tag', async () => {
|
||||
const tag = await createTag({});
|
||||
|
||||
const payload = {
|
||||
name: 'New name',
|
||||
};
|
||||
|
||||
const response = await authOwnerAgent.put(`/tags/${tag.id}`).send(payload);
|
||||
|
||||
const { id, name, updatedAt } = response.body;
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
expect(id).toBe(tag.id);
|
||||
expect(name).toBe(payload.name);
|
||||
expect(updatedAt).not.toBe(tag.updatedAt.toISOString());
|
||||
|
||||
// check updated tag in DB
|
||||
const dbTag = await Container.get(TagRepository).findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTag?.name).toBe(payload.name);
|
||||
expect(dbTag?.updatedAt.getTime()).toBeGreaterThan(tag.updatedAt.getTime());
|
||||
});
|
||||
|
||||
test('should fail if there is already a tag with a the new name', async () => {
|
||||
const toUpdateTag = await createTag({});
|
||||
const otherTag = await createTag({ name: 'Some name' });
|
||||
|
||||
const payload = {
|
||||
name: otherTag.name,
|
||||
};
|
||||
|
||||
const response = await authOwnerAgent.put(`/tags/${toUpdateTag.id}`).send(payload);
|
||||
|
||||
expect(response.statusCode).toBe(409);
|
||||
|
||||
const { message } = response.body;
|
||||
|
||||
expect(message).toBe('Tag already exists');
|
||||
|
||||
// check tags haven't be updated in DB
|
||||
const toUpdateTagFromDb = await Container.get(TagRepository).findOne({
|
||||
where: {
|
||||
id: toUpdateTag.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(toUpdateTagFromDb?.name).toEqual(toUpdateTag.name);
|
||||
expect(toUpdateTagFromDb?.createdAt.toISOString()).toEqual(toUpdateTag.createdAt.toISOString());
|
||||
expect(toUpdateTagFromDb?.updatedAt.toISOString()).toEqual(toUpdateTag.updatedAt.toISOString());
|
||||
|
||||
const otherTagFromDb = await Container.get(TagRepository).findOne({
|
||||
where: {
|
||||
id: otherTag.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(otherTagFromDb?.name).toEqual(otherTag.name);
|
||||
expect(otherTagFromDb?.createdAt.toISOString()).toEqual(otherTag.createdAt.toISOString());
|
||||
expect(otherTagFromDb?.updatedAt.toISOString()).toEqual(otherTag.updatedAt.toISOString());
|
||||
});
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
import type { SuperAgentTest } from 'supertest';
|
||||
import config from '@/config';
|
||||
import Container from 'typedi';
|
||||
import type { INode } from 'n8n-workflow';
|
||||
import { STARTING_NODES } from '@/constants';
|
||||
|
@ -250,6 +251,43 @@ describe('GET /workflows', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('should return all owned workflows filtered by name', async () => {
|
||||
const workflowName = 'Workflow 1';
|
||||
|
||||
const [workflow] = await Promise.all([
|
||||
createWorkflow({ name: workflowName }, member),
|
||||
createWorkflow({}, member),
|
||||
]);
|
||||
|
||||
const response = await authMemberAgent.get(`/workflows?name=${workflowName}`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.data.length).toBe(1);
|
||||
|
||||
const {
|
||||
id,
|
||||
connections,
|
||||
active,
|
||||
staticData,
|
||||
nodes,
|
||||
settings,
|
||||
name,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
tags: wfTags,
|
||||
} = response.body.data[0];
|
||||
|
||||
expect(id).toBeDefined();
|
||||
expect(name).toBe(workflowName);
|
||||
expect(connections).toBeDefined();
|
||||
expect(active).toBe(false);
|
||||
expect(staticData).toBeDefined();
|
||||
expect(nodes).toBeDefined();
|
||||
expect(settings).toBeDefined();
|
||||
expect(createdAt).toBeDefined();
|
||||
expect(updatedAt).toBeDefined();
|
||||
});
|
||||
|
||||
test('should return all workflows for owner', async () => {
|
||||
await Promise.all([
|
||||
createWorkflow({}, owner),
|
||||
|
@ -1111,3 +1149,308 @@ describe('PUT /workflows/:id', () => {
|
|||
expect(sharedWorkflow?.role).toEqual('workflow:owner');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /workflows/:id/tags', () => {
|
||||
test('should fail due to missing API Key', testWithAPIKey('get', '/workflows/2/tags', null));
|
||||
|
||||
test('should fail due to invalid API Key', testWithAPIKey('get', '/workflows/2/tags', 'abcXYZ'));
|
||||
|
||||
test('should fail if workflowTagsDisabled', async () => {
|
||||
config.set('workflowTagsDisabled', true);
|
||||
|
||||
const response = await authOwnerAgent.get('/workflows/2/tags');
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body.message).toBe('Workflow Tags Disabled');
|
||||
});
|
||||
|
||||
test('should fail due to non-existing workflow', async () => {
|
||||
config.set('workflowTagsDisabled', false);
|
||||
|
||||
const response = await authOwnerAgent.get('/workflows/2/tags');
|
||||
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test('should return all tags of owned workflow', async () => {
|
||||
config.set('workflowTagsDisabled', false);
|
||||
|
||||
const tags = await Promise.all([await createTag({}), await createTag({})]);
|
||||
|
||||
const workflow = await createWorkflow({ tags }, member);
|
||||
|
||||
const response = await authMemberAgent.get(`/workflows/${workflow.id}/tags`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.length).toBe(2);
|
||||
|
||||
for (const tag of response.body) {
|
||||
const { id, name, createdAt, updatedAt } = tag;
|
||||
|
||||
expect(id).toBeDefined();
|
||||
expect(name).toBeDefined();
|
||||
expect(createdAt).toBeDefined();
|
||||
expect(updatedAt).toBeDefined();
|
||||
|
||||
tags.forEach((tag: TagEntity) => {
|
||||
expect(tags.some((savedTag) => savedTag.id === tag.id)).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should return empty array if workflow does not have tags', async () => {
|
||||
config.set('workflowTagsDisabled', false);
|
||||
|
||||
const workflow = await createWorkflow({}, member);
|
||||
|
||||
const response = await authMemberAgent.get(`/workflows/${workflow.id}/tags`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /workflows/:id/tags', () => {
|
||||
test('should fail due to missing API Key', testWithAPIKey('put', '/workflows/2/tags', null));
|
||||
|
||||
test('should fail due to invalid API Key', testWithAPIKey('put', '/workflows/2/tags', 'abcXYZ'));
|
||||
|
||||
test('should fail if workflowTagsDisabled', async () => {
|
||||
config.set('workflowTagsDisabled', true);
|
||||
|
||||
const response = await authOwnerAgent.put('/workflows/2/tags').send([]);
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body.message).toBe('Workflow Tags Disabled');
|
||||
});
|
||||
|
||||
test('should fail due to non-existing workflow', async () => {
|
||||
config.set('workflowTagsDisabled', false);
|
||||
|
||||
const response = await authOwnerAgent.put('/workflows/2/tags').send([]);
|
||||
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test('should add the tags, workflow have not got tags previously', async () => {
|
||||
config.set('workflowTagsDisabled', false);
|
||||
|
||||
const workflow = await createWorkflow({}, member);
|
||||
const tags = await Promise.all([await createTag({}), await createTag({})]);
|
||||
|
||||
const payload = [
|
||||
{
|
||||
id: tags[0].id,
|
||||
},
|
||||
{
|
||||
id: tags[1].id,
|
||||
},
|
||||
];
|
||||
|
||||
const response = await authMemberAgent.put(`/workflows/${workflow.id}/tags`).send(payload);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.length).toBe(2);
|
||||
|
||||
for (const tag of response.body) {
|
||||
const { id, name, createdAt, updatedAt } = tag;
|
||||
|
||||
expect(id).toBeDefined();
|
||||
expect(name).toBeDefined();
|
||||
expect(createdAt).toBeDefined();
|
||||
expect(updatedAt).toBeDefined();
|
||||
|
||||
tags.forEach((tag: TagEntity) => {
|
||||
expect(tags.some((savedTag) => savedTag.id === tag.id)).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
// Check the association in DB
|
||||
const sharedWorkflow = await Container.get(SharedWorkflowRepository).findOne({
|
||||
where: {
|
||||
userId: member.id,
|
||||
workflowId: workflow.id,
|
||||
},
|
||||
relations: ['workflow.tags'],
|
||||
});
|
||||
|
||||
expect(sharedWorkflow?.workflow.tags).toBeDefined();
|
||||
expect(sharedWorkflow?.workflow.tags?.length).toBe(2);
|
||||
if (sharedWorkflow?.workflow.tags !== undefined) {
|
||||
for (const tag of sharedWorkflow?.workflow.tags) {
|
||||
const { id, name, createdAt, updatedAt } = tag;
|
||||
|
||||
expect(id).toBeDefined();
|
||||
expect(name).toBeDefined();
|
||||
expect(createdAt).toBeDefined();
|
||||
expect(updatedAt).toBeDefined();
|
||||
|
||||
tags.forEach((tag: TagEntity) => {
|
||||
expect(tags.some((savedTag) => savedTag.id === tag.id)).toBe(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should add the tags, workflow have some tags previously', async () => {
|
||||
config.set('workflowTagsDisabled', false);
|
||||
|
||||
const tags = await Promise.all([await createTag({}), await createTag({}), await createTag({})]);
|
||||
const oldTags = [tags[0], tags[1]];
|
||||
const newTags = [tags[0], tags[2]];
|
||||
const workflow = await createWorkflow({ tags: oldTags }, member);
|
||||
|
||||
// Check the association in DB
|
||||
const oldSharedWorkflow = await Container.get(SharedWorkflowRepository).findOne({
|
||||
where: {
|
||||
userId: member.id,
|
||||
workflowId: workflow.id,
|
||||
},
|
||||
relations: ['workflow.tags'],
|
||||
});
|
||||
|
||||
expect(oldSharedWorkflow?.workflow.tags).toBeDefined();
|
||||
expect(oldSharedWorkflow?.workflow.tags?.length).toBe(2);
|
||||
if (oldSharedWorkflow?.workflow.tags !== undefined) {
|
||||
for (const tag of oldSharedWorkflow?.workflow.tags) {
|
||||
const { id, name, createdAt, updatedAt } = tag;
|
||||
|
||||
expect(id).toBeDefined();
|
||||
expect(name).toBeDefined();
|
||||
expect(createdAt).toBeDefined();
|
||||
expect(updatedAt).toBeDefined();
|
||||
|
||||
oldTags.forEach((tag: TagEntity) => {
|
||||
expect(oldTags.some((savedTag) => savedTag.id === tag.id)).toBe(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const payload = [
|
||||
{
|
||||
id: newTags[0].id,
|
||||
},
|
||||
{
|
||||
id: newTags[1].id,
|
||||
},
|
||||
];
|
||||
|
||||
const response = await authMemberAgent.put(`/workflows/${workflow.id}/tags`).send(payload);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.length).toBe(2);
|
||||
|
||||
for (const tag of response.body) {
|
||||
const { id, name, createdAt, updatedAt } = tag;
|
||||
|
||||
expect(id).toBeDefined();
|
||||
expect(name).toBeDefined();
|
||||
expect(createdAt).toBeDefined();
|
||||
expect(updatedAt).toBeDefined();
|
||||
|
||||
newTags.forEach((tag: TagEntity) => {
|
||||
expect(newTags.some((savedTag) => savedTag.id === tag.id)).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
// Check the association in DB
|
||||
const sharedWorkflow = await Container.get(SharedWorkflowRepository).findOne({
|
||||
where: {
|
||||
userId: member.id,
|
||||
workflowId: workflow.id,
|
||||
},
|
||||
relations: ['workflow.tags'],
|
||||
});
|
||||
|
||||
expect(sharedWorkflow?.workflow.tags).toBeDefined();
|
||||
expect(sharedWorkflow?.workflow.tags?.length).toBe(2);
|
||||
if (sharedWorkflow?.workflow.tags !== undefined) {
|
||||
for (const tag of sharedWorkflow?.workflow.tags) {
|
||||
const { id, name, createdAt, updatedAt } = tag;
|
||||
|
||||
expect(id).toBeDefined();
|
||||
expect(name).toBeDefined();
|
||||
expect(createdAt).toBeDefined();
|
||||
expect(updatedAt).toBeDefined();
|
||||
|
||||
newTags.forEach((tag: TagEntity) => {
|
||||
expect(newTags.some((savedTag) => savedTag.id === tag.id)).toBe(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should fail to add the tags as one does not exist, workflow should maintain previous tags', async () => {
|
||||
config.set('workflowTagsDisabled', false);
|
||||
|
||||
const tags = await Promise.all([await createTag({}), await createTag({})]);
|
||||
const oldTags = [tags[0], tags[1]];
|
||||
const workflow = await createWorkflow({ tags: oldTags }, member);
|
||||
|
||||
// Check the association in DB
|
||||
const oldSharedWorkflow = await Container.get(SharedWorkflowRepository).findOne({
|
||||
where: {
|
||||
userId: member.id,
|
||||
workflowId: workflow.id,
|
||||
},
|
||||
relations: ['workflow.tags'],
|
||||
});
|
||||
|
||||
expect(oldSharedWorkflow?.workflow.tags).toBeDefined();
|
||||
expect(oldSharedWorkflow?.workflow.tags?.length).toBe(2);
|
||||
if (oldSharedWorkflow?.workflow.tags !== undefined) {
|
||||
for (const tag of oldSharedWorkflow?.workflow.tags) {
|
||||
const { id, name, createdAt, updatedAt } = tag;
|
||||
|
||||
expect(id).toBeDefined();
|
||||
expect(name).toBeDefined();
|
||||
expect(createdAt).toBeDefined();
|
||||
expect(updatedAt).toBeDefined();
|
||||
|
||||
oldTags.forEach((tag: TagEntity) => {
|
||||
expect(oldTags.some((savedTag) => savedTag.id === tag.id)).toBe(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const payload = [
|
||||
{
|
||||
id: oldTags[0].id,
|
||||
},
|
||||
{
|
||||
id: 'TagDoesNotExist',
|
||||
},
|
||||
];
|
||||
|
||||
const response = await authMemberAgent.put(`/workflows/${workflow.id}/tags`).send(payload);
|
||||
|
||||
expect(response.statusCode).toBe(404);
|
||||
expect(response.body.message).toBe('Some tags not found');
|
||||
|
||||
// Check the association in DB
|
||||
const sharedWorkflow = await Container.get(SharedWorkflowRepository).findOne({
|
||||
where: {
|
||||
userId: member.id,
|
||||
workflowId: workflow.id,
|
||||
},
|
||||
relations: ['workflow.tags'],
|
||||
});
|
||||
|
||||
expect(sharedWorkflow?.workflow.tags).toBeDefined();
|
||||
expect(sharedWorkflow?.workflow.tags?.length).toBe(2);
|
||||
if (sharedWorkflow?.workflow.tags !== undefined) {
|
||||
for (const tag of sharedWorkflow?.workflow.tags) {
|
||||
const { id, name, createdAt, updatedAt } = tag;
|
||||
|
||||
expect(id).toBeDefined();
|
||||
expect(name).toBeDefined();
|
||||
expect(createdAt).toBeDefined();
|
||||
expect(updatedAt).toBeDefined();
|
||||
|
||||
oldTags.forEach((tag: TagEntity) => {
|
||||
expect(oldTags.some((savedTag) => savedTag.id === tag.id)).toBe(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue