Feature/new reference cache (#511)

* added basic model for new reference cache w/ burnettk

* switched out SpecReferenceCache for ReferenceCacheModel w/ burnettk jbirddog

* pyl w/ burnettk jbirddog

* save items to the db using the new cache with generation table w/ burnettk

* bulk save for performance

* tests are passing

* actually use the new generation table - we still need a test to ensure we are using it

* added test to ensure using new cache generation

* corrected reference interface on frontend w/ burnettk

* do not perform git pull in webhook if the revision is the same as the current w/ burnettk jbirddog

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
Co-authored-by: burnettk <burnettk@users.noreply.github.com>
This commit is contained in:
jasquat 2023-09-22 11:28:59 -04:00 committed by GitHub
parent b035191964
commit 8bf38aaa1c
17 changed files with 553 additions and 122 deletions

View File

@ -0,0 +1,68 @@
"""empty message
Revision ID: 4d438975ff4d
Revises: 9d5b6c5c31a5
Create Date: 2023-09-21 16:25:44.574756
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4d438975ff4d'
down_revision = '9d5b6c5c31a5'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('cache_generation',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('cache_table', sa.String(length=255), nullable=False),
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True),
sa.Column('created_at_in_seconds', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('cache_generation', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_cache_generation_cache_table'), ['cache_table'], unique=False)
op.create_table('reference_cache',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('generation_id', sa.Integer(), nullable=False),
sa.Column('identifier', sa.String(length=255), nullable=False),
sa.Column('display_name', sa.String(length=255), nullable=False),
sa.Column('type', sa.String(length=255), nullable=False),
sa.Column('file_name', sa.String(length=255), nullable=False),
sa.Column('relative_location', sa.String(length=255), nullable=False),
sa.Column('properties', sa.JSON(), nullable=True),
sa.ForeignKeyConstraint(['generation_id'], ['cache_generation.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('generation_id', 'identifier', 'relative_location', 'type', name='reference_cache_uniq')
)
with op.batch_alter_table('reference_cache', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_reference_cache_display_name'), ['display_name'], unique=False)
batch_op.create_index(batch_op.f('ix_reference_cache_generation_id'), ['generation_id'], unique=False)
batch_op.create_index(batch_op.f('ix_reference_cache_identifier'), ['identifier'], unique=False)
batch_op.create_index(batch_op.f('ix_reference_cache_relative_location'), ['relative_location'], unique=False)
batch_op.create_index(batch_op.f('ix_reference_cache_type'), ['type'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('reference_cache', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_reference_cache_type'))
batch_op.drop_index(batch_op.f('ix_reference_cache_relative_location'))
batch_op.drop_index(batch_op.f('ix_reference_cache_identifier'))
batch_op.drop_index(batch_op.f('ix_reference_cache_generation_id'))
batch_op.drop_index(batch_op.f('ix_reference_cache_display_name'))
op.drop_table('reference_cache')
with op.batch_alter_table('cache_generation', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_cache_generation_cache_table'))
op.drop_table('cache_generation')
# ### end Alembic commands ###

View File

@ -26,6 +26,9 @@ from spiffworkflow_backend.models.human_task import HumanTaskModel # noqa: F401
from spiffworkflow_backend.models.spec_reference import ( from spiffworkflow_backend.models.spec_reference import (
SpecReferenceCache, SpecReferenceCache,
) # noqa: F401 ) # noqa: F401
from spiffworkflow_backend.models.reference_cache import (
ReferenceCacheModel,
) # noqa: F401
from spiffworkflow_backend.models.process_caller import ( from spiffworkflow_backend.models.process_caller import (
ProcessCallerCacheModel, ProcessCallerCacheModel,
) # noqa: F401 ) # noqa: F401

View File

@ -0,0 +1,35 @@
from __future__ import annotations
from typing import Any
from sqlalchemy.orm import validates
from spiffworkflow_backend.helpers.spiff_enum import SpiffEnum
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
from spiffworkflow_backend.models.db import db
class CacheGenerationTable(SpiffEnum):
reference_cache = "reference_cache"
class CacheGenerationModel(SpiffworkflowBaseDBModel):
__tablename__ = "cache_generation"
id: int = db.Column(db.Integer, primary_key=True)
cache_table: str = db.Column(db.String(255), index=True, nullable=False)
updated_at_in_seconds: int = db.Column(db.Integer)
created_at_in_seconds: int = db.Column(db.Integer)
@classmethod
def newest_generation_for_table(cls, cache_table: str) -> CacheGenerationModel | None:
order_by_clause = CacheGenerationModel.id.desc() # type: ignore
cache_generation: CacheGenerationModel | None = (
CacheGenerationModel.query.filter_by(cache_table=cache_table).order_by(order_by_clause).first()
)
return cache_generation
@validates("cache_table")
def validate_cache_table(self, key: str, value: Any) -> Any:
return self.validate_enum_field(key, value, CacheGenerationTable)

View File

@ -6,7 +6,7 @@ from datetime import datetime
from typing import Any from typing import Any
from spiffworkflow_backend.helpers.spiff_enum import SpiffEnum from spiffworkflow_backend.helpers.spiff_enum import SpiffEnum
from spiffworkflow_backend.models.spec_reference import SpecReference from spiffworkflow_backend.models.reference_cache import Reference
class FileType(SpiffEnum): class FileType(SpiffEnum):
@ -67,7 +67,7 @@ class File:
type: str type: str
last_modified: datetime last_modified: datetime
size: int size: int
references: list[SpecReference] | None = None references: list[Reference] | None = None
file_contents: bytes | None = None file_contents: bytes | None = None
process_model_id: str | None = None process_model_id: str | None = None
bpmn_process_ids: list[str] | None = None bpmn_process_ids: list[str] | None = None

View File

@ -0,0 +1,160 @@
import os
from dataclasses import dataclass
from typing import Any
from flask_marshmallow import Schema # type: ignore
from marshmallow import INCLUDE
from sqlalchemy import ForeignKey
from sqlalchemy import UniqueConstraint
from sqlalchemy.orm import relationship
from sqlalchemy.orm import validates
from spiffworkflow_backend.helpers.spiff_enum import SpiffEnum
from spiffworkflow_backend.models.cache_generation import CacheGenerationModel
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
from spiffworkflow_backend.models.db import db
# SpecReferenceNotFoundError
class ReferenceNotFoundError(Exception):
pass
class ReferenceType(SpiffEnum):
decision = "decision"
process = "process"
data_store = "data_store"
# SpecReference
@dataclass()
class Reference:
"""File Reference Information.
Includes items such as the process id and name for a BPMN,
or the Decision id and Decision name for a DMN file. There may be more than
one reference that points to a particular file - if for instance, there are
three executable processes in a collaboration within a BPMN Diagram.
"""
identifier: str # The id of the process or decision. "Process_1234"
display_name: str # The name of the process or decision. "Invoice Submission"
relative_location: str
type: str # can be 'process' or 'decision'
file_name: str # The name of the file where this process or decision is defined.
messages: dict # Any messages defined in the same file where this process is defined.
correlations: dict # Any correlations defined in the same file with this process.
start_messages: list # The names of any messages that would start this process.
called_element_ids: list # The element ids of any called elements
properties: dict
def prop_is_true(self, prop_name: str) -> bool:
return prop_name in self.properties and self.properties[prop_name] is True
def set_prop(self, prop_name: str, value: Any) -> None:
self.properties[prop_name] = value
def relative_path(self) -> str:
return os.path.join(self.relative_location, self.file_name).replace("/", os.sep)
# SpecReferenceCache
class ReferenceCacheModel(SpiffworkflowBaseDBModel):
"""A cache of information about all the Processes and Decisions defined in all files."""
__tablename__ = "reference_cache"
__table_args__ = (
UniqueConstraint("generation_id", "identifier", "relative_location", "type", name="reference_cache_uniq"),
)
id: int = db.Column(db.Integer, primary_key=True)
generation_id: int = db.Column(ForeignKey(CacheGenerationModel.id), nullable=False, index=True) # type: ignore
identifier: str = db.Column(db.String(255), index=True, nullable=False)
display_name: str = db.Column(db.String(255), index=True, nullable=False)
type: str = db.Column(db.String(255), index=True, nullable=False)
file_name: str = db.Column(db.String(255), nullable=False)
# relative to SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR
relative_location: str = db.Column(db.String(255), index=True, nullable=False)
properties: dict | None = db.Column(db.JSON)
# has_lanes = db.Column(db.Boolean())
# is_executable = db.Column(db.Boolean())
# is_primary = db.Column(db.Boolean())
generation = relationship(CacheGenerationModel)
def relative_path(self) -> str:
return os.path.join(self.relative_location, self.file_name).replace("/", os.sep)
@classmethod
def basic_query(cls) -> Any:
cache_generation = CacheGenerationModel.newest_generation_for_table("reference_cache")
basic_query = cls.query
if cache_generation is not None:
basic_query = basic_query.filter_by(generation_id=cache_generation.id)
return basic_query
@classmethod
def from_params(
cls,
identifier: str,
display_name: str,
type: str,
file_name: str,
relative_location: str,
properties: dict | None = None,
use_current_cache_generation: bool = False,
) -> "ReferenceCacheModel":
reference_cache = cls(
identifier=identifier,
display_name=display_name,
relative_location=relative_location,
type=type,
file_name=file_name,
properties=properties,
)
if use_current_cache_generation:
cache_generation = CacheGenerationModel.newest_generation_for_table("reference_cache")
if cache_generation is None:
# NOTE: we may want to raise here instead since this should never happen in real environments
# but it does happen in tests
cache_generation = CacheGenerationModel(cache_table="reference_cache")
db.session.add(cache_generation)
reference_cache.generation = cache_generation
return reference_cache
@classmethod
def from_spec_reference(cls, ref: Reference, use_current_cache_generation: bool = False) -> "ReferenceCacheModel":
reference_cache = cls.from_params(
identifier=ref.identifier,
display_name=ref.display_name,
relative_location=ref.relative_location,
type=ref.type,
file_name=ref.file_name,
properties=ref.properties,
use_current_cache_generation=use_current_cache_generation,
)
return reference_cache
@validates("type")
def validate_type(self, key: str, value: Any) -> Any:
return self.validate_enum_field(key, value, ReferenceType)
# SpecReferenceSchema
class ReferenceSchema(Schema): # type: ignore
class Meta:
model = Reference
fields = [
"identifier",
"display_name",
"process_group_id",
"relative_location",
"type",
"file_name",
"properties",
]
unknown = INCLUDE

View File

@ -16,8 +16,8 @@ from spiffworkflow_backend.models.principal import PrincipalModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance_file_data import ProcessInstanceFileDataModel from spiffworkflow_backend.models.process_instance_file_data import ProcessInstanceFileDataModel
from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
from spiffworkflow_backend.models.spec_reference import SpecReferenceSchema from spiffworkflow_backend.models.reference_cache import ReferenceSchema
from spiffworkflow_backend.models.task import TaskModel # noqa: F401 from spiffworkflow_backend.models.task import TaskModel # noqa: F401
from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.git_service import GitService from spiffworkflow_backend.services.git_service import GitService
@ -63,8 +63,8 @@ def process_list() -> Any:
This includes processes that are not the This includes processes that are not the
primary process - helpful for finding possible call activities. primary process - helpful for finding possible call activities.
""" """
references = SpecReferenceCache.query.filter_by(type="process").all() references = ReferenceCacheModel.basic_query().filter_by(type="process").all()
process_model_identifiers = [r.process_model_id for r in references] process_model_identifiers = [r.relative_location for r in references]
permitted_process_model_identifiers = ProcessModelService.process_model_identifiers_with_permission_for_user( permitted_process_model_identifiers = ProcessModelService.process_model_identifiers_with_permission_for_user(
user=g.user, user=g.user,
permission_to_check="create", permission_to_check="create",
@ -73,17 +73,20 @@ def process_list() -> Any:
) )
permitted_references = [] permitted_references = []
for spec_reference in references: for spec_reference in references:
if spec_reference.process_model_id in permitted_process_model_identifiers: if spec_reference.relative_location in permitted_process_model_identifiers:
permitted_references.append(spec_reference) permitted_references.append(spec_reference)
return SpecReferenceSchema(many=True).dump(permitted_references) return ReferenceSchema(many=True).dump(permitted_references)
def process_caller_list(bpmn_process_identifiers: list[str]) -> Any: def process_caller_list(bpmn_process_identifiers: list[str]) -> Any:
callers = ProcessCallerService.callers(bpmn_process_identifiers) callers = ProcessCallerService.callers(bpmn_process_identifiers)
references = ( references = (
SpecReferenceCache.query.filter_by(type="process").filter(SpecReferenceCache.identifier.in_(callers)).all() ReferenceCacheModel.basic_query()
.filter_by(type="process")
.filter(ReferenceCacheModel.identifier.in_(callers)) # type: ignore
.all()
) )
return SpecReferenceSchema(many=True).dump(references) return ReferenceSchema(many=True).dump(references)
def _process_data_fetcher( def _process_data_fetcher(

View File

@ -28,8 +28,8 @@ from spiffworkflow_backend.models.process_instance_queue import ProcessInstanceQ
from spiffworkflow_backend.models.process_instance_report import ProcessInstanceReportModel from spiffworkflow_backend.models.process_instance_report import ProcessInstanceReportModel
from spiffworkflow_backend.models.process_instance_report import Report from spiffworkflow_backend.models.process_instance_report import Report
from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
from spiffworkflow_backend.models.spec_reference import SpecReferenceNotFoundError from spiffworkflow_backend.models.reference_cache import ReferenceNotFoundError
from spiffworkflow_backend.models.task import TaskModel from spiffworkflow_backend.models.task import TaskModel
from spiffworkflow_backend.models.task_definition import TaskDefinitionModel from spiffworkflow_backend.models.task_definition import TaskDefinitionModel
from spiffworkflow_backend.routes.process_api_blueprint import _find_process_instance_by_id_or_raise from spiffworkflow_backend.routes.process_api_blueprint import _find_process_instance_by_id_or_raise
@ -702,13 +702,13 @@ def _get_process_instance(
process_model_with_diagram = None process_model_with_diagram = None
name_of_file_with_diagram = None name_of_file_with_diagram = None
if process_identifier: if process_identifier:
spec_reference = SpecReferenceCache.query.filter_by(identifier=process_identifier, type="process").first() spec_reference = (
ReferenceCacheModel.basic_query().filter_by(identifier=process_identifier, type="process").first()
)
if spec_reference is None: if spec_reference is None:
raise SpecReferenceNotFoundError( raise ReferenceNotFoundError(f"Could not find given process identifier in the cache: {process_identifier}")
f"Could not find given process identifier in the cache: {process_identifier}"
)
process_model_with_diagram = ProcessModelService.get_process_model(spec_reference.process_model_id) process_model_with_diagram = ProcessModelService.get_process_model(spec_reference.relative_location)
name_of_file_with_diagram = spec_reference.file_name name_of_file_with_diagram = spec_reference.file_name
process_instance.process_model_with_diagram_identifier = process_model_with_diagram.id process_instance.process_model_with_diagram_identifier = process_model_with_diagram.id
else: else:

View File

@ -1,7 +1,10 @@
from flask import current_app from flask import current_app
from spiffworkflow_backend.models.cache_generation import CacheGenerationModel
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.spec_file_service import SpecFileService from spiffworkflow_backend.services.spec_file_service import SpecFileService
from sqlalchemy import insert
class DataSetupService: class DataSetupService:
@ -16,25 +19,29 @@ class DataSetupService:
These all exist within processes located on the file system, so we can quickly reference them These all exist within processes located on the file system, so we can quickly reference them
from the database. from the database.
""" """
# Clear out all of the cached data.
SpecFileService.clear_caches()
current_app.logger.debug("DataSetupService.save_all_process_models() start") current_app.logger.debug("DataSetupService.save_all_process_models() start")
failing_process_models = [] failing_process_models = []
process_models = ProcessModelService.get_process_models(recursive=True) process_models = ProcessModelService.get_process_models(recursive=True)
SpecFileService.clear_caches() reference_objects = {}
for process_model in process_models: for process_model in process_models:
current_app.logger.debug(f"Process Model: {process_model.display_name}") current_app.logger.debug(f"Process Model: {process_model.display_name}")
try: try:
# FIXME: get_references_for_file_contents is erroring out for elements in the list
refs = SpecFileService.get_references_for_process(process_model) refs = SpecFileService.get_references_for_process(process_model)
for ref in refs: for ref in refs:
try: try:
SpecFileService.update_caches(ref) reference_cache = ReferenceCacheModel.from_spec_reference(ref)
reference_cache_unique = (
f"{reference_cache.identifier}{reference_cache.relative_location}{reference_cache.type}"
)
reference_objects[reference_cache_unique] = reference_cache
SpecFileService.update_caches_except_process(ref)
db.session.commit() db.session.commit()
except Exception as ex: except Exception as ex:
failing_process_models.append( failing_process_models.append(
( (
f"{ref.process_model_id}/{ref.file_name}", f"{ref.relative_location}/{ref.file_name}",
str(ex), str(ex),
) )
) )
@ -47,5 +54,18 @@ class DataSetupService:
) )
current_app.logger.debug("DataSetupService.save_all_process_models() end") current_app.logger.debug("DataSetupService.save_all_process_models() end")
# get inserted autoincrement primary key value back in a database agnostic way without committing the db session
ins = insert(CacheGenerationModel).values(cache_table="reference_cache") # type: ignore
res = db.session.execute(ins)
cache_generation_id = res.inserted_primary_key[0]
# add primary key value to each element in reference objects list and store in new list
reference_object_list_with_cache_generation_id = []
for reference_object in reference_objects.values():
reference_object.generation_id = cache_generation_id
reference_object_list_with_cache_generation_id.append(reference_object)
db.session.bulk_save_objects(reference_object_list_with_cache_generation_id)
db.session.commit() db.session.commit()
return failing_process_models return failing_process_models

View File

@ -183,7 +183,15 @@ class GitService:
return False return False
if "ref" not in webhook: if "ref" not in webhook:
raise InvalidGitWebhookBodyError(f"Could not find the 'ref' arg in the webhook boy: {webhook}") raise InvalidGitWebhookBodyError(f"Could not find the 'ref' arg in the webhook body: {webhook}")
if "after" not in webhook:
raise InvalidGitWebhookBodyError(f"Could not find the 'after' arg in the webhook body: {webhook}")
git_revision_before_pull = cls.get_current_revision()
git_revision_after = webhook["after"]
if git_revision_before_pull == git_revision_after:
current_app.logger.info("Skipping git pull because we already have the current git revision, git boy!")
return True
if current_app.config["SPIFFWORKFLOW_BACKEND_GIT_SOURCE_BRANCH"] is None: if current_app.config["SPIFFWORKFLOW_BACKEND_GIT_SOURCE_BRANCH"] is None:
raise MissingGitConfigsError( raise MissingGitConfigsError(

View File

@ -65,8 +65,8 @@ from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType
from spiffworkflow_backend.models.process_instance_metadata import ProcessInstanceMetadataModel from spiffworkflow_backend.models.process_instance_metadata import ProcessInstanceMetadataModel
from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
from spiffworkflow_backend.models.script_attributes_context import ScriptAttributesContext from spiffworkflow_backend.models.script_attributes_context import ScriptAttributesContext
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
from spiffworkflow_backend.models.task import TaskModel from spiffworkflow_backend.models.task import TaskModel
from spiffworkflow_backend.models.task import TaskNotFoundError from spiffworkflow_backend.models.task import TaskNotFoundError
from spiffworkflow_backend.models.task_definition import TaskDefinitionModel from spiffworkflow_backend.models.task_definition import TaskDefinitionModel
@ -1244,7 +1244,9 @@ class ProcessInstanceProcessor:
"bpmn_file_full_path_from_bpmn_process_identifier: bpmn_process_identifier is unexpectedly None" "bpmn_file_full_path_from_bpmn_process_identifier: bpmn_process_identifier is unexpectedly None"
) )
spec_reference = SpecReferenceCache.query.filter_by(identifier=bpmn_process_identifier, type="process").first() spec_reference = (
ReferenceCacheModel.basic_query().filter_by(identifier=bpmn_process_identifier, type="process").first()
)
bpmn_file_full_path = None bpmn_file_full_path = None
if spec_reference is None: if spec_reference is None:
bpmn_file_full_path = ProcessInstanceProcessor.backfill_missing_spec_reference_records( bpmn_file_full_path = ProcessInstanceProcessor.backfill_missing_spec_reference_records(
@ -1253,7 +1255,7 @@ class ProcessInstanceProcessor:
else: else:
bpmn_file_full_path = os.path.join( bpmn_file_full_path = os.path.join(
FileSystemService.root_path(), FileSystemService.root_path(),
spec_reference.relative_path, spec_reference.relative_path(),
) )
if bpmn_file_full_path is None: if bpmn_file_full_path is None:
raise ( raise (

View File

@ -8,10 +8,10 @@ from spiffworkflow_backend.models.correlation_property_cache import CorrelationP
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.file import File from spiffworkflow_backend.models.file import File
from spiffworkflow_backend.models.file import FileType from spiffworkflow_backend.models.file import FileType
from spiffworkflow_backend.models.file import SpecReference from spiffworkflow_backend.models.file import Reference
from spiffworkflow_backend.models.message_triggerable_process_model import MessageTriggerableProcessModel from spiffworkflow_backend.models.message_triggerable_process_model import MessageTriggerableProcessModel
from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.authentication_service import NotAuthorizedError from spiffworkflow_backend.services.authentication_service import NotAuthorizedError
from spiffworkflow_backend.services.custom_parser import MyCustomParser from spiffworkflow_backend.services.custom_parser import MyCustomParser
@ -32,7 +32,7 @@ class SpecFileService(FileSystemService):
""" """
@staticmethod @staticmethod
def reference_map(references: list[SpecReference]) -> dict[str, SpecReference]: def reference_map(references: list[Reference]) -> dict[str, Reference]:
"""Creates a dict with provided references organized by id.""" """Creates a dict with provided references organized by id."""
ref_map = {} ref_map = {}
for ref in references: for ref in references:
@ -42,7 +42,7 @@ class SpecFileService(FileSystemService):
@staticmethod @staticmethod
def get_references_for_process( def get_references_for_process(
process_model_info: ProcessModelInfo, process_model_info: ProcessModelInfo,
) -> list[SpecReference]: ) -> list[Reference]:
files = FileSystemService.get_files(process_model_info) files = FileSystemService.get_files(process_model_info)
references = [] references = []
for file in files: for file in files:
@ -50,7 +50,7 @@ class SpecFileService(FileSystemService):
return references return references
@classmethod @classmethod
def get_references_for_file(cls, file: File, process_model_info: ProcessModelInfo) -> list[SpecReference]: def get_references_for_file(cls, file: File, process_model_info: ProcessModelInfo) -> list[Reference]:
full_file_path = SpecFileService.full_file_path(process_model_info, file.name) full_file_path = SpecFileService.full_file_path(process_model_info, file.name)
file_contents: bytes = b"" file_contents: bytes = b""
with open(full_file_path) as f: with open(full_file_path) as f:
@ -71,7 +71,7 @@ class SpecFileService(FileSystemService):
@classmethod @classmethod
def get_references_for_file_contents( def get_references_for_file_contents(
cls, process_model_info: ProcessModelInfo, file_name: str, binary_data: bytes cls, process_model_info: ProcessModelInfo, file_name: str, binary_data: bytes
) -> list[SpecReference]: ) -> list[Reference]:
"""Uses spiffworkflow to parse BPMN and DMN files to determine how they can be externally referenced. """Uses spiffworkflow to parse BPMN and DMN files to determine how they can be externally referenced.
Returns a list of Reference objects that contain the type of reference, the id, the name. Returns a list of Reference objects that contain the type of reference, the id, the name.
@ -80,8 +80,8 @@ class SpecFileService(FileSystemService):
name = {str} 'Level 3' name = {str} 'Level 3'
type = {str} 'process' / 'decision' type = {str} 'process' / 'decision'
""" """
references: list[SpecReference] = [] references: list[Reference] = []
file_path = os.path.join(process_model_info.id_for_file_path(), file_name) os.path.join(process_model_info.id_for_file_path(), file_name)
file_type = FileSystemService.file_type(file_name) file_type = FileSystemService.file_type(file_name)
parser = MyCustomParser() parser = MyCustomParser()
parser_type = None parser_type = None
@ -116,20 +116,17 @@ class SpecFileService(FileSystemService):
called_element_ids = sub_parser.called_element_ids() called_element_ids = sub_parser.called_element_ids()
references.append( references.append(
SpecReference( Reference(
identifier=sub_parser.bpmn_id, identifier=sub_parser.bpmn_id,
display_name=sub_parser.get_name(), display_name=sub_parser.get_name(),
process_model_id=process_model_info.id, relative_location=process_model_info.id,
type=parser_type, type=parser_type,
file_name=file_name, file_name=file_name,
relative_path=file_path,
has_lanes=has_lanes,
is_executable=is_executable,
messages=messages, messages=messages,
is_primary=is_primary,
correlations=correlations, correlations=correlations,
start_messages=start_messages, start_messages=start_messages,
called_element_ids=called_element_ids, called_element_ids=called_element_ids,
properties={"is_primary": is_primary, "has_lanes": has_lanes, "is_executable": is_executable},
) )
) )
return references return references
@ -160,17 +157,19 @@ class SpecFileService(FileSystemService):
cls.validate_bpmn_xml(file_name, binary_data) cls.validate_bpmn_xml(file_name, binary_data)
references = cls.get_references_for_file_contents(process_model_info, file_name, binary_data) references = cls.get_references_for_file_contents(process_model_info, file_name, binary_data)
primary_process_ref = next((ref for ref in references if ref.is_primary and ref.is_executable), None) primary_process_ref = next(
(ref for ref in references if ref.prop_is_true("is_primary") and ref.prop_is_true("is_executable")), None
)
SpecFileService.clear_caches_for_file(file_name, process_model_info) SpecFileService.clear_caches_for_file(file_name, process_model_info)
all_called_element_ids: set[str] = set() all_called_element_ids: set[str] = set()
for ref in references: for ref in references:
# If no valid primary process is defined, default to the first process in the # If no valid primary process is defined, default to the first process in the
# updated file. # updated file.
if not primary_process_ref and ref.type == "process" and ref.is_executable: if not primary_process_ref and ref.type == "process" and ref.prop_is_true("is_executable"):
ref.is_primary = True ref.set_prop("is_primary", True)
if ref.is_primary: if ref.prop_is_true("is_primary"):
update_hash = {} update_hash = {}
if not process_model_info.primary_file_name: if not process_model_info.primary_file_name:
update_hash["primary_process_id"] = ref.identifier update_hash["primary_process_id"] = ref.identifier
@ -185,14 +184,16 @@ class SpecFileService(FileSystemService):
) )
all_called_element_ids = all_called_element_ids | set(ref.called_element_ids) all_called_element_ids = all_called_element_ids | set(ref.called_element_ids)
SpecFileService.update_caches(ref) SpecFileService.update_all_caches(ref)
if user is not None: if user is not None:
called_element_refs = SpecReferenceCache.query.filter( called_element_refs = (
SpecReferenceCache.identifier.in_(all_called_element_ids) ReferenceCacheModel.basic_query()
).all() .filter(ReferenceCacheModel.identifier.in_(all_called_element_ids)) # type: ignore
.all()
)
if len(called_element_refs) > 0: if len(called_element_refs) > 0:
process_model_identifiers: list[str] = [r.process_model_id for r in called_element_refs] process_model_identifiers: list[str] = [r.relative_location for r in called_element_refs]
permitted_process_model_identifiers = ( permitted_process_model_identifiers = (
ProcessModelService.process_model_identifiers_with_permission_for_user( ProcessModelService.process_model_identifiers_with_permission_for_user(
user=user, user=user,
@ -246,8 +247,12 @@ class SpecFileService(FileSystemService):
# fixme: Place all the caching stuff in a different service. # fixme: Place all the caching stuff in a different service.
@staticmethod @staticmethod
def update_caches(ref: SpecReference) -> None: def update_all_caches(ref: Reference) -> None:
SpecFileService.update_process_cache(ref) SpecFileService.update_process_cache(ref)
SpecFileService.update_caches_except_process(ref)
@staticmethod
def update_caches_except_process(ref: Reference) -> None:
SpecFileService.update_process_caller_cache(ref) SpecFileService.update_process_caller_cache(ref)
SpecFileService.update_message_trigger_cache(ref) SpecFileService.update_message_trigger_cache(ref)
SpecFileService.update_correlation_cache(ref) SpecFileService.update_correlation_cache(ref)
@ -256,9 +261,9 @@ class SpecFileService(FileSystemService):
def clear_caches_for_file(file_name: str, process_model_info: ProcessModelInfo) -> None: def clear_caches_for_file(file_name: str, process_model_info: ProcessModelInfo) -> None:
"""Clear all caches related to a file.""" """Clear all caches related to a file."""
records = ( records = (
db.session.query(SpecReferenceCache) db.session.query(ReferenceCacheModel)
.filter(SpecReferenceCache.file_name == file_name) .filter(ReferenceCacheModel.file_name == file_name)
.filter(SpecReferenceCache.process_model_id == process_model_info.id) .filter(ReferenceCacheModel.relative_location == process_model_info.id)
.all() .all()
) )
@ -273,38 +278,41 @@ class SpecFileService(FileSystemService):
@staticmethod @staticmethod
def clear_caches() -> None: def clear_caches() -> None:
db.session.query(SpecReferenceCache).delete() db.session.query(ReferenceCacheModel).delete()
ProcessCallerService.clear_cache() ProcessCallerService.clear_cache()
# fixme: likely the other caches should be cleared as well, but we don't have a clean way to do so yet. # fixme: likely the other caches should be cleared as well, but we don't have a clean way to do so yet.
@staticmethod @staticmethod
def update_process_cache(ref: SpecReference) -> None: def update_process_cache(ref: Reference) -> None:
process_id_lookup = ( process_id_lookup = (
SpecReferenceCache.query.filter_by(identifier=ref.identifier).filter_by(type=ref.type).first() ReferenceCacheModel.basic_query()
.filter_by(identifier=ref.identifier, relative_location=ref.relative_location, type=ref.type)
.first()
) )
if process_id_lookup is None: if process_id_lookup is None:
process_id_lookup = SpecReferenceCache.from_spec_reference(ref) process_id_lookup = ReferenceCacheModel.from_spec_reference(ref, use_current_cache_generation=True)
db.session.add(process_id_lookup) db.session.add(process_id_lookup)
else: else:
if ref.relative_path != process_id_lookup.relative_path: if ref.relative_path() != process_id_lookup.relative_path():
full_bpmn_file_path = SpecFileService.full_path_from_relative_path(process_id_lookup.relative_path) full_bpmn_file_path = SpecFileService.full_path_from_relative_path(process_id_lookup.relative_path())
# if the old relative bpmn file no longer exists, then assume things were moved around # if the old relative bpmn file no longer exists, then assume things were moved around
# on the file system. Otherwise, assume it is a duplicate process id and error. # on the file system. Otherwise, assume it is a duplicate process id and error.
if os.path.isfile(full_bpmn_file_path): if os.path.isfile(full_bpmn_file_path):
raise ProcessModelFileInvalidError( raise ProcessModelFileInvalidError(
f"Process id ({ref.identifier}) has already been used for " f"Process id ({ref.identifier}) has already been used for "
f"{process_id_lookup.relative_path}. It cannot be reused." f"{process_id_lookup.relative_path()}. It cannot be reused."
) )
else: else:
process_id_lookup.relative_path = ref.relative_path process_id_lookup.relative_location = ref.relative_location
process_id_lookup.file_name = ref.file_name
db.session.add(process_id_lookup) db.session.add(process_id_lookup)
@staticmethod @staticmethod
def update_process_caller_cache(ref: SpecReference) -> None: def update_process_caller_cache(ref: Reference) -> None:
ProcessCallerService.add_caller(ref.identifier, ref.called_element_ids) ProcessCallerService.add_caller(ref.identifier, ref.called_element_ids)
@staticmethod @staticmethod
def update_message_trigger_cache(ref: SpecReference) -> None: def update_message_trigger_cache(ref: Reference) -> None:
"""Assure we know which messages can trigger the start of a process.""" """Assure we know which messages can trigger the start of a process."""
for message_name in ref.start_messages: for message_name in ref.start_messages:
message_triggerable_process_model = MessageTriggerableProcessModel.query.filter_by( message_triggerable_process_model = MessageTriggerableProcessModel.query.filter_by(
@ -313,24 +321,24 @@ class SpecFileService(FileSystemService):
if message_triggerable_process_model is None: if message_triggerable_process_model is None:
message_triggerable_process_model = MessageTriggerableProcessModel( message_triggerable_process_model = MessageTriggerableProcessModel(
message_name=message_name, message_name=message_name,
process_model_identifier=ref.process_model_id, process_model_identifier=ref.relative_location,
) )
db.session.add(message_triggerable_process_model) db.session.add(message_triggerable_process_model)
else: else:
if message_triggerable_process_model.process_model_identifier != ref.process_model_id: if message_triggerable_process_model.process_model_identifier != ref.relative_location:
raise ProcessModelFileInvalidError( raise ProcessModelFileInvalidError(
f"Message model is already used to start process model {ref.process_model_id}" f"Message model is already used to start process model {ref.relative_location}"
) )
@staticmethod @staticmethod
def update_correlation_cache(ref: SpecReference) -> None: def update_correlation_cache(ref: Reference) -> None:
for name in ref.correlations.keys(): for name in ref.correlations.keys():
correlation_property_retrieval_expressions = ref.correlations[name]["retrieval_expressions"] correlation_property_retrieval_expressions = ref.correlations[name]["retrieval_expressions"]
for cpre in correlation_property_retrieval_expressions: for cpre in correlation_property_retrieval_expressions:
message_name = ref.messages.get(cpre["messageRef"], None) message_name = ref.messages.get(cpre["messageRef"], None)
retrieval_expression = cpre["expression"] retrieval_expression = cpre["expression"]
process_model_id = ref.process_model_id process_model_id = ref.relative_location
existing = CorrelationPropertyCache.query.filter_by( existing = CorrelationPropertyCache.query.filter_by(
name=name, name=name,

View File

@ -20,8 +20,8 @@ from spiffworkflow_backend.models.json_data import JsonDataModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventModel from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventModel
from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
from spiffworkflow_backend.models.spec_reference import SpecReferenceNotFoundError from spiffworkflow_backend.models.reference_cache import ReferenceNotFoundError
from spiffworkflow_backend.models.task import TaskModel # noqa: F401 from spiffworkflow_backend.models.task import TaskModel # noqa: F401
from spiffworkflow_backend.models.task import TaskNotFoundError from spiffworkflow_backend.models.task import TaskNotFoundError
from spiffworkflow_backend.models.task_definition import TaskDefinitionModel from spiffworkflow_backend.models.task_definition import TaskDefinitionModel
@ -683,21 +683,23 @@ class TaskService:
try: try:
filename: str | None = cls.get_spec_reference_from_bpmn_process(bpmn_process).file_name filename: str | None = cls.get_spec_reference_from_bpmn_process(bpmn_process).file_name
return filename return filename
except SpecReferenceNotFoundError: except ReferenceNotFoundError:
return None return None
@classmethod @classmethod
def get_spec_reference_from_bpmn_process(cls, bpmn_process: BpmnProcessModel) -> SpecReferenceCache: def get_spec_reference_from_bpmn_process(cls, bpmn_process: BpmnProcessModel) -> ReferenceCacheModel:
"""Get the bpmn file for a given task model. """Get the bpmn file for a given task model.
This involves several queries so avoid calling in a tight loop. This involves several queries so avoid calling in a tight loop.
""" """
bpmn_process_definition = bpmn_process.bpmn_process_definition bpmn_process_definition = bpmn_process.bpmn_process_definition
spec_reference: SpecReferenceCache | None = SpecReferenceCache.query.filter_by( spec_reference: ReferenceCacheModel | None = (
identifier=bpmn_process_definition.bpmn_identifier, type="process" ReferenceCacheModel.basic_query()
).first() .filter_by(identifier=bpmn_process_definition.bpmn_identifier, type="process")
.first()
)
if spec_reference is None: if spec_reference is None:
raise SpecReferenceNotFoundError( raise ReferenceNotFoundError(
f"Could not find given process identifier in the cache: {bpmn_process_definition.bpmn_identifier}" f"Could not find given process identifier in the cache: {bpmn_process_definition.bpmn_identifier}"
) )
return spec_reference return spec_reference

View File

@ -0,0 +1,55 @@
<?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:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_f07329e" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.0.0">
<bpmn:process id="Level1" name="Level1" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1g3dpd7</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1g3dpd7" sourceRef="StartEvent_1" targetRef="Activity_12zat0d" />
<bpmn:callActivity id="Activity_12zat0d" name="call level 2" calledElement="Level2">
<bpmn:incoming>Flow_1g3dpd7</bpmn:incoming>
<bpmn:outgoing>Flow_0qdgvah</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:sequenceFlow id="Flow_0qdgvah" sourceRef="Activity_12zat0d" targetRef="Activity_0rkbhbz" />
<bpmn:endEvent id="Event_18dla68">
<bpmn:documentation># Main Workflow
Hello {{my_other_var}}
</bpmn:documentation>
<bpmn:incoming>Flow_0upce00</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0upce00" sourceRef="Activity_0rkbhbz" targetRef="Event_18dla68" />
<bpmn:callActivity id="Activity_0rkbhbz" name="call level 2B" calledElement="Level2b">
<bpmn:incoming>Flow_0qdgvah</bpmn:incoming>
<bpmn:outgoing>Flow_0upce00</bpmn:outgoing>
</bpmn:callActivity>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Level1">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0mcej1g_di" bpmnElement="Activity_12zat0d">
<dc:Bounds x="280" y="77" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_18dla68_di" bpmnElement="Event_18dla68">
<dc:Bounds x="702" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0jddvat_di" bpmnElement="Activity_0rkbhbz">
<dc:Bounds x="420" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1g3dpd7_di" bpmnElement="Flow_1g3dpd7">
<di:waypoint x="215" y="117" />
<di:waypoint x="280" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0qdgvah_di" bpmnElement="Flow_0qdgvah">
<di:waypoint x="380" y="117" />
<di:waypoint x="420" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0upce00_di" bpmnElement="Flow_0upce00">
<di:waypoint x="520" y="117" />
<di:waypoint x="702" y="117" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -22,7 +22,7 @@ from spiffworkflow_backend.models.process_instance_report import ProcessInstance
from spiffworkflow_backend.models.process_instance_report import ReportMetadata from spiffworkflow_backend.models.process_instance_report import ReportMetadata
from spiffworkflow_backend.models.process_model import NotificationType from spiffworkflow_backend.models.process_model import NotificationType
from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
from spiffworkflow_backend.models.task import TaskModel # noqa: F401 from spiffworkflow_backend.models.task import TaskModel # noqa: F401
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.file_system_service import FileSystemService from spiffworkflow_backend.services.file_system_service import FileSystemService
@ -505,7 +505,7 @@ class TestProcessApi(BaseTest):
bpmn_file_name="simple_form", bpmn_file_name="simple_form",
) )
# When adding a process model with one Process, no decisions, and some json files, only one process is recorded. # When adding a process model with one Process, no decisions, and some json files, only one process is recorded.
assert len(SpecReferenceCache.query.all()) == 1 assert len(ReferenceCacheModel.basic_query().all()) == 1
self.create_group_and_model_with_bpmn( self.create_group_and_model_with_bpmn(
client=client, client=client,
@ -515,7 +515,7 @@ class TestProcessApi(BaseTest):
bpmn_file_location="call_activity_nested", bpmn_file_location="call_activity_nested",
) )
# When adding a process model with 4 processes and a decision, 5 new records will be in the Cache # When adding a process model with 4 processes and a decision, 5 new records will be in the Cache
assert len(SpecReferenceCache.query.all()) == 6 assert len(ReferenceCacheModel.basic_query().all()) == 6
# get the results # get the results
response = client.get( response = client.get(
@ -529,10 +529,10 @@ class TestProcessApi(BaseTest):
assert len(response.json) == 5 assert len(response.json) == 5
simple_form = next(p for p in response.json if p["identifier"] == "Process_WithForm") simple_form = next(p for p in response.json if p["identifier"] == "Process_WithForm")
assert simple_form["display_name"] == "Process With Form" assert simple_form["display_name"] == "Process With Form"
assert simple_form["process_model_id"] == "test_group_one/simple_form" assert simple_form["relative_location"] == "test_group_one/simple_form"
assert simple_form["has_lanes"] is False assert simple_form["properties"]["has_lanes"] is False
assert simple_form["is_executable"] is True assert simple_form["properties"]["is_executable"] is True
assert simple_form["is_primary"] is True assert simple_form["properties"]["is_primary"] is True
def test_process_list_with_restricted_access( def test_process_list_with_restricted_access(
self, self,
@ -547,7 +547,7 @@ class TestProcessApi(BaseTest):
bpmn_file_name="simple_form", bpmn_file_name="simple_form",
) )
# When adding a process model with one Process, no decisions, and some json files, only one process is recorded. # When adding a process model with one Process, no decisions, and some json files, only one process is recorded.
assert len(SpecReferenceCache.query.all()) == 1 assert len(ReferenceCacheModel.basic_query().all()) == 1
self.create_group_and_model_with_bpmn( self.create_group_and_model_with_bpmn(
client=client, client=client,
@ -557,7 +557,7 @@ class TestProcessApi(BaseTest):
bpmn_file_location="call_activity_nested", bpmn_file_location="call_activity_nested",
) )
# When adding a process model with 4 processes and a decision, 5 new records will be in the Cache # When adding a process model with 4 processes and a decision, 5 new records will be in the Cache
assert len(SpecReferenceCache.query.all()) == 6 assert len(ReferenceCacheModel.basic_query().all()) == 6
user_one = self.create_user_with_permission( user_one = self.create_user_with_permission(
username="user_one", target_uri="/v1.0/process-groups/test_group_one:*" username="user_one", target_uri="/v1.0/process-groups/test_group_one:*"
@ -580,10 +580,10 @@ class TestProcessApi(BaseTest):
assert len(response.json) == 1 assert len(response.json) == 1
simple_form = next(p for p in response.json if p["identifier"] == "Process_WithForm") simple_form = next(p for p in response.json if p["identifier"] == "Process_WithForm")
assert simple_form["display_name"] == "Process With Form" assert simple_form["display_name"] == "Process With Form"
assert simple_form["process_model_id"] == "test_group_one/simple_form" assert simple_form["relative_location"] == "test_group_one/simple_form"
assert simple_form["has_lanes"] is False assert simple_form["properties"]["has_lanes"] is False
assert simple_form["is_executable"] is True assert simple_form["properties"]["is_executable"] is True
assert simple_form["is_primary"] is True assert simple_form["properties"]["is_primary"] is True
def test_process_callers( def test_process_callers(
self, self,
@ -599,7 +599,7 @@ class TestProcessApi(BaseTest):
bpmn_file_name="simple_form", bpmn_file_name="simple_form",
) )
# When adding a process model with one Process, no decisions, and some json files, only one process is recorded. # When adding a process model with one Process, no decisions, and some json files, only one process is recorded.
assert len(SpecReferenceCache.query.all()) == 1 assert len(ReferenceCacheModel.basic_query().all()) == 1
# but no callers are recorded # but no callers are recorded
assert ProcessCallerService.count() == 0 assert ProcessCallerService.count() == 0
@ -611,7 +611,7 @@ class TestProcessApi(BaseTest):
bpmn_file_location="call_activity_nested", bpmn_file_location="call_activity_nested",
) )
# When adding a process model with 4 processes and a decision, 5 new records will be in the Cache # When adding a process model with 4 processes and a decision, 5 new records will be in the Cache
assert len(SpecReferenceCache.query.all()) == 6 assert len(ReferenceCacheModel.basic_query().all()) == 6
# and 4 callers recorded # and 4 callers recorded
assert ProcessCallerService.count() == 4 assert ProcessCallerService.count() == 4
@ -1330,7 +1330,7 @@ class TestProcessApi(BaseTest):
process_model_id=process_model_id, process_model_id=process_model_id,
bpmn_file_location="call_activity_nested", bpmn_file_location="call_activity_nested",
) )
spec_reference = SpecReferenceCache.query.filter_by(identifier="Level2b").first() spec_reference = ReferenceCacheModel.basic_query().filter_by(identifier="Level2b").first()
assert spec_reference assert spec_reference
modified_process_model_identifier = self.modify_process_identifier_for_path_param(process_model.id) modified_process_model_identifier = self.modify_process_identifier_for_path_param(process_model.id)
headers = self.logged_in_headers(with_super_admin_user) headers = self.logged_in_headers(with_super_admin_user)
@ -1356,7 +1356,7 @@ class TestProcessApi(BaseTest):
with open(process_instance_file_path) as f_open: with open(process_instance_file_path) as f_open:
xml_file_contents = f_open.read() xml_file_contents = f_open.read()
assert show_response.json["bpmn_xml_file_contents"] != xml_file_contents assert show_response.json["bpmn_xml_file_contents"] != xml_file_contents
spec_reference_file_path = os.path.join(file_system_root, spec_reference.relative_path) spec_reference_file_path = os.path.join(file_system_root, spec_reference.relative_path())
with open(spec_reference_file_path) as f_open: with open(spec_reference_file_path) as f_open:
xml_file_contents = f_open.read() xml_file_contents = f_open.read()
assert show_response.json["bpmn_xml_file_contents"] == xml_file_contents assert show_response.json["bpmn_xml_file_contents"] == xml_file_contents

View File

@ -5,7 +5,7 @@ from flask.app import Flask
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.process_instance_metadata import ProcessInstanceMetadataModel from spiffworkflow_backend.models.process_instance_metadata import ProcessInstanceMetadataModel
from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.base_test import BaseTest
@ -86,7 +86,7 @@ class TestProcessModel(BaseTest):
# delete all of the id lookup items to force to processor to find the correct # delete all of the id lookup items to force to processor to find the correct
# process model when running the process # process model when running the process
db.session.query(SpecReferenceCache).delete() db.session.query(ReferenceCacheModel).delete()
db.session.commit() db.session.commit()
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True, execution_strategy_name="greedy") processor.do_engine_steps(save=True, execution_strategy_name="greedy")

View File

@ -5,8 +5,9 @@ import pytest
from flask import Flask from flask import Flask
from flask.testing import FlaskClient from flask.testing import FlaskClient
from lxml import etree # type: ignore from lxml import etree # type: ignore
from spiffworkflow_backend.models.cache_generation import CacheGenerationModel
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.spec_file_service import ProcessModelFileInvalidError from spiffworkflow_backend.services.spec_file_service import ProcessModelFileInvalidError
from spiffworkflow_backend.services.spec_file_service import SpecFileService from spiffworkflow_backend.services.spec_file_service import SpecFileService
@ -22,7 +23,7 @@ class TestSpecFileService(BaseTest):
# process_model_id = "call_activity_nested" # process_model_id = "call_activity_nested"
bpmn_file_name = "call_activity_nested.bpmn" bpmn_file_name = "call_activity_nested.bpmn"
call_activity_nested_relative_file_path = os.path.join(process_group_id, process_model_id, bpmn_file_name) call_activity_nested_relative_file_path = os.path.join(process_model_id, bpmn_file_name)
def test_can_store_process_ids_for_lookup( def test_can_store_process_ids_for_lookup(
self, self,
@ -35,12 +36,12 @@ class TestSpecFileService(BaseTest):
bpmn_file_name=self.bpmn_file_name, bpmn_file_name=self.bpmn_file_name,
process_model_source_directory="call_activity_nested", process_model_source_directory="call_activity_nested",
) )
bpmn_process_id_lookups = SpecReferenceCache.query.all() bpmn_process_id_lookups = ReferenceCacheModel.basic_query().all()
assert len(bpmn_process_id_lookups) == 1 assert len(bpmn_process_id_lookups) == 1
assert bpmn_process_id_lookups[0].identifier == "Level1" assert bpmn_process_id_lookups[0].identifier == "Level1"
assert bpmn_process_id_lookups[0].relative_path == self.call_activity_nested_relative_file_path assert bpmn_process_id_lookups[0].relative_path() == self.call_activity_nested_relative_file_path
def test_fails_to_save_duplicate_process_id( def test_fails_to_save_duplicate_process_id_in_same_process_model(
self, self,
app: Flask, app: Flask,
client: FlaskClient, client: FlaskClient,
@ -48,23 +49,22 @@ class TestSpecFileService(BaseTest):
) -> None: ) -> None:
bpmn_process_identifier = "Level1" bpmn_process_identifier = "Level1"
load_test_spec( load_test_spec(
process_model_id=self.process_model_id, process_model_id="call_activity_duplicate",
bpmn_file_name=self.bpmn_file_name, bpmn_file_name=self.bpmn_file_name,
process_model_source_directory="call_activity_nested", process_model_source_directory="call_activity_duplicate",
) )
bpmn_process_id_lookups = SpecReferenceCache.query.all() bpmn_process_id_lookups = ReferenceCacheModel.basic_query().all()
assert len(bpmn_process_id_lookups) == 1 assert len(bpmn_process_id_lookups) == 1
assert bpmn_process_id_lookups[0].identifier == bpmn_process_identifier assert bpmn_process_id_lookups[0].identifier == bpmn_process_identifier
assert bpmn_process_id_lookups[0].relative_path == self.call_activity_nested_relative_file_path
with pytest.raises(ProcessModelFileInvalidError) as exception: with pytest.raises(ProcessModelFileInvalidError) as exception:
load_test_spec( load_test_spec(
"call_activity_nested_duplicate", process_model_id="call_activity_duplicate",
process_model_source_directory="call_activity_duplicate", process_model_source_directory="call_activity_duplicate",
bpmn_file_name="call_activity_nested_duplicate", bpmn_file_name="call_activity_nested_duplicate",
) )
assert f"Process id ({bpmn_process_identifier}) has already been used" in str(exception.value) assert f"Process id ({bpmn_process_identifier}) has already been used" in str(exception.value)
process_model = ProcessModelService.get_process_model("call_activity_nested_duplicate") process_model = ProcessModelService.get_process_model("call_activity_duplicate")
full_file_path = SpecFileService.full_file_path(process_model, "call_activity_nested_duplicate.bpmn") full_file_path = SpecFileService.full_file_path(process_model, "call_activity_nested_duplicate.bpmn")
assert not os.path.isfile(full_file_path) assert not os.path.isfile(full_file_path)
@ -75,10 +75,13 @@ class TestSpecFileService(BaseTest):
with_db_and_bpmn_file_cleanup: None, with_db_and_bpmn_file_cleanup: None,
) -> None: ) -> None:
bpmn_process_identifier = "Level1" bpmn_process_identifier = "Level1"
process_id_lookup = SpecReferenceCache( process_id_lookup = ReferenceCacheModel.from_params(
identifier=bpmn_process_identifier, identifier=bpmn_process_identifier,
relative_path=self.call_activity_nested_relative_file_path, display_name="WHO CARES",
relative_location=self.process_model_id,
file_name=self.bpmn_file_name,
type="process", type="process",
use_current_cache_generation=True,
) )
db.session.add(process_id_lookup) db.session.add(process_id_lookup)
db.session.commit() db.session.commit()
@ -89,10 +92,10 @@ class TestSpecFileService(BaseTest):
process_model_source_directory="call_activity_nested", process_model_source_directory="call_activity_nested",
) )
bpmn_process_id_lookups = SpecReferenceCache.query.all() bpmn_process_id_lookups = ReferenceCacheModel.basic_query().all()
assert len(bpmn_process_id_lookups) == 1 assert len(bpmn_process_id_lookups) == 1
assert bpmn_process_id_lookups[0].identifier == bpmn_process_identifier assert bpmn_process_id_lookups[0].identifier == bpmn_process_identifier
assert bpmn_process_id_lookups[0].relative_path == self.call_activity_nested_relative_file_path assert bpmn_process_id_lookups[0].relative_path() == self.call_activity_nested_relative_file_path
# this is really a test of your configuration. # this is really a test of your configuration.
# sqlite and postgres are case sensitive by default, # sqlite and postgres are case sensitive by default,
@ -102,15 +105,23 @@ class TestSpecFileService(BaseTest):
app: Flask, app: Flask,
with_db_and_bpmn_file_cleanup: None, with_db_and_bpmn_file_cleanup: None,
) -> None: ) -> None:
process_id_lookup = SpecReferenceCache( process_id_lookup = ReferenceCacheModel.from_params(
identifier="HOT", identifier="HOT",
display_name="WHO CARES",
relative_location=self.process_model_id,
file_name=self.bpmn_file_name,
type="process", type="process",
use_current_cache_generation=True,
) )
db.session.add(process_id_lookup) db.session.add(process_id_lookup)
db.session.commit() db.session.commit()
process_id_lookup = SpecReferenceCache( process_id_lookup = ReferenceCacheModel.from_params(
identifier="hot", identifier="hot",
display_name="WHO CARES",
relative_location=self.process_model_id,
file_name=self.bpmn_file_name,
type="process", type="process",
use_current_cache_generation=True,
) )
db.session.add(process_id_lookup) db.session.add(process_id_lookup)
db.session.commit() db.session.commit()
@ -123,12 +134,13 @@ class TestSpecFileService(BaseTest):
) -> None: ) -> None:
"""When a BPMN processes identifier is changed in a file, the old id is removed from the cache.""" """When a BPMN processes identifier is changed in a file, the old id is removed from the cache."""
old_identifier = "ye_old_identifier" old_identifier = "ye_old_identifier"
process_id_lookup = SpecReferenceCache( process_id_lookup = ReferenceCacheModel.from_params(
identifier=old_identifier, identifier=old_identifier,
relative_path=self.call_activity_nested_relative_file_path, display_name="WHO CARES",
relative_location=self.process_model_id,
file_name=self.bpmn_file_name, file_name=self.bpmn_file_name,
process_model_id=self.process_model_id,
type="process", type="process",
use_current_cache_generation=True,
) )
db.session.add(process_id_lookup) db.session.add(process_id_lookup)
db.session.commit() db.session.commit()
@ -139,11 +151,11 @@ class TestSpecFileService(BaseTest):
process_model_source_directory="call_activity_nested", process_model_source_directory="call_activity_nested",
) )
bpmn_process_id_lookups = SpecReferenceCache.query.all() bpmn_process_id_lookups = ReferenceCacheModel.basic_query().all()
assert len(bpmn_process_id_lookups) == 1 assert len(bpmn_process_id_lookups) == 1
assert bpmn_process_id_lookups[0].identifier != old_identifier assert bpmn_process_id_lookups[0].identifier != old_identifier
assert bpmn_process_id_lookups[0].identifier == "Level1" assert bpmn_process_id_lookups[0].identifier == "Level1"
assert bpmn_process_id_lookups[0].relative_path == self.call_activity_nested_relative_file_path assert bpmn_process_id_lookups[0].relative_path() == self.call_activity_nested_relative_file_path
def test_load_reference_information( def test_load_reference_information(
self, self,
@ -200,6 +212,63 @@ class TestSpecFileService(BaseTest):
full_file_path = SpecFileService.full_file_path(process_model, "bad_xml.bpmn") full_file_path = SpecFileService.full_file_path(process_model, "bad_xml.bpmn")
assert not os.path.isfile(full_file_path) assert not os.path.isfile(full_file_path)
def test_uses_correct_cache_generation(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
current_cache_generation = CacheGenerationModel.newest_generation_for_table("reference_cache")
assert current_cache_generation is None
load_test_spec(
process_model_id=self.process_model_id,
bpmn_file_name=self.bpmn_file_name,
process_model_source_directory="call_activity_nested",
)
bpmn_process_id_lookups = ReferenceCacheModel.basic_query().all()
assert len(bpmn_process_id_lookups) == 1
assert bpmn_process_id_lookups[0].identifier == "Level1"
assert bpmn_process_id_lookups[0].relative_path() == self.call_activity_nested_relative_file_path
current_cache_generation = CacheGenerationModel.newest_generation_for_table("reference_cache")
assert current_cache_generation is not None
assert bpmn_process_id_lookups[0].generation_id == current_cache_generation.id
# make sure it doesn't add a new entry to the cache
load_test_spec(
process_model_id=self.process_model_id,
bpmn_file_name=self.bpmn_file_name,
process_model_source_directory="call_activity_nested",
)
bpmn_process_id_lookups = ReferenceCacheModel.basic_query().all()
assert len(bpmn_process_id_lookups) == 1
assert bpmn_process_id_lookups[0].identifier == "Level1"
assert bpmn_process_id_lookups[0].relative_path() == self.call_activity_nested_relative_file_path
assert bpmn_process_id_lookups[0].generation_id == current_cache_generation.id
cache_generations = CacheGenerationModel.query.all()
assert len(cache_generations) == 1
new_cache_generation = CacheGenerationModel(cache_table="reference_cache")
db.session.add(new_cache_generation)
db.session.commit()
cache_generations = CacheGenerationModel.query.all()
assert len(cache_generations) == 2
current_cache_generation = CacheGenerationModel.newest_generation_for_table("reference_cache")
assert current_cache_generation is not None
load_test_spec(
process_model_id=self.process_model_id,
bpmn_file_name=self.bpmn_file_name,
process_model_source_directory="call_activity_nested",
)
bpmn_process_id_lookups = ReferenceCacheModel.basic_query().all()
assert len(bpmn_process_id_lookups) == 1
assert bpmn_process_id_lookups[0].identifier == "Level1"
assert bpmn_process_id_lookups[0].generation_id == current_cache_generation.id
@pytest.mark.skipif( @pytest.mark.skipif(
sys.platform == "win32", sys.platform == "win32",
reason="tmp file path is not valid xml for windows and it doesn't matter", reason="tmp file path is not valid xml for windows and it doesn't matter",

View File

@ -139,9 +139,7 @@ export interface ProcessReference {
process_model_id: string; process_model_id: string;
type: string; // either "decision" or "process" type: string; // either "decision" or "process"
file_name: string; file_name: string;
has_lanes: boolean; properties: any;
is_executable: boolean;
is_primary: boolean;
} }
export type ObjectWithStringKeysAndValues = { [key: string]: string }; export type ObjectWithStringKeysAndValues = { [key: string]: string };