work in progress -

* Link between message instance and correlations is now a link table and many-to-many relationships as recommended by SQLAlchemy
* Use the correlation keys, not the process id when accepting api messages.
This commit is contained in:
Dan 2023-02-17 10:44:03 -05:00
parent 1650486a32
commit 8e257600af
14 changed files with 404 additions and 151 deletions

View File

@ -5,6 +5,8 @@ import shutil
import pytest import pytest
from flask.app import Flask from flask.app import Flask
from flask.testing import FlaskClient from flask.testing import FlaskClient
from spiffworkflow_backend.models import message_correlation_message_instance
from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
@ -46,13 +48,14 @@ def app() -> Flask:
@pytest.fixture() @pytest.fixture()
def with_db_and_bpmn_file_cleanup() -> None: def with_db_and_bpmn_file_cleanup() -> None:
"""Process_group_resource.""" meta = db.metadata
db.session.query(HumanTaskUserModel).delete() for table in reversed(meta.sorted_tables):
print
for model in SpiffworkflowBaseDBModel._all_subclasses(): 'Clear table %s' % table
db.session.query(model).delete() db.session.execute(table.delete())
db.session.commit() db.session.commit()
try: try:
yield yield
finally: finally:

View File

@ -0,0 +1,33 @@
"""empty message
Revision ID: ac40af4ddef3
Revises: 63fc8d693b9f
Create Date: 2023-02-16 14:54:14.533029
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'ac40af4ddef3'
down_revision = '63fc8d693b9f'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('message_correlation_message_instance_unique', table_name='message_correlation_message_instance')
op.drop_column('message_correlation_message_instance', 'id')
op.create_primary_key('mcmi_pirmary_key','message_correlation_message_instance', ['message_instance_id', 'message_correlation_id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('message_correlation_message_instance', sa.Column('id', mysql.INTEGER(), autoincrement=False, nullable=False))
op.create_index('message_correlation_message_instance_unique', 'message_correlation_message_instance', ['message_instance_id', 'message_correlation_id'], unique=False)
op.create_index('ix_message_correlation_message_instance_message_instance_id', 'message_correlation_message_instance', ['message_instance_id'], unique=False)
op.create_index('ix_message_correlation_message_instance_message_correlation_id', 'message_correlation_message_instance', ['message_correlation_id'], unique=False)
# ### end Alembic commands ###

View File

@ -28,6 +28,7 @@ groups:
users: users:
[ [
admin@spiffworkflow.org, admin@spiffworkflow.org,
nelson@spiffworkflow.org
] ]
permissions: permissions:

View File

@ -12,11 +12,6 @@ from spiffworkflow_backend.models.message_correlation_property import (
) )
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
if TYPE_CHECKING:
from spiffworkflow_backend.models.message_correlation_message_instance import ( # noqa: F401
MessageCorrelationMessageInstanceModel,
)
@dataclass @dataclass
class MessageCorrelationModel(SpiffworkflowBaseDBModel): class MessageCorrelationModel(SpiffworkflowBaseDBModel):
@ -45,6 +40,4 @@ class MessageCorrelationModel(SpiffworkflowBaseDBModel):
created_at_in_seconds: int = db.Column(db.Integer) created_at_in_seconds: int = db.Column(db.Integer)
message_correlation_property = relationship("MessageCorrelationPropertyModel") message_correlation_property = relationship("MessageCorrelationPropertyModel")
message_correlations_message_instances = relationship(
"MessageCorrelationMessageInstanceModel", cascade="delete"
)

View File

@ -1,32 +1,13 @@
"""Message_correlation_message_instance.""" """Message_correlation_message_instance."""
from dataclasses import dataclass
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
from spiffworkflow_backend.models.message_correlation import MessageCorrelationModel from spiffworkflow_backend.models.message_correlation import MessageCorrelationModel
from spiffworkflow_backend.models.message_instance import MessageInstanceModel
message_correlation_message_instance_table \
@dataclass = db.Table('message_correlation_message_instance',
class MessageCorrelationMessageInstanceModel(SpiffworkflowBaseDBModel): db.Column('message_instance_id',
"""MessageCorrelationMessageInstanceModel.""" ForeignKey('message_instance.id'), primary_key=True),
db.Column('message_correlation_id',
__tablename__ = "message_correlation_message_instance" ForeignKey('message_correlation.id'),primary_key=True)
)
__table_args__ = (
db.UniqueConstraint(
"message_instance_id",
"message_correlation_id",
name="message_correlation_message_instance_unique",
),
)
id = db.Column(db.Integer, primary_key=True)
message_instance_id = db.Column(
ForeignKey(MessageInstanceModel.id), nullable=False, index=True # type: ignore
)
message_correlation_id = db.Column(
ForeignKey(MessageCorrelationModel.id), nullable=False, index=True
)

View File

@ -13,13 +13,10 @@ from sqlalchemy.orm import validates
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
from spiffworkflow_backend.models.message_correlation_message_instance import message_correlation_message_instance_table
from spiffworkflow_backend.models.message_model import MessageModel from spiffworkflow_backend.models.message_model import MessageModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
if TYPE_CHECKING:
from spiffworkflow_backend.models.message_correlation_message_instance import ( # noqa: F401
MessageCorrelationMessageInstanceModel,
)
class MessageTypes(enum.Enum): class MessageTypes(enum.Enum):
@ -38,6 +35,7 @@ class MessageStatuses(enum.Enum):
failed = "failed" failed = "failed"
@dataclass @dataclass
class MessageInstanceModel(SpiffworkflowBaseDBModel): class MessageInstanceModel(SpiffworkflowBaseDBModel):
"""Messages from a process instance that are ready to send to a receiving task.""" """Messages from a process instance that are ready to send to a receiving task."""
@ -47,10 +45,11 @@ class MessageInstanceModel(SpiffworkflowBaseDBModel):
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(db.Integer, primary_key=True)
process_instance_id: int = db.Column(ForeignKey(ProcessInstanceModel.id), nullable=False) # type: ignore process_instance_id: int = db.Column(ForeignKey(ProcessInstanceModel.id), nullable=False) # type: ignore
message_model_id: int = db.Column(ForeignKey(MessageModel.id), nullable=False) message_model_id: int = db.Column(ForeignKey(MessageModel.id), nullable=False)
message_model = relationship("MessageModel") message_model = db.relationship("MessageModel")
message_correlations_message_instances = relationship( message_correlations = db.relationship("MessageCorrelationModel",
"MessageCorrelationMessageInstanceModel", cascade="delete" secondary=message_correlation_message_instance_table,
) backref="message_instances",
cascade="all,delete")
message_type: str = db.Column(db.String(20), nullable=False) message_type: str = db.Column(db.String(20), nullable=False)
payload: str = db.Column(db.JSON) payload: str = db.Column(db.JSON)
@ -59,8 +58,6 @@ class MessageInstanceModel(SpiffworkflowBaseDBModel):
updated_at_in_seconds: int = db.Column(db.Integer) updated_at_in_seconds: int = db.Column(db.Integer)
created_at_in_seconds: int = db.Column(db.Integer) created_at_in_seconds: int = db.Column(db.Integer)
message_correlations: Optional[dict] = None
@validates("message_type") @validates("message_type")
def validate_message_type(self, key: str, value: Any) -> Any: def validate_message_type(self, key: str, value: Any) -> Any:
"""Validate_message_type.""" """Validate_message_type."""
@ -71,6 +68,19 @@ class MessageInstanceModel(SpiffworkflowBaseDBModel):
"""Validate_status.""" """Validate_status."""
return self.validate_enum_field(key, value, MessageStatuses) return self.validate_enum_field(key, value, MessageStatuses)
def correlates(self, correlation_dictionary):
"""Returns true if the given dictionary matches the correlation names and values connected to this message instance"""
for c in self.message_correlations:
# Fixme: Maybe we should look at typing the correlations and not forcing them to strings?
if c.name in correlation_dictionary and str(correlation_dictionary[c.name]) == c.value:
continue
else:
return False
return True
corrs = {}
# This runs for ALL db flushes for ANY model, not just this one even if it's in the MessageInstanceModel class # This runs for ALL db flushes for ANY model, not just this one even if it's in the MessageInstanceModel class
# so this may not be worth it or there may be a better way to do it # so this may not be worth it or there may be a better way to do it

View File

@ -12,6 +12,7 @@ from flask.wrappers import Response
from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.models.message_correlation import MessageCorrelationModel from spiffworkflow_backend.models.message_correlation import MessageCorrelationModel
from spiffworkflow_backend.models.message_correlation_property import MessageCorrelationPropertyModel
from spiffworkflow_backend.models.message_instance import MessageInstanceModel from spiffworkflow_backend.models.message_instance import MessageInstanceModel
from spiffworkflow_backend.models.message_model import MessageModel from spiffworkflow_backend.models.message_model import MessageModel
from spiffworkflow_backend.models.message_triggerable_process_model import ( from spiffworkflow_backend.models.message_triggerable_process_model import (
@ -110,79 +111,57 @@ def message_send(
raise ( raise (
ApiError( ApiError(
error_code="missing_payload", error_code="missing_payload",
message="Body is missing payload.", message="Please include a 'payload' in the JSON body that contains the message contents.",
status_code=400, status_code=400,
) )
) )
process_instance = None process_instance = None
if "process_instance_id" in body:
# to make sure we have a valid process_instance_id # Is there a running instance that is waiting for this message?
process_instance = _find_process_instance_by_id_or_raise( message_instances = MessageInstanceModel.query.filter_by(message_model_id=message_model.id).all()
body["process_instance_id"] correlations = MessageCorrelationPropertyModel.query.filter_by(message_model_id=message_model.id).all()
# do any waiting message instances have matching correlations?
matching_message = None
for message_instance in message_instances:
if message_instance.correlates(body["payload"]):
matching_message = message_instance
process_instance = None
if matching_message:
process_instance = ProcessInstanceModel.query.filter_by(id = matching_message.process_instance_id).first()
if matching_message and process_instance and process_instance.status != ProcessInstanceStatus.waiting.value:
ApiError(
error_code="message_not_accepted",
message=(
f"The process that can accept message '{message_identifier}' with the given correlation keys"
f" is not currently waiting for that message. It is currently in the a '{process_instance.status}' state."
),
status_code=400,
) )
elif matching_message and process_instance:
if process_instance.status == ProcessInstanceStatus.suspended.value:
raise ApiError(
error_code="process_instance_is_suspended",
message=(
f"Process Instance '{process_instance.id}' is suspended and cannot"
" accept messages.'"
),
status_code=400,
)
if process_instance.status == ProcessInstanceStatus.terminated.value:
raise ApiError(
error_code="process_instance_is_terminated",
message=(
f"Process Instance '{process_instance.id}' is terminated and cannot"
" accept messages.'"
),
status_code=400,
)
message_instance = MessageInstanceModel.query.filter_by(
process_instance_id=process_instance.id,
message_model_id=message_model.id,
message_type="receive",
status="ready",
).first()
if message_instance is None:
raise (
ApiError(
error_code="cannot_find_waiting_message",
message=(
"Could not find waiting message for identifier"
f" {message_identifier} and process instance"
f" {process_instance.id}"
),
status_code=400,
)
)
MessageService.process_message_receive( MessageService.process_message_receive(
message_instance, message_model.name, body["payload"] message_instance, message_model.name, body["payload"]
) )
else: else:
# We don't have a process model waiting on this message, perhaps some process should be started?
message_triggerable_process_model = ( message_triggerable_process_model = (
MessageTriggerableProcessModel.query.filter_by( MessageTriggerableProcessModel.query.filter_by(
message_model_id=message_model.id message_model_id=message_model.id
).first() ).first()
) )
if message_triggerable_process_model is None: if message_triggerable_process_model is None:
raise ( raise (
ApiError( ApiError(
error_code="cannot_start_message", error_code="cannot_start_message",
message=( message=(
"Message with identifier cannot be start with message:" f"No process instances correlate with the given message id of '{message_identifier}'. "
f" {message_identifier}" f"And this message name is not currently associated with any process Start Event."),
),
status_code=400, status_code=400,
) )
) )
process_instance = MessageService.process_message_triggerable_process_model( process_instance = MessageService.process_message_triggerable_process_model(
message_triggerable_process_model, message_triggerable_process_model,
message_model.name, message_model.name,

View File

@ -8,9 +8,6 @@ from sqlalchemy import select
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.message_correlation import MessageCorrelationModel from spiffworkflow_backend.models.message_correlation import MessageCorrelationModel
from spiffworkflow_backend.models.message_correlation_message_instance import (
MessageCorrelationMessageInstanceModel,
)
from spiffworkflow_backend.models.message_instance import MessageInstanceModel from spiffworkflow_backend.models.message_instance import MessageInstanceModel
from spiffworkflow_backend.models.message_triggerable_process_model import ( from spiffworkflow_backend.models.message_triggerable_process_model import (
MessageTriggerableProcessModel, MessageTriggerableProcessModel,
@ -166,11 +163,8 @@ class MessageService:
message_instances_receive: list[MessageInstanceModel], message_instances_receive: list[MessageInstanceModel],
) -> Optional[MessageInstanceModel]: ) -> Optional[MessageInstanceModel]:
"""Get_message_instance_receive.""" """Get_message_instance_receive."""
message_correlations_send = (
MessageCorrelationModel.query.join(MessageCorrelationMessageInstanceModel) message_correlations_send = message_instance_send.message_correlations
.filter_by(message_instance_id=message_instance_send.id)
.all()
)
message_correlation_filter = [] message_correlation_filter = []
for message_correlation_send in message_correlations_send: for message_correlation_send in message_correlations_send:
@ -196,7 +190,7 @@ class MessageService:
or_(*message_correlation_filter), or_(*message_correlation_filter),
) )
) )
.join(MessageCorrelationMessageInstanceModel) # type: ignore .join(message_correlation_message_instance_table) # type: ignore
.filter_by( .filter_by(
message_instance_id=message_instance_receive.id, message_instance_id=message_instance_receive.id,
) )

View File

@ -61,9 +61,6 @@ from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.human_task import HumanTaskModel from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel
from spiffworkflow_backend.models.message_correlation import MessageCorrelationModel from spiffworkflow_backend.models.message_correlation import MessageCorrelationModel
from spiffworkflow_backend.models.message_correlation_message_instance import (
MessageCorrelationMessageInstanceModel,
)
from spiffworkflow_backend.models.message_correlation_property import ( from spiffworkflow_backend.models.message_correlation_property import (
MessageCorrelationPropertyModel, MessageCorrelationPropertyModel,
) )
@ -1389,7 +1386,7 @@ class ProcessInstanceProcessor:
"message_correlation_property": ( "message_correlation_property": (
message_correlation_property message_correlation_property
), ),
"name": message_correlation_key, "name": message_correlation_property_identifier,
"value": message_correlation_property_value, "value": message_correlation_property_value,
} }
) )
@ -1399,27 +1396,19 @@ class ProcessInstanceProcessor:
message_model_id=message_model.id, message_model_id=message_model.id,
payload=bpmn_message.payload, payload=bpmn_message.payload,
) )
db.session.add(message_instance)
db.session.commit()
correlation_models = []
for message_correlation in message_correlations: for message_correlation in message_correlations:
message_correlation = MessageCorrelationModel( correlation_models.append(MessageCorrelationModel(
process_instance_id=self.process_instance_model.id, process_instance_id=self.process_instance_model.id,
message_correlation_property_id=message_correlation[ message_correlation_property_id=message_correlation[
"message_correlation_property" "message_correlation_property"
].id, ].id,
name=message_correlation["name"], name=message_correlation["name"],
value=message_correlation["value"], value=message_correlation["value"],
) ))
db.session.add(message_correlation) message_instance.message_correlations = correlation_models
db.session.commit() db.session.add(message_instance)
message_correlation_message_instance = (
MessageCorrelationMessageInstanceModel(
message_instance_id=message_instance.id,
message_correlation_id=message_correlation.id,
)
)
db.session.add(message_correlation_message_instance)
db.session.commit() db.session.commit()
def queue_waiting_receive_messages(self) -> None: def queue_waiting_receive_messages(self) -> None:
@ -1465,31 +1454,28 @@ class ProcessInstanceProcessor:
message_type="receive", message_type="receive",
message_model_id=message_model.id, message_model_id=message_model.id,
) )
db.session.add(message_instance)
for ( for (
spiff_correlation_property spiff_correlation_property
) in waiting_task.task_spec.event_definition.correlation_properties: ) in waiting_task.task_spec.event_definition.correlation_properties:
# NOTE: we may have to cycle through keys here message_correlation = next((mc for mc in message_instance.message_correlations
# not sure yet if it's valid for a property to be associated with multiple keys if mc.name == spiff_correlation_property.name), None)
correlation_key_name = spiff_correlation_property.correlation_keys[0] if not message_correlation:
message_correlation = ( expression = spiff_correlation_property.expression
MessageCorrelationModel.query.filter_by( correlation_value = ProcessInstanceProcessor._script_engine.evaluate(waiting_task, expression)
process_instance_id=self.process_instance_model.id, correlation_name = spiff_correlation_property.name
name=correlation_key_name, message_prop = MessageCorrelationPropertyModel.query.\
) filter_by(identifier=correlation_name).\
.join(MessageCorrelationPropertyModel) filter_by(message_model_id=message_model.id).first()
.filter_by(identifier=spiff_correlation_property.name)
.first()
)
message_correlation_message_instance = (
MessageCorrelationMessageInstanceModel(
message_instance_id=message_instance.id,
message_correlation_id=message_correlation.id,
)
)
db.session.add(message_correlation_message_instance)
message_correlation = MessageCorrelationModel(
process_instance_id=self.process_instance_model.id,
message_correlation_property_id=message_prop.id,
name=correlation_name,
value=correlation_value,
)
message_instance.message_correlations.append(message_correlation)
db.session.add(message_instance)
db.session.commit() db.session.commit()
def increment_spiff_step(self) -> None: def increment_spiff_step(self) -> None:

View File

@ -176,7 +176,7 @@ class SpecFileService(FileSystemService):
file_type = FileSystemService.file_type(file_name) file_type = FileSystemService.file_type(file_name)
if file_type.value == FileType.bpmn.value: if file_type.value == FileType.bpmn.value:
validator = BpmnValidator() validator = BpmnValidator()
parser = MyCustomParser(validator=validator) parser = MyCustomParser()
try: try:
parser.add_bpmn_xml( parser.add_bpmn_xml(
cls.get_etree_from_xml_bytes(binary_data), filename=file_name cls.get_etree_from_xml_bytes(binary_data), filename=file_name

View File

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:collaboration id="Collaboration_0oye1os">
<bpmn:participant id="message_initiator" name="Message Initiator" processRef="message_send_process" />
<bpmn:participant id="message-receiver" name="Message Receiver" />
<bpmn:messageFlow id="message_send_flow" name="Message Send Flow" sourceRef="send_message" targetRef="message-receiver" />
<bpmn:messageFlow id="message_response_flow" name="Message Response Flow" sourceRef="message-receiver" targetRef="receive_message_response" />
<bpmn:textAnnotation id="TextAnnotation_0oxbpew">
<bpmn:text>The messages sent here are about an Invoice that can be uniquely identified by the customer_id ("sartography") and a purchase order number (1001)
It will fire a message connected to the invoice keys above, starting another process, which can communicate back to this specific process instance using the correct key.</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1d6q7zd" sourceRef="message_initiator" targetRef="TextAnnotation_0oxbpew" />
<bpmn:correlationKey name="invoice">
<bpmn:correlationPropertyRef>po_number</bpmn:correlationPropertyRef>
<bpmn:correlationPropertyRef>customer_id</bpmn:correlationPropertyRef>
</bpmn:correlationKey>
</bpmn:collaboration>
<bpmn:correlationProperty id="po_number" name="Purchase Order Number">
<bpmn:correlationPropertyRetrievalExpression messageRef="request_approval">
<bpmn:formalExpression>po_number</bpmn:formalExpression>
</bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="approval_result">
<bpmn:formalExpression>po_number</bpmn:formalExpression>
</bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty>
<bpmn:correlationProperty id="customer_id" name="Customer ID">
<bpmn:correlationPropertyRetrievalExpression messageRef="request_approval">
<bpmn:formalExpression>customer_id</bpmn:formalExpression>
</bpmn:correlationPropertyRetrievalExpression>
<bpmn:correlationPropertyRetrievalExpression messageRef="approval_result">
<bpmn:formalExpression>customer_id</bpmn:formalExpression>
</bpmn:correlationPropertyRetrievalExpression>
</bpmn:correlationProperty>
<bpmn:process id="message_send_process" name="Message Send Process" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_10conab</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_037vpjk" sourceRef="send_message" targetRef="receive_message_response" />
<bpmn:sequenceFlow id="Flow_1qgz6p0" sourceRef="receive_message_response" targetRef="Event_0kndoyu" />
<bpmn:sequenceFlow id="Flow_10conab" sourceRef="StartEvent_1" targetRef="invoice_form" />
<bpmn:endEvent id="Event_0kndoyu">
<bpmn:incoming>Flow_1qgz6p0</bpmn:incoming>
</bpmn:endEvent>
<bpmn:intermediateCatchEvent id="receive_message_response" name="Receive Approval Result">
<bpmn:incoming>Flow_037vpjk</bpmn:incoming>
<bpmn:outgoing>Flow_1qgz6p0</bpmn:outgoing>
<bpmn:messageEventDefinition id="MessageEventDefinition_1l3n0zr" messageRef="approval_result" />
</bpmn:intermediateCatchEvent>
<bpmn:sendTask id="send_message" name="Request Approval" messageRef="request_approval">
<bpmn:extensionElements>
<spiffworkflow:preScript>the_topic = "first_conversation" </spiffworkflow:preScript>
</bpmn:extensionElements>
<bpmn:incoming>Flow_02lw0q9</bpmn:incoming>
<bpmn:outgoing>Flow_037vpjk</bpmn:outgoing>
</bpmn:sendTask>
<bpmn:sequenceFlow id="Flow_02lw0q9" sourceRef="invoice_form" targetRef="send_message" />
<bpmn:userTask id="invoice_form" name="Create Invoice">
<bpmn:extensionElements>
<spiffworkflow:properties>
<spiffworkflow:property name="formJsonSchemaFilename" value="invoice_form.json" />
<spiffworkflow:property name="formUiSchemaFilename" value="invoice_ui.json" />
</spiffworkflow:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_10conab</bpmn:incoming>
<bpmn:outgoing>Flow_02lw0q9</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmn:message id="request_approval" name="Request Approval">
<bpmn:extensionElements>
<spiffworkflow:messagePayload>{
"customer_id": customer_id,
"po_number": po_number,
"amount": amount,
"description": description,
}</spiffworkflow:messagePayload>
</bpmn:extensionElements>
</bpmn:message>
<bpmn:message id="approval_result" name="Approval Result">
<bpmn:extensionElements>
<spiffworkflow:messageVariable>the_payload</spiffworkflow:messageVariable>
</bpmn:extensionElements>
</bpmn:message>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_0oye1os">
<bpmndi:BPMNShape id="Participant_0bjh770_di" bpmnElement="message_initiator" isHorizontal="true">
<dc:Bounds x="120" y="52" width="600" height="338" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0kndoyu_di" bpmnElement="Event_0kndoyu">
<dc:Bounds x="622" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0yt48xb_di" bpmnElement="receive_message_response">
<dc:Bounds x="532" y="159" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="508" y="129" width="86" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0vm33bu_di" bpmnElement="send_message">
<dc:Bounds x="390" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0798vfz_di" bpmnElement="invoice_form">
<dc:Bounds x="240" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_037vpjk_di" bpmnElement="Flow_037vpjk">
<di:waypoint x="490" y="177" />
<di:waypoint x="532" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1qgz6p0_di" bpmnElement="Flow_1qgz6p0">
<di:waypoint x="568" y="177" />
<di:waypoint x="622" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_10conab_di" bpmnElement="Flow_10conab">
<di:waypoint x="215" y="177" />
<di:waypoint x="240" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_02lw0q9_di" bpmnElement="Flow_02lw0q9">
<di:waypoint x="340" y="177" />
<di:waypoint x="390" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Participant_158b3ei_di" bpmnElement="message-receiver" isHorizontal="true">
<dc:Bounds x="120" y="350" width="600" height="60" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_0oxbpew_di" bpmnElement="TextAnnotation_0oxbpew">
<dc:Bounds x="760" y="-30" width="226.98863220214844" height="155.9943084716797" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_1d6q7zd_di" bpmnElement="Association_1d6q7zd">
<di:waypoint x="699" y="52" />
<di:waypoint x="760" y="15" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1ueajoz_di" bpmnElement="message_send_flow">
<di:waypoint x="410" y="217" />
<di:waypoint x="410" y="350" />
<bpmndi:BPMNLabel>
<dc:Bounds x="413" y="302" width="74" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1n96n67_di" bpmnElement="message_response_flow">
<di:waypoint x="550" y="350" />
<di:waypoint x="550" y="195" />
<bpmndi:BPMNLabel>
<dc:Bounds x="552" y="294" width="77" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -5,9 +5,6 @@ from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
from spiffworkflow_backend.models.message_correlation import MessageCorrelationModel from spiffworkflow_backend.models.message_correlation import MessageCorrelationModel
from spiffworkflow_backend.models.message_correlation_message_instance import (
MessageCorrelationMessageInstanceModel,
)
from spiffworkflow_backend.models.message_instance import MessageInstanceModel from spiffworkflow_backend.models.message_instance import MessageInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel

View File

@ -0,0 +1,123 @@
"""Test_message_service."""
import pytest
from flask import Flask
from flask.testing import FlaskClient
from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.routes.messages_controller import message_send
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
from spiffworkflow_backend.models.message_correlation import MessageCorrelationModel
from spiffworkflow_backend.models.message_instance import MessageInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.message_service import MessageService
from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiffworkflow_backend.services.process_instance_service import (
ProcessInstanceService,
)
class TestMessageService(BaseTest):
"""TestMessageService."""
def test_message_sent(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""This example workflow will send a message called 'request_approval' and then wait for a response messge
of 'approval_result. This test assures that it will fire the message with the correct correlation properties
and will respond only to a message called "approval_result' that has the matching correlation properties."""
process_group_id = "test_group"
self.create_process_group(
client, with_super_admin_user, process_group_id, process_group_id
)
process_model = load_test_spec(
"test_group/message",
process_model_source_directory="message",
bpmn_file_name="message_send_receive.bpmn",
)
self.process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier(
process_model.id,
with_super_admin_user,
)
processor_send_receive = ProcessInstanceProcessor(self.process_instance)
processor_send_receive.do_engine_steps(save=True)
task = processor_send_receive.get_all_user_tasks()[0]
human_task = self.process_instance.active_human_tasks[0]
spiff_task = processor_send_receive.__class__.get_task_by_bpmn_identifier(
human_task.task_name, processor_send_receive.bpmn_process_instance
)
self.payload = {
"customer_id": "Sartography",
"po_number": 1001,
"description": "We build a new feature for messages!",
"amount": "100.00"
}
ProcessInstanceService.complete_form_task(
processor_send_receive,
task,
self.payload,
with_super_admin_user,
human_task,
)
processor_send_receive.save()
self.assure_a_message_was_sent()
self.assure_there_is_a_process_waiting_on_a_message()
## Should return an error when making an API call for the wrong po number
with pytest.raises(ApiError):
message_send("approval_result", {'payload': {'po_number' : 5001}})
## Sound return an error when making an API call for right po number, wrong client
with pytest.raises(ApiError):
message_send("approval_result", {'payload': {'po_number' : 1001, 'customer_id': 'jon'}})
## No error when calling with the correct parameters
response = message_send("approval_result", {'payload': {'po_number' : 1001, 'customer_id': 'Sartography'}})
def assure_a_message_was_sent(self):
# There should be one new send message for the given process instance.
send_messages = MessageInstanceModel.query. \
filter_by(message_type = "send"). \
filter_by(process_instance_id = self.process_instance.id).all()
assert len(send_messages) == 1
send_message = send_messages[0]
# The payload should match because of how it is written in the Send task.
assert send_message.payload == self.payload, "The send message should match up with the payload"
assert send_message.message_model.identifier == "request_approval"
assert send_message.status == "ready"
assert len(send_message.message_correlations) == 2
message_instance_result = MessageInstanceModel.query.all()
self.assure_correlation_properties_are_right(send_message)
def assure_there_is_a_process_waiting_on_a_message(self):
# There should be one new send message for the given process instance.
waiting_messages = MessageInstanceModel.query. \
filter_by(message_type = "receive"). \
filter_by(process_instance_id = self.process_instance.id).all()
assert len(waiting_messages) == 1
waiting_message = waiting_messages[0]
self.assure_correlation_properties_are_right(waiting_message)
def assure_correlation_properties_are_right(self, message):
# Correlation Properties should match up
po_curr = next(c for c in message.message_correlations if c.name == "po_number")
customer_curr = next(c for c in message.message_correlations if c.name == "customer_id")
assert po_curr is not None
assert customer_curr is not None
assert po_curr.value == '1001'
assert customer_curr.value == "Sartography"

View File

@ -8065,7 +8065,7 @@
}, },
"node_modules/bpmn-js-spiffworkflow": { "node_modules/bpmn-js-spiffworkflow": {
"version": "0.0.8", "version": "0.0.8",
"resolved": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#aca23dc56e5d37aa1ed0a3cf11acb55f76a36da7", "resolved": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#f1f008e3e39be43b016718fca6a38b248ab4ecf7",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"inherits": "^2.0.4", "inherits": "^2.0.4",
@ -38214,7 +38214,7 @@
} }
}, },
"bpmn-js-spiffworkflow": { "bpmn-js-spiffworkflow": {
"version": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#aca23dc56e5d37aa1ed0a3cf11acb55f76a36da7", "version": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#f1f008e3e39be43b016718fca6a38b248ab4ecf7",
"from": "bpmn-js-spiffworkflow@sartography/bpmn-js-spiffworkflow#main", "from": "bpmn-js-spiffworkflow@sartography/bpmn-js-spiffworkflow#main",
"requires": { "requires": {
"inherits": "^2.0.4", "inherits": "^2.0.4",