Compare commits

...
Author SHA1 Message Date
Darin Kelkhoff 910d5c9858 fix(updateRecordFieldStep): Fix cases where possible values weren't being translated in step summary/log (when "tableName." prefix was present).
Also factor some static util method out, for use in application-defined steps of a similar nature.
2025-09-23 11:13:18 -05:00
Darin Kelkhoff 6ef1727eb8 feat(updateRecordFieldStep): Adjust possible value filters that use "${input.myFieldName}" to consider getting that value (via coalescing) from "??${input.workflow.myFieldName}"
Add initial tests on this class
2025-09-23 11:11:53 -05:00
Darin Kelkhoff fce13986b4 feat(storeRevision): more error checking and update format of exception message if there are errors
- put newlines between sub-errors (UI will display as multiple lines)
- put Step # in front of all per-step error messages
- Check for null workflow step type specifically
- Check for missing or invalid JSON for steps and links
- avoid NPE if null user in session
- initial test on this process (at least its validation logic)
2025-09-23 10:52:05 -05:00
james.maes c8f3bb4312 chore: bump snapshot version after release 2025-09-18 14:47:43 -05:00
james.maes 9e302794f1 chore: bump qfmd version to RC1 to allow for distibution 2025-09-18 14:41:44 -05:00
james.maes 05cc348966 chore(deps): Updated parent pom and qqq-orb to latest published versions 2025-09-18 12:18:14 -05:00
james.maes 983b4ffd59 chore: bump snapshot version after release 2025-09-18 12:10:19 -05:00
james.maes 64a202aa41 fix(pom): Updated parent pom version to 1.4.0 (parent pom fix) 2025-09-18 10:03:12 -05:00
james.maes 0fe3bb9735 Merge branch 'feature/circleci-orb-upgrade' into develop 2025-09-18 09:45:46 -05:00
james.maes 1368c052dd chore(circleci): testing new orb version that will address versions 2025-09-18 09:02:04 -05:00
james.maes 6d669469b0 fix: fixed version not using revision in packaging - bump to 1.3.1 2025-09-17 09:40:14 -05:00
james.maes b45be4221d fix: fixed version not using revision in packaging - bump to 1.2.1 2025-09-17 09:35:17 -05:00
james.maes 85d175edd6 fix: updated to parent pom 1.2.0 - supporting snapshot jars from maven central 2025-09-17 09:31:53 -05:00
james.maes a75ec6f263 Empty commit and push - testing circleci qqq-orb 2025-09-17 09:16:04 -05:00
james.maes e35a1bffa0 chore: Updated to meet requirements for maven central and qqq-orb 2025-09-17 08:37:21 -05:00
15 changed files with 811 additions and 146 deletions

No files matched your search

