diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 357e109278247..24cf19b88a8bd 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-cycle */ /* eslint-disable prefer-spread */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable no-param-reassign */ @@ -47,6 +48,9 @@ import { WorkflowRunner, } from '.'; import config = require('../config'); +import { User } from './databases/entities/User'; +import { whereClause } from './WorkflowHelpers'; +import { WorkflowEntity } from './databases/entities/WorkflowEntity'; const WEBHOOK_PROD_UNREGISTERED_HINT = `The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)`; @@ -333,20 +337,30 @@ export class ActiveWorkflowRunner { * @returns {string[]} * @memberof ActiveWorkflowRunner */ - async getActiveWorkflows(userId?: string): Promise { - // TODO UM: make userId mandatory? - const queryBuilder = Db.collections.Workflow!.createQueryBuilder('w'); - queryBuilder.andWhere('active = :active', { active: true }); - queryBuilder.select('w.id'); - if (userId) { - queryBuilder.innerJoin('w.shared', 'shared'); - queryBuilder.andWhere('shared.user = :userId', { userId }); + async getActiveWorkflows(user?: User): Promise { + let activeWorkflows: WorkflowEntity[] = []; + + if (!user || user.globalRole.name === 'owner') { + activeWorkflows = await Db.collections.Workflow!.find({ + select: ['id'], + where: { active: true }, + }); + } else { + const shared = await Db.collections.SharedWorkflow!.find({ + relations: ['workflow'], + where: whereClause({ + user, + entityType: 'workflow', + }), + }); + + activeWorkflows = shared.reduce((acc, cur) => { + if (cur.workflow.active) acc.push(cur.workflow); + return acc; + }, []); } - const activeWorkflows = (await queryBuilder.getMany()) as IWorkflowDb[]; - return activeWorkflows.filter( - (workflow) => this.activationErrors[workflow.id.toString()] === undefined, - ); + return activeWorkflows.filter((workflow) => this.activationErrors[workflow.id] === undefined); } /** diff --git a/packages/cli/src/ResponseHelper.ts b/packages/cli/src/ResponseHelper.ts index e8430c695d9fd..dd61f0bbea49d 100644 --- a/packages/cli/src/ResponseHelper.ts +++ b/packages/cli/src/ResponseHelper.ts @@ -133,6 +133,9 @@ export function sendErrorResponse(res: Response, error: ResponseError, shouldLog res.status(httpStatusCode).json(response); } +const isUniqueConstraintError = (error: Error) => + ['unique', 'duplicate'].some((s) => error.message.toLowerCase().includes(s)); + /** * A helper function which does not just allow to return Promises it also makes sure that * all the responses have the same format @@ -148,10 +151,12 @@ export function send(processFunction: (req: Request, res: Response) => Promise => { - delete req.body.id; // ignore if sent by mistake - const incomingData = req.body; + ResponseHelper.send(async (req: WorkflowRequest.Create) => { + delete req.body.id; // delete if sent - const newWorkflow = new WorkflowEntity(); + const newWorkflow = new WorkflowEntity(); - Object.assign(newWorkflow, incomingData); - newWorkflow.name = incomingData.name.trim(); + Object.assign(newWorkflow, req.body); - const incomingTagOrder = incomingData.tags.slice(); + await WorkflowHelpers.validateWorkflow(newWorkflow); - if (incomingData.tags.length) { - newWorkflow.tags = await Db.collections.Tag!.findByIds(incomingData.tags, { - select: ['id', 'name'], - }); - } + await this.externalHooks.run('workflow.create', [newWorkflow]); - // check credentials for old format - await WorkflowHelpers.replaceInvalidCredentials(newWorkflow); + const { tags: tagIds } = req.body; - await this.externalHooks.run('workflow.create', [newWorkflow]); + if (tagIds?.length) { + newWorkflow.tags = await Db.collections.Tag!.findByIds(tagIds, { + select: ['id', 'name'], + }); + } - await WorkflowHelpers.validateWorkflow(newWorkflow); - const savedWorkflow = (await Db.collections - .Workflow!.save(newWorkflow) - .catch(WorkflowHelpers.throwDuplicateEntryError)) as WorkflowEntity; - savedWorkflow.tags = TagHelpers.sortByRequestOrder(savedWorkflow.tags, incomingTagOrder); - try { - await UserManagementHelpers.saveWorkflowOwnership(savedWorkflow, req.user as User); - } catch (error) { - // TODO UM: decide if this is fatal and we must rollback or - // log and treat it elsewhere. - LoggerProxy.debug('Error saving workflow ownership', { error }); - } + await WorkflowHelpers.replaceInvalidCredentials(newWorkflow); - // @ts-ignore - savedWorkflow.id = savedWorkflow.id.toString(); - await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]); - void InternalHooksManager.getInstance().onWorkflowCreated(newWorkflow as IWorkflowBase); - return savedWorkflow; - }, - ), + let savedWorkflow: undefined | WorkflowEntity; + + await getConnection().transaction(async (transactionManager) => { + savedWorkflow = await transactionManager.save(newWorkflow); + + const role = await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'workflow', + }); + + const newSharedWorkflow = new SharedWorkflow(); + + Object.assign(newSharedWorkflow, { + role, + user: req.user, + workflow: savedWorkflow, + }); + + await transactionManager.save(newSharedWorkflow); + }); + + if (!savedWorkflow) { + throw new ResponseHelper.ResponseError('Failed to save workflow', undefined, 500); + } + + if (tagIds) { + savedWorkflow.tags = TagHelpers.sortByRequestOrder(savedWorkflow.tags, { + requestOrder: tagIds, + }); + } + + await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]); + void InternalHooksManager.getInstance().onWorkflowCreated(newWorkflow); + + const { id, ...rest } = savedWorkflow; + + return { id: id.toString(), ...rest }; + }), ); // Reads and returns workflow data from an URL @@ -793,236 +811,257 @@ class App { // Returns workflows this.app.get( `/${this.restEndpoint}/workflows`, - ResponseHelper.send(async (req: express.Request, res: express.Response) => { - const queryBuilder = Db.collections.Workflow!.createQueryBuilder('w'); - queryBuilder.select(['w.id', 'w.name', 'w.active', 'w.createdAt', 'w.updatedAt']); - queryBuilder.leftJoinAndSelect('w.tags', 't'); - - if (req.query.filter) { - const jsonFilters = JSON.parse(req.query.filter as string); - const keys = Object.keys(jsonFilters); - keys.forEach((key) => { - queryBuilder.andWhere(`w.${key} = :${key}`, { [key]: jsonFilters[key] }); + ResponseHelper.send(async (req: WorkflowRequest.GetAll) => { + let workflows: WorkflowEntity[] = []; + + const filter: Record = req.query.filter ? JSON.parse(req.query.filter) : {}; + + if (req.user.globalRole.name === 'owner') { + workflows = await Db.collections.Workflow!.find({ + select: ['id', 'name', 'active', 'createdAt', 'updatedAt'], + relations: ['tags'], + where: filter, + }); + } else { + const shared = await Db.collections.SharedWorkflow!.find({ + relations: ['workflow', 'workflow.tags'], + where: whereClause({ + user: req.user, + entityType: 'workflow', + }), + }); + + if (!shared.length) return []; + + workflows = await Db.collections.Workflow!.find({ + relations: ['tags'], + select: ['id', 'name', 'active', 'createdAt', 'updatedAt'], + where: { + id: In(shared.map(({ workflow }) => workflow.id)), + ...filter, + }, }); } - // Simplified checks for now. Get only - // workflows I have access to. - queryBuilder.innerJoin('w.shared', 'shared'); - queryBuilder.andWhere('shared.userId = :userId', { userId: (req.user as User).id }); - const workflows = await queryBuilder.getMany(); + return workflows.map((workflow) => { + const { id, tags, ...rest } = workflow; - workflows.forEach((workflow) => { - // @ts-ignore - workflow.id = workflow.id.toString(); - // @ts-ignore - workflow.tags = workflow.tags.map(({ id, name }) => ({ id: id.toString(), name })); + return { + id: id.toString(), + ...rest, + tags: tags?.map(({ id, ...rest }) => ({ id: id.toString(), ...rest })) ?? [], + }; }); - return workflows; }), ); this.app.get( `/${this.restEndpoint}/workflows/new`, - ResponseHelper.send( - async (req: NameRequest, res: express.Response): Promise<{ name: string }> => { - const requestedName = - req.query.name && req.query.name !== '' ? req.query.name : this.defaultWorkflowName; + ResponseHelper.send(async (req: WorkflowRequest.NewName) => { + const requestedName = + req.query.name && req.query.name !== '' ? req.query.name : this.defaultWorkflowName; - return await GenericHelpers.generateUniqueName(requestedName, 'workflow'); - }, - ), + return await GenericHelpers.generateUniqueName(requestedName, 'workflow'); + }), ); // Returns a specific workflow this.app.get( `/${this.restEndpoint}/workflows/:id`, - ResponseHelper.send( - async ( - req: express.Request, - res: express.Response, - ): Promise => { - const qb = Db.collections.Workflow!.createQueryBuilder('w'); - qb.leftJoinAndSelect('w.tags', 't'); - qb.andWhere('w.id = :workflowId', { workflowId: req.params.id }); - - qb.innerJoin('w.shared', 'shared'); - // @ts-ignore - qb.andWhere('shared.userId = :userId', { userId: req.user.id }); + ResponseHelper.send(async (req: WorkflowRequest.Get) => { + const { id: workflowId } = req.params; + + const shared = await Db.collections.SharedWorkflow!.findOne({ + relations: ['workflow', 'workflow.tags'], + where: whereClause({ + user: req.user, + entityType: 'workflow', + entityId: workflowId, + }), + }); - const workflow = await qb.getOne(); + if (!shared) return {}; - if (workflow === undefined) { - return undefined; - } + const { + workflow: { id, tags, ...rest }, + } = shared; - // @ts-ignore - workflow.id = workflow.id.toString(); - // @ts-ignore - workflow.tags.forEach((tag) => (tag.id = tag.id.toString())); - return workflow; - }, - ), + return { + id: id.toString(), + ...rest, + tags: tags?.map(({ id, ...rest }) => ({ id: id.toString(), ...rest })) ?? [], + }; + }), ); // Updates an existing workflow this.app.patch( `/${this.restEndpoint}/workflows/:id`, - ResponseHelper.send( - async (req: express.Request, res: express.Response): Promise => { - const { tags, ...updateData } = req.body; + ResponseHelper.send(async (req: WorkflowRequest.Update) => { + const { id: workflowId } = req.params; + + const updateData = new WorkflowEntity(); + const { tags, ...rest } = req.body; + Object.assign(updateData, rest); + + const shared = await Db.collections.SharedWorkflow!.findOne({ + relations: ['workflow'], + where: whereClause({ + user: req.user, + entityType: 'workflow', + entityId: workflowId, + }), + }); - const { id } = req.params; - updateData.id = id; + if (!shared) { + throw new ResponseHelper.ResponseError( + `Workflow with ID "${workflowId}" could not be found to be updated.`, + undefined, + 404, + ); + } - let isActive = false; - const qb = Db.collections.Workflow!.createQueryBuilder('w'); - qb.andWhere('w.id = :workflowId', { workflowId: req.params.id }); - qb.innerJoin('w.shared', 'shared'); - // @ts-ignore - qb.andWhere('shared.userId = :userId', { userId: req.user.id }); - const workflowSearch = await qb.getOne(); - if (workflowSearch) { - isActive = workflowSearch.active; - } else { - throw new ResponseHelper.ResponseError( - `Workflow with id "${id}" could not be found to be updated.`, - undefined, - 404, - ); - } + // check credentials for old format + await WorkflowHelpers.replaceInvalidCredentials(updateData); - // check credentials for old format - await WorkflowHelpers.replaceInvalidCredentials(updateData as WorkflowEntity); + await this.externalHooks.run('workflow.update', [updateData]); - await this.externalHooks.run('workflow.update', [updateData]); + const isActive = await this.activeWorkflowRunner.isActive(workflowId); - if (isActive) { - // When workflow gets saved always remove it as the triggers could have been - // changed and so the changes would not take effect - await this.activeWorkflowRunner.remove(id); - } + if (isActive) { + // When workflow gets saved always remove it as the triggers could have been + // changed and so the changes would not take effect + await this.activeWorkflowRunner.remove(workflowId); + } - if (updateData.settings) { - if (updateData.settings.timezone === 'DEFAULT') { - // Do not save the default timezone - delete updateData.settings.timezone; - } - if (updateData.settings.saveDataErrorExecution === 'DEFAULT') { - // Do not save when default got set - delete updateData.settings.saveDataErrorExecution; - } - if (updateData.settings.saveDataSuccessExecution === 'DEFAULT') { - // Do not save when default got set - delete updateData.settings.saveDataSuccessExecution; - } - if (updateData.settings.saveManualExecutions === 'DEFAULT') { - // Do not save when default got set - delete updateData.settings.saveManualExecutions; - } - if ( - parseInt(updateData.settings.executionTimeout as string, 10) === this.executionTimeout - ) { - // Do not save when default got set - delete updateData.settings.executionTimeout; - } + if (updateData.settings) { + if (updateData.settings.timezone === 'DEFAULT') { + // Do not save the default timezone + delete updateData.settings.timezone; + } + if (updateData.settings.saveDataErrorExecution === 'DEFAULT') { + // Do not save when default got set + delete updateData.settings.saveDataErrorExecution; + } + if (updateData.settings.saveDataSuccessExecution === 'DEFAULT') { + // Do not save when default got set + delete updateData.settings.saveDataSuccessExecution; + } + if (updateData.settings.saveManualExecutions === 'DEFAULT') { + // Do not save when default got set + delete updateData.settings.saveManualExecutions; } + if ( + parseInt(updateData.settings.executionTimeout as string, 10) === this.executionTimeout + ) { + // Do not save when default got set + delete updateData.settings.executionTimeout; + } + } - // required due to atomic update - updateData.updatedAt = this.getCurrentDate(); + // required due to atomic update + updateData.updatedAt = this.getCurrentDate(); - await WorkflowHelpers.validateWorkflow(updateData); - await Db.collections - .Workflow!.update(id, updateData) - .catch(WorkflowHelpers.throwDuplicateEntryError); + await WorkflowHelpers.validateWorkflow(updateData); - if (tags) { - const tablePrefix = config.get('database.tablePrefix'); - await TagHelpers.removeRelations(req.params.id, tablePrefix); + await Db.collections.Workflow!.update(workflowId, updateData); - if (tags.length) { - await TagHelpers.createRelations(req.params.id, tags, tablePrefix); - } + if (tags) { + const tablePrefix = config.get('database.tablePrefix'); + await TagHelpers.removeRelations(workflowId, tablePrefix); + + if (tags.length) { + await TagHelpers.createRelations(workflowId, tags, tablePrefix); } + } - // We sadly get nothing back from "update". Neither if it updated a record - // nor the new value. So query now the hopefully updated entry. - const workflow = await Db.collections.Workflow!.findOne(id, { relations: ['tags'] }); + // We sadly get nothing back from "update". Neither if it updated a record + // nor the new value. So query now the hopefully updated entry. + const updatedWorkflow = await Db.collections.Workflow!.findOne(workflowId, { + relations: ['tags'], + }); - if (workflow === undefined) { - throw new ResponseHelper.ResponseError( - `Workflow with id "${id}" could not be found to be updated.`, - undefined, - 400, - ); - } + if (updatedWorkflow === undefined) { + throw new ResponseHelper.ResponseError( + `Workflow with ID "${workflowId}" could not be found to be updated.`, + undefined, + 400, + ); + } - if (tags?.length) { - workflow.tags = TagHelpers.sortByRequestOrder(workflow.tags, tags); - } + if (req.body.tags?.length) { + updatedWorkflow.tags = TagHelpers.sortByRequestOrder(updatedWorkflow.tags, { + requestOrder: req.body.tags, + }); + } - await this.externalHooks.run('workflow.afterUpdate', [workflow]); - void InternalHooksManager.getInstance().onWorkflowSaved(workflow as IWorkflowBase); + await this.externalHooks.run('workflow.afterUpdate', [updatedWorkflow]); + void InternalHooksManager.getInstance().onWorkflowSaved(updatedWorkflow as IWorkflowBase); - if (workflow.active) { - // When the workflow is supposed to be active add it again - try { - await this.externalHooks.run('workflow.activate', [workflow]); - await this.activeWorkflowRunner.add(id, isActive ? 'update' : 'activate'); - } catch (error) { - // If workflow could not be activated set it again to inactive - updateData.active = false; - // @ts-ignore - await Db.collections.Workflow!.update(id, updateData); + if (updatedWorkflow.active) { + // When the workflow is supposed to be active add it again + try { + await this.externalHooks.run('workflow.activate', [updatedWorkflow]); + await this.activeWorkflowRunner.add(workflowId, isActive ? 'update' : 'activate'); + } catch (error) { + // If workflow could not be activated set it again to inactive + updateData.active = false; + // @ts-ignore + await Db.collections.Workflow!.update(workflowId, updateData); - // Also set it in the returned data - workflow.active = false; + // Also set it in the returned data + updatedWorkflow.active = false; - // Now return the original error for UI to display - throw error; - } + // Now return the original error for UI to display + throw error; } + } - // @ts-ignore - workflow.id = workflow.id.toString(); - return workflow; - }, - ), + const { id, ...remainder } = updatedWorkflow; + + return { + id: id.toString(), + ...remainder, + }; + }), ); // Deletes a specific workflow this.app.delete( `/${this.restEndpoint}/workflows/:id`, - ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const { id } = req.params; - - await this.externalHooks.run('workflow.delete', [id]); + ResponseHelper.send(async (req: WorkflowRequest.Delete) => { + const { id: workflowId } = req.params; + + await this.externalHooks.run('workflow.delete', [workflowId]); + + const shared = await Db.collections.SharedWorkflow!.findOne({ + relations: ['workflow'], + where: whereClause({ + user: req.user, + entityType: 'workflow', + entityId: workflowId, + }), + }); - let isActive = false; - const qb = Db.collections.Workflow!.createQueryBuilder('w'); - qb.andWhere('w.id = :workflowId', { workflowId: req.params.id }); - qb.innerJoin('w.shared', 'shared'); - // @ts-ignore - qb.andWhere('shared.userId', { userId: req.user.id }); - const workflow = await qb.getOne(); - if (workflow) { - isActive = workflow.active; - } else { + if (!shared) { throw new ResponseHelper.ResponseError( - `Workflow with id "${id}" could not be found to be deleted.`, + `Workflow with ID "${workflowId}" could not be found to be deleted.`, undefined, 400, ); } + const isActive = await this.activeWorkflowRunner.isActive(workflowId); + if (isActive) { - // Before deleting a workflow deactivate it - await this.activeWorkflowRunner.remove(id); + // deactivate before deleting + await this.activeWorkflowRunner.remove(workflowId); } - await Db.collections.Workflow!.delete(id); - void InternalHooksManager.getInstance().onWorkflowDeleted(id); - await this.externalHooks.run('workflow.afterDelete', [id]); + await Db.collections.Workflow!.delete(workflowId); + + void InternalHooksManager.getInstance().onWorkflowDeleted(workflowId); + await this.externalHooks.run('workflow.afterDelete', [workflowId]); return true; }), @@ -1140,9 +1179,7 @@ class App { await this.externalHooks.run('tag.beforeCreate', [newTag]); await TagHelpers.validateTag(newTag); - const tag = await Db.collections - .Tag!.save(newTag) - .catch(TagHelpers.throwDuplicateEntryError); + const tag = await Db.collections.Tag!.save(newTag); await this.externalHooks.run('tag.afterCreate', [tag]); @@ -1168,9 +1205,7 @@ class App { await this.externalHooks.run('tag.beforeUpdate', [newTag]); await TagHelpers.validateTag(newTag); - const tag = await Db.collections - .Tag!.save(newTag) - .catch(TagHelpers.throwDuplicateEntryError); + const tag = await Db.collections.Tag!.save(newTag); await this.externalHooks.run('tag.afterUpdate', [tag]); @@ -1240,6 +1275,7 @@ class App { const additionalData = await WorkflowExecuteAdditionalData.getBase(currentNodeParameters); // TODO UM: restrict user access to credentials he cannot use. + // @ts-ignore additionalData.userId = (req.user as User).id; return loadDataInstance.getOptions(methodName, additionalData); @@ -1406,41 +1442,38 @@ class App { // Returns the active workflow ids this.app.get( `/${this.restEndpoint}/active`, - ResponseHelper.send( - async (req: express.Request, res: express.Response): Promise => { - const activeWorkflows = await this.activeWorkflowRunner.getActiveWorkflows( - (req.user as User).id, - ); - return activeWorkflows.map((workflow) => workflow.id.toString()); - }, - ), + ResponseHelper.send(async (req: WorkflowRequest.GetAllActive) => { + const activeWorkflows = await this.activeWorkflowRunner.getActiveWorkflows(req.user); + + return activeWorkflows.map(({ id }) => id.toString()); + }), ); // Returns if the workflow with the given id had any activation errors this.app.get( `/${this.restEndpoint}/active/error/:id`, - ResponseHelper.send( - async ( - req: express.Request, - res: express.Response, - ): Promise => { - const { id } = req.params; - - const qb = Db.collections.Workflow!.createQueryBuilder('w'); - qb.andWhere('w.id = :id', { id }); - - qb.innerJoin('w.shared', 'shared'); - qb.andWhere('shared.userId = :userId', { userId: (req.user as User).id }); - - const workflow = await qb.getOne(); + ResponseHelper.send(async (req: WorkflowRequest.GetAllActivationErrors) => { + const { id: workflowId } = req.params; + + const shared = await Db.collections.SharedWorkflow!.findOne({ + relations: ['workflow'], + where: whereClause({ + user: req.user, + entityType: 'workflow', + entityId: workflowId, + }), + }); - if (workflow === undefined) { - return undefined; - } + if (!shared) { + throw new ResponseHelper.ResponseError( + `Workflow with ID "${workflowId}" could not be found.`, + undefined, + 400, + ); + } - return this.activeWorkflowRunner.getActivationError(id); - }, - ), + return this.activeWorkflowRunner.getActivationError(workflowId); + }), ); // ---------------------------------------- diff --git a/packages/cli/src/TagHelpers.ts b/packages/cli/src/TagHelpers.ts index dc54fb24691d1..743e0cd595313 100644 --- a/packages/cli/src/TagHelpers.ts +++ b/packages/cli/src/TagHelpers.ts @@ -15,17 +15,18 @@ import { ITagWithCountDb } from './Interfaces'; // ---------------------------------- /** - * Sort a `TagEntity[]` by the order of the tag IDs in the incoming request. + * Sort tags based on the order of the tag IDs in the request. */ -export function sortByRequestOrder(tagsDb: TagEntity[], tagIds: string[]) { - const tagMap = tagsDb.reduce((acc, tag) => { - // @ts-ignore - tag.id = tag.id.toString(); - acc[tag.id] = tag; +export function sortByRequestOrder( + tags: TagEntity[], + { requestOrder }: { requestOrder: string[] }, +) { + const tagMap = tags.reduce>((acc, tag) => { + acc[tag.id.toString()] = tag; return acc; - }, {} as { [key: string]: TagEntity }); + }, {}); - return tagIds.map((tagId) => tagMap[tagId]); + return requestOrder.map((tagId) => tagMap[tagId]); } // ---------------------------------- @@ -45,15 +46,6 @@ export async function validateTag(newTag: TagEntity) { } } -export function throwDuplicateEntryError(error: Error) { - const errorMessage = error.message.toLowerCase(); - if (errorMessage.includes('unique') || errorMessage.includes('duplicate')) { - throw new ResponseHelper.ResponseError('Tag name already exists', undefined, 400); - } - - throw new ResponseHelper.ResponseError(errorMessage, undefined, 400); -} - // ---------------------------------- // queries // ---------------------------------- diff --git a/packages/cli/src/WorkflowHelpers.ts b/packages/cli/src/WorkflowHelpers.ts index ebdcf47f3a1dc..146f1ba176fb7 100644 --- a/packages/cli/src/WorkflowHelpers.ts +++ b/packages/cli/src/WorkflowHelpers.ts @@ -496,7 +496,7 @@ export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promi return workflow; } -// TODO: Deduplicate `validateWorkflow` and `throwDuplicateEntryError` with TagHelpers? +// TODO: Deduplicate `validateWorkflow` with TagHelpers? // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export async function validateWorkflow(newWorkflow: WorkflowEntity) { @@ -508,20 +508,6 @@ export async function validateWorkflow(newWorkflow: WorkflowEntity) { } } -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function throwDuplicateEntryError(error: Error) { - const errorMessage = error.message.toLowerCase(); - if (errorMessage.includes('unique') || errorMessage.includes('duplicate')) { - throw new ResponseHelper.ResponseError( - 'There is already a workflow with this name', - undefined, - 400, - ); - } - - throw new ResponseHelper.ResponseError(errorMessage, undefined, 400); -} - export type NameRequest = Express.Request & { query: { name?: string; @@ -529,8 +515,8 @@ export type NameRequest = Express.Request & { }; /** - * Build a `where` clause for a `find()` or `findOne()` operation - * in the `shared_workflow` or `shared_credentials` tables. + * Build a `where` clause for a TypeORM entity search, + * checking for member access if the user is not an owner. */ export function whereClause({ user, @@ -543,6 +529,7 @@ export function whereClause({ }): WhereClause { const where: WhereClause = entityId ? { [entityType]: { id: entityId } } : {}; + // TODO: Decide if owner access should be restricted if (user.globalRole.name !== 'owner') { where.user = { id: user.id }; } diff --git a/packages/cli/src/databases/entities/WorkflowEntity.ts b/packages/cli/src/databases/entities/WorkflowEntity.ts index c15d56f0ba7a3..5975c3a5e1d16 100644 --- a/packages/cli/src/databases/entities/WorkflowEntity.ts +++ b/packages/cli/src/databases/entities/WorkflowEntity.ts @@ -60,7 +60,9 @@ export class WorkflowEntity implements IWorkflowDb { id: number; @Index({ unique: true }) - @Length(1, 128, { message: 'Workflow name must be 1 to 128 characters long.' }) + @Length(1, 128, { + message: 'Workflow name must be $constraint1 to $constraint2 characters long.', + }) @Column({ length: 128 }) name: string; diff --git a/packages/cli/src/requests.d.ts b/packages/cli/src/requests.d.ts index d1c35cfef9ead..afdd149114847 100644 --- a/packages/cli/src/requests.d.ts +++ b/packages/cli/src/requests.d.ts @@ -1,6 +1,7 @@ /* eslint-disable import/no-cycle */ import express = require('express'); import { IExecutionDeleteFilter } from '.'; +import { IConnections, INode, IWorkflowSettings } from '../../workflow/dist/src'; import { User } from './databases/entities/User'; export type AuthenticatedRequest< @@ -29,3 +30,55 @@ export declare namespace ExecutionRequest { type Stop = AuthenticatedRequest<{ id: string }>; type GetAllCurrent = AuthenticatedRequest<{}, {}, {}, GetAllCurrentQsParam>; } + +// ---------------------------------- +// requests to /workflows +// ---------------------------------- + +type CreateWorkflowPayload = Partial<{ + id: string; // delete if sent + name: string; + nodes: INode[]; + connections: object; + active: boolean; + settings: object; + tags: string[]; +}>; + +type UpdateWorkflowPayload = Partial<{ + id: string; + name: string; + nodes: INode[]; + connections: IConnections; + settings: IWorkflowSettings; + active: boolean; + tags: string[]; +}>; + +export declare namespace WorkflowRequest { + type Payload = Partial<{ + id: string; // delete if sent in body + name: string; + nodes: INode[]; + connections: IConnections; + settings: IWorkflowSettings; + active: boolean; + tags: string[]; + }>; + + type Create = AuthenticatedRequest<{}, {}, Payload>; + + type Get = AuthenticatedRequest<{ id: string }>; + + type Delete = Get; + + type Update = AuthenticatedRequest<{ id: string }, {}, Payload>; + + type NewName = express.Request<{}, {}, {}, { name?: string }>; + + type GetAll = AuthenticatedRequest<{}, {}, {}, { filter: string }>; + + type GetAllActive = AuthenticatedRequest; + + type GetAllActivationErrors = Get; +} diff --git a/packages/editor-ui/src/components/mixins/restApi.ts b/packages/editor-ui/src/components/mixins/restApi.ts index cffbb5ce78d45..10d566e321bf5 100644 --- a/packages/editor-ui/src/components/mixins/restApi.ts +++ b/packages/editor-ui/src/components/mixins/restApi.ts @@ -114,7 +114,7 @@ export const restApi = Vue.extend({ return self.restApi().makeRestApiRequest('POST', `/workflows/run`, startRunData); }, - // Creates new credentials + // Creates a new workflow createNewWorkflow: (sendData: IWorkflowDataUpdate): Promise => { return self.restApi().makeRestApiRequest('POST', `/workflows`, sendData); },