diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 6e373b103d..8d0281b97c 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -585,20 +585,7 @@ class App { return undefined; } - result.tags = await getConnection() - .createQueryBuilder() - .select('tag_entity.id', 'id') - .addSelect('tag_entity.name', 'name') - .from('tag_entity', 'tag_entity') - .where(qb => { - return "id IN " + qb.subQuery() - .select('tagId') - .from('workflow_entity', 'workflow_entity') - .leftJoin('workflows_tags', 'workflows_tags', 'workflows_tags.workflowId = workflow_entity.id') - .where("workflow_entity.id = :id", { id: Number(req.params.id) }) - .getQuery(); - }) - .getRawMany(); + result.tags = await TagHelpers.getWorkflowTags(req.params.id); // Convert to response format in which the id is a string (result as IWorkflowBase as IWorkflowResponse).id = result.id.toString(); @@ -718,40 +705,6 @@ class App { return true; })); - // Adds a tag to a workflow - this.app.post(`/${this.restEndpoint}/workflows/:workflowId/tags/:tagId`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<{ workflowId: number, tagId: number }> => { - const workflowId = Number(req.params.workflowId); - const tagId = Number(req.params.tagId); - - await TagHelpers.validateId(tagId); - await TagHelpers.validateNoRelation(workflowId, tagId); - - await getConnection().createQueryBuilder() - .insert() - .into('workflows_tags') - .values([ { workflowId, tagId } ]) - .execute(); - - return { workflowId, tagId }; - })); - - // Removes a tag from a workflow - this.app.delete(`/${this.restEndpoint}/workflows/:workflowId/tags/:tagId`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const workflowId = Number(req.params.workflowId); - const tagId = Number(req.params.tagId); - - await TagHelpers.validateId(tagId); - await TagHelpers.validateRelation(workflowId, tagId); - - await getConnection().createQueryBuilder() - .delete() - .from('workflows_tags') - .where('workflowId = :workflowId AND tagId = :tagId', { workflowId, tagId }) - .execute(); - - return true; - })); - this.app.post(`/${this.restEndpoint}/workflows/run`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const workflowData = req.body.workflowData; const runData: IRunData | undefined = req.body.runData; @@ -801,26 +754,9 @@ class App { // Retrieves all tags, with or without usage count this.app.get(`/${this.restEndpoint}/tags`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise> => { - const withUsageCount = req.query.withUsageCount === 'true'; - - if (withUsageCount) { - return await getConnection().createQueryBuilder() - .select('tag_entity.id', 'id') - .addSelect('tag_entity.name', 'name') - .addSelect('COUNT(workflow_entity.id)', 'usageCount') - .from('tag_entity', 'tag_entity') - .leftJoin('workflows_tags', 'workflows_tags', 'workflows_tags.tagId = tag_entity.id') - .leftJoin('workflow_entity', 'workflow_entity', 'workflows_tags.workflowId = workflow_entity.id') - .groupBy('tag_entity.id') - .getRawMany(); - } - - return await getConnection().createQueryBuilder() - .select('tag_entity.id', 'id') - .addSelect('tag_entity.name', 'name') - .from('tag_entity', 'tag_entity') - .groupBy('tag_entity.id') - .getRawMany(); + return req.query.withUsageCount === 'true' + ? await TagHelpers.getAllTagsWithUsageCount() + : await TagHelpers.getAllTags(); })); // Creates a tag @@ -828,8 +764,8 @@ class App { TagHelpers.validateRequestBody(req.body); const { name } = req.body; - await TagHelpers.validateName(name); TagHelpers.validateLength(name); + await TagHelpers.validateName(name); const newTag: ITagBase = { name, @@ -855,8 +791,8 @@ class App { TagHelpers.validateRequestBody(req.body); const { name } = req.body; - await TagHelpers.validateName(name); TagHelpers.validateLength(name); + await TagHelpers.validateName(name); const id = Number(req.params.id); await TagHelpers.validateId(id); diff --git a/packages/cli/src/TagHelpers.ts b/packages/cli/src/TagHelpers.ts index 0905864878..e66ad778d2 100644 --- a/packages/cli/src/TagHelpers.ts +++ b/packages/cli/src/TagHelpers.ts @@ -1,5 +1,4 @@ import { - FindManyOptions, FindOneOptions, getConnection, In, @@ -10,17 +9,10 @@ import { ResponseHelper, } from "."; -/** - * Validate whether a tag name exists so that it cannot be used for a tag create or tag update operation. - */ -export async function validateName(name: string): Promise | never { - 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); - } -} +// ---------------------------------- +// validators +// ---------------------------------- /** * Validate whether a tag ID exists so that it can be used for a workflow create or tag update operation. @@ -44,11 +36,14 @@ export function validateLength(name: string): void | never { } /** - * Validate whether the request body for a create/update operation has a `name` property. + * Validate whether a tag name exists so that it cannot be used for a tag create or tag update operation. */ -export function validateRequestBody({ name }: { name: string }): void | never { - if (!name) { - throw new ResponseHelper.ResponseError(`Property 'name' missing from request body.`, undefined, 400); +export async function validateName(name: string): Promise | never { + 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); } } @@ -56,7 +51,7 @@ export function validateRequestBody({ name }: { name: string }): void | never { * 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 | never { - const result = await findRelation(workflowId, tagId); + const result = await findRelations(workflowId, tagId); if (result.length) { throw new ResponseHelper.ResponseError(`Workflow ID ${workflowId} and tag ID ${tagId} are already related.`, undefined, 400); @@ -67,7 +62,7 @@ export async function validateNoRelation(workflowId: number, tagId: number): Pro * 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 | never { - const result = await findRelation(workflowId, tagId); + const result = await findRelations(workflowId, tagId); if (!result.length) { throw new ResponseHelper.ResponseError(`Workflow ID ${workflowId} and tag ID ${tagId} are not related.`, undefined, 400); @@ -75,9 +70,70 @@ export async function validateRelation(workflowId: number, tagId: number): Promi } /** - * Find a relation between a workflow and a tag, if any. + * Validate whether the request body for a create/update operation has a `name` property. */ -async function findRelation(workflowId: number, tagId: number): Promise> { +export function validateRequestBody({ name }: { name: string | undefined }): void | never { + if (!name) { + throw new ResponseHelper.ResponseError(`Property 'name' missing from request body.`, undefined, 400); + } +} + + +// ---------------------------------- +// queries +// ---------------------------------- + +/** + * Retrieve all existing tags, whether linked to a workflow or not. + */ +export async function getAllTags(): Promise> { + return await getConnection().createQueryBuilder() + .select('tag_entity.id', 'id') + .addSelect('tag_entity.name', 'name') + .from('tag_entity', 'tag_entity') + .groupBy('tag_entity.id') + .getRawMany(); +} + +/** + * Retrieve all existing tags, whether linked to a workflow or not, + * including how many workflows each tag is linked to. + */ +export async function getAllTagsWithUsageCount(): Promise> { + return await getConnection().createQueryBuilder() + .select('tag_entity.id', 'id') + .addSelect('tag_entity.name', 'name') + .addSelect('COUNT(workflow_entity.id)', 'usageCount') + .from('tag_entity', 'tag_entity') + .leftJoin('workflows_tags', 'workflows_tags', 'workflows_tags.tagId = tag_entity.id') + .leftJoin('workflow_entity', 'workflow_entity', 'workflows_tags.workflowId = workflow_entity.id') + .groupBy('tag_entity.id') + .getRawMany(); +} + +/** + * Retrieve tag IDs and names, to be used in an API response. + */ +export async function getTagsForResponseData( + tagIds: number[] +): Promise> { + return await Db.collections.Tag!.find({ + select: ['id', 'name'], + where: { id: In(tagIds) }, + }); +} + +/** + * Find if a workflow and a tag are related. + */ +async function findRelations( + workflowId: number, + tagId: number +): Promise> { return await getConnection().createQueryBuilder() .select() .from('workflows_tags', 'workflows_tags') @@ -85,6 +141,43 @@ async function findRelation(workflowId: number, tagId: number): Promise> { + return await getConnection().createQueryBuilder() + .select('tag_entity.id', 'id') + .addSelect('tag_entity.name', 'name') + .from('tag_entity', 'tag_entity') + .where(qb => { + return "id IN " + qb.subQuery() + .select('tagId') + .from('workflow_entity', 'workflow_entity') + .leftJoin('workflows_tags', 'workflows_tags', 'workflows_tags.workflowId = workflow_entity.id') + .where("workflow_entity.id = :id", { id: workflowId }) + .getQuery(); + }) + .getRawMany(); +} + + +// ---------------------------------- +// mutations +// ---------------------------------- + +/** + * Link a workflow to one or more tags. + */ +export async function createTagWorkflowRelations(workflowId: string, tagIds: number[]) { + await getConnection().createQueryBuilder() + .insert() + .into('workflows_tags') + .values(tagIds.map(tagId => ({ workflowId, tagId }))) + .execute(); +} + /** * Remove all tags for a workflow during a tag update operation. */ @@ -95,24 +188,3 @@ export async function deleteAllTagsForWorkflow(workflowId: string) { .where('workflowId = :id', { id: workflowId }) .execute(); } - -/** - * Associate a workflow with one or many tags. - */ -export async function createTagWorkflowRelations(workflowId: string, tagIds: number[]) { - await getConnection().createQueryBuilder() - .insert() - .into('workflows_tags') - .values(tagIds.map(tagId => ({ workflowId, tagId }))) - .execute(); -} - -/** - * Return tag IDs and names, only for use in a response. - */ -export async function getTagsForResponseData(tagIds: number[]) { - return await Db.collections.Tag!.find({ - select: ['id', 'name'], - where: { id: In(tagIds) }, - }); -}