Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93581b13a1 | ||
|
|
0c1a7c24e6 | ||
|
|
1fc7091657 | ||
|
|
566793ff00 | ||
|
|
d119fd8a90 | ||
|
|
6db3d2aa0f | ||
|
|
6846aff6dc | ||
|
|
4f115e53c6 | ||
|
|
20f0d38dd1 | ||
|
|
09eca84ecb | ||
|
|
0950a439e4 | ||
|
|
7e67a581e0 | ||
|
|
6c3de53631 | ||
|
|
41c6cba913 | ||
|
|
51e4a14778 | ||
|
|
aa804e2fc5 | ||
|
|
07f72dae49 | ||
|
|
910d5c9858 | ||
|
|
6ef1727eb8 | ||
|
|
fce13986b4 | ||
|
|
c8f3bb4312 | ||
|
|
9e302794f1 | ||
|
|
05cc348966 | ||
|
|
983b4ffd59 | ||
|
|
64a202aa41 | ||
|
|
0fe3bb9735 | ||
|
|
1368c052dd | ||
|
|
6d669469b0 | ||
|
|
b45be4221d | ||
|
|
85d175edd6 | ||
|
|
a75ec6f263 | ||
|
|
e35a1bffa0 |
No files matched your search
+58
-50
@@ -1,60 +1,68 @@
|
|||||||
version: 2.1
|
version: 2.1
|
||||||
|
|
||||||
executors:
|
orbs:
|
||||||
java-executor:
|
qqq-orb: kingsrook/qqq-orb@0.3.12
|
||||||
docker:
|
|
||||||
- image: cimg/openjdk:17.0
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
verify:
|
|
||||||
executor: java-executor
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-maven-cache-{{ checksum "pom.xml" }}
|
|
||||||
- run:
|
|
||||||
name: Verify with Maven
|
|
||||||
command: mvn -s .circleci/mvn-settings.xml verify
|
|
||||||
- store_test_results:
|
|
||||||
path: target/surefire-reports
|
|
||||||
- save_cache:
|
|
||||||
paths:
|
|
||||||
- ~/.m2/repository
|
|
||||||
key: v1-maven-cache-{{ checksum "pom.xml" }}
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
executor: java-executor
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-maven-cache-{{ checksum "pom.xml" }}
|
|
||||||
- run:
|
|
||||||
name: Deploy with Maven
|
|
||||||
command: mvn -s .circleci/mvn-settings.xml deploy
|
|
||||||
- store_test_results:
|
|
||||||
path: target/surefire-reports
|
|
||||||
- save_cache:
|
|
||||||
paths:
|
|
||||||
- ~/.m2/repository
|
|
||||||
key: v1-maven-cache-{{ checksum "pom.xml" }}
|
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
test_only:
|
||||||
build_and_deploy:
|
|
||||||
jobs:
|
jobs:
|
||||||
- verify:
|
- qqq-orb/mvn_test_only:
|
||||||
context: qqq-maven-registry-credentials
|
context: [qqq-maven-registry-credentials]
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
ignore:
|
ignore: /(develop|main|release/.*|hotfix/.*|integration.*)/
|
||||||
- main
|
tags:
|
||||||
- feature/initial-dev
|
only: []
|
||||||
- deploy:
|
|
||||||
context: qqq-maven-registry-credentials
|
publish_snapshot:
|
||||||
|
jobs:
|
||||||
|
- qqq-orb/mvn_publish:
|
||||||
|
context: [qqq-maven-registry-credentials]
|
||||||
|
branch_type: snapshot
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- main
|
- develop
|
||||||
- feature/initial-dev
|
|
||||||
|
publish_feature:
|
||||||
|
jobs:
|
||||||
|
- qqq-orb/mvn_publish:
|
||||||
|
context: [qqq-maven-registry-credentials]
|
||||||
|
branch_type: feature
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- feature/.*
|
||||||
|
tags:
|
||||||
|
only: [/publish.*/]
|
||||||
|
|
||||||
|
publish_release_candidate:
|
||||||
|
jobs:
|
||||||
|
- qqq-orb/mvn_publish:
|
||||||
|
context: [qqq-maven-registry-credentials]
|
||||||
|
branch_type: release_candidate
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- /release\/.*/
|
||||||
|
|
||||||
|
publish_release:
|
||||||
|
jobs:
|
||||||
|
- qqq-orb/mvn_publish:
|
||||||
|
context: [qqq-maven-registry-credentials]
|
||||||
|
branch_type: release
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: /.*/
|
||||||
|
tags:
|
||||||
|
only: [/v.*/]
|
||||||
|
|
||||||
|
publish_hotfix_release:
|
||||||
|
jobs:
|
||||||
|
- qqq-orb/mvn_publish:
|
||||||
|
context: [qqq-maven-registry-credentials]
|
||||||
|
branch_type: hotfix
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- /hotfix\/.*/
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
|
|
||||||
<servers>
|
|
||||||
<server>
|
|
||||||
<id>github-qqq-maven-registry</id>
|
|
||||||
<username>${env.QQQ_MAVEN_REGISTRY_USERNAME}</username>
|
|
||||||
<password>${env.QQQ_MAVEN_REGISTRY_PASSWORD}</password>
|
|
||||||
</server>
|
|
||||||
</servers>
|
|
||||||
</settings>
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to QBit User Role Permissions will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Comprehensive README documentation following QQQ sub repo standards
|
||||||
|
- Standard GitHub files (LICENSE, CHANGELOG, CONTRIBUTING, SECURITY, CODE_OF_CONDUCT)
|
||||||
|
- Proper project structure and documentation
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated README to follow QQQ framework standards
|
||||||
|
- Improved project documentation and structure
|
||||||
|
- Enhanced GitHub repository setup
|
||||||
|
|
||||||
|
## [0.30.0-SNAPSHOT] - 2024-01-XX
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial QBit implementation for user role permissions
|
||||||
|
- Database schema for users, roles, and permissions
|
||||||
|
- Java classes for User, Role, Permission, UserRole, and RolePermission
|
||||||
|
- Integration with QQQ Backend Core framework
|
||||||
|
- H2 database support for testing
|
||||||
|
- JUnit 5 test framework integration
|
||||||
|
- AssertJ assertions for testing
|
||||||
|
- Checkstyle configuration for code quality
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Initial public release of QBit User Role Permissions
|
||||||
|
- Established QBit development standards
|
||||||
|
- Created modular architecture for permission management
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 For Detailed Information
|
||||||
|
|
||||||
|
**📖 [Complete Documentation Wiki](https://github.com/Kingsrook/qqq/wiki)** - Start here for comprehensive guides
|
||||||
|
- **[🏠 Home](https://github.com/Kingsrook/qqq/wiki/Home)** - Project overview and quick start
|
||||||
|
- **[🚀 Release Flow](https://github.com/Kingsrook/qqq/wiki/Release-Flow)** - Detailed release process
|
||||||
|
- **[🏷️ Changelog & Tagging](https://github.com/Kingsrook/qqq/wiki/Changelog-and-Tagging)** - Commit conventions and release notes
|
||||||
|
- **[🔧 Developer Onboarding](https://github.com/Kingsrook/qqq/wiki/Developer-Onboarding)** - Setup and contribution guide
|
||||||
|
|
||||||
|
## 🔄 Version Compatibility
|
||||||
|
|
||||||
|
QBit User Role Permissions follows semantic versioning:
|
||||||
|
- **MAJOR** versions may contain breaking changes
|
||||||
|
- **MINOR** versions add new functionality (backward compatible)
|
||||||
|
- **PATCH** versions contain bug fixes (backward compatible)
|
||||||
|
|
||||||
|
For detailed compatibility information, see [QQQ Compatibility Matrix](https://github.com/Kingsrook/qqq/wiki/Compatibility-Matrix).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Thank you for using QBit User Role Permissions!** 🚀
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
|
||||||
|
See Project level [Code of Conduct](https://github.com/Kingsrook/qqq/blob/develop/CODE_OF_CONDUCT.md)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Code Style Guide
|
||||||
|
|
||||||
|
* See Project [Code Style Guide](https://github.com/Kingsrook/qqq/blob/develop/CODE_STYLE.md)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Contributing to QBit User Role Permissions
|
||||||
|
|
||||||
|
* See Project [Contributing](https://github.com/Kingsrook/qqq/blob/develop/CONTRIBUTING.md)
|
||||||
|
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
## QBit: workflows
|
# QBit: Workflows
|
||||||
|
|
||||||
|
[](https://github.com/Kingsrook/qbit-workflows)
|
||||||
|
[](https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||||
|
[](https://adoptium.net/)
|
||||||
|
|
||||||
### Overview
|
### Overview
|
||||||
*Note: This is one of the original QBit implementations - so, some of the mechanics of how
|
*Note: This is one of the original QBit implementations - so, some of the mechanics of how
|
||||||
@@ -7,6 +11,28 @@ creation... Please excuse any dust or not-quite-round wheels you find here!*
|
|||||||
|
|
||||||
This QBit provides TODO
|
This QBit provides TODO
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
### Technology Stack
|
||||||
|
|
||||||
|
- **Java**: Java 17+ with UTF-8 encoding
|
||||||
|
- **Maven**: Build system with plugin management
|
||||||
|
- **QQQ Framework**: Built on the QQQ low-code platform
|
||||||
|
- **QBit Pattern**: Follows QBit component architecture
|
||||||
|
|
||||||
|
### Core Capabilities
|
||||||
|
|
||||||
|
- **Workflow Management**: TODO - Define workflow capabilities
|
||||||
|
- **Process Automation**: TODO - Define automation features
|
||||||
|
- **Integration**: TODO - Define integration capabilities
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- **Java 17+** (required for QQQ features)
|
||||||
|
- **Maven 3.8+** (for build system)
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
#### Pom dependency
|
#### Pom dependency
|
||||||
@@ -31,3 +57,53 @@ TODO
|
|||||||
### Dependencies
|
### Dependencies
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
**📖 [Complete Documentation Wiki](https://github.com/Kingsrook/qqq/wiki)** - Start here for comprehensive guides
|
||||||
|
|
||||||
|
- **[🏠 Home](https://github.com/Kingsrook/qqq/wiki/Home)** - Project overview and quick start
|
||||||
|
- **[🏗️ Architecture](https://github.com/Kingsrook/qqq/wiki/High-Level-Architecture)** - System design and principles
|
||||||
|
- **[🔧 Development](https://github.com/Kingsrook/qqq/wiki/Developer-Onboarding)** - Setup and contribution guide
|
||||||
|
- **[📦 Modules](https://github.com/Kingsrook/qqq/wiki/Core-Modules)** - Available components and usage
|
||||||
|
- **[🚀 Building](https://github.com/Kingsrook/qqq/wiki/Building-Locally)** - Local development workflow
|
||||||
|
- **[🔧 QBit Development](https://github.com/Kingsrook/qqq/wiki/QBit-Development)** - QBit development guide
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
**Important**: This repository is a component of the QQQ framework. All contributions, issues, and discussions should go through the main QQQ repository.
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
|
||||||
|
1. **Fork the main QQQ repository**: https://github.com/Kingsrook/qqq
|
||||||
|
2. **Create a feature branch**: `git checkout -b feature/amazing-feature`
|
||||||
|
3. **Make your changes** (including QBit changes if applicable)
|
||||||
|
4. **Run tests**: `mvn test`
|
||||||
|
5. **Commit your changes**: `git commit -m 'Add amazing feature'`
|
||||||
|
6. **Push to the branch**: `git push origin feature/amazing-feature`
|
||||||
|
7. **Open a Pull Request** to the main QQQ repository
|
||||||
|
|
||||||
|
### Code Standards
|
||||||
|
|
||||||
|
- **Maven**: Follow Maven best practices
|
||||||
|
- **Documentation**: Update relevant documentation
|
||||||
|
- **Versioning**: Follow semantic versioning
|
||||||
|
- **QBit Pattern**: Follow QBit component architecture
|
||||||
|
|
||||||
|
**First time contributing?** Start with our [Developer Onboarding Guide](https://github.com/Kingsrook/qqq/wiki/Developer-Onboarding) to get your environment set up.
|
||||||
|
|
||||||
|
## 🏢 About Kingsrook
|
||||||
|
|
||||||
|
QBit Workflows is built by **[Kingsrook](https://qrun.io)** - making engineers more productive through intelligent automation and developer tools.
|
||||||
|
|
||||||
|
- **Website**: [https://qrun.io](https://qrun.io)
|
||||||
|
- **Contact**: [contact@kingsrook.com](mailto:contact@kingsrook.com)
|
||||||
|
- **GitHub**: [https://github.com/Kingsrook](https://github.com/Kingsrook)
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the **GNU Affero General Public License v3.0** - see the [LICENSE.txt](LICENSE.txt) file for details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ready to build workflows with full control?** [Get started with QQQ today!](https://github.com/Kingsrook/qqq/wiki/Developer-Onboarding)
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
* See Project [Security Policy](https://github.com/Kingsrook/qqq/blob/develop/SECURITY.md)
|
||||||
@@ -21,37 +21,53 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
|
<properties>
|
||||||
|
<revision>0.114.0-SNAPSHOT</revision>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>com.kingsrook</groupId>
|
||||||
|
<artifactId>qbit-build-parent</artifactId>
|
||||||
|
<version>1.4.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
<groupId>com.kingsrook.qbits</groupId>
|
<groupId>com.kingsrook.qbits</groupId>
|
||||||
<artifactId>qbit-workflows</artifactId>
|
<artifactId>qbit-workflows</artifactId>
|
||||||
<version>0.1.13-SNAPSHOT</version>
|
<version>${revision}</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<name>QBit Workflows</name>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<description>Workflow management and process automation module for the QBit framework</description>
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<url>https://github.com/Kingsrook/qbit-workflows</url>
|
||||||
<maven.compiler.release>17</maven.compiler.release>
|
|
||||||
<maven.compiler.target>17</maven.compiler.target>
|
<licenses>
|
||||||
</properties>
|
<license>
|
||||||
|
<name>GNU Affero General Public License v3.0</name>
|
||||||
|
<url>https://www.gnu.org/licenses/agpl-3.0.html</url>
|
||||||
|
<distribution>repo</distribution>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<developers>
|
||||||
|
<developer>
|
||||||
|
<id>kingsrook</id>
|
||||||
|
<name>Kingsrook Team</name>
|
||||||
|
<email>contact@kingsrook.com</email>
|
||||||
|
<organization>Kingsrook, LLC</organization>
|
||||||
|
<organizationUrl>https://kingsrook.com</organizationUrl>
|
||||||
|
</developer>
|
||||||
|
</developers>
|
||||||
|
|
||||||
|
<scm>
|
||||||
|
<connection>scm:git:git://github.com/Kingsrook/qbit-workflows.git</connection>
|
||||||
|
<developerConnection>scm:git:ssh://github.com:Kingsrook/qbit-workflows.git</developerConnection>
|
||||||
|
<url>https://github.com/Kingsrook/qbit-workflows</url>
|
||||||
|
<tag>HEAD</tag>
|
||||||
|
</scm>
|
||||||
|
|
||||||
<dependencyManagement>
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.kingsrook.qqq</groupId>
|
|
||||||
<artifactId>qqq-bom-pom</artifactId>
|
|
||||||
<version>0.27.9</version>
|
|
||||||
<type>pom</type>
|
|
||||||
<scope>import</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.junit</groupId>
|
|
||||||
<artifactId>junit-bom</artifactId>
|
|
||||||
<version>5.10.0</version>
|
|
||||||
<type>pom</type>
|
|
||||||
<scope>import</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</dependencyManagement>
|
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -71,7 +87,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.kingsrook.qqq</groupId>
|
<groupId>com.kingsrook.qqq</groupId>
|
||||||
<artifactId>qqq-frontend-material-dashboard</artifactId>
|
<artifactId>qqq-frontend-material-dashboard</artifactId>
|
||||||
<version>0.27.0-20250915.210441-5</version>
|
<version>0.27.0-RC.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -87,24 +103,4 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<repositories>
|
|
||||||
<repository>
|
|
||||||
<id>sonatype-snapshots</id>
|
|
||||||
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
|
|
||||||
</repository>
|
|
||||||
<repository>
|
|
||||||
<id>github-qqq-maven-registry</id>
|
|
||||||
<name>GitHub QQQ Maven Registry</name>
|
|
||||||
<url>https://maven.pkg.github.com/Kingsrook/qqq-maven-registry</url>
|
|
||||||
</repository>
|
|
||||||
</repositories>
|
|
||||||
|
|
||||||
<distributionManagement>
|
|
||||||
<repository>
|
|
||||||
<id>github-qqq-maven-registry</id>
|
|
||||||
<name>GitHub QQQ Maven Registry</name>
|
|
||||||
<url>https://maven.pkg.github.com/Kingsrook/qqq-maven-registry</url>
|
|
||||||
</repository>
|
|
||||||
</distributionManagement>
|
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
@@ -28,7 +28,7 @@ package com.kingsrook.qbits.workflows.definition;
|
|||||||
** ONE is a regular statement, that just flows to its next.
|
** ONE is a regular statement, that just flows to its next.
|
||||||
** CONTAINER is itself a no-op, but it wraps (for UI reasons) a sequence
|
** CONTAINER is itself a no-op, but it wraps (for UI reasons) a sequence
|
||||||
** TWO is for an if/else
|
** TWO is for an if/else
|
||||||
** Unclear exactly how we'd do variable number of outbounds (e.g., a switch)
|
** VARIABLE is for a variable number - 1 or more
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
public enum OutboundLinkMode
|
public enum OutboundLinkMode
|
||||||
{
|
{
|
||||||
@@ -36,4 +36,5 @@ public enum OutboundLinkMode
|
|||||||
ONE,
|
ONE,
|
||||||
CONTAINER,
|
CONTAINER,
|
||||||
TWO,
|
TWO,
|
||||||
|
VARIABLE
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,7 @@ import java.io.Serializable;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import com.kingsrook.qbits.workflows.execution.WorkflowStepExecutorInterface;
|
import com.kingsrook.qbits.workflows.execution.WorkflowStepExecutorInterface;
|
||||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||||
@@ -56,8 +57,10 @@ public class WorkflowStepType implements Serializable
|
|||||||
private OutboundLinkMode outboundLinkMode;
|
private OutboundLinkMode outboundLinkMode;
|
||||||
private List<OutboundLinkOption> outboundLinkOptions;
|
private List<OutboundLinkOption> outboundLinkOptions;
|
||||||
|
|
||||||
private ArrayList<QFieldMetaData> inputFields;
|
private Set<String> allowedDescendantStepTypes;
|
||||||
private ArrayList<String> inputWidgetNames;
|
|
||||||
|
private ArrayList<QFieldMetaData> inputFields = new ArrayList<>();
|
||||||
|
private ArrayList<String> inputWidgetNames = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -83,6 +86,17 @@ public class WorkflowStepType implements Serializable
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* for a variable-output type step, it must dynamically define its outbound
|
||||||
|
* links
|
||||||
|
***************************************************************************/
|
||||||
|
public List<OutboundLinkOption> getDynamicOutboundLinkOptions(Integer workflowId, Map<String, Serializable> inputValues) throws QException
|
||||||
|
{
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
@@ -409,6 +423,7 @@ public class WorkflowStepType implements Serializable
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* Getter for validator
|
* Getter for validator
|
||||||
* @see #withValidator(QCodeReference)
|
* @see #withValidator(QCodeReference)
|
||||||
@@ -445,4 +460,42 @@ public class WorkflowStepType implements Serializable
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Getter for allowedDescendantStepTypes
|
||||||
|
* @see #withAllowedDescendantStepTypes(Set)
|
||||||
|
*******************************************************************************/
|
||||||
|
public Set<String> getAllowedDescendantStepTypes()
|
||||||
|
{
|
||||||
|
return (this.allowedDescendantStepTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Setter for allowedDescendantStepTypes
|
||||||
|
* @see #withAllowedDescendantStepTypes(Set)
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setAllowedDescendantStepTypes(Set<String> allowedDescendantStepTypes)
|
||||||
|
{
|
||||||
|
this.allowedDescendantStepTypes = allowedDescendantStepTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Fluent setter for allowedDescendantStepTypes
|
||||||
|
*
|
||||||
|
* @param allowedDescendantStepTypes
|
||||||
|
* to limit what step types can be placed inside this step type within a graph.
|
||||||
|
* for example,
|
||||||
|
* @return this
|
||||||
|
*******************************************************************************/
|
||||||
|
public WorkflowStepType withAllowedDescendantStepTypes(Set<String> allowedDescendantStepTypes)
|
||||||
|
{
|
||||||
|
this.allowedDescendantStepTypes = allowedDescendantStepTypes;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -25,16 +25,19 @@ package com.kingsrook.qbits.workflows.definition;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import com.kingsrook.qbits.workflows.execution.WorkflowTypeExecutorInterface;
|
import com.kingsrook.qbits.workflows.execution.WorkflowTypeExecutorInterface;
|
||||||
import com.kingsrook.qbits.workflows.execution.WorkflowTypeTesterInterface;
|
import com.kingsrook.qbits.workflows.execution.WorkflowTypeTesterInterface;
|
||||||
|
import com.kingsrook.qbits.workflows.execution.WorkflowTypeValidatorInterface;
|
||||||
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
|
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
|
||||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
|
|
||||||
@@ -50,9 +53,11 @@ public class WorkflowType implements Serializable
|
|||||||
private String label;
|
private String label;
|
||||||
private String description;
|
private String description;
|
||||||
private QCodeReference executor;
|
private QCodeReference executor;
|
||||||
|
private QCodeReference validator;
|
||||||
private QCodeReference tester;
|
private QCodeReference tester;
|
||||||
|
|
||||||
private ArrayList<WorkflowStepTypeCategory> stepTypeCategories;
|
protected Map<String, List<QHelpContent>> helpContent;
|
||||||
|
private ArrayList<WorkflowStepTypeCategory> stepTypeCategories;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -81,6 +86,11 @@ public class WorkflowType implements Serializable
|
|||||||
qInstanceValidator.validateSimpleCodeReference("WorkflowType [" + name + "]: executor ", executor, WorkflowTypeExecutorInterface.class);
|
qInstanceValidator.validateSimpleCodeReference("WorkflowType [" + name + "]: executor ", executor, WorkflowTypeExecutorInterface.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(validator != null)
|
||||||
|
{
|
||||||
|
qInstanceValidator.validateSimpleCodeReference("WorkflowType [" + name + "]: validator ", validator, WorkflowTypeValidatorInterface.class);
|
||||||
|
}
|
||||||
|
|
||||||
if(tester != null)
|
if(tester != null)
|
||||||
{
|
{
|
||||||
qInstanceValidator.validateSimpleCodeReference("WorkflowType [" + name + "]: tester ", tester, WorkflowTypeTesterInterface.class);
|
qInstanceValidator.validateSimpleCodeReference("WorkflowType [" + name + "]: tester ", tester, WorkflowTypeTesterInterface.class);
|
||||||
@@ -266,6 +276,8 @@ public class WorkflowType implements Serializable
|
|||||||
return Collections.emptyMap();
|
return Collections.emptyMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* Getter for tester
|
* Getter for tester
|
||||||
* @see #withTester(QCodeReference)
|
* @see #withTester(QCodeReference)
|
||||||
@@ -303,4 +315,92 @@ public class WorkflowType implements Serializable
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for helpContent
|
||||||
|
*******************************************************************************/
|
||||||
|
public Map<String, List<QHelpContent>> getHelpContent()
|
||||||
|
{
|
||||||
|
return (this.helpContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for helpContent
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setHelpContent(Map<String, List<QHelpContent>> helpContent)
|
||||||
|
{
|
||||||
|
this.helpContent = helpContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for helpContent
|
||||||
|
*******************************************************************************/
|
||||||
|
public WorkflowType withHelpContent(Map<String, List<QHelpContent>> helpContent)
|
||||||
|
{
|
||||||
|
this.helpContent = helpContent;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for helpContent
|
||||||
|
*******************************************************************************/
|
||||||
|
public WorkflowType withHelpContent(String key, QHelpContent helpContent)
|
||||||
|
{
|
||||||
|
if(this.helpContent == null)
|
||||||
|
{
|
||||||
|
this.helpContent = new HashMap<>();
|
||||||
|
}
|
||||||
|
if(!this.helpContent.containsKey(key))
|
||||||
|
{
|
||||||
|
this.helpContent.put(key, new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.helpContent.get(key).add(helpContent);
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Getter for validator
|
||||||
|
* @see #withValidator(QCodeReference)
|
||||||
|
*******************************************************************************/
|
||||||
|
public QCodeReference getValidator()
|
||||||
|
{
|
||||||
|
return (this.validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Setter for validator
|
||||||
|
* @see #withValidator(QCodeReference)
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setValidator(QCodeReference validator)
|
||||||
|
{
|
||||||
|
this.validator = validator;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Fluent setter for validator
|
||||||
|
*
|
||||||
|
* @param validator reference to implementation of
|
||||||
|
* {@link com.kingsrook.qbits.workflows.execution.WorkflowTypeValidatorInterface}
|
||||||
|
*
|
||||||
|
* @return this
|
||||||
|
*******************************************************************************/
|
||||||
|
public WorkflowType withValidator(QCodeReference validator)
|
||||||
|
{
|
||||||
|
this.validator = validator;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -203,6 +203,8 @@ public class WorkflowsRegistry implements QSupplementalInstanceMetaData, QHelpCo
|
|||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
* plug in to the QQQ Help Content system, to allow help content to set
|
* plug in to the QQQ Help Content system, to allow help content to set
|
||||||
* descriptions and labels for workflow steps.
|
* descriptions and labels for workflow steps.
|
||||||
|
*
|
||||||
|
* Note: helpContent can come in here as null, if it has been, e.g., deleted!
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
@Override
|
@Override
|
||||||
public void acceptHelpContent(QInstance qInstance, QHelpContent helpContent, Map<String, String> nameValuePairs)
|
public void acceptHelpContent(QInstance qInstance, QHelpContent helpContent, Map<String, String> nameValuePairs)
|
||||||
@@ -215,11 +217,18 @@ public class WorkflowsRegistry implements QSupplementalInstanceMetaData, QHelpCo
|
|||||||
{
|
{
|
||||||
if("description".equals(nameValuePairs.get("slot")))
|
if("description".equals(nameValuePairs.get("slot")))
|
||||||
{
|
{
|
||||||
workflowStepType.setDescription(helpContent.getContentAsHtml());
|
workflowStepType.setDescription(helpContent == null ? null : helpContent.getContentAsHtml());
|
||||||
}
|
}
|
||||||
else if("label".equals(nameValuePairs.get("slot")))
|
else if("label".equals(nameValuePairs.get("slot")))
|
||||||
{
|
{
|
||||||
workflowStepType.setLabel(helpContent.getContent());
|
if(helpContent != null)
|
||||||
|
{
|
||||||
|
workflowStepType.setLabel(helpContent.getContent());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG.info("Not removing workflow step type label after deleted help content (original label is not known...", logPair("name", workflowStepTypeName));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if(nameValuePairs.containsKey("field"))
|
else if(nameValuePairs.containsKey("field"))
|
||||||
{
|
{
|
||||||
@@ -229,9 +238,10 @@ public class WorkflowsRegistry implements QSupplementalInstanceMetaData, QHelpCo
|
|||||||
.withType(QFieldType.STRING)
|
.withType(QFieldType.STRING)
|
||||||
.withLabel("Step Description")
|
.withLabel("Step Description")
|
||||||
.withName("qswdDescription")
|
.withName("qswdDescription")
|
||||||
.withHelpContent(helpContent)
|
|
||||||
.withGridColumns(12);
|
.withGridColumns(12);
|
||||||
|
|
||||||
|
newField.setHelpContents(helpContent == null ? null : ListBuilder.of(helpContent));
|
||||||
|
|
||||||
List<QFieldMetaData> existingFields = workflowStepType.getInputFields();
|
List<QFieldMetaData> existingFields = workflowStepType.getInputFields();
|
||||||
existingFields.add(newField);
|
existingFields.add(newField);
|
||||||
workflowStepType.setInputFields(existingFields);
|
workflowStepType.setInputFields(existingFields);
|
||||||
@@ -242,13 +252,24 @@ public class WorkflowsRegistry implements QSupplementalInstanceMetaData, QHelpCo
|
|||||||
{
|
{
|
||||||
if(nameValuePairs.get("field").equals(fieldMetaData.getName()))
|
if(nameValuePairs.get("field").equals(fieldMetaData.getName()))
|
||||||
{
|
{
|
||||||
fieldMetaData.setHelpContents(ListBuilder.of(helpContent));
|
fieldMetaData.setHelpContents(helpContent == null ? null : ListBuilder.of(helpContent));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if(nameValuePairs.containsKey("workflow") && nameValuePairs.containsKey("field"))
|
||||||
|
{
|
||||||
|
if(workflowTypes != null)
|
||||||
|
{
|
||||||
|
for(String workflowTypeName : workflowTypes.keySet())
|
||||||
|
{
|
||||||
|
String key = "workflow:" + nameValuePairs.get("workflow") + ";field:" + nameValuePairs.get("field");
|
||||||
|
workflowTypes.get(workflowTypeName).withHelpContent(key, helpContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qbits.workflows.execution;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowRunLogStep;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* state data carried throughout the {@link WorkflowExecutor} - which is
|
||||||
|
* useful to "leak out" in a managed way, e.g., for step types that do their
|
||||||
|
* own more sophisticated kind of execution, e.g., {@link WorkflowMultiForkingStepExecutorInterface}
|
||||||
|
***************************************************************************/
|
||||||
|
record ExecutionPayload(Map<Integer, WorkflowStep> stepMap, ListingHash<Integer, WorkflowLink> linkMap, WorkflowTypeExecutorInterface workflowTypeExecutor, AtomicInteger seqNo, List<WorkflowRunLogStep> logStepList)
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qbits.workflows.execution;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* given a graph, find the first "join point" where multiple forks come together.
|
||||||
|
* Credit: https://chatgpt.com/share/68000e8f-879c-8011-bb33-0564eb77a1f2
|
||||||
|
*******************************************************************************/
|
||||||
|
public class FindFirstJoinPoint
|
||||||
|
{
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
public static Integer executeForFromStepNo(Integer fromStepNo, Map<Integer, WorkflowStep> stepMap, ListingHash<Integer, WorkflowLink> linksMapByFromStepNo)
|
||||||
|
{
|
||||||
|
List<Integer> startLinks = CollectionUtils.nonNullList(linksMapByFromStepNo.get(fromStepNo)).stream().map(ws -> ws.getToStepNo()).toList();
|
||||||
|
return execute(startLinks, stepMap, linksMapByFromStepNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
public static Integer executeForLinks(Collection<WorkflowLink> startLinks, Map<Integer, WorkflowStep> stepMap, ListingHash<Integer, WorkflowLink> linksMapByFromStepNo)
|
||||||
|
{
|
||||||
|
return execute(startLinks.stream().map(ws -> ws.getToStepNo()).toList(), stepMap, linksMapByFromStepNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
@SuppressWarnings("Java8MapApi")
|
||||||
|
public static Integer execute(Collection<Integer> startStepNos, Map<Integer, WorkflowStep> stepMap, ListingHash<Integer, WorkflowLink> linksMapByFromStepNo)
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// visited: map each node to a set of source step numbers that have reached it. //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
ListingHash<Integer, Integer> visited = new ListingHash<>();
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Initialize the multi-source BFS queue. Each entry carries the current node id and its originating source. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Deque<BFSQueueNode> queue = new ArrayDeque<>();
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Seed the queue with each starting node, marking each as visited by itself. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
for(Integer node : startStepNos)
|
||||||
|
{
|
||||||
|
visited.add(node, node);
|
||||||
|
queue.add(new BFSQueueNode(node, node));
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////
|
||||||
|
// Perform the multi-source BFS. //
|
||||||
|
///////////////////////////////////
|
||||||
|
while(!queue.isEmpty())
|
||||||
|
{
|
||||||
|
BFSQueueNode firstNode = queue.removeFirst();
|
||||||
|
Integer current = firstNode.current;
|
||||||
|
Integer source = firstNode.source;
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
// Get the list of outgoing links/neighbors for the current node. //
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
List<WorkflowLink> neighbors = Objects.requireNonNullElse(linksMapByFromStepNo.get(current), Collections.emptyList());
|
||||||
|
for(WorkflowLink neighbor : neighbors)
|
||||||
|
{
|
||||||
|
Integer neighborId = neighbor.getToStepNo();
|
||||||
|
if(neighborId == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Retrieve or initialize the set of sources that visited this neighbor. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
List<Integer> sourcesSet = visited.get(neighborId);
|
||||||
|
if(sourcesSet == null)
|
||||||
|
{
|
||||||
|
sourcesSet = new ArrayList<>();
|
||||||
|
visited.put(neighborId, sourcesSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
// If the current source has not visited the neighbor yet, add it. //
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
if(!sourcesSet.contains(source))
|
||||||
|
{
|
||||||
|
sourcesSet.add(source);
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////
|
||||||
|
// Check if the neighbor was reached from multiple sources. //
|
||||||
|
// todo: should this be it has to equal all of them? //
|
||||||
|
// if(sourcesSet.size() == startStepNos.size()) //
|
||||||
|
//////////////////////////////////////////////////////////////
|
||||||
|
if(sourcesSet.size() > 1)
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// This is the first join point encountered – return the corresponding WorkflowStep. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
return (stepMap.get(neighborId).getStepNo());
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
// Continue the BFS from this neighbor for the current source. //
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
queue.push(new BFSQueueNode(neighborId, source));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
// If no join point was found across all paths, return null. //
|
||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
private record BFSQueueNode(Integer current, Integer source)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -42,6 +42,8 @@ public class WorkflowExecutionContext
|
|||||||
private QBackendTransaction transaction;
|
private QBackendTransaction transaction;
|
||||||
private WorkflowRunLog workflowRunLog;
|
private WorkflowRunLog workflowRunLog;
|
||||||
|
|
||||||
|
private WorkflowExecutor executor;
|
||||||
|
|
||||||
private boolean isTestRun = false;
|
private boolean isTestRun = false;
|
||||||
|
|
||||||
|
|
||||||
@@ -235,4 +237,26 @@ public class WorkflowExecutionContext
|
|||||||
return (this);
|
return (this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Getter for executor
|
||||||
|
* @see #setExecutor(WorkflowExecutor)
|
||||||
|
*******************************************************************************/
|
||||||
|
WorkflowExecutor getExecutor()
|
||||||
|
{
|
||||||
|
return (this.executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Setter for executor
|
||||||
|
* @param executor the object executing the workflow.
|
||||||
|
*******************************************************************************/
|
||||||
|
void setExecutor(WorkflowExecutor executor)
|
||||||
|
{
|
||||||
|
this.executor = executor;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Stack;
|
import java.util.Stack;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import com.kingsrook.qbits.workflows.definition.OutboundLinkMode;
|
import com.kingsrook.qbits.workflows.definition.OutboundLinkMode;
|
||||||
import com.kingsrook.qbits.workflows.definition.WorkflowStepType;
|
import com.kingsrook.qbits.workflows.definition.WorkflowStepType;
|
||||||
@@ -71,6 +72,7 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
|||||||
private WorkflowRunLog inputWorkflowRunLog;
|
private WorkflowRunLog inputWorkflowRunLog;
|
||||||
|
|
||||||
private Stack<Integer> containerStack = new Stack<>();
|
private Stack<Integer> containerStack = new Stack<>();
|
||||||
|
private ExecutionPayload payload;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -167,31 +169,10 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
|||||||
///////////////
|
///////////////
|
||||||
// step loop //
|
// step loop //
|
||||||
///////////////
|
///////////////
|
||||||
Integer stepNo = workflowRevision.getStartStepNo();
|
ExecutionPayload payload = new ExecutionPayload(stepMap, linkMap, workflowTypeExecutor, new AtomicInteger(1), logStepList);
|
||||||
int seqNo = 1;
|
this.payload = payload;
|
||||||
while(stepNo != null)
|
context.setExecutor(this);
|
||||||
{
|
runStepLoop(workflowRevision.getStartStepNo(), context, null);
|
||||||
WorkflowStep step = stepMap.get(stepNo);
|
|
||||||
if(step == null)
|
|
||||||
{
|
|
||||||
throw new QException("Step not found by stepNo: " + stepNo);
|
|
||||||
}
|
|
||||||
|
|
||||||
WorkflowRunLogStep workflowRunLogStep = new WorkflowRunLogStep();
|
|
||||||
workflowRunLogStep.setWorkflowStepId(step.getId());
|
|
||||||
workflowRunLogStep.setSeqNo(seqNo);
|
|
||||||
workflowRunLogStep.setStartTimestamp(Instant.now());
|
|
||||||
logStepList.add(workflowRunLogStep);
|
|
||||||
|
|
||||||
WorkflowStepOutput workflowStepOutput = executeStep(step, workflowTypeExecutor, context);
|
|
||||||
workflowRunLogStep.setOutputData(ValueUtils.getValueAsString(workflowStepOutput.outputData()));
|
|
||||||
workflowRunLogStep.setMessage(workflowStepOutput.message());
|
|
||||||
|
|
||||||
stepNo = getNextStepNo(workflowStepOutput.outputData(), step, linkMap, stepMap, false);
|
|
||||||
|
|
||||||
workflowRunLogStep.setEndTimestamp(Instant.now());
|
|
||||||
seqNo++;
|
|
||||||
}
|
|
||||||
|
|
||||||
workflowTypeExecutor.postRun(context);
|
workflowTypeExecutor.postRun(context);
|
||||||
|
|
||||||
@@ -239,6 +220,51 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* @return true if the workflow should immediately stop
|
||||||
|
***************************************************************************/
|
||||||
|
boolean runStepLoop(Integer stepNo, WorkflowExecutionContext context, Integer stopStepNo) throws QException
|
||||||
|
{
|
||||||
|
while(stepNo != null)
|
||||||
|
{
|
||||||
|
WorkflowStep step = payload.stepMap().get(stepNo);
|
||||||
|
if(step == null)
|
||||||
|
{
|
||||||
|
throw new QException("Step not found by stepNo: " + stepNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkflowRunLogStep workflowRunLogStep = new WorkflowRunLogStep();
|
||||||
|
workflowRunLogStep.setWorkflowStepId(step.getId());
|
||||||
|
workflowRunLogStep.setStartTimestamp(Instant.now());
|
||||||
|
|
||||||
|
WorkflowStepOutput workflowStepOutput = executeStep(step, payload, context);
|
||||||
|
|
||||||
|
stepNo = getNextStepNo(workflowStepOutput.outputData(), step, payload.linkMap(), payload.stepMap(), false);
|
||||||
|
|
||||||
|
workflowRunLogStep.setEndTimestamp(Instant.now());
|
||||||
|
workflowRunLogStep.setOutputData(ValueUtils.getValueAsString(workflowStepOutput.outputData()));
|
||||||
|
workflowRunLogStep.setMessage(workflowStepOutput.message());
|
||||||
|
workflowRunLogStep.setSeqNo(payload.seqNo().getAndIncrement());
|
||||||
|
payload.logStepList().add(workflowRunLogStep);
|
||||||
|
|
||||||
|
if(stopStepNo != null && Objects.equals(stepNo, stopStepNo))
|
||||||
|
{
|
||||||
|
LOG.debug("Stopping at requested step", logPair("stopStepNo", stepNo));
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(workflowStepOutput.shouldWorkflowImmediatelyStop())
|
||||||
|
{
|
||||||
|
LOG.info("Stopping step loop due to step output indicating workflow should immediately stop", logPair("stepNo", stepNo));
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
@@ -332,13 +358,23 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Integer fromStepNo = fromStep.getStepNo();
|
Integer fromStepNo = fromStep.getStepNo();
|
||||||
List<WorkflowLink> links = linkMap.get(fromStepNo);
|
List<WorkflowLink> links = linkMap.get(fromStepNo);
|
||||||
Class<? extends Serializable> stepOutputClass = stepOutput == null ? null : stepOutput.getClass();
|
|
||||||
|
if(OutboundLinkMode.VARIABLE.equals(fromWorkflowStepType.getOutboundLinkMode()))
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// after a VARIABLE step (where its branches were all executed in //
|
||||||
|
// processMultiForkingStep), find the first join point, and step to there. //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
Integer joinStepNo = FindFirstJoinPoint.execute(links.stream().map(wl -> wl.getToStepNo()).toList(), stepMap, linkMap);
|
||||||
|
return (joinStepNo);
|
||||||
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
// look for a link between the fromStepNo matching the stepOutput / condition value //
|
// look for a link between the fromStepNo matching the stepOutput / condition value //
|
||||||
//////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
List<String> evaluatedConditionValues = new ArrayList<>();
|
||||||
for(WorkflowLink link : CollectionUtils.nonNullList(links))
|
for(WorkflowLink link : CollectionUtils.nonNullList(links))
|
||||||
{
|
{
|
||||||
if(link.getConditionValue() == null)
|
if(link.getConditionValue() == null)
|
||||||
@@ -350,8 +386,10 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
evaluatedConditionValues.add(link.getConditionValue());
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
Class<? extends Serializable> stepOutputClass = stepOutput == null ? null : stepOutput.getClass();
|
||||||
Serializable valueAsType = stepOutputClass == null ? link.getConditionValue() : ValueUtils.getValueAsType(stepOutputClass, link.getConditionValue());
|
Serializable valueAsType = stepOutputClass == null ? link.getConditionValue() : ValueUtils.getValueAsType(stepOutputClass, link.getConditionValue());
|
||||||
if(Objects.equals(valueAsType, stepOutput))
|
if(Objects.equals(valueAsType, stepOutput))
|
||||||
{
|
{
|
||||||
@@ -365,6 +403,11 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(CollectionUtils.nullSafeHasContents(evaluatedConditionValues))
|
||||||
|
{
|
||||||
|
LOG.info("Evaluated at least 1 link condition value, but found no matches", logPair("evaluatedConditionValues", evaluatedConditionValues));
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////
|
||||||
// if we didn't find a next-step, but there is something on the stack, //
|
// if we didn't find a next-step, but there is something on the stack, //
|
||||||
// then look for a pop out of that frame //
|
// then look for a pop out of that frame //
|
||||||
@@ -385,7 +428,7 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
|||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
private WorkflowStepOutput executeStep(WorkflowStep step, WorkflowTypeExecutorInterface workflowTypeExecutor, WorkflowExecutionContext context) throws QException
|
private WorkflowStepOutput executeStep(WorkflowStep step, ExecutionPayload payload, WorkflowExecutionContext context) throws QException
|
||||||
{
|
{
|
||||||
WorkflowStepType workflowStepType = WorkflowsRegistry.of(QContext.getQInstance()).getWorkflowStepType(step.getWorkflowStepTypeName());
|
WorkflowStepType workflowStepType = WorkflowsRegistry.of(QContext.getQInstance()).getWorkflowStepType(step.getWorkflowStepTypeName());
|
||||||
if(workflowStepType == null)
|
if(workflowStepType == null)
|
||||||
@@ -399,8 +442,7 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
|||||||
}
|
}
|
||||||
|
|
||||||
WorkflowStepExecutorInterface workflowStepExecutor = QCodeLoader.getAdHoc(WorkflowStepExecutorInterface.class, workflowStepType.getExecutor());
|
WorkflowStepExecutorInterface workflowStepExecutor = QCodeLoader.getAdHoc(WorkflowStepExecutorInterface.class, workflowStepType.getExecutor());
|
||||||
|
payload.workflowTypeExecutor().preStep(step, context);
|
||||||
workflowTypeExecutor.preStep(step, context);
|
|
||||||
|
|
||||||
JSONObject jsonObject = JsonUtils.toJSONObject(step.getInputValuesJson());
|
JSONObject jsonObject = JsonUtils.toJSONObject(step.getInputValuesJson());
|
||||||
Map<String, Object> mapValues = jsonObject.toMap();
|
Map<String, Object> mapValues = jsonObject.toMap();
|
||||||
@@ -415,13 +457,23 @@ public class WorkflowExecutor extends AbstractQActionBiConsumer<WorkflowInput, W
|
|||||||
}
|
}
|
||||||
|
|
||||||
WorkflowStepOutput workflowStepOutput = workflowStepExecutor.execute(step, inputValues, context);
|
WorkflowStepOutput workflowStepOutput = workflowStepExecutor.execute(step, inputValues, context);
|
||||||
workflowStepOutput = workflowTypeExecutor.postStep(step, context, workflowStepOutput);
|
workflowStepOutput = payload.workflowTypeExecutor().postStep(step, context, workflowStepOutput);
|
||||||
|
|
||||||
return workflowStepOutput;
|
return workflowStepOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
ExecutionPayload getPayload()
|
||||||
|
{
|
||||||
|
return (this.payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
|
|||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qbits.workflows.execution;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowRunLogStep;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||||
|
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* interface for workflow step executors that are of a variable output type,
|
||||||
|
* and which can execute 0 or more of their outbound links.
|
||||||
|
***************************************************************************/
|
||||||
|
public interface WorkflowMultiForkingStepExecutorInterface extends WorkflowStepExecutorInterface
|
||||||
|
{
|
||||||
|
QLogger LOG = QLogger.getLogger(WorkflowMultiForkingStepExecutorInterface.class);
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* the Execute method for such steps should perform an executor.runStepLoop
|
||||||
|
* for each of the outbound links of the step (assuming they are supposed
|
||||||
|
* allowed to run).
|
||||||
|
***************************************************************************/
|
||||||
|
@Override
|
||||||
|
default WorkflowStepOutput execute(WorkflowStep step, Map<String, Serializable> inputValues, WorkflowExecutionContext context) throws QException
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////
|
||||||
|
// get the various sub-workflows ("fork links") to run from the step //
|
||||||
|
///////////////////////////////////////////////////////////////////////
|
||||||
|
WorkflowExecutor executor = context.getExecutor();
|
||||||
|
ExecutionPayload payload = executor.getPayload();
|
||||||
|
|
||||||
|
List<WorkflowLink> forkLinks = payload.linkMap().get(step.getStepNo());
|
||||||
|
preRun(step, inputValues, context);
|
||||||
|
sortForks(forkLinks, step, inputValues, context);
|
||||||
|
|
||||||
|
Integer stopStepNo = FindFirstJoinPoint.execute(forkLinks.stream().map(wl -> wl.getToStepNo()).toList(), payload.stepMap(), payload.linkMap());
|
||||||
|
boolean shouldWorkflowImmediatelyStop = false;
|
||||||
|
for(WorkflowLink link : forkLinks)
|
||||||
|
{
|
||||||
|
WorkflowRunLogStep workflowRunLogStep = new WorkflowRunLogStep();
|
||||||
|
workflowRunLogStep.setWorkflowStepId(step.getId());
|
||||||
|
workflowRunLogStep.setSeqNo(payload.seqNo().getAndIncrement());
|
||||||
|
workflowRunLogStep.setStartTimestamp(Instant.now());
|
||||||
|
|
||||||
|
WorkflowStepOutput workflowStepOutput = conditionallyExecuteFork(step, link, inputValues, context);
|
||||||
|
if(Objects.equals(workflowStepOutput.outputData(), true))
|
||||||
|
{
|
||||||
|
shouldWorkflowImmediatelyStop = executor.runStepLoop(link.getToStepNo(), context, stopStepNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
workflowRunLogStep.setOutputData(ValueUtils.getValueAsString(workflowStepOutput.outputData()));
|
||||||
|
workflowRunLogStep.setMessage(workflowStepOutput.message());
|
||||||
|
workflowRunLogStep.setEndTimestamp(Instant.now());
|
||||||
|
payload.logStepList().add(workflowRunLogStep);
|
||||||
|
|
||||||
|
if(shouldWorkflowImmediatelyStop)
|
||||||
|
{
|
||||||
|
LOG.info("Stopping fork-link loop, due to step output indicating workflow should immediately stop", logPair("linkStartStepNo", link.getToStepNo()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkflowStepOutput workflowStepOutput = makeWorkflowStepOutput(step, inputValues, context);
|
||||||
|
postRun(step, inputValues, context);
|
||||||
|
|
||||||
|
if(shouldWorkflowImmediatelyStop)
|
||||||
|
{
|
||||||
|
workflowStepOutput = new WorkflowStepOutput(workflowStepOutput.outputData(), workflowStepOutput.message(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return workflowStepOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* make sure the forks are executed in the order that the step expects.
|
||||||
|
***************************************************************************/
|
||||||
|
void sortForks(List<WorkflowLink> forkLinks, WorkflowStep step, Map<String, Serializable> inputValues, WorkflowExecutionContext context);
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* decide if a particular fork should be executed - and if so, then do so.
|
||||||
|
***************************************************************************/
|
||||||
|
WorkflowStepOutput conditionallyExecuteFork(WorkflowStep step, WorkflowLink fork, Map<String, Serializable> inputValues, WorkflowExecutionContext context) throws QException;
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* make output for the entire step, e.g., taking into account all of its
|
||||||
|
* forks.
|
||||||
|
***************************************************************************/
|
||||||
|
WorkflowStepOutput makeWorkflowStepOutput(WorkflowStep step, Map<String, Serializable> inputValues, WorkflowExecutionContext context) throws QException;
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* optional code to run before anything else in this interface
|
||||||
|
***************************************************************************/
|
||||||
|
default void preRun(WorkflowStep step, Map<String, Serializable> inputValues, WorkflowExecutionContext context) throws QException
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* optional code to run after anything else in this interface
|
||||||
|
***************************************************************************/
|
||||||
|
default void postRun(WorkflowStep step, Map<String, Serializable> inputValues, WorkflowExecutionContext context) throws QException
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,16 +28,26 @@ import java.io.Serializable;
|
|||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
public record WorkflowStepOutput(Serializable outputData, String message)
|
public record WorkflowStepOutput(Serializable outputData, String message, boolean shouldWorkflowImmediatelyStop)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Constructor
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public WorkflowStepOutput(Serializable outputData, String message)
|
||||||
|
{
|
||||||
|
this(outputData, message, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Constructor
|
** Constructor
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public WorkflowStepOutput(Serializable outputData)
|
public WorkflowStepOutput(Serializable outputData)
|
||||||
{
|
{
|
||||||
this(outputData, null);
|
this(outputData, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -48,7 +58,7 @@ public record WorkflowStepOutput(Serializable outputData, String message)
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public WorkflowStepOutput()
|
public WorkflowStepOutput()
|
||||||
{
|
{
|
||||||
this(null, null);
|
this(null, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qbits.workflows.execution;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* interface for the code that validates the overall structure of a workflow,
|
||||||
|
* e.g., when it's being saved. This would be where you'd validate relationships
|
||||||
|
* between steps (e.g., this step must have some other step within it)
|
||||||
|
*******************************************************************************/
|
||||||
|
public interface WorkflowTypeValidatorInterface
|
||||||
|
{
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* validate that a workflow can be saved.
|
||||||
|
*
|
||||||
|
* @param workflow the workflow record under which a new revision is being saved
|
||||||
|
* @param workflowRevision the new revision record that's been built
|
||||||
|
* @param workflowSteps list of workflow steps being saved
|
||||||
|
* @param workflowLinks list of workflow links being saved
|
||||||
|
* @param errors out param - any validation errors should be added to this list.
|
||||||
|
***************************************************************************/
|
||||||
|
void validate(QRecord workflow, QRecord workflowRevision, List<WorkflowStep> workflowSteps, List<WorkflowLink> workflowLinks, List<String> errors) throws QException;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qbits.workflows.execution.nodes;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* list of nodes in a graph representation of a workflow
|
||||||
|
***************************************************************************/
|
||||||
|
public record NodeSequence(List<StepNode> nodes)
|
||||||
|
{
|
||||||
|
/*******************************************************************************
|
||||||
|
** Constructor
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public NodeSequence()
|
||||||
|
{
|
||||||
|
this(new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
public void add(StepNode node)
|
||||||
|
{
|
||||||
|
nodes.add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
public StepNode get(int i)
|
||||||
|
{
|
||||||
|
return nodes.get(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
public int size()
|
||||||
|
{
|
||||||
|
return (nodes.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qbits.workflows.execution.nodes;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* Single step, plus, if it opens a nested scope (e.g., branches, or a container)
|
||||||
|
* then those subSequences, along with their conditionalValues (guards/labels)
|
||||||
|
* as map keys
|
||||||
|
***************************************************************************/
|
||||||
|
public record StepNode(WorkflowStep step, Map<String, NodeSequence> subSequences)
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Constructor for step w/o subSequences
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public StepNode(WorkflowStep step)
|
||||||
|
{
|
||||||
|
this(step, new HashMap<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+224
@@ -0,0 +1,224 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qbits.workflows.execution.nodes;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import com.kingsrook.qbits.workflows.definition.OutboundLinkMode;
|
||||||
|
import com.kingsrook.qbits.workflows.definition.WorkflowStepType;
|
||||||
|
import com.kingsrook.qbits.workflows.definition.WorkflowsRegistry;
|
||||||
|
import com.kingsrook.qbits.workflows.execution.FindFirstJoinPoint;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||||
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
|
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Class that can convert a list of steps & links (as a workflow revision is
|
||||||
|
* stored in a database) to a hierarchical graph data structure - a list of nodes,
|
||||||
|
* each of which is a step, and optionally a map of lists of sub-branches
|
||||||
|
* (which can be more useful for various kinds of processing).
|
||||||
|
*******************************************************************************/
|
||||||
|
public class StepNodeGraphBuilder
|
||||||
|
{
|
||||||
|
private static final QLogger LOG = QLogger.getLogger(StepNodeGraphBuilder.class);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
public static NodeSequence buildNodeSequence(Integer startStepNo, List<WorkflowStep> workflowSteps, List<WorkflowLink> workflowLinks)
|
||||||
|
{
|
||||||
|
Map<Integer, WorkflowStep> stepMap = CollectionUtils.listToMap(workflowSteps, ws -> ws.getStepNo());
|
||||||
|
ListingHash<Integer, WorkflowLink> linksFromMap = new ListingHash<>();
|
||||||
|
Map<Integer, Set<Integer>> linksToMap = new HashMap<>();
|
||||||
|
|
||||||
|
for(WorkflowLink workflowLink : workflowLinks)
|
||||||
|
{
|
||||||
|
linksFromMap.add(workflowLink.getFromStepNo(), workflowLink);
|
||||||
|
|
||||||
|
if(workflowLink.getToStepNo() != null)
|
||||||
|
{
|
||||||
|
Set<Integer> subToSet = linksToMap.computeIfAbsent(workflowLink.getToStepNo(), k -> new HashSet<>());
|
||||||
|
subToSet.add(workflowLink.getFromStepNo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeSequence nodeSequence = new NodeSequence();
|
||||||
|
buildNodeSequence(nodeSequence, stepMap.get(startStepNo), stepMap, linksFromMap, null, 0);
|
||||||
|
return (nodeSequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
private static void buildNodeSequence(NodeSequence nodeSequence, WorkflowStep startStep, Map<Integer, WorkflowStep> stepMap, ListingHash<Integer, WorkflowLink> linksFromMap, Integer joinStepNo, Integer depth)
|
||||||
|
{
|
||||||
|
WorkflowsRegistry workflowsRegistry = WorkflowsRegistry.of(QContext.getQInstance());
|
||||||
|
|
||||||
|
WorkflowStep currentWorkflowStep = startStep;
|
||||||
|
while(currentWorkflowStep != null)
|
||||||
|
{
|
||||||
|
WorkflowStepType workflowStepType = workflowsRegistry.getWorkflowStepType(currentWorkflowStep.getWorkflowStepTypeName());
|
||||||
|
if(workflowStepType == null)
|
||||||
|
{
|
||||||
|
LOG.warn("Unrecognized workflow step type: " + currentWorkflowStep.getWorkflowStepTypeName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Objects.equals(currentWorkflowStep.getStepNo(), joinStepNo))
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if we've reached the join step, it's the end of this sequence, so return //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StepNode stepNode = new StepNode(currentWorkflowStep);
|
||||||
|
nodeSequence.add(stepNode);
|
||||||
|
|
||||||
|
List<WorkflowLink> outboundLinks = linksFromMap.get(currentWorkflowStep.getStepNo());
|
||||||
|
if(CollectionUtils.nullSafeIsEmpty(outboundLinks))
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if no outbound links, then we're at the end of this sequence, so return //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if(outboundLinks.size() == 1 && !StringUtils.hasContent(outboundLinks.get(0).getConditionValue())) // ?? avoid being tripped by a branch w/ only 1 side populated at the end of the graph
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////
|
||||||
|
// 1 outbound link here means we're not in a switch/if-else/fork //
|
||||||
|
///////////////////////////////////////////////////////////////////
|
||||||
|
WorkflowLink outboundLink = outboundLinks.get(0);
|
||||||
|
currentWorkflowStep = outboundLink.getToStepNo() == null ? null : stepMap.get(outboundLink.getToStepNo());
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// todo is this == null okay? //
|
||||||
|
////////////////////////////////
|
||||||
|
if(currentWorkflowStep == null || currentWorkflowStep.getStepNo().equals(joinStepNo))
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
// this means we're ending a nested (recursive) call //
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(workflowStepType.getOutboundLinkMode().equals(OutboundLinkMode.TWO) || workflowStepType.getOutboundLinkMode().equals(OutboundLinkMode.VARIABLE))
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// this means we are starting a fork. find where it ends - where the branches first re-converge //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Integer branchJoin = FindFirstJoinPoint.executeForLinks(outboundLinks, stepMap, linksFromMap);
|
||||||
|
|
||||||
|
////////////////////////////////////////////
|
||||||
|
// for each branch, make a recursive call //
|
||||||
|
////////////////////////////////////////////
|
||||||
|
for(WorkflowLink outboundLink : outboundLinks)
|
||||||
|
{
|
||||||
|
String conditionValue = outboundLink.getConditionValue();
|
||||||
|
NodeSequence branchSequence = new NodeSequence();
|
||||||
|
stepNode.subSequences().put(conditionValue, branchSequence);
|
||||||
|
|
||||||
|
if(branchJoin != null && Objects.equals(branchJoin, outboundLink.getToStepNo()))
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
// this indicates an empty branch - so continue to next outbound link //
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if this is a step out of the branch (e.g., to a join), then don't process it in here. //
|
||||||
|
// also though, this line doesn't actually get hit, so... //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(Objects.equals(outboundLink.getToStepNo(), joinStepNo))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(outboundLink.getToStepNo() != null)
|
||||||
|
{
|
||||||
|
buildNodeSequence(branchSequence, stepMap.get(outboundLink.getToStepNo()), stepMap, linksFromMap, branchJoin, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// after handling all the branches, then continue the loop with the join step //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
currentWorkflowStep = branchJoin == null ? null : stepMap.get(branchJoin);
|
||||||
|
}
|
||||||
|
else if(workflowStepType.getOutboundLinkMode().equals(OutboundLinkMode.CONTAINER))
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////
|
||||||
|
// if the container is non-empty, there should be a push link for it //
|
||||||
|
///////////////////////////////////////////////////////////////////////
|
||||||
|
Optional<WorkflowLink> pushLink = outboundLinks.stream().filter(ol -> "push".equals(ol.getConditionValue())).findFirst();
|
||||||
|
if(pushLink.isPresent())
|
||||||
|
{
|
||||||
|
if(pushLink.get().getToStepNo() != null)
|
||||||
|
{
|
||||||
|
NodeSequence containerSequence = new NodeSequence();
|
||||||
|
stepNode.subSequences().put("push", containerSequence);
|
||||||
|
buildNodeSequence(containerSequence, stepMap.get(pushLink.get().getToStepNo()), stepMap, linksFromMap, null, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<WorkflowLink> popLink = outboundLinks.stream().filter(ol -> "pop".equals(ol.getConditionValue())).findFirst();
|
||||||
|
if(popLink.isPresent())
|
||||||
|
{
|
||||||
|
if(popLink.get().getToStepNo() != null)
|
||||||
|
{
|
||||||
|
currentWorkflowStep = stepMap.get(popLink.get().getToStepNo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG.warn("expected to have multiple outbound links for a step of type: " + currentWorkflowStep.getWorkflowStepTypeName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+10
@@ -98,6 +98,11 @@ public class RecordWorkflowInputRecordFilterWidget extends AbstractWidgetRendere
|
|||||||
public static void applyApiNameAndVersionFromInputToWidgetData(RenderWidgetInput input, FilterAndColumnsSetupData widgetData)
|
public static void applyApiNameAndVersionFromInputToWidgetData(RenderWidgetInput input, FilterAndColumnsSetupData widgetData)
|
||||||
{
|
{
|
||||||
String apiName = input.getQueryParams().get("workflowRevision.apiName");
|
String apiName = input.getQueryParams().get("workflowRevision.apiName");
|
||||||
|
if(!StringUtils.hasContent(apiName))
|
||||||
|
{
|
||||||
|
apiName = input.getQueryParams().get("apiName");
|
||||||
|
}
|
||||||
|
|
||||||
if(StringUtils.hasContent(apiName))
|
if(StringUtils.hasContent(apiName))
|
||||||
{
|
{
|
||||||
widgetData.setApiName(apiName);
|
widgetData.setApiName(apiName);
|
||||||
@@ -113,6 +118,11 @@ public class RecordWorkflowInputRecordFilterWidget extends AbstractWidgetRendere
|
|||||||
}
|
}
|
||||||
|
|
||||||
String apiVersion = input.getQueryParams().get("workflowRevision.apiVersion");
|
String apiVersion = input.getQueryParams().get("workflowRevision.apiVersion");
|
||||||
|
if(!StringUtils.hasContent(apiVersion))
|
||||||
|
{
|
||||||
|
apiVersion = input.getQueryParams().get("apiVersion");
|
||||||
|
}
|
||||||
|
|
||||||
if(StringUtils.hasContent(apiVersion))
|
if(StringUtils.hasContent(apiVersion))
|
||||||
{
|
{
|
||||||
widgetData.setApiVersion(apiVersion);
|
widgetData.setApiVersion(apiVersion);
|
||||||
|
|||||||
+18
-8
@@ -83,18 +83,28 @@ public class RecordWorkflowUtils
|
|||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
* expects name in inputValues to be "queryFilterJson"
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
public static QQueryFilter getFilterFromInput(Map<String, ?> inputValues) throws QException
|
public static QQueryFilter getFilterFromInput(Map<String, ?> inputValues) throws QException
|
||||||
|
{
|
||||||
|
Object object = inputValues.get("queryFilterJson");
|
||||||
|
if(object == null)
|
||||||
|
{
|
||||||
|
throw (new QException("Could not find filter in input values"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFilterFromJsonOrObject(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* supported input types are: QQueryFilter, String (json), Map.
|
||||||
|
***************************************************************************/
|
||||||
|
public static QQueryFilter getFilterFromJsonOrObject(Object object) throws QException
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Object object = inputValues.get("queryFilterJson");
|
|
||||||
if(object == null)
|
|
||||||
{
|
|
||||||
throw (new QException("Could not find filter in input values"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(object instanceof QQueryFilter qQueryFilter)
|
if(object instanceof QQueryFilter qQueryFilter)
|
||||||
{
|
{
|
||||||
return (qQueryFilter);
|
return (qQueryFilter);
|
||||||
@@ -114,7 +124,7 @@ public class RecordWorkflowUtils
|
|||||||
}
|
}
|
||||||
catch(IOException e)
|
catch(IOException e)
|
||||||
{
|
{
|
||||||
throw new QException("Error getting filter from input values", e);
|
throw new QException("Error getting filter from Object", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+60
-4
@@ -25,16 +25,21 @@ package com.kingsrook.qbits.workflows.implementations.recordworkflows;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.ListIterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import com.kingsrook.qbits.workflows.definition.WorkflowStepType;
|
||||||
|
import com.kingsrook.qbits.workflows.definition.WorkflowsRegistry;
|
||||||
import com.kingsrook.qbits.workflows.implementations.WorkflowStepUtils;
|
import com.kingsrook.qbits.workflows.implementations.WorkflowStepUtils;
|
||||||
import com.kingsrook.qbits.workflows.model.WorkflowRevision;
|
import com.kingsrook.qbits.workflows.model.WorkflowRevision;
|
||||||
import com.kingsrook.qqq.backend.core.actions.values.SearchPossibleValueSourceAction;
|
import com.kingsrook.qqq.backend.core.actions.values.SearchPossibleValueSourceAction;
|
||||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
|
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceOutput;
|
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference;
|
import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference;
|
||||||
@@ -128,12 +133,24 @@ public class UpdateInputRecordFieldMetaDataAdjuster implements FormAdjusterInter
|
|||||||
{
|
{
|
||||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||||
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, newValue);
|
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, newValue);
|
||||||
|
updatedField = fieldAndJoinTable.field().clone();
|
||||||
|
updatedField.setName("value");
|
||||||
|
updatedField.setLabel("Value (" + fieldAndJoinTable.field().getLabel() + ")");
|
||||||
|
}
|
||||||
|
|
||||||
if(fieldAndJoinTable != null)
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// check the workflow registry to see if there is help content for this field //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
WorkflowsRegistry workflowsRegistry = WorkflowsRegistry.of(QContext.getQInstance());
|
||||||
|
WorkflowStepType workflowStepType = workflowsRegistry.getWorkflowStepType(UpdateInputRecordFieldStep.NAME);
|
||||||
|
if(workflowStepType != null)
|
||||||
|
{
|
||||||
|
for(QFieldMetaData fieldMetaData : CollectionUtils.nonNullList(workflowStepType.getInputFields()))
|
||||||
{
|
{
|
||||||
updatedField = fieldAndJoinTable.field().clone();
|
if(fieldMetaData.getName().equals("value"))
|
||||||
updatedField.setName("value");
|
{
|
||||||
updatedField.setLabel("Value (" + fieldAndJoinTable.field().getLabel() + ")");
|
updatedField.setHelpContents(fieldMetaData.getHelpContents());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,6 +160,8 @@ public class UpdateInputRecordFieldMetaDataAdjuster implements FormAdjusterInter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adjustPossibleValueSourceFilterCriteriaInputs(updatedField.getPossibleValueSourceFilter());
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
// build the output object, starting with the new meta-data of the 'value' field //
|
// build the output object, starting with the new meta-data of the 'value' field //
|
||||||
///////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -186,6 +205,43 @@ public class UpdateInputRecordFieldMetaDataAdjuster implements FormAdjusterInter
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* if the field has a possible-value-source filter on it, and that filter
|
||||||
|
* uses an input field e.g., ${input.someField} - consider that this value
|
||||||
|
* *might* be an attribute on the workflow so add an optional
|
||||||
|
* ??${input.workflow.someField} after it.
|
||||||
|
***************************************************************************/
|
||||||
|
static void adjustPossibleValueSourceFilterCriteriaInputs(QQueryFilter filter)
|
||||||
|
{
|
||||||
|
if(filter == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(QFilterCriteria criteria : CollectionUtils.nonNullList(filter.getCriteria()))
|
||||||
|
{
|
||||||
|
List<Serializable> values = CollectionUtils.nonNullList(criteria.getValues());
|
||||||
|
ListIterator<Serializable> valueIterator = values.listIterator();
|
||||||
|
while(valueIterator.hasNext())
|
||||||
|
{
|
||||||
|
Serializable value = valueIterator.next();
|
||||||
|
if(value instanceof String stringValue && stringValue.startsWith("${input."))
|
||||||
|
{
|
||||||
|
String inputFieldName = stringValue.replace("${input.", "").replace("}", "");
|
||||||
|
String updatedValue = stringValue + "??${input.workflow." + inputFieldName + "}";
|
||||||
|
valueIterator.set(updatedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(QQueryFilter subFilter : CollectionUtils.nonNullList(filter.getSubFilters()))
|
||||||
|
{
|
||||||
|
adjustPossibleValueSourceFilterCriteriaInputs(subFilter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
|
|||||||
+88
-29
@@ -60,7 +60,10 @@ import org.json.JSONObject;
|
|||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** workflow step that updates a record with one value in one field
|
* Workflow step that updates a record with one value in one field
|
||||||
|
*
|
||||||
|
* This class includes some public static methods that can be useful for other
|
||||||
|
* workflow step implementations that have similar functionality.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class UpdateInputRecordFieldStep extends WorkflowStepType implements WorkflowStepExecutorInterface, WorkflowStepValidatorInterface
|
public class UpdateInputRecordFieldStep extends WorkflowStepType implements WorkflowStepExecutorInterface, WorkflowStepValidatorInterface
|
||||||
{
|
{
|
||||||
@@ -101,7 +104,15 @@ public class UpdateInputRecordFieldStep extends WorkflowStepType implements Work
|
|||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
* For a given fieldName, tableName, and api and version (which come from a
|
||||||
|
* workflowRevision), get QFieldMetaData for that field (if it's in that
|
||||||
|
* API Version).
|
||||||
|
*
|
||||||
|
* @param fieldNameMaybeWithTableNamePrefix - field name - which can be
|
||||||
|
* prefixed by tableName + "."
|
||||||
|
* @param tableName table name
|
||||||
|
* @param workflowRevision from which api name and version will come
|
||||||
|
* @return optional containing QFieldMetaData if found, or empty if not.
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
public static Optional<QFieldMetaData> getApiField(String fieldNameMaybeWithTableNamePrefix, String tableName, WorkflowRevision workflowRevision) throws QException
|
public static Optional<QFieldMetaData> getApiField(String fieldNameMaybeWithTableNamePrefix, String tableName, WorkflowRevision workflowRevision) throws QException
|
||||||
{
|
{
|
||||||
@@ -121,6 +132,31 @@ public class UpdateInputRecordFieldStep extends WorkflowStepType implements Work
|
|||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
@Override
|
@Override
|
||||||
public String getDynamicStepSummary(Integer workflowId, Map<String, Serializable> inputValues) throws QException
|
public String getDynamicStepSummary(Integer workflowId, Map<String, Serializable> inputValues) throws QException
|
||||||
|
{
|
||||||
|
QRecord workflowRecord = GetAction.execute(Workflow.TABLE_NAME, workflowId);
|
||||||
|
String tableName = workflowRecord.getValueString("tableName");
|
||||||
|
return getStepSummary(inputValues, tableName, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* Helper for {@link #getDynamicStepSummary(Integer, Map)} or
|
||||||
|
* {@link #execute(WorkflowStep, Map, WorkflowExecutionContext)} to generate
|
||||||
|
* a string like "${fieldLabel} will be / was set to ${value}" or "... cleared out"
|
||||||
|
* where the value will be a translated possible value if applicable.
|
||||||
|
*
|
||||||
|
* @param inputValues map of values user assigned to the step. expected to
|
||||||
|
* include String fieldName and String value.
|
||||||
|
* @param tableName table that the field should come from. Many times this
|
||||||
|
* may be the tableName on the workflow - but it could be
|
||||||
|
* a hard-coded tableName in other implementations of this
|
||||||
|
* step type.
|
||||||
|
* @param isPastTense to format the message as "will be" or "was". e.g.,
|
||||||
|
* for getDynamicStepSummary would be false ("will be")
|
||||||
|
* and for execute would be true ("was").
|
||||||
|
***************************************************************************/
|
||||||
|
public static String getStepSummary(Map<String, Serializable> inputValues, String tableName, boolean isPastTense)
|
||||||
{
|
{
|
||||||
String fieldName = ValueUtils.getValueAsString(inputValues.get("fieldName"));
|
String fieldName = ValueUtils.getValueAsString(inputValues.get("fieldName"));
|
||||||
String value = ValueUtils.getValueAsString(inputValues.get("value"));
|
String value = ValueUtils.getValueAsString(inputValues.get("value"));
|
||||||
@@ -137,45 +173,38 @@ public class UpdateInputRecordFieldStep extends WorkflowStepType implements Work
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return getStepSummary(workflowId, fieldName, apiName, apiVersion, value, false);
|
return getStepSummary(fieldName, tableName, apiName, apiVersion, value, isPastTense);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
*
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
private String getStepSummary(Integer workflowId, String fieldName, String apiName, String apiVersion, String value, boolean isPastTense)
|
private static String getStepSummary(String fieldName, String tableName, String apiName, String apiVersion, String value, boolean isPastTense)
|
||||||
{
|
{
|
||||||
String fieldLabel = null;
|
String fieldLabel = null;
|
||||||
QTableMetaData table = null;
|
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||||
|
|
||||||
if(StringUtils.hasContent(fieldName))
|
if(StringUtils.hasContent(fieldName))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if(workflowId != null)
|
WorkflowRevision workflowRevision = new WorkflowRevision().withApiName(apiName).withApiVersion(apiVersion);
|
||||||
|
if(WorkflowStepUtils.useApi(workflowRevision))
|
||||||
{
|
{
|
||||||
QRecord workflowRecord = GetAction.execute(Workflow.TABLE_NAME, workflowId);
|
Optional<QFieldMetaData> optionalApiField = getApiField(fieldName, tableName, workflowRevision);
|
||||||
table = QContext.getQInstance().getTable(workflowRecord.getValueString("tableName"));
|
if(optionalApiField.isPresent())
|
||||||
|
|
||||||
WorkflowRevision workflowRevision = new WorkflowRevision().withApiName(apiName).withApiVersion(apiVersion);
|
|
||||||
if(WorkflowStepUtils.useApi(workflowRevision))
|
|
||||||
{
|
{
|
||||||
Optional<QFieldMetaData> optionalApiField = getApiField(fieldName, table.getName(), workflowRevision);
|
fieldLabel = optionalApiField.get().getLabel();
|
||||||
if(optionalApiField.isPresent())
|
|
||||||
{
|
|
||||||
// fieldName = optionalApiField.field().getName(); // hmm?
|
|
||||||
fieldLabel = optionalApiField.get().getLabel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, fieldName);
|
|
||||||
fieldName = fieldAndJoinTable.field().getName();
|
|
||||||
fieldLabel = fieldAndJoinTable.field().getLabel();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, fieldName);
|
||||||
|
fieldName = fieldAndJoinTable.field().getName();
|
||||||
|
fieldLabel = fieldAndJoinTable.field().getLabel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
@@ -183,15 +212,22 @@ public class UpdateInputRecordFieldStep extends WorkflowStepType implements Work
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String verb = isPastTense ? "was" : "will be";
|
|
||||||
|
|
||||||
if(StringUtils.hasContent(fieldLabel))
|
if(StringUtils.hasContent(fieldLabel))
|
||||||
{
|
{
|
||||||
|
String verb = isPastTense ? "was" : "will be";
|
||||||
if(StringUtils.hasContent(value))
|
if(StringUtils.hasContent(value))
|
||||||
{
|
{
|
||||||
String displayValue = value;
|
String displayValue = value;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// field names from this PVS are always table.field - but QValueFormatter doesn't expect that - so strip away table. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(fieldName != null && fieldName.startsWith(tableName + "."))
|
||||||
|
{
|
||||||
|
fieldName = fieldName.replace(tableName + ".", "");
|
||||||
|
}
|
||||||
|
|
||||||
QRecord record = new QRecord().withValue(fieldName, value);
|
QRecord record = new QRecord().withValue(fieldName, value);
|
||||||
QValueFormatter.setDisplayValuesInRecordsIncludingPossibleValueTranslations(table, List.of(record));
|
QValueFormatter.setDisplayValuesInRecordsIncludingPossibleValueTranslations(table, List.of(record));
|
||||||
displayValue = record.getDisplayValue(fieldName);
|
displayValue = record.getDisplayValue(fieldName);
|
||||||
@@ -241,7 +277,8 @@ public class UpdateInputRecordFieldStep extends WorkflowStepType implements Work
|
|||||||
|
|
||||||
context.doesRecordNeedUpdated.set(true);
|
context.doesRecordNeedUpdated.set(true);
|
||||||
|
|
||||||
String stepSummary = getStepSummary(context.getWorkflow().getId(), fieldName, context.getWorkflowRevision().getApiName(), context.getWorkflowRevision().getApiVersion(), value, true);
|
String tableName = context.getWorkflow().getTableName();
|
||||||
|
String stepSummary = getStepSummary(fieldName, tableName, context.getWorkflowRevision().getApiName(), context.getWorkflowRevision().getApiVersion(), value, true);
|
||||||
return new WorkflowStepOutput(value, stepSummary);
|
return new WorkflowStepOutput(value, stepSummary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,12 +321,34 @@ public class UpdateInputRecordFieldStep extends WorkflowStepType implements Work
|
|||||||
@Override
|
@Override
|
||||||
public void validate(WorkflowStep step, Map<String, Serializable> inputValues, QRecord workflowRevision, QRecord workflow, List<String> errors) throws QException
|
public void validate(WorkflowStep step, Map<String, Serializable> inputValues, QRecord workflowRevision, QRecord workflow, List<String> errors) throws QException
|
||||||
{
|
{
|
||||||
if(WorkflowStepUtils.useApi(new WorkflowRevision(workflowRevision)))
|
validate(workflow.getValueString("tableName"), inputValues, workflowRevision, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* implementation logic for {@link #validate(WorkflowStep, Map, QRecord, QRecord, List)}.
|
||||||
|
* makes sure that, if the workflow uses API, that the field is a valid field
|
||||||
|
* for the api version being used in the workflow revision.
|
||||||
|
*
|
||||||
|
* @param tableName table that the field should come from. Many times this
|
||||||
|
* may be the tableName on the workflow - but it could be
|
||||||
|
* a hard-coded tableName in other implementations of this
|
||||||
|
* step type.
|
||||||
|
* @param inputValues map of values user assigned to the step. expected to
|
||||||
|
* include String fieldName.
|
||||||
|
* @param workflowRevision from which api name and version will come
|
||||||
|
* @param errors Out param - list of error message strings, which will be
|
||||||
|
* added to if the input fieldName isn't in the table for the
|
||||||
|
* api version on the workflowRevision
|
||||||
|
***************************************************************************/
|
||||||
|
public static void validate(String tableName, Map<String, Serializable> inputValues, QRecord workflowRevision, List<String> errors) throws QException
|
||||||
|
{
|
||||||
|
if(workflowRevision != null && WorkflowStepUtils.useApi(new WorkflowRevision(workflowRevision)))
|
||||||
{
|
{
|
||||||
String fieldName = ValueUtils.getValueAsString(inputValues.get("fieldName"));
|
String fieldName = ValueUtils.getValueAsString(inputValues.get("fieldName"));
|
||||||
String apiName = ValueUtils.getValueAsString(workflowRevision.getValueString("apiName"));
|
String apiName = ValueUtils.getValueAsString(workflowRevision.getValueString("apiName"));
|
||||||
String apiVersion = ValueUtils.getValueAsString(workflowRevision.getValueString("apiVersion"));
|
String apiVersion = ValueUtils.getValueAsString(workflowRevision.getValueString("apiVersion"));
|
||||||
String tableName = ValueUtils.getValueAsString(workflow.getValueString("tableName"));
|
|
||||||
|
|
||||||
Optional<QFieldMetaData> optionalApiField = getApiField(fieldName, tableName, new WorkflowRevision(workflowRevision));
|
Optional<QFieldMetaData> optionalApiField = getApiField(fieldName, tableName, new WorkflowRevision(workflowRevision));
|
||||||
if(optionalApiField.isEmpty())
|
if(optionalApiField.isEmpty())
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ public class WorkflowLink extends QRecordEntity
|
|||||||
@QField(isRequired = true)
|
@QField(isRequired = true)
|
||||||
private Integer fromStepNo;
|
private Integer fromStepNo;
|
||||||
|
|
||||||
@QField(isRequired = true)
|
@QField()
|
||||||
private Integer toStepNo;
|
private Integer toStepNo;
|
||||||
|
|
||||||
@QField(maxLength = 20, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
@QField(maxLength = 20, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||||
|
|||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qbits.workflows.processes;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import com.kingsrook.qbits.workflows.definition.OutboundLinkMode;
|
||||||
|
import com.kingsrook.qbits.workflows.definition.OutboundLinkOption;
|
||||||
|
import com.kingsrook.qbits.workflows.definition.WorkflowStepType;
|
||||||
|
import com.kingsrook.qbits.workflows.definition.WorkflowsRegistry;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||||
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* process to get values that the backend must compute about a workflow step,
|
||||||
|
* based on input values, e.g., as a user is editing the step in a UI.
|
||||||
|
*
|
||||||
|
* Originally this was just the step summary (e.g., workflowStepType.getDynamicStepSummary)
|
||||||
|
*
|
||||||
|
* Then, when variable outboundLinkModes were added, also responsible for returning
|
||||||
|
* the link options to use for a step, based on its input values.
|
||||||
|
*
|
||||||
|
*******************************************************************************/
|
||||||
|
public class GetWorkflowStepDynamicValuesProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||||
|
{
|
||||||
|
public static final String NAME = "getWorkflowStepDynamicValues";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
@Override
|
||||||
|
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||||
|
{
|
||||||
|
return new QProcessMetaData()
|
||||||
|
.withName(NAME)
|
||||||
|
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
|
||||||
|
.withStep(new QBackendStepMetaData()
|
||||||
|
.withName("execute")
|
||||||
|
.withCode(new QCodeReference(getClass()))
|
||||||
|
.withInputData(new QFunctionInputMetaData()
|
||||||
|
.withField(new QFieldMetaData("workflowId", QFieldType.INTEGER))
|
||||||
|
.withField(new QFieldMetaData("workflowStepTypeName", QFieldType.STRING))
|
||||||
|
.withField(new QFieldMetaData("values", QFieldType.INTEGER)) // JSON
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
@Override
|
||||||
|
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
|
||||||
|
{
|
||||||
|
Integer workflowId = runBackendStepInput.getValueInteger("workflowId");
|
||||||
|
|
||||||
|
String workflowStepTypeName = runBackendStepInput.getValueString("workflowStepTypeName");
|
||||||
|
String values = runBackendStepInput.getValueString("values");
|
||||||
|
Map<String, Serializable> valueAsMap = ValueUtils.getValueAsMap(values);
|
||||||
|
|
||||||
|
WorkflowStepType workflowStepType = WorkflowsRegistry.of(QContext.getQInstance()).getWorkflowStepType(workflowStepTypeName);
|
||||||
|
runBackendStepOutput.addValue("summary", workflowStepType.getDynamicStepSummary(workflowId, valueAsMap));
|
||||||
|
|
||||||
|
if(OutboundLinkMode.VARIABLE.equals(workflowStepType.getOutboundLinkMode()))
|
||||||
|
{
|
||||||
|
List<OutboundLinkOption> dynamicOutboundLinkOptions = workflowStepType.getDynamicOutboundLinkOptions(workflowId, valueAsMap);
|
||||||
|
if(dynamicOutboundLinkOptions != null)
|
||||||
|
{
|
||||||
|
ArrayList<OutboundLinkOption> outboundLinkOptions = CollectionUtils.useOrWrap(dynamicOutboundLinkOptions, new TypeToken<>() {});
|
||||||
|
runBackendStepOutput.addValue("outboundLinkOptions", outboundLinkOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+1
-1
@@ -48,6 +48,7 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
|||||||
** process to get a dynamic summary of a workflow step, e.g., as it's being
|
** process to get a dynamic summary of a workflow step, e.g., as it's being
|
||||||
** edited in a UI.
|
** edited in a UI.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@Deprecated(since = "0.115.0 - November 2025 - Replaced by GetWorkflowStepDynamicValuesProcess")
|
||||||
public class GetWorkflowStepSummaryProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
public class GetWorkflowStepSummaryProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||||
{
|
{
|
||||||
public static final String NAME = "getWorkflowStepSummary";
|
public static final String NAME = "getWorkflowStepSummary";
|
||||||
@@ -58,7 +59,6 @@ public class GetWorkflowStepSummaryProcess implements BackendStep, MetaDataProdu
|
|||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
@Override
|
@Override
|
||||||
|
|
||||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||||
{
|
{
|
||||||
return new QProcessMetaData()
|
return new QProcessMetaData()
|
||||||
|
|||||||
+27
-1
@@ -41,12 +41,14 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
|
|||||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.helpcontent.HelpContent;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendFieldMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendFieldMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||||
@@ -116,8 +118,12 @@ public class GetWorkflowTypeDefinitionProcess implements BackendStep, MetaDataPr
|
|||||||
throw new QException("Workflow type not found: " + workflowTypeName);
|
throw new QException("Workflow type not found: " + workflowTypeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// save any help content on current workflow and put back after customization //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Map<String, List<QHelpContent>> helpContent = workflowType.getHelpContent();
|
||||||
workflowType = workflowType.customizeBasedOnWorkflow(workflowRecord);
|
workflowType = workflowType.customizeBasedOnWorkflow(workflowRecord);
|
||||||
|
workflowType.setHelpContent(helpContent);
|
||||||
runBackendStepOutput.addValue("workflowType", workflowType);
|
runBackendStepOutput.addValue("workflowType", workflowType);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -167,4 +173,24 @@ public class GetWorkflowTypeDefinitionProcess implements BackendStep, MetaDataPr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
protected Map<String, List<HelpContent>> getWorkflowTypeHelpContent(Map<String, HelpContent> helpContentMap)
|
||||||
|
{
|
||||||
|
Map<String, List<HelpContent>> workflowTypeHelpContent = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
String[] workflowTypeKeys = new String[] { "workflow:editor;field:workflowName", "workflow:editor;field:versionNo", "workflow:editor;field:apiName", "workflow:editor;field:apiVersion", "workflow:tester;field:workflowTestScenarioId" };
|
||||||
|
for(String key : workflowTypeKeys)
|
||||||
|
{
|
||||||
|
if(helpContentMap.containsKey(key))
|
||||||
|
{
|
||||||
|
workflowTypeHelpContent.put(key, List.of(helpContentMap.get(key)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (workflowTypeHelpContent);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+67
-10
@@ -29,8 +29,10 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.kingsrook.qbits.workflows.definition.WorkflowStepType;
|
import com.kingsrook.qbits.workflows.definition.WorkflowStepType;
|
||||||
|
import com.kingsrook.qbits.workflows.definition.WorkflowType;
|
||||||
import com.kingsrook.qbits.workflows.definition.WorkflowsRegistry;
|
import com.kingsrook.qbits.workflows.definition.WorkflowsRegistry;
|
||||||
import com.kingsrook.qbits.workflows.execution.WorkflowStepValidatorInterface;
|
import com.kingsrook.qbits.workflows.execution.WorkflowStepValidatorInterface;
|
||||||
|
import com.kingsrook.qbits.workflows.execution.WorkflowTypeValidatorInterface;
|
||||||
import com.kingsrook.qbits.workflows.model.Workflow;
|
import com.kingsrook.qbits.workflows.model.Workflow;
|
||||||
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||||
import com.kingsrook.qbits.workflows.model.WorkflowRevision;
|
import com.kingsrook.qbits.workflows.model.WorkflowRevision;
|
||||||
@@ -140,6 +142,38 @@ public class StoreNewWorkflowRevisionProcess implements BackendStep, MetaDataPro
|
|||||||
throw (new QUserFacingException("Workflow not found by id: " + workflowId));
|
throw (new QUserFacingException("Workflow not found by id: " + workflowId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String stepsJSON = runBackendStepInput.getValueString("steps");
|
||||||
|
if(!StringUtils.hasContent(stepsJSON))
|
||||||
|
{
|
||||||
|
throw (new QUserFacingException("Workflow steps input was not provided."));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<WorkflowStep> workflowSteps;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
workflowSteps = JsonUtils.toObject(stepsJSON, new TypeReference<>() {});
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
throw (new QUserFacingException("Error parsing workflow steps json: " + e.getMessage(), e));
|
||||||
|
}
|
||||||
|
|
||||||
|
String linksJSON = runBackendStepInput.getValueString("links");
|
||||||
|
if(!StringUtils.hasContent(linksJSON))
|
||||||
|
{
|
||||||
|
throw (new QUserFacingException("Workflow links input was not provided."));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<WorkflowLink> workflowLinks;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
workflowLinks = JsonUtils.toObject(linksJSON, new TypeReference<>() {});
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
throw (new QUserFacingException("Error parsing workflow links json: " + e.getMessage(), e));
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////
|
////////////////////////////
|
||||||
// look up next versionNo //
|
// look up next versionNo //
|
||||||
////////////////////////////
|
////////////////////////////
|
||||||
@@ -166,15 +200,15 @@ public class StoreNewWorkflowRevisionProcess implements BackendStep, MetaDataPro
|
|||||||
// insert the workflow revision //
|
// insert the workflow revision //
|
||||||
//////////////////////////////////
|
//////////////////////////////////
|
||||||
String commitMessage = runBackendStepInput.getValueString("commitMessage");
|
String commitMessage = runBackendStepInput.getValueString("commitMessage");
|
||||||
|
String userName = ObjectUtils.tryElse(() -> QContext.getQSession().getUser().getFullName(), "Unknown");
|
||||||
WorkflowRevision workflowRevision = new WorkflowRevision();
|
WorkflowRevision workflowRevision = new WorkflowRevision();
|
||||||
workflowRevision.setWorkflowId(workflowId);
|
workflowRevision.setWorkflowId(workflowId);
|
||||||
workflowRevision.setVersionNo(versionNo);
|
workflowRevision.setVersionNo(versionNo);
|
||||||
workflowRevision.setCommitMessage(StringUtils.hasContent(commitMessage) ? commitMessage : "New workflow revision created by " + QContext.getQSession().getUser().getFullName());
|
workflowRevision.setCommitMessage(StringUtils.hasContent(commitMessage) ? commitMessage : "New workflow revision created by " + userName);
|
||||||
workflowRevision.setApiName(runBackendStepInput.getValueString("apiName"));
|
workflowRevision.setApiName(runBackendStepInput.getValueString("apiName"));
|
||||||
workflowRevision.setApiVersion(runBackendStepInput.getValueString("apiVersion"));
|
workflowRevision.setApiVersion(runBackendStepInput.getValueString("apiVersion"));
|
||||||
workflowRevision.setAuthor(ObjectUtils.tryElse(() -> QContext.getQSession().getUser().getFullName(), "Unknown"));
|
workflowRevision.setAuthor(userName);
|
||||||
|
|
||||||
List<WorkflowStep> workflowSteps = JsonUtils.toObject(runBackendStepInput.getValueString("steps"), new TypeReference<>() {});
|
|
||||||
if(!workflowSteps.isEmpty())
|
if(!workflowSteps.isEmpty())
|
||||||
{
|
{
|
||||||
workflowRevision.setStartStepNo(workflowSteps.get(0).getStepNo());
|
workflowRevision.setStartStepNo(workflowSteps.get(0).getStepNo());
|
||||||
@@ -208,19 +242,24 @@ public class StoreNewWorkflowRevisionProcess implements BackendStep, MetaDataPro
|
|||||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// store steps (first setting revision id, validating, and putting a fresh summary on each) //
|
// store steps (first setting revision id, validating, and putting a fresh summary on each) //
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
List<String> errors = new ArrayList<>();
|
List<String> errors = new ArrayList<>();
|
||||||
|
List<String> errorsForThisStep = new ArrayList<>();
|
||||||
workflowSteps.forEach(step ->
|
workflowSteps.forEach(step ->
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
step.setWorkflowRevisionId(insertedRevisionId);
|
step.setWorkflowRevisionId(insertedRevisionId);
|
||||||
|
|
||||||
WorkflowStepType workflowStepType = WorkflowsRegistry.of(QContext.getQInstance()).getWorkflowStepType(step.getWorkflowStepTypeName());
|
WorkflowStepType workflowStepType = WorkflowsRegistry.of(QContext.getQInstance()).getWorkflowStepType(step.getWorkflowStepTypeName());
|
||||||
Map<String, Serializable> inputValues = ValueUtils.getValueAsMap(step.getInputValuesJson());
|
if(workflowStepType == null)
|
||||||
|
{
|
||||||
|
throw (new QUserFacingException("Unknown workflow step type " + step.getWorkflowStepTypeName()));
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////
|
////////////////////////////////////
|
||||||
// summary - if it fails, :shrug: //
|
// summary - if it fails, :shrug: //
|
||||||
////////////////////////////////////
|
////////////////////////////////////
|
||||||
|
Map<String, Serializable> inputValues = ValueUtils.getValueAsMap(step.getInputValuesJson());
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
step.setSummary(workflowStepType.getDynamicStepSummary(workflowId, inputValues));
|
step.setSummary(workflowStepType.getDynamicStepSummary(workflowId, inputValues));
|
||||||
@@ -237,18 +276,37 @@ public class StoreNewWorkflowRevisionProcess implements BackendStep, MetaDataPro
|
|||||||
if(validatorCodeReference != null)
|
if(validatorCodeReference != null)
|
||||||
{
|
{
|
||||||
WorkflowStepValidatorInterface validator = QCodeLoader.getAdHoc(WorkflowStepValidatorInterface.class, validatorCodeReference);
|
WorkflowStepValidatorInterface validator = QCodeLoader.getAdHoc(WorkflowStepValidatorInterface.class, validatorCodeReference);
|
||||||
validator.validate(step, inputValues, workflowRevisionRecord, workflowRecord, errors);
|
validator.validate(step, inputValues, workflowRevisionRecord, workflowRecord, errorsForThisStep);
|
||||||
|
if(!errorsForThisStep.isEmpty())
|
||||||
|
{
|
||||||
|
///////////////////////////////////////
|
||||||
|
// put Step # in front of all errors //
|
||||||
|
///////////////////////////////////////
|
||||||
|
errors.addAll(errorsForThisStep.stream().map(e -> "Step " + step.getStepNo() + ": " + e).toList());
|
||||||
|
errorsForThisStep.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
errors.add("Error processing step [" + step.getSummary() + "]: " + e.getMessage());
|
errors.add("Error processing Step " + step.getStepNo() + ": " + e.getMessage());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
// do workflow-level validation as well, if so configured //
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
WorkflowType workflowType = WorkflowsRegistry.of(QContext.getQInstance()).getWorkflowType(workflowRecord.getValueString("workflowTypeName"));
|
||||||
|
if(workflowType.getValidator() != null)
|
||||||
|
{
|
||||||
|
WorkflowTypeValidatorInterface validator = QCodeLoader.getAdHoc(WorkflowTypeValidatorInterface.class, workflowType.getValidator());
|
||||||
|
validator.validate(workflowRecord, workflowRevisionRecord, workflowSteps, workflowLinks, errors);
|
||||||
|
}
|
||||||
|
|
||||||
if(!errors.isEmpty())
|
if(!errors.isEmpty())
|
||||||
{
|
{
|
||||||
throw (new QUserFacingException("Validation errors in steps: " + StringUtils.joinWithCommasAndAnd(errors)));
|
String message = errors.size() + " validation error" + StringUtils.plural(errors) + " occurred before the workflow could be saved:\n" + StringUtils.join("\n", errors);
|
||||||
|
throw (new QUserFacingException(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
new InsertAction().execute(new InsertInput(WorkflowStep.TABLE_NAME).withRecordEntities(workflowSteps).withTransaction(transaction));
|
new InsertAction().execute(new InsertInput(WorkflowStep.TABLE_NAME).withRecordEntities(workflowSteps).withTransaction(transaction));
|
||||||
@@ -256,7 +314,6 @@ public class StoreNewWorkflowRevisionProcess implements BackendStep, MetaDataPro
|
|||||||
/////////////////
|
/////////////////
|
||||||
// store links //
|
// store links //
|
||||||
/////////////////
|
/////////////////
|
||||||
List<WorkflowLink> workflowLinks = JsonUtils.toObject(runBackendStepInput.getValueString("links"), new TypeReference<>() {});
|
|
||||||
workflowLinks.forEach(link -> link.setWorkflowRevisionId(insertedRevisionId));
|
workflowLinks.forEach(link -> link.setWorkflowRevisionId(insertedRevisionId));
|
||||||
new InsertAction().execute(new InsertInput(WorkflowLink.TABLE_NAME).withRecordEntities(workflowLinks).withTransaction(transaction));
|
new InsertAction().execute(new InsertInput(WorkflowLink.TABLE_NAME).withRecordEntities(workflowLinks).withTransaction(transaction));
|
||||||
|
|
||||||
|
|||||||
@@ -32,11 +32,13 @@ import com.kingsrook.qbits.workflows.definition.WorkflowType;
|
|||||||
import com.kingsrook.qbits.workflows.definition.WorkflowsRegistry;
|
import com.kingsrook.qbits.workflows.definition.WorkflowsRegistry;
|
||||||
import com.kingsrook.qbits.workflows.execution.WorkflowExecutionContext;
|
import com.kingsrook.qbits.workflows.execution.WorkflowExecutionContext;
|
||||||
import com.kingsrook.qbits.workflows.execution.WorkflowInput;
|
import com.kingsrook.qbits.workflows.execution.WorkflowInput;
|
||||||
|
import com.kingsrook.qbits.workflows.execution.WorkflowMultiForkingStepExecutorInterface;
|
||||||
import com.kingsrook.qbits.workflows.execution.WorkflowStepExecutorInterface;
|
import com.kingsrook.qbits.workflows.execution.WorkflowStepExecutorInterface;
|
||||||
import com.kingsrook.qbits.workflows.execution.WorkflowStepOutput;
|
import com.kingsrook.qbits.workflows.execution.WorkflowStepOutput;
|
||||||
import com.kingsrook.qbits.workflows.execution.WorkflowTypeExecutorInterface;
|
import com.kingsrook.qbits.workflows.execution.WorkflowTypeExecutorInterface;
|
||||||
import com.kingsrook.qbits.workflows.execution.WorkflowTypeTesterInterface;
|
import com.kingsrook.qbits.workflows.execution.WorkflowTypeTesterInterface;
|
||||||
import com.kingsrook.qbits.workflows.model.Workflow;
|
import com.kingsrook.qbits.workflows.model.Workflow;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||||
import com.kingsrook.qbits.workflows.model.WorkflowRevision;
|
import com.kingsrook.qbits.workflows.model.WorkflowRevision;
|
||||||
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
@@ -56,6 +58,8 @@ public class TestWorkflowDefinitions
|
|||||||
public static final String BOOLEAN_CONDITIONAL = "booleanConditional";
|
public static final String BOOLEAN_CONDITIONAL = "booleanConditional";
|
||||||
public static final String CONTAINER = "container";
|
public static final String CONTAINER = "container";
|
||||||
|
|
||||||
|
public static final String SPLIT_BY_LETTERS_IN_NAME = "splitByLettersInName";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
@@ -93,6 +97,12 @@ public class TestWorkflowDefinitions
|
|||||||
.withOutboundLinkMode(OutboundLinkMode.CONTAINER)
|
.withOutboundLinkMode(OutboundLinkMode.CONTAINER)
|
||||||
.withLabel("Container")
|
.withLabel("Container")
|
||||||
.withDescription("Group steps in the UI"));
|
.withDescription("Group steps in the UI"));
|
||||||
|
|
||||||
|
WorkflowsRegistry.of(QContext.getQInstance()).registerWorkflowStepType(new WorkflowStepType()
|
||||||
|
.withName(SPLIT_BY_LETTERS_IN_NAME)
|
||||||
|
.withOutboundLinkMode(OutboundLinkMode.VARIABLE)
|
||||||
|
.withLabel("Split by letters in name")
|
||||||
|
.withExecutor(new QCodeReference(SplitByLettersInNameExecutor.class)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -225,4 +235,57 @@ public class TestWorkflowDefinitions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
public static class SplitByLettersInNameExecutor implements WorkflowMultiForkingStepExecutorInterface
|
||||||
|
{
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
@Override
|
||||||
|
public WorkflowStepOutput conditionallyExecuteFork(WorkflowStep step, WorkflowLink fork, Map<String, Serializable> inputValues, WorkflowExecutionContext context)
|
||||||
|
{
|
||||||
|
String conditionValue = fork.getConditionValue();
|
||||||
|
String name = ValueUtils.getValueAsString(context.getValues().get("name"));
|
||||||
|
|
||||||
|
if(name.contains(conditionValue))
|
||||||
|
{
|
||||||
|
return (new WorkflowStepOutput(true, "name [" + name + "] contains [" + conditionValue + "]"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (new WorkflowStepOutput(false, "name [" + name + "] does not contain [" + conditionValue + "]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
@Override
|
||||||
|
public WorkflowStepOutput makeWorkflowStepOutput(WorkflowStep step, Map<String, Serializable> inputValues, WorkflowExecutionContext context) throws QException
|
||||||
|
{
|
||||||
|
return new WorkflowStepOutput(1, "Output");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
@Override
|
||||||
|
public void sortForks(List<WorkflowLink> forkLinks, WorkflowStep step, Map<String, Serializable> inputValues, WorkflowExecutionContext context)
|
||||||
|
{
|
||||||
|
/////////////////////
|
||||||
|
// noop by default //
|
||||||
|
/////////////////////
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,6 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import com.kingsrook.qbits.workflows.implementations.recordworkflows.RecordWorkflowsDefinition;
|
|
||||||
import com.kingsrook.qbits.workflows.model.Workflow;
|
import com.kingsrook.qbits.workflows.model.Workflow;
|
||||||
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||||
import com.kingsrook.qbits.workflows.model.WorkflowRevision;
|
import com.kingsrook.qbits.workflows.model.WorkflowRevision;
|
||||||
@@ -149,7 +148,7 @@ public class WorkflowsTestDataSource
|
|||||||
public static Workflow insertWorkflowAndInitialRevision(String workflowTypeName, String tableName, String apiName, String apiVersion) throws QException
|
public static Workflow insertWorkflowAndInitialRevision(String workflowTypeName, String tableName, String apiName, String apiVersion) throws QException
|
||||||
{
|
{
|
||||||
QRecord workflow = new InsertAction().execute(new InsertInput(Workflow.TABLE_NAME).withRecordEntity(new Workflow()
|
QRecord workflow = new InsertAction().execute(new InsertInput(Workflow.TABLE_NAME).withRecordEntity(new Workflow()
|
||||||
.withWorkflowTypeName(RecordWorkflowsDefinition.WORKFLOW_TYPE)
|
.withWorkflowTypeName(workflowTypeName)
|
||||||
.withTableName(tableName)
|
.withTableName(tableName)
|
||||||
.withName("test-" + UUID.randomUUID())
|
.withName("test-" + UUID.randomUUID())
|
||||||
)).getRecords().get(0);
|
)).getRecords().get(0);
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qbits.workflows.definition;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import com.kingsrook.qbits.workflows.BaseTest;
|
||||||
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Unit test for WorkflowsRegistry
|
||||||
|
*******************************************************************************/
|
||||||
|
class WorkflowsRegistryTest extends BaseTest
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void test() throws QException
|
||||||
|
{
|
||||||
|
String stepTypeName = "myStepType";
|
||||||
|
WorkflowsRegistry workflowsRegistry = new WorkflowsRegistry();
|
||||||
|
|
||||||
|
workflowsRegistry.registerWorkflowStepType(new WorkflowStepType()
|
||||||
|
.withName(stepTypeName)
|
||||||
|
.withDescription("originalDescription")
|
||||||
|
.withLabel("originalLabel")
|
||||||
|
.withInputFields(List.of(new QFieldMetaData("myField", QFieldType.STRING))));
|
||||||
|
|
||||||
|
/////////////////////
|
||||||
|
// set description //
|
||||||
|
/////////////////////
|
||||||
|
workflowsRegistry.acceptHelpContent(QContext.getQInstance(), new QHelpContent("newDescription"), Map.of("workflowStepType", stepTypeName, "slot", "description"));
|
||||||
|
assertEquals("newDescription", workflowsRegistry.getWorkflowStepType(stepTypeName).getDescription());
|
||||||
|
|
||||||
|
///////////////////////
|
||||||
|
// clear description //
|
||||||
|
///////////////////////
|
||||||
|
workflowsRegistry.acceptHelpContent(QContext.getQInstance(), null, Map.of("workflowStepType", stepTypeName, "slot", "description"));
|
||||||
|
assertNull(workflowsRegistry.getWorkflowStepType(stepTypeName).getDescription());
|
||||||
|
|
||||||
|
///////////////
|
||||||
|
// set label //
|
||||||
|
///////////////
|
||||||
|
workflowsRegistry.acceptHelpContent(QContext.getQInstance(), new QHelpContent("newLabel"), Map.of("workflowStepType", stepTypeName, "slot", "label"));
|
||||||
|
assertEquals("newLabel", workflowsRegistry.getWorkflowStepType(stepTypeName).getLabel());
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////
|
||||||
|
// clear label, though we can't, so it says as it was //
|
||||||
|
////////////////////////////////////////////////////////
|
||||||
|
workflowsRegistry.acceptHelpContent(QContext.getQInstance(), null, Map.of("workflowStepType", stepTypeName, "slot", "label"));
|
||||||
|
assertEquals("newLabel", workflowsRegistry.getWorkflowStepType(stepTypeName).getLabel());
|
||||||
|
|
||||||
|
////////////////////
|
||||||
|
// set field help //
|
||||||
|
////////////////////
|
||||||
|
workflowsRegistry.acceptHelpContent(QContext.getQInstance(), new QHelpContent("fieldHelp"), Map.of("workflowStepType", stepTypeName, "field", "myField"));
|
||||||
|
assertEquals("fieldHelp", workflowsRegistry.getWorkflowStepType(stepTypeName).getInputFields().get(0).getHelpContents().get(0).getContent());
|
||||||
|
|
||||||
|
//////////////////////
|
||||||
|
// clear field help //
|
||||||
|
//////////////////////
|
||||||
|
workflowsRegistry.acceptHelpContent(QContext.getQInstance(), null, Map.of("workflowStepType", stepTypeName, "field", "myField"));
|
||||||
|
assertNull(workflowsRegistry.getWorkflowStepType(stepTypeName).getInputFields().get(0).getHelpContents());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qbits.workflows.execution;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import com.kingsrook.qbits.workflows.BaseTest;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Unit test for FindFirstJoinPoint
|
||||||
|
*******************************************************************************/
|
||||||
|
class FindFirstJoinPointTest extends BaseTest
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* 1
|
||||||
|
* / | \
|
||||||
|
* / | \
|
||||||
|
* 2 3 4
|
||||||
|
* | | / \
|
||||||
|
* | 5 6 7
|
||||||
|
* | | | |
|
||||||
|
* | | | 8
|
||||||
|
* | | \ /
|
||||||
|
* | | 9
|
||||||
|
* \ | /
|
||||||
|
* 10
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void test()
|
||||||
|
{
|
||||||
|
Map<Integer, WorkflowStep> nodeMap = makeNodeMap(10);
|
||||||
|
|
||||||
|
ListingHash<Integer, WorkflowLink> outEdges = new ListingHash<>();
|
||||||
|
addEdge(outEdges, 1, 2);
|
||||||
|
addEdge(outEdges, 1, 3);
|
||||||
|
addEdge(outEdges, 1, 4);
|
||||||
|
addEdge(outEdges, 2, 10);
|
||||||
|
addEdge(outEdges, 3, 5);
|
||||||
|
addEdge(outEdges, 4, 6);
|
||||||
|
addEdge(outEdges, 5, 10);
|
||||||
|
addEdge(outEdges, 6, 9);
|
||||||
|
addEdge(outEdges, 7, 8);
|
||||||
|
addEdge(outEdges, 8, 9);
|
||||||
|
addEdge(outEdges, 9, 10);
|
||||||
|
|
||||||
|
/////////////////////////////////
|
||||||
|
// start from the top (node 1) //
|
||||||
|
/////////////////////////////////
|
||||||
|
assertEquals(10, FindFirstJoinPoint.execute(List.of(2, 3, 4), nodeMap, outEdges));
|
||||||
|
|
||||||
|
////////////////////////////////////////////
|
||||||
|
// start from the rightmost fork (node 4) //
|
||||||
|
////////////////////////////////////////////
|
||||||
|
assertEquals(9, FindFirstJoinPoint.execute(List.of(6, 7), nodeMap, outEdges));
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// remove node 10 - so they all just end and don't re-join (so expect null out) //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
nodeMap.remove(10);
|
||||||
|
outEdges.remove(9);
|
||||||
|
outEdges.remove(2);
|
||||||
|
outEdges.remove(5);
|
||||||
|
assertNull(FindFirstJoinPoint.execute(List.of(2, 3, 4), nodeMap, outEdges));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
private void addEdge(ListingHash<Integer, WorkflowLink> outEdges, Integer fromStepNo, Integer toStepNo)
|
||||||
|
{
|
||||||
|
outEdges.add(fromStepNo, new WorkflowLink().withToStepNo(toStepNo));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
private Map<Integer, WorkflowStep> makeNodeMap(int n)
|
||||||
|
{
|
||||||
|
Map<Integer, WorkflowStep> nodeMap = new HashMap<>();
|
||||||
|
for(int i = 1; i <= n; i++)
|
||||||
|
{
|
||||||
|
addNode(nodeMap, i);
|
||||||
|
}
|
||||||
|
return (nodeMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
private void addNode(Map<Integer, WorkflowStep> nodeMap, Integer stepNo)
|
||||||
|
{
|
||||||
|
nodeMap.put(stepNo, new WorkflowStep().withStepNo(stepNo));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -29,8 +29,16 @@ import com.kingsrook.qbits.workflows.BaseTest;
|
|||||||
import com.kingsrook.qbits.workflows.TestWorkflowDefinitions;
|
import com.kingsrook.qbits.workflows.TestWorkflowDefinitions;
|
||||||
import com.kingsrook.qbits.workflows.WorkflowsTestDataSource;
|
import com.kingsrook.qbits.workflows.WorkflowsTestDataSource;
|
||||||
import com.kingsrook.qbits.workflows.model.Workflow;
|
import com.kingsrook.qbits.workflows.model.Workflow;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
@@ -301,6 +309,183 @@ class WorkflowExecutorTest extends BaseTest
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testMultiForkingStep() throws Exception
|
||||||
|
{
|
||||||
|
TestWorkflowDefinitions.registerTestWorkflowTypes();
|
||||||
|
|
||||||
|
///////////////////////////////////
|
||||||
|
// build a workflow with 3 forks //
|
||||||
|
// . 1 //
|
||||||
|
// ./ | \ //
|
||||||
|
// 2 3 4 //
|
||||||
|
///////////////////////////////////
|
||||||
|
Workflow workflow = WorkflowsTestDataSource.insertWorkflowAndInitialRevision(TestWorkflowDefinitions.TEST_WORKFLOW_TYPE, null);
|
||||||
|
WorkflowsTestDataSource.insertSteps(workflow, List.of(
|
||||||
|
WorkflowsTestDataSource.newStep(1, TestWorkflowDefinitions.SPLIT_BY_LETTERS_IN_NAME, Map.of()),
|
||||||
|
WorkflowsTestDataSource.newStep(2, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 1)), // "a"
|
||||||
|
WorkflowsTestDataSource.newStep(3, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 2)), // "b"
|
||||||
|
WorkflowsTestDataSource.newStep(4, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 4)) // "c"
|
||||||
|
));
|
||||||
|
WorkflowsTestDataSource.insertLinks(workflow, List.of(
|
||||||
|
WorkflowsTestDataSource.newLink(1, 2, "a"), // += 1
|
||||||
|
WorkflowsTestDataSource.newLink(1, 3, "b"), // += 2
|
||||||
|
WorkflowsTestDataSource.newLink(1, 4, "c"), // += 3
|
||||||
|
WorkflowsTestDataSource.newLink(2, null),
|
||||||
|
WorkflowsTestDataSource.newLink(3, null),
|
||||||
|
WorkflowsTestDataSource.newLink(4, null)
|
||||||
|
));
|
||||||
|
|
||||||
|
UnsafeFunction<String, Integer, ?> run = (String name) ->
|
||||||
|
{
|
||||||
|
WorkflowOutput output = executeWorkflow(workflow.getId(), Map.of("seedValue", 0, "name", name));
|
||||||
|
return ValueUtils.getValueAsInteger(output.getContext().getValues().get("sum"));
|
||||||
|
};
|
||||||
|
|
||||||
|
assertEquals(1, run.apply("dave"));
|
||||||
|
assertEquals(3, run.apply("barry"));
|
||||||
|
assertEquals(5, run.apply("carl"));
|
||||||
|
assertEquals(7, run.apply("braces"));
|
||||||
|
assertEquals(0, run.apply("lou"));
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
// add a step (5) after the 3 forks //
|
||||||
|
//////////////////////////////////////
|
||||||
|
WorkflowsTestDataSource.insertSteps(workflow, List.of(
|
||||||
|
WorkflowsTestDataSource.newStep(5, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 8))
|
||||||
|
));
|
||||||
|
WorkflowsTestDataSource.insertLinks(workflow, List.of(
|
||||||
|
WorkflowsTestDataSource.newLink(2, 5),
|
||||||
|
WorkflowsTestDataSource.newLink(3, 5),
|
||||||
|
WorkflowsTestDataSource.newLink(4, 5)
|
||||||
|
));
|
||||||
|
|
||||||
|
//////////////////////////////////
|
||||||
|
// get rid of the links to null //
|
||||||
|
//////////////////////////////////
|
||||||
|
new DeleteAction().execute(new DeleteInput(WorkflowLink.TABLE_NAME).withQueryFilter(new QQueryFilter(new QFilterCriteria("toStepNo", QCriteriaOperator.IS_BLANK))));
|
||||||
|
|
||||||
|
assertEquals(10, run.apply("bobby"));
|
||||||
|
assertEquals(15, run.apply("braces"));
|
||||||
|
assertEquals(8, run.apply("lou"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testNestedMultiForkingStep() throws Exception
|
||||||
|
{
|
||||||
|
TestWorkflowDefinitions.registerTestWorkflowTypes();
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// build a workflow with 3 top-level forks, one of which has 2 nested ones //
|
||||||
|
// . 1 //
|
||||||
|
// ./ | \ //
|
||||||
|
// 2 3 4 //
|
||||||
|
// | | | \ //
|
||||||
|
// | | 5 6 //
|
||||||
|
// .\ \ / / //
|
||||||
|
// . 7 //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
Workflow workflow = WorkflowsTestDataSource.insertWorkflowAndInitialRevision(TestWorkflowDefinitions.TEST_WORKFLOW_TYPE, null);
|
||||||
|
WorkflowsTestDataSource.insertSteps(workflow, List.of(
|
||||||
|
WorkflowsTestDataSource.newStep(1, TestWorkflowDefinitions.SPLIT_BY_LETTERS_IN_NAME, Map.of()),
|
||||||
|
WorkflowsTestDataSource.newStep(2, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 1)), // "a"
|
||||||
|
WorkflowsTestDataSource.newStep(3, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 2)), // "b"
|
||||||
|
WorkflowsTestDataSource.newStep(4, TestWorkflowDefinitions.SPLIT_BY_LETTERS_IN_NAME, Map.of()),
|
||||||
|
WorkflowsTestDataSource.newStep(5, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 4)), // "c"
|
||||||
|
WorkflowsTestDataSource.newStep(6, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 8)), // "d"
|
||||||
|
WorkflowsTestDataSource.newStep(7, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 16))
|
||||||
|
));
|
||||||
|
WorkflowsTestDataSource.insertLinks(workflow, List.of(
|
||||||
|
WorkflowsTestDataSource.newLink(1, 2, "a"), // += 1
|
||||||
|
WorkflowsTestDataSource.newLink(1, 3, "b"), // += 2
|
||||||
|
WorkflowsTestDataSource.newLink(1, 4, ""), //
|
||||||
|
WorkflowsTestDataSource.newLink(4, 5, "c"), // += 4
|
||||||
|
WorkflowsTestDataSource.newLink(4, 6, "d"), // += 8
|
||||||
|
WorkflowsTestDataSource.newLink(2, 7), // += 16
|
||||||
|
WorkflowsTestDataSource.newLink(3, 7), // += 16
|
||||||
|
WorkflowsTestDataSource.newLink(5, 7), // += 16
|
||||||
|
WorkflowsTestDataSource.newLink(6, 7) // += 16
|
||||||
|
));
|
||||||
|
|
||||||
|
UnsafeFunction<String, Integer, ?> run = (String name) ->
|
||||||
|
{
|
||||||
|
WorkflowOutput output = executeWorkflow(workflow.getId(), Map.of("seedValue", 0, "name", name));
|
||||||
|
return ValueUtils.getValueAsInteger(output.getContext().getValues().get("sum"));
|
||||||
|
};
|
||||||
|
|
||||||
|
assertEquals(17, run.apply("allen"));
|
||||||
|
assertEquals(19, run.apply("barry"));
|
||||||
|
assertEquals(21, run.apply("carl"));
|
||||||
|
assertEquals(24, run.apply("donny"));
|
||||||
|
assertEquals(16, run.apply("lou"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testStackedMultiForkingStep() throws Exception
|
||||||
|
{
|
||||||
|
TestWorkflowDefinitions.registerTestWorkflowTypes();
|
||||||
|
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
// build a workflow with two 2-level forks //
|
||||||
|
// . 1 //
|
||||||
|
// ./ \ //
|
||||||
|
// 2 3 //
|
||||||
|
// .\ / //
|
||||||
|
// . 4 //
|
||||||
|
// ./ \ //
|
||||||
|
// 5 6 //
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
Workflow workflow = WorkflowsTestDataSource.insertWorkflowAndInitialRevision(TestWorkflowDefinitions.TEST_WORKFLOW_TYPE, null);
|
||||||
|
WorkflowsTestDataSource.insertSteps(workflow, List.of(
|
||||||
|
WorkflowsTestDataSource.newStep(1, TestWorkflowDefinitions.SPLIT_BY_LETTERS_IN_NAME, Map.of()),
|
||||||
|
WorkflowsTestDataSource.newStep(2, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 1)), // "a"
|
||||||
|
WorkflowsTestDataSource.newStep(3, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 2)), // "b"
|
||||||
|
WorkflowsTestDataSource.newStep(4, TestWorkflowDefinitions.SPLIT_BY_LETTERS_IN_NAME, Map.of()),
|
||||||
|
WorkflowsTestDataSource.newStep(5, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 4)), // "c"
|
||||||
|
WorkflowsTestDataSource.newStep(6, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("x", 8)) // "d"
|
||||||
|
));
|
||||||
|
WorkflowsTestDataSource.insertLinks(workflow, List.of(
|
||||||
|
WorkflowsTestDataSource.newLink(1, 2, "a"), // += 1
|
||||||
|
WorkflowsTestDataSource.newLink(1, 3, "b"), // += 2
|
||||||
|
WorkflowsTestDataSource.newLink(2, 4),
|
||||||
|
WorkflowsTestDataSource.newLink(3, 4),
|
||||||
|
WorkflowsTestDataSource.newLink(4, 5, "c"), // += 4
|
||||||
|
WorkflowsTestDataSource.newLink(4, 6, "d"), // += 8
|
||||||
|
WorkflowsTestDataSource.newLink(5, null),
|
||||||
|
WorkflowsTestDataSource.newLink(6, null)
|
||||||
|
));
|
||||||
|
|
||||||
|
UnsafeFunction<String, Integer, ?> run = (String name) ->
|
||||||
|
{
|
||||||
|
WorkflowOutput output = executeWorkflow(workflow.getId(), Map.of("seedValue", 0, "name", name));
|
||||||
|
return ValueUtils.getValueAsInteger(output.getContext().getValues().get("sum"));
|
||||||
|
};
|
||||||
|
|
||||||
|
assertEquals(1, run.apply("allen"));
|
||||||
|
assertEquals(3, run.apply("barry"));
|
||||||
|
assertEquals(5, run.apply("carl"));
|
||||||
|
assertEquals(8, run.apply("donny"));
|
||||||
|
assertEquals(4, run.apply("connie"));
|
||||||
|
assertEquals(12, run.apply("doc"));
|
||||||
|
assertEquals(15, run.apply("abcd"));
|
||||||
|
assertEquals(0, run.apply("lou"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
|
|||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qbits.workflows.execution.nodes;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import com.kingsrook.qbits.workflows.BaseTest;
|
||||||
|
import com.kingsrook.qbits.workflows.TestWorkflowDefinitions;
|
||||||
|
import com.kingsrook.qbits.workflows.WorkflowsTestDataSource;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowLink;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowStep;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Unit test for StepNodeGraphBuilder
|
||||||
|
*******************************************************************************/
|
||||||
|
class StepNodeGraphBuilderTest extends BaseTest
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@BeforeEach
|
||||||
|
void beforeEach() throws QException
|
||||||
|
{
|
||||||
|
TestWorkflowDefinitions.registerTestWorkflowTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testSimpleFlat()
|
||||||
|
{
|
||||||
|
List<WorkflowStep> steps = new ArrayList<>();
|
||||||
|
WorkflowStep step1 = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, null);
|
||||||
|
WorkflowStep step2 = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, null);
|
||||||
|
WorkflowStep step3 = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, null);
|
||||||
|
|
||||||
|
List<WorkflowLink> links = new ArrayList<>();
|
||||||
|
links.add(WorkflowsTestDataSource.newLink(step1.getStepNo(), step2.getStepNo()));
|
||||||
|
links.add(WorkflowsTestDataSource.newLink(step2.getStepNo(), step3.getStepNo()));
|
||||||
|
|
||||||
|
NodeSequence nodeSequence = new StepNodeGraphBuilder().buildNodeSequence(step1.getStepNo(), steps, links);
|
||||||
|
|
||||||
|
assertSame(step1, nodeSequence.get(0).step());
|
||||||
|
assertSame(step2, nodeSequence.get(1).step());
|
||||||
|
assertSame(step3, nodeSequence.get(2).step());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* we'll evolve this graph through these 4 versions:
|
||||||
|
*
|
||||||
|
* . add1 > add1 > add1 . > (wrap it all in a container)
|
||||||
|
* . | > | > |
|
||||||
|
* .branch > branch > branch
|
||||||
|
* ./ > / \ > / \
|
||||||
|
* add2 > add2 add3 > add2 add3
|
||||||
|
* . \ /
|
||||||
|
* . add4
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testSimpleBranch()
|
||||||
|
{
|
||||||
|
List<WorkflowStep> steps = new ArrayList<>();
|
||||||
|
List<WorkflowLink> links = new ArrayList<>();
|
||||||
|
|
||||||
|
WorkflowStep add1 = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, null);
|
||||||
|
WorkflowStep branch = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.BOOLEAN_CONDITIONAL, null);
|
||||||
|
WorkflowStep add2 = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, null);
|
||||||
|
|
||||||
|
links.add(WorkflowsTestDataSource.newLink(add1.getStepNo(), branch.getStepNo()));
|
||||||
|
links.add(WorkflowsTestDataSource.newLink(branch.getStepNo(), add2.getStepNo(), "true"));
|
||||||
|
|
||||||
|
{
|
||||||
|
NodeSequence nodeSequence = StepNodeGraphBuilder.buildNodeSequence(add1.getStepNo(), steps, links);
|
||||||
|
assertEquals(2, nodeSequence.size());
|
||||||
|
assertSame(add1, nodeSequence.get(0).step());
|
||||||
|
assertSame(branch, nodeSequence.get(1).step());
|
||||||
|
|
||||||
|
StepNode branchNode = nodeSequence.get(1);
|
||||||
|
assertEquals(1, branchNode.subSequences().size());
|
||||||
|
assertEquals(1, branchNode.subSequences().get("true").size());
|
||||||
|
assertSame(add2, branchNode.subSequences().get("true").get(0).step());
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
// add a step in the false branch //
|
||||||
|
////////////////////////////////////
|
||||||
|
WorkflowStep add3 = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, null);
|
||||||
|
links.add(WorkflowsTestDataSource.newLink(branch.getStepNo(), add3.getStepNo(), "false"));
|
||||||
|
|
||||||
|
{
|
||||||
|
NodeSequence nodeSequence = StepNodeGraphBuilder.buildNodeSequence(add1.getStepNo(), steps, links);
|
||||||
|
assertEquals(2, nodeSequence.size());
|
||||||
|
assertSame(add1, nodeSequence.get(0).step());
|
||||||
|
assertSame(branch, nodeSequence.get(1).step());
|
||||||
|
|
||||||
|
StepNode branchNode = nodeSequence.get(1);
|
||||||
|
assertEquals(2, branchNode.subSequences().size());
|
||||||
|
assertEquals(1, branchNode.subSequences().get("true").size());
|
||||||
|
assertSame(add2, branchNode.subSequences().get("true").get(0).step());
|
||||||
|
assertEquals(1, branchNode.subSequences().get("false").size());
|
||||||
|
assertSame(add3, branchNode.subSequences().get("false").get(0).step());
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////
|
||||||
|
// add a step after the branch now, and re-run //
|
||||||
|
/////////////////////////////////////////////////
|
||||||
|
WorkflowStep add4 = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, null);
|
||||||
|
links.add(WorkflowsTestDataSource.newLink(add2.getStepNo(), add4.getStepNo()));
|
||||||
|
links.add(WorkflowsTestDataSource.newLink(add3.getStepNo(), add4.getStepNo()));
|
||||||
|
|
||||||
|
{
|
||||||
|
NodeSequence nodeSequence = StepNodeGraphBuilder.buildNodeSequence(add1.getStepNo(), steps, links);
|
||||||
|
assertEquals(3, nodeSequence.size());
|
||||||
|
assertSame(add1, nodeSequence.get(0).step());
|
||||||
|
assertSame(branch, nodeSequence.get(1).step());
|
||||||
|
assertSame(add4, nodeSequence.get(2).step());
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////
|
||||||
|
// now put it all in a container //
|
||||||
|
///////////////////////////////////
|
||||||
|
WorkflowStep container = WorkflowsTestDataSource.newStep(steps, TestWorkflowDefinitions.CONTAINER, null);
|
||||||
|
links.add(WorkflowsTestDataSource.newLink(container.getStepNo(), add1.getStepNo(), "push"));
|
||||||
|
{
|
||||||
|
NodeSequence nodeSequence = StepNodeGraphBuilder.buildNodeSequence(container.getStepNo(), steps, links);
|
||||||
|
assertEquals(1, nodeSequence.size());
|
||||||
|
assertSame(container, nodeSequence.get(0).step());
|
||||||
|
|
||||||
|
StepNode containerNode = nodeSequence.get(0);
|
||||||
|
assertEquals(1, containerNode.subSequences().size());
|
||||||
|
NodeSequence containedSequence = containerNode.subSequences().get("push");
|
||||||
|
|
||||||
|
assertEquals(3, containedSequence.size());
|
||||||
|
assertSame(add1, containedSequence.get(0).step());
|
||||||
|
assertSame(branch, containedSequence.get(1).step());
|
||||||
|
assertSame(add4, containedSequence.get(2).step());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qbits.workflows.implementations.recordworkflows;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import com.kingsrook.qbits.workflows.BaseTest;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendFieldMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||||
|
import com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster.FormAdjusterInput;
|
||||||
|
import com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster.FormAdjusterOutput;
|
||||||
|
import com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster.RunFormAdjusterProcess;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Unit test for UpdateInputRecordFieldMetaDataAdjuster
|
||||||
|
*******************************************************************************/
|
||||||
|
class UpdateInputRecordFieldMetaDataAdjusterTest extends BaseTest
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testExecute() throws QException
|
||||||
|
{
|
||||||
|
Map<String, Serializable> allValues = Map.of(
|
||||||
|
"workflowValuesJSON", JsonUtils.toJson(Map.of("tableName", TABLE_NAME_PERSON)),
|
||||||
|
"workflowRevisionValuesJSON", JsonUtils.toJson(Map.of()),
|
||||||
|
"value", 1
|
||||||
|
);
|
||||||
|
|
||||||
|
///////////////////////
|
||||||
|
// run for on-change //
|
||||||
|
///////////////////////
|
||||||
|
FormAdjusterOutput onChangeOutput = new UpdateInputRecordFieldMetaDataAdjuster().execute(new FormAdjusterInput()
|
||||||
|
.withFieldName("fieldName")
|
||||||
|
.withNewValue(TABLE_NAME_PERSON + ".firstName")
|
||||||
|
.withAllValues(allValues)
|
||||||
|
.withEvent(RunFormAdjusterProcess.EVENT_ON_CHANGE));
|
||||||
|
assertEquals(Set.of("value"), onChangeOutput.getFieldsToClear());
|
||||||
|
|
||||||
|
assertEquals(1, onChangeOutput.getUpdatedFieldMetaData().size());
|
||||||
|
assertThat(onChangeOutput.getUpdatedFieldDisplayValues()).isNullOrEmpty();
|
||||||
|
QFrontendFieldMetaData updatedValueField = onChangeOutput.getUpdatedFieldMetaData().get("value");
|
||||||
|
assertEquals("Value (First Name)", updatedValueField.getLabel());
|
||||||
|
|
||||||
|
/////////////////////
|
||||||
|
// run for on-load //
|
||||||
|
/////////////////////
|
||||||
|
new InsertAction().execute(new InsertInput(TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("id", 1).withValue("name", "Square")));
|
||||||
|
|
||||||
|
FormAdjusterOutput onLoadOutput = new UpdateInputRecordFieldMetaDataAdjuster().execute(new FormAdjusterInput()
|
||||||
|
.withFieldName("fieldName")
|
||||||
|
.withNewValue(TABLE_NAME_PERSON + ".favoriteShapeId")
|
||||||
|
.withAllValues(allValues)
|
||||||
|
.withEvent(RunFormAdjusterProcess.EVENT_ON_LOAD));
|
||||||
|
assertThat(onLoadOutput.getFieldsToClear()).isNullOrEmpty();
|
||||||
|
assertEquals(Map.of("value", "Square"), onLoadOutput.getUpdatedFieldDisplayValues());
|
||||||
|
updatedValueField = onLoadOutput.getUpdatedFieldMetaData().get("value");
|
||||||
|
assertEquals("Value (Favorite Shape)", updatedValueField.getLabel());
|
||||||
|
assertEquals(TABLE_NAME_SHAPE, updatedValueField.getPossibleValueSourceName());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testAdjustPossibleValueSourceFilterCriteriaInputs()
|
||||||
|
{
|
||||||
|
QFieldMetaData field = new QFieldMetaData();
|
||||||
|
UpdateInputRecordFieldMetaDataAdjuster.adjustPossibleValueSourceFilterCriteriaInputs(field.getPossibleValueSourceFilter());
|
||||||
|
assertNull(field.getPossibleValueSourceFilter());
|
||||||
|
|
||||||
|
field.setPossibleValueSourceFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 1)));
|
||||||
|
UpdateInputRecordFieldMetaDataAdjuster.adjustPossibleValueSourceFilterCriteriaInputs(field.getPossibleValueSourceFilter());
|
||||||
|
assertEquals(1, field.getPossibleValueSourceFilter().getCriteria().get(0).getValues().get(0));
|
||||||
|
|
||||||
|
field.setPossibleValueSourceFilter(new QQueryFilter(new QFilterCriteria("someId", QCriteriaOperator.EQUALS, "${input.someId}")));
|
||||||
|
UpdateInputRecordFieldMetaDataAdjuster.adjustPossibleValueSourceFilterCriteriaInputs(field.getPossibleValueSourceFilter());
|
||||||
|
assertEquals("${input.someId}??${input.workflow.someId}", field.getPossibleValueSourceFilter().getCriteria().get(0).getValues().get(0));
|
||||||
|
|
||||||
|
field.setPossibleValueSourceFilter(new QQueryFilter().withSubFilter(new QQueryFilter(new QFilterCriteria("someId", QCriteriaOperator.EQUALS, "${input.someId}"))));
|
||||||
|
UpdateInputRecordFieldMetaDataAdjuster.adjustPossibleValueSourceFilterCriteriaInputs(field.getPossibleValueSourceFilter());
|
||||||
|
assertEquals("${input.someId}??${input.workflow.someId}", field.getPossibleValueSourceFilter().getSubFilters().get(0).getCriteria().get(0).getValues().get(0));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qbits.workflows.implementations.recordworkflows;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import com.kingsrook.qbits.workflows.BaseTest;
|
||||||
|
import com.kingsrook.qbits.workflows.model.WorkflowRevision;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Unit test for UpdateInputRecordFieldStep
|
||||||
|
*******************************************************************************/
|
||||||
|
class UpdateInputRecordFieldStepTest extends BaseTest
|
||||||
|
{
|
||||||
|
private static WorkflowRevision workflowRevisionV1 = new WorkflowRevision().withApiName(API_NAME).withApiVersion(V1);
|
||||||
|
private static WorkflowRevision workflowRevisionV2 = new WorkflowRevision().withApiName(API_NAME).withApiVersion(V2);
|
||||||
|
private static WorkflowRevision workflowRevisionV3 = new WorkflowRevision().withApiName(API_NAME).withApiVersion(V3);
|
||||||
|
private static WorkflowRevision workflowRevisionWithoutApi = new WorkflowRevision();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testGetApiField() throws QException
|
||||||
|
{
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".firstName", TABLE_NAME_PERSON, workflowRevisionV1))
|
||||||
|
.isPresent().get().hasFieldOrPropertyWithValue("name", "firstName");
|
||||||
|
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getApiField("firstName", TABLE_NAME_PERSON, workflowRevisionV1))
|
||||||
|
.isPresent().get().hasFieldOrPropertyWithValue("name", "firstName");
|
||||||
|
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getApiField("noSuchField", TABLE_NAME_PERSON, workflowRevisionV1))
|
||||||
|
.isEmpty();
|
||||||
|
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".noSuchField", TABLE_NAME_PERSON, workflowRevisionV1))
|
||||||
|
.isEmpty();
|
||||||
|
|
||||||
|
/////////////////////////////////////
|
||||||
|
// this field is excluded from api //
|
||||||
|
/////////////////////////////////////
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".debt", TABLE_NAME_PERSON, workflowRevisionV1))
|
||||||
|
.isEmpty();
|
||||||
|
|
||||||
|
///////////////////////////////
|
||||||
|
// this field is added in V3 //
|
||||||
|
///////////////////////////////
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".salary", TABLE_NAME_PERSON, workflowRevisionV1))
|
||||||
|
.isEmpty();
|
||||||
|
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".salary", TABLE_NAME_PERSON, workflowRevisionV3))
|
||||||
|
.isPresent().get().hasFieldOrPropertyWithValue("name", "salary");
|
||||||
|
|
||||||
|
//////////////////////////////////////////
|
||||||
|
// confirm shoeCount/noOfShoes renaming //
|
||||||
|
//////////////////////////////////////////
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".shoeCount", TABLE_NAME_PERSON, workflowRevisionV1))
|
||||||
|
.isPresent().get().hasFieldOrPropertyWithValue("name", "shoeCount");
|
||||||
|
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".shoeCount", TABLE_NAME_PERSON, workflowRevisionV2))
|
||||||
|
.isEmpty();
|
||||||
|
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".noOfShoes", TABLE_NAME_PERSON, workflowRevisionV1))
|
||||||
|
.isEmpty();
|
||||||
|
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getApiField(TABLE_NAME_PERSON + ".noOfShoes", TABLE_NAME_PERSON, workflowRevisionV2))
|
||||||
|
.isPresent().get().hasFieldOrPropertyWithValue("name", "noOfShoes");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testGetStepSummary() throws QException
|
||||||
|
{
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getStepSummary(Map.of("fieldName", TABLE_NAME_PERSON + ".firstName", "value", "John"), TABLE_NAME_PERSON, false))
|
||||||
|
.isEqualTo("First Name will be set to 'John'");
|
||||||
|
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getStepSummary(Map.of("fieldName", TABLE_NAME_PERSON + ".firstName", "value", "John"), TABLE_NAME_PERSON, true))
|
||||||
|
.isEqualTo("First Name was set to 'John'");
|
||||||
|
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getStepSummary(Map.of("fieldName", TABLE_NAME_PERSON + ".firstName", "value", ""), TABLE_NAME_PERSON, false))
|
||||||
|
.isEqualTo("First Name will be cleared out");
|
||||||
|
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getStepSummary(Map.of("fieldName", TABLE_NAME_PERSON + ".firstName"), TABLE_NAME_PERSON, true))
|
||||||
|
.isEqualTo("First Name was cleared out");
|
||||||
|
|
||||||
|
new InsertAction().execute(new InsertInput(TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("id", 1).withValue("name", "Square")));
|
||||||
|
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getStepSummary(Map.of("fieldName", TABLE_NAME_PERSON + ".favoriteShapeId", "value", "1"), TABLE_NAME_PERSON, false))
|
||||||
|
.isEqualTo("Favorite Shape will be set to 'Square'");
|
||||||
|
|
||||||
|
assertThat(UpdateInputRecordFieldStep.getStepSummary(Map.of("fieldName", TABLE_NAME_PERSON + ".favoriteShapeId", "value", "1"), TABLE_NAME_PERSON, true))
|
||||||
|
.isEqualTo("Favorite Shape was set to 'Square'");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testValidate() throws QException
|
||||||
|
{
|
||||||
|
assertThat(runValidate(Map.of("fieldName", TABLE_NAME_PERSON + ".firstName"), workflowRevisionV1)).isEmpty();
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// debt field - found if not using api, else not found as it is excluded from api //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
assertThat(runValidate(Map.of("fieldName", TABLE_NAME_PERSON + ".debt"), workflowRevisionWithoutApi)).isEmpty();
|
||||||
|
assertThat(runValidate(Map.of("fieldName", TABLE_NAME_PERSON + ".debt"), workflowRevisionV1)).hasSize(1)
|
||||||
|
.element(0).asString()
|
||||||
|
.contains("Could not find field 'person.debt' in table '" + TABLE_NAME_PERSON + "' for API '" + API_NAME + "' version 'v1'");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
private List<String> runValidate(Map<String, Serializable> inputValues, WorkflowRevision workflowRevision) throws QException
|
||||||
|
{
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
UpdateInputRecordFieldStep.validate(TABLE_NAME_PERSON, inputValues, workflowRevision == null ? null : workflowRevision.toQRecord(), errors);
|
||||||
|
return (errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qbits.workflows.processes;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import com.kingsrook.qbits.workflows.BaseTest;
|
||||||
|
import com.kingsrook.qbits.workflows.TestWorkflowDefinitions;
|
||||||
|
import com.kingsrook.qbits.workflows.WorkflowsTestDataSource;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Unit test for StoreNewWorkflowRevisionProcess
|
||||||
|
*******************************************************************************/
|
||||||
|
class StoreNewWorkflowRevisionProcessTest extends BaseTest
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void test() throws QException
|
||||||
|
{
|
||||||
|
TestWorkflowDefinitions.registerTestWorkflowTypes();
|
||||||
|
Integer workflowId = WorkflowsTestDataSource.insertTestWorkflow();
|
||||||
|
|
||||||
|
RunBackendStepInput input = new RunBackendStepInput();
|
||||||
|
|
||||||
|
RunBackendStepOutput output = new RunBackendStepOutput();
|
||||||
|
assertThatThrownBy(() -> new StoreNewWorkflowRevisionProcess().run(input, output))
|
||||||
|
.hasMessageContaining("Workflow id is required");
|
||||||
|
|
||||||
|
input.addValue("workflowId", -999);
|
||||||
|
assertThatThrownBy(() -> new StoreNewWorkflowRevisionProcess().run(input, output))
|
||||||
|
.hasMessageContaining("Workflow not found by id: -999");
|
||||||
|
|
||||||
|
input.addValue("workflowId", workflowId);
|
||||||
|
assertThatThrownBy(() -> new StoreNewWorkflowRevisionProcess().run(input, output))
|
||||||
|
.hasMessageContaining("steps input was not provided");
|
||||||
|
|
||||||
|
input.addValue("steps", "1, 2, 3");
|
||||||
|
assertThatThrownBy(() -> new StoreNewWorkflowRevisionProcess().run(input, output))
|
||||||
|
.hasMessageContaining("Error parsing workflow steps");
|
||||||
|
|
||||||
|
input.addValue("steps", JsonUtils.toJson(List.of(
|
||||||
|
WorkflowsTestDataSource.newStep(1, "myStepType", Map.of("foo", 1)),
|
||||||
|
WorkflowsTestDataSource.newStep(2, "yourStepType", Map.of("bar", true, "baz", false))
|
||||||
|
)));
|
||||||
|
assertThatThrownBy(() -> new StoreNewWorkflowRevisionProcess().run(input, output))
|
||||||
|
.hasMessageContaining("links input was not provided");
|
||||||
|
|
||||||
|
input.addValue("links", "not json");
|
||||||
|
assertThatThrownBy(() -> new StoreNewWorkflowRevisionProcess().run(input, output))
|
||||||
|
.hasMessageContaining("Error parsing workflow links");
|
||||||
|
|
||||||
|
input.addValue("links", JsonUtils.toJson(List.of(WorkflowsTestDataSource.newLink(1, 2))));
|
||||||
|
assertThatThrownBy(() -> new StoreNewWorkflowRevisionProcess().run(input, output))
|
||||||
|
.hasMessageContaining("2 validation errors occurred before the workflow could be saved:")
|
||||||
|
.hasMessageContaining("Error processing Step 1: Unknown workflow step type myStepType")
|
||||||
|
.hasMessageContaining("Error processing Step 2: Unknown workflow step type yourStepType");
|
||||||
|
|
||||||
|
input.addValue("steps", JsonUtils.toJson(List.of(
|
||||||
|
WorkflowsTestDataSource.newStep(1, TestWorkflowDefinitions.ADD_X_TO_SUM_ACTION, Map.of("foo", 1)),
|
||||||
|
WorkflowsTestDataSource.newStep(2, TestWorkflowDefinitions.BOOLEAN_CONDITIONAL, Map.of("bar", true, "baz", false))
|
||||||
|
)));
|
||||||
|
new StoreNewWorkflowRevisionProcess().run(input, output);
|
||||||
|
assertNotNull(output.getValue("workflowRevisionId"));
|
||||||
|
assertEquals(2, output.getValue("versionNo"));
|
||||||
|
|
||||||
|
new StoreNewWorkflowRevisionProcess().run(input, output);
|
||||||
|
assertEquals(3, output.getValue("versionNo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in new issue
Block a user