Implemented version history

This commit is contained in:
Gürkan Güran 2022-08-29 18:22:58 +02:00
parent 8fc3bac949
commit d614a0dbe6
45 changed files with 554 additions and 38 deletions

View file

@ -257,6 +257,12 @@ export namespace Components {
"hide": () => Promise<void>;
"show": () => Promise<void>;
"workflowDefinition"?: WorkflowDefinition;
"workflowVersions": Array<WorkflowDefinition>;
}
interface ElsaWorkflowDefinitionVersionHistory {
"selectedVersion": WorkflowDefinition;
"serverUrl": string;
"workflowVersions": Array<WorkflowDefinition>;
}
interface ElsaWorkflowInstanceBrowser {
}
@ -569,6 +575,12 @@ declare global {
prototype: HTMLElsaWorkflowDefinitionPropertiesEditorElement;
new (): HTMLElsaWorkflowDefinitionPropertiesEditorElement;
};
interface HTMLElsaWorkflowDefinitionVersionHistoryElement extends Components.ElsaWorkflowDefinitionVersionHistory, HTMLStencilElement {
}
var HTMLElsaWorkflowDefinitionVersionHistoryElement: {
prototype: HTMLElsaWorkflowDefinitionVersionHistoryElement;
new (): HTMLElsaWorkflowDefinitionVersionHistoryElement;
};
interface HTMLElsaWorkflowInstanceBrowserElement extends Components.ElsaWorkflowInstanceBrowser, HTMLStencilElement {
}
var HTMLElsaWorkflowInstanceBrowserElement: {
@ -664,6 +676,7 @@ declare global {
"elsa-workflow-definition-editor-toolbox": HTMLElsaWorkflowDefinitionEditorToolboxElement;
"elsa-workflow-definition-editor-toolbox-activities": HTMLElsaWorkflowDefinitionEditorToolboxActivitiesElement;
"elsa-workflow-definition-properties-editor": HTMLElsaWorkflowDefinitionPropertiesEditorElement;
"elsa-workflow-definition-version-history": HTMLElsaWorkflowDefinitionVersionHistoryElement;
"elsa-workflow-instance-browser": HTMLElsaWorkflowInstanceBrowserElement;
"elsa-workflow-instance-properties": HTMLElsaWorkflowInstancePropertiesElement;
"elsa-workflow-instance-viewer": HTMLElsaWorkflowInstanceViewerElement;
@ -893,8 +906,20 @@ declare namespace LocalJSX {
"graph"?: Graph;
}
interface ElsaWorkflowDefinitionPropertiesEditor {
"onDeleteVersionClicked"?: (event: CustomEvent<WorkflowDefinition>) => void;
"onRevertVersionClicked"?: (event: CustomEvent<WorkflowDefinition>) => void;
"onVersionSelected"?: (event: CustomEvent<WorkflowDefinition>) => void;
"onWorkflowPropsUpdated"?: (event: CustomEvent<WorkflowDefinitionPropsUpdatedArgs>) => void;
"workflowDefinition"?: WorkflowDefinition;
"workflowVersions"?: Array<WorkflowDefinition>;
}
interface ElsaWorkflowDefinitionVersionHistory {
"onDeleteVersionClicked"?: (event: CustomEvent<WorkflowDefinition>) => void;
"onRevertVersionClicked"?: (event: CustomEvent<WorkflowDefinition>) => void;
"onVersionSelected"?: (event: CustomEvent<WorkflowDefinition>) => void;
"selectedVersion"?: WorkflowDefinition;
"serverUrl"?: string;
"workflowVersions"?: Array<WorkflowDefinition>;
}
interface ElsaWorkflowInstanceBrowser {
"onWorkflowInstanceSelected"?: (event: CustomEvent<WorkflowInstanceSummary>) => void;
@ -976,6 +1001,7 @@ declare namespace LocalJSX {
"elsa-workflow-definition-editor-toolbox": ElsaWorkflowDefinitionEditorToolbox;
"elsa-workflow-definition-editor-toolbox-activities": ElsaWorkflowDefinitionEditorToolboxActivities;
"elsa-workflow-definition-properties-editor": ElsaWorkflowDefinitionPropertiesEditor;
"elsa-workflow-definition-version-history": ElsaWorkflowDefinitionVersionHistory;
"elsa-workflow-instance-browser": ElsaWorkflowInstanceBrowser;
"elsa-workflow-instance-properties": ElsaWorkflowInstanceProperties;
"elsa-workflow-instance-viewer": ElsaWorkflowInstanceViewer;
@ -1036,6 +1062,7 @@ declare module "@stencil/core" {
"elsa-workflow-definition-editor-toolbox": LocalJSX.ElsaWorkflowDefinitionEditorToolbox & JSXBase.HTMLAttributes<HTMLElsaWorkflowDefinitionEditorToolboxElement>;
"elsa-workflow-definition-editor-toolbox-activities": LocalJSX.ElsaWorkflowDefinitionEditorToolboxActivities & JSXBase.HTMLAttributes<HTMLElsaWorkflowDefinitionEditorToolboxActivitiesElement>;
"elsa-workflow-definition-properties-editor": LocalJSX.ElsaWorkflowDefinitionPropertiesEditor & JSXBase.HTMLAttributes<HTMLElsaWorkflowDefinitionPropertiesEditorElement>;
"elsa-workflow-definition-version-history": LocalJSX.ElsaWorkflowDefinitionVersionHistory & JSXBase.HTMLAttributes<HTMLElsaWorkflowDefinitionVersionHistoryElement>;
"elsa-workflow-instance-browser": LocalJSX.ElsaWorkflowInstanceBrowser & JSXBase.HTMLAttributes<HTMLElsaWorkflowInstanceBrowserElement>;
"elsa-workflow-instance-properties": LocalJSX.ElsaWorkflowInstanceProperties & JSXBase.HTMLAttributes<HTMLElsaWorkflowInstancePropertiesElement>;
"elsa-workflow-instance-viewer": LocalJSX.ElsaWorkflowInstanceViewer & JSXBase.HTMLAttributes<HTMLElsaWorkflowInstanceViewerElement>;

View file

@ -1,8 +1,10 @@
export {BulkActionsIcon} from "./bulk-actions";
export {DeleteIcon} from "./delete";
export {EditIcon} from "./edit";
export {RevertIcon} from "./revert";
export {PageSizeIcon} from "./page-size-icon";
export {PublishIcon} from "./publish";
export {PublishedIcon} from "./published";
export {OrderByIcon} from "./order-by";
export {UnPublishIcon} from "./un-publish";
export {WorkflowIcon} from "./workflow";

View file

@ -0,0 +1,7 @@
import {FunctionalComponent, h} from "@stencil/core";
export const PublishedIcon: FunctionalComponent = () =>
<svg class="h-6 w-6 text-green-500" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>;

View file

@ -0,0 +1,8 @@
import {FunctionalComponent, h} from "@stencil/core";
export const RevertIcon: FunctionalComponent = () =>
<svg class="h-6 w-6 text-gray-500" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M9 11l-4 4l4 4m-4 -4h11a4 4 0 0 0 0 -8h-1"/>
</svg>;

View file

@ -202,7 +202,7 @@ export class FlowchartComponent implements ContainerActivityComponent {
@Method()
async import(root: Activity): Promise<void> {
return this.importInternal(root);
return await this.importInternal(root);
}
@Method()

View file

@ -9,13 +9,15 @@ import {
ActivitySelectedArgs,
ChildActivitySelectedArgs,
ContainerSelectedArgs,
GraphUpdatedArgs,
GraphUpdatedArgs,
VersionedEntity
} from '../../../models';
import {ActivityIdUpdatedArgs, ActivityUpdatedArgs} from './activity-properties-editor';
import {ActivityDriverRegistry, ActivityNameFormatter, EventBus, PluginRegistry, PortProviderRegistry} from '../../../services';
import {MonacoEditorSettings} from "../../../services/monaco-editor-settings";
import {ActivityPropertyChangedEventArgs, WorkflowDefinitionPropsUpdatedArgs, WorkflowDefinitionUpdatedArgs, WorkflowEditorEventTypes} from "../models/ui";
import {WorkflowDefinition} from "../models/entities";
import {WorkflowDefinitionsApi} from "../../workflow-definitions/services/api";
@Component({
tag: 'elsa-workflow-definition-editor',
@ -34,6 +36,7 @@ export class WorkflowDefinitionEditor {
private readonly emitActivityChangedDebounced: (e: ActivityPropertyChangedEventArgs) => void;
private readonly updateModelDebounced: () => void;
private readonly saveChangesDebounced: () => void;
private readonly workflowDefinitionApi: WorkflowDefinitionsApi;
constructor() {
this.eventBus = Container.get(EventBus);
@ -43,6 +46,7 @@ export class WorkflowDefinitionEditor {
this.emitActivityChangedDebounced = debounce(this.emitActivityChanged, 100);
this.updateModelDebounced = debounce(this.updateModel, 10);
this.saveChangesDebounced = debounce(this.saveChanges, 1000);
this.workflowDefinitionApi = Container.get(WorkflowDefinitionsApi);
}
@Prop() workflowDefinition?: WorkflowDefinition;
@ -50,6 +54,7 @@ export class WorkflowDefinitionEditor {
@Event() workflowUpdated: EventEmitter<WorkflowDefinitionUpdatedArgs>
@State() private workflowDefinitionState: WorkflowDefinition;
@State() private selectedActivity?: Activity;
@State() workflowVersions: Array<WorkflowDefinition> = [];
@Watch('monacoLibPath')
private handleMonacoLibPath(value: string) {
@ -60,6 +65,7 @@ export class WorkflowDefinitionEditor {
@Watch('workflowDefinition')
async onWorkflowDefinitionChanged(value: WorkflowDefinition) {
await this.importWorkflow(value);
await this.loadWorkflowVersions();
}
@Listen('resize', {target: 'window'})
@ -116,6 +122,7 @@ export class WorkflowDefinitionEditor {
@Method()
async updateWorkflowDefinition(workflowDefinition: WorkflowDefinition): Promise<void> {
this.workflowDefinitionState = workflowDefinition;
await this.loadWorkflowVersions();
}
@Method()
@ -138,8 +145,19 @@ export class WorkflowDefinitionEditor {
return workflowDefinition;
}
loadWorkflowVersions = async () => {
if(this.workflowDefinitionState.definitionId != null && this.workflowDefinitionState.definitionId.length > 0)
{
this.workflowVersions = await this.workflowDefinitionApi.getVersions(this.workflowDefinitionState.definitionId);
}
else {
this.workflowVersions = [];
}
}
async componentWillLoad() {
await this.updateWorkflowDefinition(this.workflowDefinition);
await this.loadWorkflowVersions();
}
async componentDidLoad() {
@ -176,7 +194,8 @@ export class WorkflowDefinitionEditor {
};
private saveChanges = async (): Promise<void> => {
this.workflowUpdated.emit({workflowDefinition: this.workflowDefinitionState});
const latestVersion = this.workflowVersions.find(v => v.isLatest);
this.workflowUpdated.emit({workflowDefinition: this.workflowDefinitionState, latestVersionNumber: latestVersion?.version});
};
private updateLayout = async () => {
@ -231,6 +250,28 @@ export class WorkflowDefinitionEditor {
this.saveChangesDebounced();
}
onVersionSelected = async (e: CustomEvent<WorkflowDefinition>) => {
const workflowToView = e.detail;
const workflowDefinition = await this.workflowDefinitionApi.get({definitionId: workflowToView.definitionId, versionOptions: {version: workflowToView.version}});
this.importWorkflow(workflowDefinition);
};
onDeleteVersionClicked = async (e: CustomEvent<WorkflowDefinition>) => {
const workflowToDelete = e.detail;
await this.workflowDefinitionApi.deleteVersion({definitionId: workflowToDelete.definitionId, version: workflowToDelete.version});
const latestWorkflowDefinition = await this.workflowDefinitionApi.get({definitionId: workflowToDelete.definitionId, versionOptions: {isLatest: true}});
await this.loadWorkflowVersions();
this.importWorkflow(latestWorkflowDefinition);
};
onRevertVersionClicked = async (e: CustomEvent<WorkflowDefinition>) => {
const workflowToRevert = e.detail;
await this.workflowDefinitionApi.revertVersion({definitionId: workflowToRevert.definitionId, version: workflowToRevert.version});
const workflowDefinition = await this.workflowDefinitionApi.get({definitionId: workflowToRevert.definitionId, versionOptions: {isLatest: true}});
await this.loadWorkflowVersions();
this.importWorkflow(workflowDefinition);
};
render() {
return (
@ -254,7 +295,11 @@ export class WorkflowDefinitionEditor {
<div class="object-editor-container">
<elsa-workflow-definition-properties-editor
workflowDefinition={this.workflowDefinitionState}
workflowVersions={this.workflowVersions}
onWorkflowPropsUpdated={e => this.onWorkflowPropsUpdated(e)}
onVersionSelected={e => this.onVersionSelected(e)}
onDeleteVersionClicked={e => this.onDeleteVersionClicked(e)}
onRevertVersionClicked={e => this.onRevertVersionClicked(e)}
/>
</div>
</elsa-panel>

View file

@ -6,7 +6,8 @@ import {PropertiesTabModel, TabModel, WorkflowDefinitionPropsUpdatedArgs, Workfl
import {FormEntry} from "../../../components/shared/forms/form-entry";
import {isNullOrWhitespace} from "../../../utils";
import {InfoList} from "../../../components/shared/forms/info-list";
import {TabChangedArgs, Variable} from "../../../models";
import {TabChangedArgs, Variable, VersionedEntity} from "../../../models";
import {WorkflowDefinitionsApi} from "../../workflow-definitions/services/api";
@Component({
tag: 'elsa-workflow-definition-properties-editor',
@ -14,9 +15,11 @@ import {TabChangedArgs, Variable} from "../../../models";
export class WorkflowDefinitionPropertiesEditor {
private readonly eventBus: EventBus;
private slideOverPanel: HTMLElsaSlideOverPanelElement;
private readonly workflowDefinitionApi: WorkflowDefinitionsApi;
constructor() {
this.eventBus = Container.get(EventBus);
this.workflowDefinitionApi = Container.get(WorkflowDefinitionsApi);
this.model = {
tabModels: [],
@ -24,7 +27,11 @@ export class WorkflowDefinitionPropertiesEditor {
}
@Prop() workflowDefinition?: WorkflowDefinition;
@Prop() workflowVersions: Array<WorkflowDefinition>;
@Event() workflowPropsUpdated: EventEmitter<WorkflowDefinitionPropsUpdatedArgs>;
@Event() versionSelected: EventEmitter<WorkflowDefinition>;
@Event() deleteVersionClicked: EventEmitter<WorkflowDefinition>;
@Event() revertVersionClicked: EventEmitter<WorkflowDefinition>;
@State() private model: WorkflowPropertiesEditorModel;
@State() private selectedTabIndex: number = 0;
@ -43,6 +50,11 @@ export class WorkflowDefinitionPropertiesEditor {
await this.createModel();
}
@Watch('workflowVersions')
async onWorkflowVersionsChanged() {
await this.createModel();
}
async componentWillLoad() {
await this.createModel();
}
@ -127,7 +139,15 @@ export class WorkflowDefinitionPropertiesEditor {
}
}
model.tabModels = [propertiesTabModel, variablesTabModel];
const versionHistoryTabModel: TabModel = {
name: 'versionHistory',
tab: {
displayText: 'Version History',
content: () => this.renderVersionHistoryTab()
}
}
model.tabModels = [propertiesTabModel, variablesTabModel, versionHistoryTabModel];
const args: WorkflowPropertiesEditorDisplayingArgs = {model};
@ -152,6 +172,15 @@ export class WorkflowDefinitionPropertiesEditor {
</div>
};
private renderVersionHistoryTab = () => {
return <div>
<elsa-workflow-definition-version-history
selectedVersion={this.workflowDefinition}
workflowVersions={this.workflowVersions}
/>
</div>
};
private onSelectedTabIndexChanged = (e: CustomEvent<TabChangedArgs>) => this.selectedTabIndex = e.detail.selectedTabIndex;
private onPropertyEditorChanged = (apply: (w: WorkflowDefinition) => void) => {

View file

@ -0,0 +1,100 @@
import {Component, Event, EventEmitter, h, Host, Method, Prop, State, Watch} from '@stencil/core';
import {Container} from "typedi";
import {EventBus} from "../../../services";
import {WorkflowDefinition} from "../models/entities";
import {VersionedEntity} from "../../../models";
import {WorkflowDefinitionsApi} from "../../workflow-definitions/services/api";
import {DeleteIcon, RevertIcon, PublishedIcon} from "../../../components/icons/tooling";
import moment from "moment";
@Component({
tag: 'elsa-workflow-definition-version-history',
})
export class WorkflowDefinitionVersionHistory {
private readonly eventBus: EventBus;
private readonly workflowDefinitionApi: WorkflowDefinitionsApi;
constructor() {
this.eventBus = Container.get(EventBus);
this.workflowDefinitionApi = Container.get(WorkflowDefinitionsApi);
}
@Prop() selectedVersion: WorkflowDefinition;
@Prop() workflowVersions: Array<WorkflowDefinition>;
@Prop() serverUrl: string;
@Event() versionSelected: EventEmitter<WorkflowDefinition>;
@Event() deleteVersionClicked: EventEmitter<WorkflowDefinition>;
@Event() revertVersionClicked: EventEmitter<WorkflowDefinition>;
@Watch('workflowDefinition')
async onWorkflowDefinitionChanged(value: WorkflowDefinition) {
}
async componentWillLoad() {
}
onViewVersionClick = (e: Event, version: WorkflowDefinition) => {
e.preventDefault();
this.versionSelected.emit(version);
};
onDeleteVersionClick = async (e: Event, version: WorkflowDefinition) => {
e.preventDefault();
this.deleteVersionClicked.emit(version);
};
onRevertVersionClick = (e: Event, version: WorkflowDefinition) => {
e.preventDefault();
this.revertVersionClicked.emit(version);
};
render() {
return (
<div>
<table>
<thead>
<tr>
<th/>
<th>Version</th>
<th>Created</th>
<th/>
<th/>
</tr>
</thead>
<tbody>
{this.workflowVersions.map(v => {
return (
<tr>
<td>{v.isPublished ? <PublishedIcon/> : ""}</td>
<td>{v.version}</td>
<td>{moment(v.createdAt).format('DD-MM-YYYY HH:mm:ss')}</td>
<td>
<button onClick={e => this.onViewVersionClick(e, v)}
type="button"
disabled={this.selectedVersion.version == v.version}
class="btn btn-secondary">
View
</button>
</td>
<td>
{v.isPublished || v.isPublished ? undefined :
<elsa-context-menu
menuItems={[
{text: 'Delete', clickHandler: e => this.onDeleteVersionClick(e, v), icon: <DeleteIcon/>},
{text: 'Reverse', clickHandler: e => this.onRevertVersionClick(e, v), icon: <RevertIcon/>},
]}
/>
}
</td>
</tr>
);
}
)}
</tbody>
</table>
</div>
);
}
}

View file

@ -27,6 +27,7 @@ export interface ActivityPropertyChangedEventArgs {
export interface WorkflowDefinitionUpdatedArgs {
workflowDefinition: WorkflowDefinition;
latestVersionNumber: number;
}
export interface WorkflowDefinitionPropsUpdatedArgs {

View file

@ -19,6 +19,7 @@ import {WorkflowDefinitionUpdatedArgs} from "./models/ui";
import {PublishClickedArgs} from "./components/publish-button";
import {WorkflowDefinitionsApi} from "./services/api";
import {DefaultActions, ModalDialogInstance, ModalDialogService} from "../../components/shared/modal-dialog";
import {isEqual} from 'lodash'
const FlowchartTypeName = 'Elsa.Flowchart';
@ -106,8 +107,20 @@ export class WorkflowDefinitionsPlugin implements Plugin {
};
private onWorkflowUpdated = async (e: CustomEvent<WorkflowDefinitionUpdatedArgs>) => {
const workflowDefinition = e.detail.workflowDefinition;
await this.saveWorkflowDefinition(workflowDefinition, false);
const updatedWorkflowDefinition = e.detail.workflowDefinition;
if(e.detail.latestVersionNumber == undefined)
{
await this.saveWorkflowDefinition(updatedWorkflowDefinition, false);
return;
}
if(updatedWorkflowDefinition.version == e.detail.latestVersionNumber)
{
const currentWorkflowDefinition = await this.api.get({definitionId: updatedWorkflowDefinition.definitionId, versionOptions: {version: updatedWorkflowDefinition.version}});
if(!isEqual(currentWorkflowDefinition.root.activities, updatedWorkflowDefinition.root.activities))
await this.saveWorkflowDefinition(updatedWorkflowDefinition, false);
}
}
private onBrowseWorkflowDefinitions = async () => {

View file

@ -1,6 +1,6 @@
import { getVersionOptionsString, serializeQueryString } from '../../../utils';
import {WorkflowDefinition, WorkflowDefinitionSummary} from "../models/entities";
import {Activity, PagedList, Variable, VersionOptions} from "../../../models";
import {Activity, PagedList, Variable, VersionedEntity, VersionOptions} from "../../../models";
import {Service} from "typedi";
import {ElsaApiClientProvider} from "../../../services";
@ -30,6 +30,18 @@ export class WorkflowDefinitionsApi {
return response.data;
}
async deleteVersion(request: DeleteWorkflowVersionRequest): Promise<WorkflowDefinition> {
const httpClient = await this.getHttpClient();
const response = await httpClient.delete<WorkflowDefinition>(`workflow-definitions/${request.definitionId}/version/${request.version}`);
return response.data;
}
async revertVersion(request: RevertWorkflowVersionRequest): Promise<WorkflowDefinition> {
const httpClient = await this.getHttpClient();
const response = await httpClient.post<WorkflowDefinition>(`workflow-definitions/${request.definitionId}/revert/${request.version}`);
return response.data;
}
async post(request: SaveWorkflowDefinitionRequest): Promise<WorkflowDefinition> {
const httpClient = await this.getHttpClient();
const response = await httpClient.post<WorkflowDefinition>('workflow-definitions', request);
@ -47,6 +59,12 @@ export class WorkflowDefinitionsApi {
return response.data;
}
async getVersions(workflowDefinitionId: string): Promise<Array<WorkflowDefinition>> {
const httpClient = await this.getHttpClient();
const response = await httpClient.get<Array<WorkflowDefinition>>(`workflow-definitions/${workflowDefinitionId}/versions`);
return response.data;
}
async list(request: ListWorkflowDefinitionsRequest): Promise<PagedList<WorkflowDefinitionSummary>> {
const queryString: any = {};
@ -137,6 +155,7 @@ export class WorkflowDefinitionsApi {
export interface SaveWorkflowDefinitionRequest {
definitionId: string;
version?: number;
name?: string;
description?: string;
publish: boolean;
@ -155,6 +174,16 @@ export interface DeleteWorkflowDefinitionRequest {
definitionId: string;
}
export interface DeleteWorkflowVersionRequest {
definitionId: string;
version: number;
}
export interface RevertWorkflowVersionRequest {
definitionId: string;
version: number;
}
export interface RetractWorkflowDefinitionRequest {
definitionId: string;
}

View file

@ -13,6 +13,7 @@ export class WorkflowDefinitionManager {
public saveWorkflow = async (definition: WorkflowDefinition, publish: boolean): Promise<WorkflowDefinition> => {
const request: SaveWorkflowDefinitionRequest = {
definitionId: definition.definitionId,
version: definition.version,
name: definition.name,
description: definition.description,
publish: publish,

View file

@ -58,7 +58,7 @@ public class WorkflowInstanceGrain : WorkflowInstanceGrainBase
throw new Exception($"No workflow instance found with ID {workflowInstanceId}");
var workflowDefinitionId = workflowInstance.DefinitionId;
var workflow = await _workflowDefinitionStore.FindByDefinitionIdAsync(workflowDefinitionId, VersionOptions.SpecificVersion(workflowInstance.Version), cancellationToken);
var workflow = (await _workflowDefinitionStore.FindByDefinitionIdAsync(workflowDefinitionId, VersionOptions.SpecificVersion(workflowInstance.Version), cancellationToken)).FirstOrDefault();
if (workflow == null)
throw new Exception($"No workflow definition found with ID {workflowDefinitionId}");
@ -79,7 +79,7 @@ public class WorkflowInstanceGrain : WorkflowInstanceGrainBase
var workflowDefinitionId = request.DefinitionId;
var correlationId = request.CorrelationId == "" ? default : request.CorrelationId;
var workflowInstance = await _workflowInstanceFactory.CreateAsync(workflowDefinitionId, versionOptions, correlationId, cancellationToken);
var workflow = await _workflowDefinitionStore.FindByDefinitionIdAsync(workflowDefinitionId, VersionOptions.SpecificVersion(workflowInstance.Version), cancellationToken);
var workflow = (await _workflowDefinitionStore.FindByDefinitionIdAsync(workflowDefinitionId, VersionOptions.SpecificVersion(workflowInstance.Version), cancellationToken)).FirstOrDefault();
if (workflow == null)
throw new Exception($"No workflow definition found with ID {workflowDefinitionId}");
@ -100,7 +100,7 @@ public class WorkflowInstanceGrain : WorkflowInstanceGrainBase
var workflowDefinitionId = request.DefinitionId;
var bookmark = request.Bookmark;
var input = request.Input?.Deserialize();
var workflow = await _workflowDefinitionStore.FindByDefinitionIdAsync(workflowDefinitionId, versionOptions, cancellationToken);
var workflow = (await _workflowDefinitionStore.FindByDefinitionIdAsync(workflowDefinitionId, versionOptions, cancellationToken)).FirstOrDefault();
if (workflow == null)
throw new Exception($"No workflow definition found with ID {workflowDefinitionId}");

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Abstractions;
@ -34,7 +35,7 @@ public class BulkPublish : ElsaEndpoint<Request, Response>
foreach (var definitionId in request.DefinitionIds)
{
var definition = await _store.FindByDefinitionIdAsync(definitionId, VersionOptions.Latest, cancellationToken);
var definition = (await _store.FindByDefinitionIdAsync(definitionId, VersionOptions.Latest, cancellationToken)).FirstOrDefault();
if (definition == null)
{

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Abstractions;
@ -33,7 +34,7 @@ public class BulkRetract : ElsaEndpoint<Request, Response>
foreach (var definitionId in request.DefinitionIds)
{
var definition = await _store.FindByDefinitionIdAsync(definitionId, VersionOptions.Latest, cancellationToken);
var definition = (await _store.FindByDefinitionIdAsync(definitionId, VersionOptions.Latest, cancellationToken)).FirstOrDefault();
if (definition == null)
{

View file

@ -42,7 +42,7 @@ public class Export : ElsaEndpoint<Request>
{
var serializerOptions = _serializerOptionsProvider.CreateApiOptions();
var versionOptions = request.VersionOptions != null ? VersionOptions.FromString(request.VersionOptions) : VersionOptions.Latest;
var definition = await _store.FindByDefinitionIdAsync(request.DefinitionId, versionOptions, cancellationToken);
var definition = (await _store.FindByDefinitionIdAsync(request.DefinitionId, versionOptions, cancellationToken)).FirstOrDefault();
if (definition == null)
{

View file

@ -1,3 +1,4 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Abstractions;
@ -26,7 +27,7 @@ public class Get : ElsaEndpoint<Request, WorkflowDefinitionResponse, WorkflowDef
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var versionOptions = request.VersionOptions != null ? VersionOptions.FromString(request.VersionOptions) : VersionOptions.Latest;
var definition = await _store.FindByDefinitionIdAsync(request.DefinitionId, versionOptions, cancellationToken);
var definition = (await _store.FindByDefinitionIdAsync(request.DefinitionId, versionOptions, cancellationToken)).FirstOrDefault();
if (definition == null)
{

View file

@ -43,7 +43,7 @@ public class Import : ElsaEndpoint<WorkflowDefinitionRequest, WorkflowDefinition
// Get a workflow draft version.
var draft = !string.IsNullOrWhiteSpace(definitionId)
? await _workflowDefinitionPublisher.GetDraftAsync(definitionId, cancellationToken)
? await _workflowDefinitionPublisher.GetDraftAsync(definitionId, cancellationToken:cancellationToken)
: default;
var isNew = draft == null;

View file

@ -42,7 +42,7 @@ public class Post : ElsaEndpoint<WorkflowDefinitionRequest, WorkflowDefinitionRe
// Get a workflow draft version.
var draft = !string.IsNullOrWhiteSpace(definitionId)
? await _workflowDefinitionPublisher.GetDraftAsync(definitionId, cancellationToken)
? await _workflowDefinitionPublisher.GetDraftAsync(definitionId, request.Version, cancellationToken)
: default;
var isNew = draft == null;

View file

@ -1,3 +1,4 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Abstractions;
@ -28,7 +29,7 @@ public class Publish : ElsaEndpoint<Request, WorkflowDefinitionResponse, Workflo
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var definition = await _store.FindByDefinitionIdAsync(request.DefinitionId, VersionOptions.Latest, cancellationToken);
var definition = (await _store.FindByDefinitionIdAsync(request.DefinitionId, VersionOptions.Latest, cancellationToken)).FirstOrDefault();
if (definition == null)
{

View file

@ -1,3 +1,4 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Abstractions;
@ -28,7 +29,7 @@ public class Retract : ElsaEndpoint<Request, WorkflowDefinitionResponse, Workflo
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var definition = await _store.FindByDefinitionIdAsync(request.DefinitionId, VersionOptions.LatestOrPublished, cancellationToken);
var definition = (await _store.FindByDefinitionIdAsync(request.DefinitionId, VersionOptions.LatestOrPublished, cancellationToken)).FirstOrDefault();
if (definition == null)
{

View file

@ -0,0 +1,37 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Workflows.Management.Services;
using FastEndpoints;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Version;
public class DeleteVersion : EndpointWithoutRequest
{
private readonly IWorkflowDefinitionManager _workflowDefinitionManager;
public DeleteVersion(IWorkflowDefinitionManager workflowDefinitionManager)
{
_workflowDefinitionManager = workflowDefinitionManager;
}
public override void Configure()
{
Delete("workflow-definitions/{definitionId}/version/{version}");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken ct)
{
var definitionId = Route<string>("definitionId")!;
var version = Route<int>("version");
var result = await _workflowDefinitionManager.DeleteVersionAsync(definitionId, version, ct);
if (!result)
{
await SendNotFoundAsync(ct);
return;
}
await SendOkAsync(ct);
}
}

View file

@ -0,0 +1,39 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Models;
using Elsa.Workflows.Persistence.Services;
using FastEndpoints;
using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Version;
public class ListVersions : EndpointWithoutRequest
{
private readonly IWorkflowDefinitionStore _store;
public ListVersions(IWorkflowDefinitionStore store)
{
_store = store;
}
public override void Configure()
{
Get("workflow-definitions/{definitionId}/versions");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken ct)
{
var definitionId = Route<string>("definitionId")!;
var result = await _store.FindByDefinitionIdAsync(definitionId, VersionOptions.All, ct);
if (!result.Any())
{
await SendNotFoundAsync(ct);
return;
}
await SendAsync(result, StatusCodes.Status200OK, ct);
}
}

View file

@ -0,0 +1,32 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Workflows.Management.Services;
using FastEndpoints;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Version;
public class RevertVersion : EndpointWithoutRequest
{
private readonly IWorkflowDefinitionManager _workflowDefinitionManager;
public RevertVersion(IWorkflowDefinitionManager workflowDefinitionManager)
{
_workflowDefinitionManager = workflowDefinitionManager;
}
public override void Configure()
{
Post("workflow-definitions/{definitionId}/revert/{version}");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken ct)
{
var definitionId = Route<string>("definitionId")!;
var version = Route<int>("version");
await _workflowDefinitionManager.RevertVersionAsync(definitionId, version, ct);
await SendOkAsync(ct);
}
}

View file

@ -9,6 +9,7 @@ public class WorkflowDefinitionRequest
public string? DefinitionId { get; set; }
public string? Name { get; set; }
public string? Description { get; set; } = default!;
public int? Version { get; set; }
public ICollection<VariableDefinition>? Variables { get; set; } = default!;
public IDictionary<string, object>? Metadata { get; set; } = default!;
public IDictionary<string, object>? ApplicationProperties { get; set; } = default!;

View file

@ -1,6 +1,10 @@
using Elsa.Mediator.Services;
using Elsa.Models;
using Elsa.Persistence.Common.Extensions;
using Elsa.Workflows.Core.Services;
using Elsa.Workflows.Management.Notifications;
using Elsa.Workflows.Management.Services;
using Elsa.Workflows.Persistence.Entities;
using Elsa.Workflows.Persistence.Services;
namespace Elsa.Workflows.Management.Implementations;
@ -9,11 +13,19 @@ public class WorkflowDefinitionManager : IWorkflowDefinitionManager
{
private readonly IWorkflowDefinitionStore _store;
private readonly IEventPublisher _eventPublisher;
private readonly IWorkflowDefinitionPublisher _workflowPublisher;
private readonly IIdentityGenerator _identityGenerator;
public WorkflowDefinitionManager(IWorkflowDefinitionStore store, IEventPublisher eventPublisher)
public WorkflowDefinitionManager(
IWorkflowDefinitionStore store,
IEventPublisher eventPublisher,
IWorkflowDefinitionPublisher workflowPublisher,
IIdentityGenerator identityGenerator)
{
_store = store;
_eventPublisher = eventPublisher;
_workflowPublisher = workflowPublisher;
_identityGenerator = identityGenerator;
}
public async Task<int> DeleteByDefinitionIdAsync(string definitionId, CancellationToken cancellationToken = default)
@ -30,4 +42,55 @@ public class WorkflowDefinitionManager : IWorkflowDefinitionManager
await _eventPublisher.PublishAsync(new WorkflowDefinitionsDeleted(ids), cancellationToken);
return count;
}
public async Task<bool> DeleteVersionAsync(string definitionId, int versionToDelete, CancellationToken cancellationToken = default)
{
var workflows = (await _store.FindLatestAndPublishedByDefinitionIdAsync(definitionId, cancellationToken)).ToList();
var latestVersion = workflows.WithVersion(VersionOptions.Latest).First();
var publishedVersion = workflows.WithVersion(VersionOptions.Published).FirstOrDefault();
if (versionToDelete == publishedVersion?.Version)
{
throw new Exception("Published version cannot be deleted");
}
var isDeleted = await _store.DeleteByDefinitionIdAndVersionAsync(definitionId, versionToDelete, cancellationToken) > 0;
if (isDeleted)
{
await _eventPublisher.PublishAsync(new WorkflowDefinitionVersionDeleted(definitionId, versionToDelete), cancellationToken);
if (latestVersion.Version == versionToDelete)
{
var lastVersion = await _store.FindLastVersionByDefinitionIdAsync(definitionId, cancellationToken);
if (lastVersion is not null)
{
lastVersion.IsLatest = true;
await _store.SaveAsync(lastVersion, cancellationToken);
}
}
}
return isDeleted;
}
public async Task<WorkflowDefinition> RevertVersionAsync(string definitionId, int version, CancellationToken cancellationToken = default)
{
var publishedAndLatestVersions = (await _store.FindLatestAndPublishedByDefinitionIdAsync(definitionId, cancellationToken)).ToList();
if (publishedAndLatestVersions.Any(v => v.Version == version))
{
throw new Exception("Latest or published versions cannot be reverted");
}
var latestVersion = publishedAndLatestVersions.First(v => v.IsLatest);
latestVersion.IsLatest = false;
await _store.SaveAsync(latestVersion, cancellationToken);
var draft = await _workflowPublisher.GetDraftAsync(definitionId, version, cancellationToken);
draft!.Id = _identityGenerator.GenerateId();
draft.Version = latestVersion.Version + 1;
draft.IsLatest = true;
await _store.SaveAsync(draft, cancellationToken);
return draft;
}
}

View file

@ -56,7 +56,7 @@ namespace Elsa.Workflows.Management.Implementations
public async Task<WorkflowDefinition?> PublishAsync(string definitionId, CancellationToken cancellationToken = default)
{
var definition = await _workflowDefinitionStore.FindByDefinitionIdAsync(definitionId, VersionOptions.Latest, cancellationToken);
var definition = (await _workflowDefinitionStore.FindByDefinitionIdAsync(definitionId, VersionOptions.Latest, cancellationToken)).FirstOrDefault();
if (definition == null)
return null;
@ -94,7 +94,7 @@ namespace Elsa.Workflows.Management.Implementations
public async Task<WorkflowDefinition?> RetractAsync(string definitionId, CancellationToken cancellationToken = default)
{
var definition = await _workflowDefinitionStore.FindByDefinitionIdAsync(definitionId, VersionOptions.Published, cancellationToken);
var definition = (await _workflowDefinitionStore.FindByDefinitionIdAsync(definitionId, VersionOptions.Published, cancellationToken)).FirstOrDefault();
if (definition == null)
return null;
@ -116,9 +116,9 @@ namespace Elsa.Workflows.Management.Implementations
return definition;
}
public async Task<WorkflowDefinition?> GetDraftAsync(string definitionId, CancellationToken cancellationToken = default)
public async Task<WorkflowDefinition?> GetDraftAsync(string definitionId, int? version, CancellationToken cancellationToken = default)
{
var definition = await _workflowDefinitionStore.FindByDefinitionIdAsync(definitionId, VersionOptions.Latest, cancellationToken);
var definition = (await _workflowDefinitionStore.FindByDefinitionIdAsync(definitionId, version.HasValue ? VersionOptions.SpecificVersion(version.Value) : VersionOptions.Latest, cancellationToken)).FirstOrDefault();
if (definition == null)
return null;
@ -139,7 +139,7 @@ namespace Elsa.Workflows.Management.Implementations
{
var draft = definition;
var definitionId = definition.DefinitionId;
var latestVersion = await _workflowDefinitionStore.FindByDefinitionIdAsync(definitionId, VersionOptions.Latest, cancellationToken);
var latestVersion = (await _workflowDefinitionStore.FindByDefinitionIdAsync(definitionId, VersionOptions.Latest, cancellationToken)).FirstOrDefault();
if (latestVersion is { IsPublished: true, IsLatest: true })
{

View file

@ -0,0 +1,5 @@
using Elsa.Mediator.Services;
namespace Elsa.Workflows.Management.Notifications;
public record WorkflowDefinitionVersionDeleted(string DefinitionId, int Version) : INotification;

View file

@ -1,7 +1,11 @@
using Elsa.Workflows.Persistence.Entities;
namespace Elsa.Workflows.Management.Services;
public interface IWorkflowDefinitionManager
{
Task<int> DeleteByDefinitionIdAsync(string definitionId, CancellationToken cancellationToken = default);
Task<bool> DeleteVersionAsync(string definitionId, int version, CancellationToken cancellationToken = default);
Task<int> BulkDeleteByDefinitionIdsAsync(IEnumerable<string> definitionIds, CancellationToken cancellationToken = default);
Task<WorkflowDefinition> RevertVersionAsync(string definitionId, int version, CancellationToken cancellationToken = default);
}

View file

@ -9,7 +9,7 @@ namespace Elsa.Workflows.Management.Services
Task<WorkflowDefinition> PublishAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default);
Task<WorkflowDefinition?> RetractAsync(string definitionId, CancellationToken cancellationToken = default);
Task<WorkflowDefinition> RetractAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default);
Task<WorkflowDefinition?> GetDraftAsync(string definitionId, CancellationToken cancellationToken = default);
Task<WorkflowDefinition?> GetDraftAsync(string definitionId, int? version = null, CancellationToken cancellationToken = default);
Task<WorkflowDefinition> SaveDraftAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default);
}
}

View file

@ -18,4 +18,8 @@
<ProjectReference Include="..\Elsa.Workflows.Persistence.EntityFrameworkCore\Elsa.Workflows.Persistence.EntityFrameworkCore.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations" />
</ItemGroup>
</Project>

View file

@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations
{
[DbContext(typeof(WorkflowsDbContext))]
[Migration("20220814124730_Initial")]
[Migration("20220828110255_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -56,6 +56,9 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("WorkflowVersion")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ActivityId")
@ -188,6 +191,9 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("WorkflowVersion")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ActivityId")
@ -214,6 +220,9 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations
b.HasIndex("WorkflowInstanceId")
.HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowInstanceId");
b.HasIndex("WorkflowVersion")
.HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowVersion");
b.ToTable("WorkflowExecutionLogRecords");
});

View file

@ -19,6 +19,7 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations
Data = table.Column<string>(type: "TEXT", nullable: true),
WorkflowDefinitionId = table.Column<string>(type: "TEXT", nullable: false),
WorkflowInstanceId = table.Column<string>(type: "TEXT", nullable: false),
WorkflowVersion = table.Column<int>(type: "INTEGER", nullable: false),
CorrelationId = table.Column<string>(type: "TEXT", nullable: true),
ActivityId = table.Column<string>(type: "TEXT", nullable: false),
ActivityInstanceId = table.Column<string>(type: "TEXT", nullable: false),
@ -59,6 +60,7 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations
Id = table.Column<string>(type: "TEXT", nullable: false),
WorkflowDefinitionId = table.Column<string>(type: "TEXT", nullable: false),
WorkflowInstanceId = table.Column<string>(type: "TEXT", nullable: false),
WorkflowVersion = table.Column<int>(type: "INTEGER", nullable: false),
ActivityInstanceId = table.Column<string>(type: "TEXT", nullable: false),
ParentActivityInstanceId = table.Column<string>(type: "TEXT", nullable: true),
ActivityId = table.Column<string>(type: "TEXT", nullable: false),
@ -209,6 +211,11 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations
table: "WorkflowExecutionLogRecords",
column: "WorkflowInstanceId");
migrationBuilder.CreateIndex(
name: "IX_WorkflowExecutionLogRecord_WorkflowVersion",
table: "WorkflowExecutionLogRecords",
column: "WorkflowVersion");
migrationBuilder.CreateIndex(
name: "IX_WorkflowInstance_CorrelationId",
table: "WorkflowInstances",

View file

@ -54,6 +54,9 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("WorkflowVersion")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ActivityId")
@ -186,6 +189,9 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("WorkflowVersion")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ActivityId")
@ -212,6 +218,9 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations
b.HasIndex("WorkflowInstanceId")
.HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowInstanceId");
b.HasIndex("WorkflowVersion")
.HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowVersion");
b.ToTable("WorkflowExecutionLogRecords");
});

View file

@ -19,6 +19,7 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.EntityConfiguration
builder.HasIndex(x => x.EventName).HasDatabaseName($"IX_{nameof(WorkflowExecutionLogRecord)}_{nameof(WorkflowExecutionLogRecord.EventName)}");
builder.HasIndex(x => x.WorkflowDefinitionId).HasDatabaseName($"IX_{nameof(WorkflowExecutionLogRecord)}_{nameof(WorkflowExecutionLogRecord.WorkflowDefinitionId)}");
builder.HasIndex(x => x.WorkflowInstanceId).HasDatabaseName($"IX_{nameof(WorkflowExecutionLogRecord)}_{nameof(WorkflowExecutionLogRecord.WorkflowInstanceId)}");
builder.HasIndex(x => x.WorkflowVersion).HasDatabaseName($"IX_{nameof(WorkflowExecutionLogRecord)}_{nameof(WorkflowExecutionLogRecord.WorkflowVersion)}");
}
}
}

View file

@ -6,6 +6,7 @@ using Elsa.Persistence.EntityFrameworkCore.Common.Services;
using Elsa.Workflows.Persistence.Entities;
using Elsa.Workflows.Persistence.Models;
using Elsa.Workflows.Persistence.Services;
using Open.Linq.AsyncExtensions;
namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Implementations;
@ -17,11 +18,11 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore
public async Task<WorkflowDefinition?> FindByIdAsync(string id, CancellationToken cancellationToken = default) =>
await _store.FindAsync(x => x.Id == id, cancellationToken);
public async Task<WorkflowDefinition?> FindByDefinitionIdAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
public async Task<IEnumerable<WorkflowDefinition>> FindByDefinitionIdAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
{
Expression<Func<WorkflowDefinition, bool>> predicate = x => x.DefinitionId == definitionId;
predicate = predicate.WithVersion(versionOptions);
return await _store.FindAsync(predicate, cancellationToken);
return await _store.FindManyAsync(predicate, cancellationToken);
}
public async Task<WorkflowDefinition?> FindByNameAsync(string name, VersionOptions versionOptions, CancellationToken cancellationToken = default)
@ -48,6 +49,14 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore
public async Task<IEnumerable<WorkflowDefinition>> FindLatestAndPublishedByDefinitionIdAsync(string definitionId, CancellationToken cancellationToken = default) =>
await _store.FindManyAsync(x => x.DefinitionId == definitionId && (x.IsLatest || x.IsPublished), cancellationToken);
public async Task<WorkflowDefinition?> FindLastVersionByDefinitionIdAsync(string definitionId, CancellationToken cancellationToken)
{
await using var dbContext = await _store.CreateDbContextAsync(cancellationToken);
var set = dbContext.WorkflowDefinitions;
var query = set.AsQueryable();
return query.Where(w => w.DefinitionId == definitionId).OrderByDescending(w => w.Version).FirstOrDefault();
}
public async Task SaveAsync(WorkflowDefinition record, CancellationToken cancellationToken = default) => await _store.SaveAsync(record, cancellationToken);
public async Task SaveManyAsync(IEnumerable<WorkflowDefinition> records, CancellationToken cancellationToken = default) => await _store.SaveManyAsync(records, cancellationToken);
@ -60,6 +69,15 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore
await dbContext.WorkflowBookmarks.DeleteWhereAsync(dbContext, x => x.WorkflowDefinitionId == definitionId, cancellationToken);
return await dbContext.WorkflowDefinitions.DeleteWhereAsync(dbContext, x => x.DefinitionId == definitionId, cancellationToken);
}
public async Task<int> DeleteByDefinitionIdAndVersionAsync(string definitionId, int version, CancellationToken cancellationToken = default)
{
await using var dbContext = await _store.CreateDbContextAsync(cancellationToken);
await dbContext.WorkflowExecutionLogRecords.DeleteWhereAsync(dbContext, x => x.WorkflowDefinitionId == definitionId && x.WorkflowVersion == version, cancellationToken);
await dbContext.WorkflowInstances.DeleteWhereAsync(dbContext, x => x.DefinitionId == definitionId && x.Version == version, cancellationToken);
await dbContext.WorkflowBookmarks.DeleteWhereAsync(dbContext, x => x.WorkflowDefinitionId == definitionId && x.WorkflowVersion == version, cancellationToken);
return await dbContext.WorkflowDefinitions.DeleteWhereAsync(dbContext, x => x.DefinitionId == definitionId && x.Version == version, cancellationToken);
}
public async Task<int> DeleteByDefinitionIdsAsync(IEnumerable<string> definitionIds, CancellationToken cancellationToken = default)
{

View file

@ -10,6 +10,7 @@ public class WorkflowBookmark : Entity
public string? Data { get; set; }
public string WorkflowDefinitionId { get; init; } = default!;
public string WorkflowInstanceId { get; init; } = default!;
public int WorkflowVersion { get; init; } = default!;
public string? CorrelationId { get; init; }
public string ActivityId { get; init; } = default!;
public string ActivityInstanceId { get; init; } = default!;
@ -23,6 +24,7 @@ public class WorkflowBookmark : Entity
Id = bookmark.Id,
WorkflowDefinitionId = workflowInstance.DefinitionId,
WorkflowInstanceId = workflowInstance.Id,
WorkflowVersion = workflowInstance.Version,
CorrelationId = workflowInstance.CorrelationId,
Hash = bookmark.Hash,
Data = bookmark.Data,

View file

@ -6,6 +6,7 @@ namespace Elsa.Workflows.Persistence.Entities
{
public string WorkflowDefinitionId { get; set; } = default!;
public string WorkflowInstanceId { get; set; } = default!;
public int WorkflowVersion { get; init; }
public string ActivityInstanceId { get; set; } = default!;
public string? ParentActivityInstanceId { get; set; }
public string ActivityId { get; set; } = default!;

View file

@ -32,9 +32,9 @@ public class MemoryWorkflowDefinitionStore : IWorkflowDefinitionStore
return Task.FromResult(definition);
}
public Task<WorkflowDefinition?> FindByDefinitionIdAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
public Task<IEnumerable<WorkflowDefinition>> FindByDefinitionIdAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
{
var definition = _store.Find(x => x.DefinitionId == definitionId && x.WithVersion(versionOptions));
var definition = _store.FindMany(x => x.DefinitionId == definitionId && x.WithVersion(versionOptions));
return Task.FromResult(definition);
}
@ -64,6 +64,12 @@ public class MemoryWorkflowDefinitionStore : IWorkflowDefinitionStore
return Task.FromResult(definitions);
}
public Task<WorkflowDefinition?> FindLastVersionByDefinitionIdAsync(string definitionId, CancellationToken cancellationToken)
{
var query = _store.List();
return Task.FromResult(query.Where(w => w.DefinitionId == definitionId).OrderByDescending(w => w.Version).FirstOrDefault());
}
public Task SaveAsync(WorkflowDefinition record, CancellationToken cancellationToken = default)
{
_store.Save(record);
@ -84,6 +90,14 @@ public class MemoryWorkflowDefinitionStore : IWorkflowDefinitionStore
var result = _store.DeleteWhere(x => x.DefinitionId == definitionId);
return Task.FromResult(result);
}
public Task<int> DeleteByDefinitionIdAndVersionAsync(string definitionId, int version, CancellationToken cancellationToken = default)
{
_bookmarkStore.DeleteWhere(x => x.WorkflowDefinitionId == definitionId && x.WorkflowVersion == version);
_instanceStore.DeleteWhere(x => x.DefinitionId == definitionId && x.Version == version);
var result = _store.DeleteWhere(x => x.DefinitionId == definitionId && x.Version == version);
return Task.FromResult(result);
}
public Task<int> DeleteByDefinitionIdsAsync(IEnumerable<string> definitionIds, CancellationToken cancellationToken = default)
{

View file

@ -14,7 +14,7 @@ public interface IWorkflowDefinitionStore
/// <summary>
/// Finds a workflow definition by its logical definition ID and specified version options.
/// </summary>
Task<WorkflowDefinition?> FindByDefinitionIdAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default);
Task<IEnumerable<WorkflowDefinition>> FindByDefinitionIdAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default);
/// <summary>
/// Finds a workflow definition by name and specified version options.
@ -23,9 +23,11 @@ public interface IWorkflowDefinitionStore
Task<IEnumerable<WorkflowDefinitionSummary>> FindManySummariesAsync(IEnumerable<string> definitionIds, VersionOptions? versionOptions = default, CancellationToken cancellationToken = default);
Task<IEnumerable<WorkflowDefinition>> FindLatestAndPublishedByDefinitionIdAsync(string definitionId, CancellationToken cancellationToken = default);
Task<WorkflowDefinition?> FindLastVersionByDefinitionIdAsync(string definitionId, CancellationToken cancellationToken);
Task SaveAsync(WorkflowDefinition record, CancellationToken cancellationToken = default);
Task SaveManyAsync(IEnumerable<WorkflowDefinition> records, CancellationToken cancellationToken = default);
Task<int> DeleteByDefinitionIdAsync(string definitionId, CancellationToken cancellationToken = default);
Task<int> DeleteByDefinitionIdAndVersionAsync(string definitionId, int version, CancellationToken cancellationToken = default);
Task<int> DeleteByDefinitionIdsAsync(IEnumerable<string> definitionIds, CancellationToken cancellationToken = default);
Task<Page<WorkflowDefinitionSummary>> ListSummariesAsync(

View file

@ -44,10 +44,10 @@ public class PopulateWorkflowDefinitionStore : IHostedService
private async Task AddOrUpdateAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default)
{
// Check if there's already a workflow definition by the definition ID and version.
var existingDefinition = await _workflowDefinitionStore.FindByDefinitionIdAsync(
var existingDefinition = (await _workflowDefinitionStore.FindByDefinitionIdAsync(
definition.DefinitionId,
VersionOptions.SpecificVersion(definition.Version),
cancellationToken);
cancellationToken)).FirstOrDefault();
if (existingDefinition == null)
{

View file

@ -51,7 +51,7 @@ public class DefaultWorkflowInvoker : IWorkflowInvoker
public async Task<RunWorkflowResult> InvokeAsync(WorkflowInstance workflowInstance, Bookmark? bookmark = default, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default)
{
var workflow = await _workflowDefinitionStore.FindByDefinitionIdAsync(workflowInstance.DefinitionId, VersionOptions.SpecificVersion(workflowInstance.Version), cancellationToken);
var workflow = (await _workflowDefinitionStore.FindByDefinitionIdAsync(workflowInstance.DefinitionId, VersionOptions.SpecificVersion(workflowInstance.Version), cancellationToken)).FirstOrDefault();
if (workflow == null)
throw new Exception($"Workflow instance references a workflow that does not exist");
@ -73,7 +73,7 @@ public class DefaultWorkflowInvoker : IWorkflowInvoker
private async Task<WorkflowDefinition> GetWorkflowDefinitionAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
{
var definition = await _workflowDefinitionStore.FindByDefinitionIdAsync(definitionId, versionOptions, cancellationToken);
var definition = (await _workflowDefinitionStore.FindByDefinitionIdAsync(definitionId, versionOptions, cancellationToken)).FirstOrDefault();
if (definition == null)
throw new Exception($"Workflow instance references a workflow that does not exist");

View file

@ -33,7 +33,7 @@ public class WorkflowInstanceFactory : IWorkflowInstanceFactory
public async Task<WorkflowInstance> CreateAsync(string workflowDefinitionId, VersionOptions versionOptions, string? correlationId, CancellationToken cancellationToken = default)
{
var workflow = (await _workflowDefinitionStore.FindByDefinitionIdAsync(workflowDefinitionId, versionOptions, cancellationToken))!;
var workflow = (await _workflowDefinitionStore.FindByDefinitionIdAsync(workflowDefinitionId, versionOptions, cancellationToken)).FirstOrDefault()!;
return await CreateAsync(workflow, correlationId, cancellationToken);
}

View file

@ -56,7 +56,7 @@ public class ResumeWorkflowInstructionInterpreter : WorkflowInstructionInterpret
return null;
}
var definition = await _workflowDefinitionStore.FindByDefinitionIdAsync(workflowDefinitionId, VersionOptions.SpecificVersion(workflowInstance.Version), cancellationToken);
var definition = (await _workflowDefinitionStore.FindByDefinitionIdAsync(workflowDefinitionId, VersionOptions.SpecificVersion(workflowInstance.Version), cancellationToken)).FirstOrDefault();
if (definition == null)
{

View file

@ -38,6 +38,7 @@ public class PersistWorkflowExecutionLogMiddleware : WorkflowExecutionMiddleware
EventName = x.EventName,
WorkflowDefinitionId = context.Workflow.Identity.DefinitionId,
WorkflowInstanceId = context.Id,
WorkflowVersion = context.Workflow.Version,
Source = x.Source,
Payload = x.Payload,
Timestamp = x.Timestamp