+47 -50
View File
@@ -1,60 +1,57 @@
version: 2.1
executors:
java-executor:
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" }}
orbs:
qqq-orb: kingsrook/qqq-orb@0.3.7
workflows:
version: 2
build_and_deploy:
test_only:
jobs:
- verify:
context: qqq-maven-registry-credentials
- qqq-orb/mvn_test_only:
context: [ qqq-maven-registry-credentials ]
filters:
branches:
ignore:
- main
- feature/initial-dev
- deploy:
context: qqq-maven-registry-credentials
ignore: /(develop|main|release/.*|hotfix/.*|integration.*)/
tags:
only: []
publish_snapshot:
jobs:
- qqq-orb/mvn_publish:
context: [ qqq-maven-registry-credentials ]
branch_type: snapshot
filters:
branches:
only:
- main
- feature/initial-dev
- develop
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\/.*/
-9
View File
@@ -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>
+58
View File
@@ -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!** 🚀
+4
View File
@@ -0,0 +1,4 @@
# Code of Conduct
See Project level [Code of Conduct](https://github.com/Kingsrook/qqq/blob/develop/CODE_OF_CONDUCT.md)
+3
View File
@@ -0,0 +1,3 @@
# Code Style Guide
* See Project [Code Style Guide](https://github.com/Kingsrook/qqq/blob/develop/CODE_STYLE.md)
+4
View File
@@ -0,0 +1,4 @@
# Contributing to QBit User Role Permissions
* See Project [Contributing](https://github.com/Kingsrook/qqq/blob/develop/CONTRIBUTING.md)
+77 -1
View File
@@ -1,4 +1,8 @@
## QBit: workflows
# QBit: Workflows
[![Version](https://img.shields.io/badge/version-TODO-blue.svg)](https://github.com/Kingsrook/qbit-workflows)
[![License](https://img.shields.io/badge/license-GNU%20Affero%20GPL%20v3-green.svg)](https://www.gnu.org/licenses/agpl-3.0.en.html)
[![Java](https://img.shields.io/badge/java-17+-blue.svg)](https://adoptium.net/)
### Overview
*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
## 🏗️ 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
#### Pom dependency
@@ -31,3 +57,53 @@ TODO
### Dependencies
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)
+3
View File
@@ -0,0 +1,3 @@
# Security Policy
* See Project [Security Policy](https://github.com/Kingsrook/qqq/blob/develop/SECURITY.md)
+43 -47
View File
@@ -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">
<modelVersion>4.0.0</modelVersion>
<properties>
<revision>0.4.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>
<artifactId>qbit-workflows</artifactId>
<version>0.1.13-SNAPSHOT</version>
<version>${revision}</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.release>17</maven.compiler.release>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<name>QBit Workflows</name>
<description>Workflow management and process automation module for the QBit framework</description>
<url>https://github.com/Kingsrook/qbit-workflows</url>
<licenses>
<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>
<dependency>
@@ -71,7 +87,7 @@
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-frontend-material-dashboard</artifactId>
<version>0.27.0-20250915.210441-5</version>
<version>0.27.0-RC.1</version>
</dependency>
<dependency>
@@ -87,24 +103,4 @@
</dependency>
</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>
@@ -25,6 +25,7 @@ package com.kingsrook.qbits.workflows.implementations.recordworkflows;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -35,6 +36,8 @@ import com.kingsrook.qqq.backend.core.actions.values.SearchPossibleValueSourceAc
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.SearchPossibleValueSourceOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference;
@@ -143,6 +146,8 @@ public class UpdateInputRecordFieldMetaDataAdjuster implements FormAdjusterInter
}
}
adjustPossibleValueSourceFilterCriteriaInputs(updatedField.getPossibleValueSourceFilter());
///////////////////////////////////////////////////////////////////////////////////
// build the output object, starting with the new meta-data of the 'value' field //
///////////////////////////////////////////////////////////////////////////////////
@@ -186,6 +191,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);
}
}
/***************************************************************************
**
***************************************************************************/
@@ -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
{
@@ -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
{
@@ -121,6 +132,31 @@ public class UpdateInputRecordFieldStep extends WorkflowStepType implements Work
***************************************************************************/
@Override
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 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;
QTableMetaData table = null;
QTableMetaData table = QContext.getQInstance().getTable(tableName);
if(StringUtils.hasContent(fieldName))
{
try
{
if(workflowId != null)
WorkflowRevision workflowRevision = new WorkflowRevision().withApiName(apiName).withApiVersion(apiVersion);
if(WorkflowStepUtils.useApi(workflowRevision))
{
QRecord workflowRecord = GetAction.execute(Workflow.TABLE_NAME, workflowId);
table = QContext.getQInstance().getTable(workflowRecord.getValueString("tableName"));
WorkflowRevision workflowRevision = new WorkflowRevision().withApiName(apiName).withApiVersion(apiVersion);
if(WorkflowStepUtils.useApi(workflowRevision))
Optional<QFieldMetaData> optionalApiField = getApiField(fieldName, tableName, workflowRevision);
if(optionalApiField.isPresent())
{
Optional<QFieldMetaData> optionalApiField = getApiField(fieldName, table.getName(), workflowRevision);
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();
fieldLabel = optionalApiField.get().getLabel();
}
}
else
{
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, fieldName);
fieldName = fieldAndJoinTable.field().getName();
fieldLabel = fieldAndJoinTable.field().getLabel();
}
}
catch(Exception e)
{
@@ -183,15 +212,22 @@ public class UpdateInputRecordFieldStep extends WorkflowStepType implements Work
}
}
String verb = isPastTense ? "was" : "will be";
if(StringUtils.hasContent(fieldLabel))
{
String verb = isPastTense ? "was" : "will be";
if(StringUtils.hasContent(value))
{
String displayValue = value;
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);
QValueFormatter.setDisplayValuesInRecordsIncludingPossibleValueTranslations(table, List.of(record));
displayValue = record.getDisplayValue(fieldName);
@@ -241,7 +277,8 @@ public class UpdateInputRecordFieldStep extends WorkflowStepType implements Work
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);
}
@@ -284,12 +321,34 @@ public class UpdateInputRecordFieldStep extends WorkflowStepType implements Work
@Override
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 apiName = ValueUtils.getValueAsString(workflowRevision.getValueString("apiName"));
String apiVersion = ValueUtils.getValueAsString(workflowRevision.getValueString("apiVersion"));
String tableName = ValueUtils.getValueAsString(workflow.getValueString("tableName"));
Optional<QFieldMetaData> optionalApiField = getApiField(fieldName, tableName, new WorkflowRevision(workflowRevision));
if(optionalApiField.isEmpty())
@@ -140,6 +140,38 @@ public class StoreNewWorkflowRevisionProcess implements BackendStep, MetaDataPro
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 //
////////////////////////////
@@ -166,15 +198,15 @@ public class StoreNewWorkflowRevisionProcess implements BackendStep, MetaDataPro
// insert the workflow revision //
//////////////////////////////////
String commitMessage = runBackendStepInput.getValueString("commitMessage");
String userName = ObjectUtils.tryElse(() -> QContext.getQSession().getUser().getFullName(), "Unknown");
WorkflowRevision workflowRevision = new WorkflowRevision();
workflowRevision.setWorkflowId(workflowId);
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.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())
{
workflowRevision.setStartStepNo(workflowSteps.get(0).getStepNo());
@@ -208,19 +240,24 @@ public class StoreNewWorkflowRevisionProcess implements BackendStep, MetaDataPro
//////////////////////////////////////////////////////////////////////////////////////////////
// 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 ->
{
try
{
step.setWorkflowRevisionId(insertedRevisionId);
WorkflowStepType workflowStepType = WorkflowsRegistry.of(QContext.getQInstance()).getWorkflowStepType(step.getWorkflowStepTypeName());
Map<String, Serializable> inputValues = ValueUtils.getValueAsMap(step.getInputValuesJson());
WorkflowStepType workflowStepType = WorkflowsRegistry.of(QContext.getQInstance()).getWorkflowStepType(step.getWorkflowStepTypeName());
if(workflowStepType == null)
{
throw (new QUserFacingException("Unknown workflow step type " + step.getWorkflowStepTypeName()));
}
////////////////////////////////////
// summary - if it fails, :shrug: //
////////////////////////////////////
Map<String, Serializable> inputValues = ValueUtils.getValueAsMap(step.getInputValuesJson());
try
{
step.setSummary(workflowStepType.getDynamicStepSummary(workflowId, inputValues));
@@ -237,18 +274,27 @@ public class StoreNewWorkflowRevisionProcess implements BackendStep, MetaDataPro
if(validatorCodeReference != null)
{
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)
{
errors.add("Error processing step [" + step.getSummary() + "]: " + e.getMessage());
errors.add("Error processing Step " + step.getStepNo() + ": " + e.getMessage());
}
});
if(!errors.isEmpty())
{
throw (new QUserFacingException("Validation errors in steps: " + StringUtils.joinWithCommasAndAnd(errors)));
String message = errors.size() + " validation error" + StringUtils.plural(errors) + " occurred within the workflow's steps:\n" + StringUtils.join("\n", errors);
throw (new QUserFacingException(message));
}
new InsertAction().execute(new InsertInput(WorkflowStep.TABLE_NAME).withRecordEntities(workflowSteps).withTransaction(transaction));
@@ -256,7 +302,6 @@ public class StoreNewWorkflowRevisionProcess implements BackendStep, MetaDataPro
/////////////////
// store links //
/////////////////
List<WorkflowLink> workflowLinks = JsonUtils.toObject(runBackendStepInput.getValueString("links"), new TypeReference<>() {});
workflowLinks.forEach(link -> link.setWorkflowRevisionId(insertedRevisionId));
new InsertAction().execute(new InsertInput(WorkflowLink.TABLE_NAME).withRecordEntities(workflowLinks).withTransaction(transaction));
@@ -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));
}
}
@@ -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);
}
}
@@ -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 within the workflow's steps:")
.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"));
}
}