From 67edd5b7ff9db5b4fdc7d50debde7b36185a8aed Mon Sep 17 00:00:00 2001 From: Akshat Dubey Date: Mon, 28 Jun 2021 13:59:03 +0530 Subject: [PATCH] fix(bpmn-service): fix workflow model audit fields GH-239 --- .../workflow-ms-example/src/application.ts | 4 +- .../src/providers/bpmn.provider.ts | 21 +++- .../src/services/camunda.service.ts | 25 ++-- .../src/services/http.service.ts | 4 +- services/bpmn-service/openapi.json | 46 +++++++- services/bpmn-service/openapi.md | 107 ++++++++++++++++-- .../src/controllers/workflow.controller.ts | 57 +++++++++- .../src/providers/workflow-helper.service.ts | 5 + .../workflow-version.repository.ts | 4 +- .../src/repositories/workflow.repository.ts | 10 +- services/bpmn-service/src/types.ts | 3 + 11 files changed, 243 insertions(+), 43 deletions(-) diff --git a/sandbox/workflow-ms-example/src/application.ts b/sandbox/workflow-ms-example/src/application.ts index e7b342e66a..29c00fed6f 100644 --- a/sandbox/workflow-ms-example/src/application.ts +++ b/sandbox/workflow-ms-example/src/application.ts @@ -15,7 +15,6 @@ import {BPMTask} from '@sourceloop/bpmn-service/dist/bpm-task'; import path from 'path'; import {SayHelloCommand} from './commands/sayhello.command'; import {BpmnProvider} from './providers/bpmn.provider'; -import {MySequence} from './sequence'; export {ApplicationConfig}; @@ -26,7 +25,6 @@ export class WorkflowHelloworldApplication extends BootMixin( super(options); // Set up the custom sequence - this.sequence(MySequence); // Set up default home page this.static('/', path.join(__dirname, '../public')); @@ -39,7 +37,7 @@ export class WorkflowHelloworldApplication extends BootMixin( this.bind(WorkflowServiceBindings.Config).toDynamicValue(() => { return { - useCustomSequence: true, + useCustomSequence: false, workflowEngineBaseUrl: process.env.CAMUNDA_URL, }; }); diff --git a/sandbox/workflow-ms-example/src/providers/bpmn.provider.ts b/sandbox/workflow-ms-example/src/providers/bpmn.provider.ts index a3b4f81648..f9daf29b7b 100644 --- a/sandbox/workflow-ms-example/src/providers/bpmn.provider.ts +++ b/sandbox/workflow-ms-example/src/providers/bpmn.provider.ts @@ -1,6 +1,8 @@ import {bind, BindingScope, Provider, service} from '@loopback/core'; import {AnyObject} from '@loopback/repository'; +import {HttpErrors} from '@loopback/rest'; import {WorflowManager, Workflow, WorkflowDto} from '@sourceloop/bpmn-service'; +import {WorkflowVersion} from '../../../../services/bpmn-service/dist'; import {CamundaService} from '../services/camunda.service'; @bind({scope: BindingScope.TRANSIENT}) @@ -44,7 +46,7 @@ export class BpmnProvider implements Provider { Buffer.from(workflowDto.bpmnFile, 'utf-8'), ); let version = 1; - let id = response.id; + let id; if (response.deployedProcessDefinitions) { const processDefinition = Object.values( //NOSONAR @@ -52,6 +54,10 @@ export class BpmnProvider implements Provider { )[0] as AnyObject; //NOSONAR version = processDefinition.version; id = processDefinition.id; + } else { + throw new HttpErrors.BadRequest( + 'Workflow with same name and definition already exists', + ); } return { version: version, @@ -86,10 +92,19 @@ export class BpmnProvider implements Provider { fileRef: workflowDto.bpmnFile, }; }, - deleteWorkflowById: async workflow => { - await this.camunda.delete(workflow.externalIdentifier); + deleteWorkflowById: async (workflow: Workflow) => { + await this.camunda.delete( + workflow.workflowVersions.map( + (version: WorkflowVersion) => version.externalWorkflowId as string, + ), + ); return workflow; }, + + deleteWorkflowVersionById: async (version: WorkflowVersion) => { + await this.camunda.deleteVersion(version.externalWorkflowId); + return version; + }, }; } } diff --git a/sandbox/workflow-ms-example/src/services/camunda.service.ts b/sandbox/workflow-ms-example/src/services/camunda.service.ts index 0d827cb3ab..1e8fdd303f 100644 --- a/sandbox/workflow-ms-example/src/services/camunda.service.ts +++ b/sandbox/workflow-ms-example/src/services/camunda.service.ts @@ -19,9 +19,6 @@ export class CamundaService { this.baseUrl = config?.workflowEngineBaseUrl; } - /* - * Add service methods here - */ async create(name: string, file: Buffer) { const form = new FormData(); form.append(`${name}.bpmn`, file.toString('utf-8'), { @@ -32,16 +29,24 @@ export class CamundaService { return this.http.postFormData(`${this.baseUrl}/deployment/create`, form); } - async delete(id: string, cascade = true) { - return this.http.delete(`${this.baseUrl}/deployment/${id}`, { - query: { - cascade: cascade, - }, - }); + async delete(ids: string[]) { + return Promise.all( + ids.map(id => + this.http.delete(`${this.baseUrl}/process-definition/${id}`, { + query: { + cascade: true, + }, + }), + ), + ); + } + + async deleteVersion(id: string) { + return this.http.delete(`${this.baseUrl}/process-definition/${id}`); } async get(id: string) { - return this.http.get(`${this.baseUrl}/deployment/${id}`); + return this.http.get(`${this.baseUrl}/process-definition/${id}`); } async execute(id: string, input: AnyObject) { diff --git a/sandbox/workflow-ms-example/src/services/http.service.ts b/sandbox/workflow-ms-example/src/services/http.service.ts index e246db0d29..84c535d20e 100644 --- a/sandbox/workflow-ms-example/src/services/http.service.ts +++ b/sandbox/workflow-ms-example/src/services/http.service.ts @@ -69,7 +69,9 @@ export class HttpClientService { private async handleRes(res: Response) { if (res.status === STATUS_CODE.OK) { - return res.json(); + return res.json().catch(e => ({})); + } else if (res.status === STATUS_CODE.NO_CONTENT) { + return {}; } else { throw new HttpErrors.BadRequest(await res.text()); } diff --git a/services/bpmn-service/openapi.json b/services/bpmn-service/openapi.json index 4aed586a88..26ac61dac8 100644 --- a/services/bpmn-service/openapi.json +++ b/services/bpmn-service/openapi.json @@ -18,7 +18,7 @@ ], "security": [ { - "bearerAuth": [] + "HTTPBearer": [] } ], "responses": { @@ -49,6 +49,44 @@ "operationId": "WorkflowController.startWorkflow" } }, + "/workflow/{id}/{version}": { + "delete": { + "x-controller-name": "WorkflowController", + "x-operation-name": "deleteVersionById", + "tags": [ + "WorkflowController" + ], + "security": [ + { + "HTTPBearer": [] + } + ], + "responses": { + "204": { + "description": "Workflow DELETE success" + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "version", + "in": "path", + "schema": { + "type": "number" + }, + "required": true + } + ], + "operationId": "WorkflowController.deleteVersionById" + } + }, "/workflow/{id}": { "patch": { "x-controller-name": "WorkflowController", @@ -58,7 +96,7 @@ ], "security": [ { - "bearerAuth": [] + "HTTPBearer": [] } ], "responses": { @@ -118,7 +156,7 @@ ], "security": [ { - "bearerAuth": [] + "HTTPBearer": [] } ], "responses": { @@ -148,7 +186,7 @@ ], "security": [ { - "bearerAuth": [] + "HTTPBearer": [] } ], "responses": { diff --git a/services/bpmn-service/openapi.md b/services/bpmn-service/openapi.md index c299d03706..0112457e1e 100644 --- a/services/bpmn-service/openapi.md +++ b/services/bpmn-service/openapi.md @@ -44,7 +44,8 @@ const inputBody = '{ "input": {} }'; const headers = { - 'Content-Type':'application/json' + 'Content-Type':'application/json', + 'Authorization':'Bearer {access-token}' }; fetch('/workflow/{id}/execute', @@ -68,7 +69,8 @@ const inputBody = { "input": {} }; const headers = { - 'Content-Type':'application/json' + 'Content-Type':'application/json', + 'Authorization':'Bearer {access-token}' }; fetch('/workflow/{id}/execute', @@ -111,7 +113,74 @@ fetch('/workflow/{id}/execute', + +## WorkflowController.deleteVersionById + + + +> Code samples + +```javascript + +const headers = { + 'Authorization':'Bearer {access-token}' +}; + +fetch('/workflow/{id}/{version}', +{ + method: 'DELETE', + + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +```javascript--nodejs +const fetch = require('node-fetch'); + +const headers = { + 'Authorization':'Bearer {access-token}' +}; + +fetch('/workflow/{id}/{version}', +{ + method: 'DELETE', + + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +`DELETE /workflow/{id}/{version}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|id|path|string|true|none| +|version|path|number|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Workflow DELETE success|None| + + ## WorkflowController.updateById @@ -127,7 +196,8 @@ const inputBody = '{ "inputSchema": {} }'; const headers = { - 'Content-Type':'application/json' + 'Content-Type':'application/json', + 'Authorization':'Bearer {access-token}' }; fetch('/workflow/{id}', @@ -152,7 +222,8 @@ const inputBody = { "inputSchema": {} }; const headers = { - 'Content-Type':'application/json' + 'Content-Type':'application/json', + 'Authorization':'Bearer {access-token}' }; fetch('/workflow/{id}', @@ -196,7 +267,7 @@ fetch('/workflow/{id}', ## WorkflowController.count @@ -262,10 +333,15 @@ This operation does not require authentication ```javascript +const headers = { + 'Authorization':'Bearer {access-token}' +}; + fetch('/workflow/{id}', { - method: 'DELETE' + method: 'DELETE', + headers: headers }) .then(function(res) { return res.json(); @@ -278,10 +354,15 @@ fetch('/workflow/{id}', ```javascript--nodejs const fetch = require('node-fetch'); +const headers = { + 'Authorization':'Bearer {access-token}' +}; + fetch('/workflow/{id}', { - method: 'DELETE' + method: 'DELETE', + headers: headers }) .then(function(res) { return res.json(); @@ -307,7 +388,7 @@ fetch('/workflow/{id}', ## WorkflowController.create @@ -324,7 +405,8 @@ const inputBody = '{ }'; const headers = { 'Content-Type':'application/json', - 'Accept':'application/json' + 'Accept':'application/json', + 'Authorization':'Bearer {access-token}' }; fetch('/workflow', @@ -350,7 +432,8 @@ const inputBody = { }; const headers = { 'Content-Type':'application/json', - 'Accept':'application/json' + 'Accept':'application/json', + 'Authorization':'Bearer {access-token}' }; fetch('/workflow', @@ -416,7 +499,7 @@ fetch('/workflow', ## WorkflowController.find diff --git a/services/bpmn-service/src/controllers/workflow.controller.ts b/services/bpmn-service/src/controllers/workflow.controller.ts index 9aca088e1d..d22a78573d 100644 --- a/services/bpmn-service/src/controllers/workflow.controller.ts +++ b/services/bpmn-service/src/controllers/workflow.controller.ts @@ -11,10 +11,13 @@ import { post, requestBody, } from '@loopback/rest'; -import {CONTENT_TYPE, STATUS_CODE} from '@sourceloop/core'; +import { + CONTENT_TYPE, + OPERATION_SECURITY_SPEC, + STATUS_CODE, +} from '@sourceloop/core'; import {authenticate, STRATEGY} from 'loopback4-authentication'; import {authorize} from 'loopback4-authorization'; -import {OPERATION_SECURITY_SPEC} from '../constants/security-specs'; import {ErrorKeys} from '../enums/error-keys.enum'; import {PermissionKey} from '../enums/permission-key.enum'; import {WorkflowServiceBindings} from '../keys'; @@ -225,7 +228,7 @@ export class WorkflowController { filter?: Filter, ): Promise { return this.workflowRepository.find(filter, { - includes: ['workflowVersions'], + include: ['workflowVersions'], }); } @@ -254,8 +257,54 @@ export class WorkflowController { }, }) async deleteById(@param.path.string('id') id: string): Promise { - const workflow = await this.workflowRepository.findById(id); + const workflow = await this.workflowRepository.findById(id, { + include: ['workflowVersions'], + }); await this.workflowManagerService.deleteWorkflowById(workflow); + await this.workflowVersionRepository.deleteAll({ + workflowId: workflow.id, + }); + await this.workflowRepository.deleteById(workflow.id); + } + + @authenticate(STRATEGY.BEARER) + @authorize({permissions: [PermissionKey.DeleteWorkflow]}) + @del(`${basePath}/{id}/{version}`, { + security: OPERATION_SECURITY_SPEC, + responses: { + '204': { + description: 'Workflow DELETE success', + }, + }, + }) + async deleteVersionById( + @param.path.string('id') id: string, + @param.path.number('version') versionNumber: number, + ): Promise { + const workflow = await this.workflowRepository.findById(id); + if (workflow.workflowVersion === versionNumber) { + throw new HttpErrors.BadRequest( + 'Can not delete latest version of a workflow', + ); + } + const version = await this.workflowVersionRepository.findOne({ + where: { + workflowId: id, + version: versionNumber, + }, + }); + if (!this.workflowManagerService.deleteWorkflowVersionById) { + throw new HttpErrors.InternalServerError( + 'Version Delete Provider Missing', + ); + } + + if (version) { + await this.workflowManagerService.deleteWorkflowVersionById(version); + await this.workflowVersionRepository.deleteById(version.id); + } else { + throw new HttpErrors.NotFound(ErrorKeys.VersionNotFound); + } } private async initWorkers(workflowName: string) { diff --git a/services/bpmn-service/src/providers/workflow-helper.service.ts b/services/bpmn-service/src/providers/workflow-helper.service.ts index f03d956a7d..d287a022e3 100644 --- a/services/bpmn-service/src/providers/workflow-helper.service.ts +++ b/services/bpmn-service/src/providers/workflow-helper.service.ts @@ -31,6 +31,11 @@ export class WorkflowProvider implements Provider { 'deleteWorkflowById function not implemented', ); }, + deleteWorkflowVersionById: async () => { + throw new HttpErrors.BadRequest( + 'deleteWorkflowVersionById function not implemented', + ); + }, }; } } diff --git a/services/bpmn-service/src/repositories/workflow-version.repository.ts b/services/bpmn-service/src/repositories/workflow-version.repository.ts index b3059efc73..68a82134f8 100644 --- a/services/bpmn-service/src/repositories/workflow-version.repository.ts +++ b/services/bpmn-service/src/repositories/workflow-version.repository.ts @@ -11,12 +11,12 @@ import {WorkflowRepository} from './workflow.repository'; export class WorkflowVersionRepository extends DefaultCrudRepository< WorkflowVersion, - typeof WorkflowVersion.prototype.version, + typeof WorkflowVersion.prototype.id, WorkflowVersionRelations > { public readonly workflow: BelongsToAccessor< Workflow, - typeof WorkflowVersion.prototype.version + typeof WorkflowVersion.prototype.id >; constructor( @inject(`datasources.${WorkflowCacheSourceName}`) diff --git a/services/bpmn-service/src/repositories/workflow.repository.ts b/services/bpmn-service/src/repositories/workflow.repository.ts index 27156e0047..9fa2a5151f 100644 --- a/services/bpmn-service/src/repositories/workflow.repository.ts +++ b/services/bpmn-service/src/repositories/workflow.repository.ts @@ -1,17 +1,19 @@ import {Getter, inject} from '@loopback/core'; import { - DefaultCrudRepository, HasManyRepositoryFactory, juggler, repository, } from '@loopback/repository'; -import {IAuthUserWithPermissions} from '@sourceloop/core'; +import { + DefaultUserModifyCrudRepository, + IAuthUserWithPermissions, +} from '@sourceloop/core'; import {AuthenticationBindings} from 'loopback4-authentication'; import {Workflow, WorkflowRelations, WorkflowVersion} from '../models'; import {WorkflowCacheSourceName} from '../types'; import {WorkflowVersionRepository} from './workflow-version.repository'; -export class WorkflowRepository extends DefaultCrudRepository< +export class WorkflowRepository extends DefaultUserModifyCrudRepository< Workflow, typeof Workflow.prototype.id, WorkflowRelations @@ -31,7 +33,7 @@ export class WorkflowRepository extends DefaultCrudRepository< IAuthUserWithPermissions | undefined >, ) { - super(Workflow, dataSource); + super(Workflow, dataSource, getCurrentUser); this.workflowVersions = this.createHasManyRepositoryFactoryFor( 'workflowVersions', workflowVersionRepositoryGetter, diff --git a/services/bpmn-service/src/types.ts b/services/bpmn-service/src/types.ts index a6a3753182..b531cba3da 100644 --- a/services/bpmn-service/src/types.ts +++ b/services/bpmn-service/src/types.ts @@ -30,6 +30,9 @@ export interface WorflowManager { createWorkflow(workflowDto: WorkflowDto): Promise; updateWorkflow(workflowDto: WorkflowDto): Promise; deleteWorkflowById(workflow: Workflow): Promise; + deleteWorkflowVersionById?( + version: WorkflowVersion, + ): Promise; } export interface ExecutionInputValidator {