Compare commits

..

10 Commits

Author SHA1 Message Date
d9f5b5ac72 added tests 2025-07-19 10:03:26 -05:00
eb48f9cc87 added tests 2025-07-19 09:38:20 -05:00
90e702112a continuing shrugged shoulders 2025-07-17 18:54:10 -05:00
1fa4d72e99 shrug shoulders 2025-07-17 18:41:31 -05:00
2560744a59 updated more tests 2025-07-17 17:55:38 -05:00
809c2ca92e fixed bad package name 2025-07-17 17:45:31 -05:00
7711e6eb35 added test coverage 2025-07-17 17:36:34 -05:00
a3328635aa accidentally changed extract step for bulk edit 2025-07-17 16:25:34 -05:00
eda411c074 fixed unit tests 2025-07-17 14:04:06 -05:00
0a236b8c36 initial checkin of support of bulk load with file 2025-07-17 13:21:49 -05:00
282 changed files with 2695 additions and 15133 deletions

View File

@ -1,26 +0,0 @@
#!/bin/bash
############################################################################
## Script to collect all JaCoCo reports from different modules into a
## single directory for easier artifact storage in CI.
############################################################################
mkdir -p /home/circleci/jacoco-reports/
##############################################################
## Find all module directories that have target/site/jacoco ##
##############################################################
for module_dir in */; do
if [ -d "${module_dir}target/site/jacoco" ]; then
module_name=$(basename "${module_dir%/}")
target_dir="/home/circleci/jacoco-reports/${module_name}"
echo "Collecting JaCoCo reports for module: ${module_name}"
cp -r "${module_dir}target/site/jacoco" "${target_dir}"
echo "Copied JaCoCo reports for ${module_name} to ${target_dir}"
fi
done
echo "All JaCoCo reports collected to /home/circleci/jacoco-reports/"

View File

@ -1,48 +0,0 @@
#!/bin/bash
############################################################################
## Script to concatenate all .txt files in the surefire-reports directory
## into a single artifact that can be stored in CI.
############################################################################
mkdir -p /home/circleci/test-output-artifacts/
###################################################################
## Find all module directories that have target/surefire-reports ##
###################################################################
for module_dir in */; do
if [ -d "${module_dir}target/surefire-reports" ]; then
module_name=$(basename "${module_dir%/}")
output_file="/home/circleci/test-output-artifacts/${module_name}-test-output.txt"
echo "Processing module: ${module_name}"
echo "Output file: ${output_file}"
##################################################################
## Concatenate all .txt files in the surefire-reports directory ##
##################################################################
if [ -n "$(find "${module_dir}target/surefire-reports" -name "*.txt" -type f)" ]; then
echo "=== Test Output for ${module_name} ===" > "${output_file}"
echo "Generated at: $(date)" >> "${output_file}"
echo "==========================================" >> "${output_file}"
echo "" >> "${output_file}"
##############################################
## Sort files to ensure consistent ordering ##
##############################################
find "${module_dir}target/surefire-reports" -name "*.txt" -type f | sort | while read -r txt_file; do
echo "--- File: $(basename "${txt_file}") ---" >> "${output_file}"
cat "${txt_file}" >> "${output_file}"
echo "" >> "${output_file}"
echo "--- End of $(basename "${txt_file}") ---" >> "${output_file}"
echo "" >> "${output_file}"
echo "" >> "${output_file}"
echo "" >> "${output_file}"
done
echo "Concatenated test output for ${module_name} to ${output_file}"
else
echo "No .txt files found in ${module_dir}target/surefire-reports"
fi
fi
done

View File

@ -5,7 +5,35 @@ orbs:
browser-tools: circleci/browser-tools@1.4.7
commands:
mvn_build:
store_jacoco_site:
parameters:
module:
type: string
steps:
- store_artifacts:
path: << parameters.module >>/target/site/jacoco/index.html
when: always
- store_artifacts:
path: << parameters.module >>/target/site/jacoco/jacoco-resources
when: always
install_java17:
steps:
- run:
name: Install Java 17
command: |
sudo apt-get update
sudo apt install -y openjdk-17-jdk
sudo rm /etc/alternatives/java
sudo ln -s /usr/lib/jvm/java-17-openjdk-amd64/bin/java /etc/alternatives/java
- run:
## used by jacoco uncovered class reporting in pom.xml
name: Install html2text
command: |
sudo apt-get update
sudo apt-get install -y html2text
mvn_verify:
steps:
- checkout
- restore_cache:
@ -17,41 +45,30 @@ commands:
name: Write .env
command: |
echo "RDBMS_PASSWORD=$RDBMS_PASSWORD" >> qqq-sample-project/.env
- run:
name: Run Maven Compile
command: |
mvn -s .circleci/mvn-settings.xml -T4 --no-transfer-progress compile
- save_cache:
paths:
- ~/.m2
key: v1-dependencies-{{ checksum "pom.xml" }}
mvn_verify:
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "pom.xml" }}
- run:
name: Run Maven Verify
command: |
mvn -s .circleci/mvn-settings.xml -T4 --no-transfer-progress verify
- run:
name: Collect JaCoCo reports
command: .circleci/collect-jacoco-reports.sh
when: always
- store_artifacts:
path: /home/circleci/jacoco-reports
destination: jacoco-reports
when: always
- run:
name: Concatenate test output files
command: .circleci/concatenate-test-output.sh
when: always
- store_artifacts:
path: /home/circleci/test-output-artifacts
destination: test-output
when: always
mvn -s .circleci/mvn-settings.xml -T4 verify
- store_jacoco_site:
module: qqq-backend-core
- store_jacoco_site:
module: qqq-backend-module-filesystem
- store_jacoco_site:
module: qqq-backend-module-rdbms
- store_jacoco_site:
module: qqq-backend-module-api
- store_jacoco_site:
module: qqq-middleware-api
- store_jacoco_site:
module: qqq-middleware-javalin
- store_jacoco_site:
module: qqq-middleware-picocli
- store_jacoco_site:
module: qqq-middleware-slack
- store_jacoco_site:
module: qqq-language-support-javascript
- store_jacoco_site:
module: qqq-sample-project
- run:
name: Save test results
command: |
@ -60,6 +77,10 @@ commands:
when: always
- store_test_results:
path: ~/test-results
- save_cache:
paths:
- ~/.m2
key: v1-dependencies-{{ checksum "pom.xml" }}
check_middleware_api_versions:
steps:
@ -70,8 +91,8 @@ commands:
- run:
name: Build and Run ValidateApiVersions
command: |
mvn -s .circleci/mvn-settings.xml -T4 --no-transfer-progress install -DskipTests
mvn -s .circleci/mvn-settings.xml -T4 --no-transfer-progress -pl qqq-middleware-javalin package appassembler:assemble -DskipTests
mvn -s .circleci/mvn-settings.xml -T4 install -DskipTests
mvn -s .circleci/mvn-settings.xml -pl qqq-middleware-javalin package appassembler:assemble -DskipTests
qqq-middleware-javalin/target/appassembler/bin/ValidateApiVersions -r $(pwd)
mvn_jar_deploy:
@ -87,7 +108,7 @@ commands:
- run:
name: Run Maven Jar Deploy
command: |
mvn -s .circleci/mvn-settings.xml -T4 --no-transfer-progress flatten:flatten jar:jar deploy:deploy
mvn -s .circleci/mvn-settings.xml -T4 flatten:flatten jar:jar deploy:deploy
- save_cache:
paths:
- ~/.m2
@ -114,25 +135,19 @@ commands:
when: always
jobs:
build:
executor: localstack/default
steps:
- mvn_build
test:
mvn_test:
executor: localstack/default
steps:
## - localstack/startup
- install_java17
- mvn_verify
api_version_check:
executor: localstack/default
steps:
- check_middleware_api_versions
mvn_deploy:
executor: localstack/default
steps:
- mvn_build
## - localstack/startup
- install_java17
- mvn_verify
- check_middleware_api_versions
- mvn_jar_deploy
@ -146,31 +161,13 @@ jobs:
workflows:
test_only:
jobs:
- build:
- mvn_test:
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
filters:
branches:
ignore: /(dev|integration.*)/
tags:
ignore: /(version|snapshot)-.*/
- test:
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
requires:
- build
filters:
branches:
ignore: /(dev|integration.*)/
tags:
ignore: /(version|snapshot)-.*/
- api_version_check:
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
requires:
- build
filters:
branches:
ignore: /(dev|integration.*)/
tags:
ignore: /(version|snapshot)-.*/
deploy:
jobs:

View File

@ -30,20 +30,6 @@ There are a few useful IntelliJ settings files, under `qqq-dev-tools/intellij`:
One will likely also want the [Kingsrook Commentator
Plugin](https://plugins.jetbrains.com/plugin/19325-kingsrook-commentator).
## Test Logging
By default, when ran from the command line, mvn surefire will make each test's
output (e.g., System.out, err, printStackTrace, and all logger calls) go into a
file under target/surefire-reports/${className}.txt.
The system property `-DtestOutputToFile=false` can be given on the command line
to get all of this output on the console.
In the IDE (e.g,. IntelliJ), output goes to the Console.
In CircleCI, output goes to files, and those files are concatenated together and
stored as artifacts.
## License
QQQ - Low-code Application Framework for Engineers. \
Copyright (C) 2020-2024. Kingsrook, LLC \

32
pom.xml
View File

@ -59,7 +59,6 @@
<coverage.instructionCoveredRatioMinimum>0.80</coverage.instructionCoveredRatioMinimum>
<coverage.classCoveredRatioMinimum>0.95</coverage.classCoveredRatioMinimum>
<plugin.shade.phase>none</plugin.shade.phase>
<testOutputToFile>true</testOutputToFile>
</properties>
<profiles>
@ -142,8 +141,6 @@
<configuration>
<!-- Sets the VM argument line used when integration tests are run. -->
<argLine>@{jaCoCoArgLine}</argLine>
<!-- Reduce console output for cleaner JUnit output -->
<redirectTestOutputToFile>${testOutputToFile}</redirectTestOutputToFile>
</configuration>
</plugin>
<plugin>
@ -247,29 +244,30 @@ if [ ! -e target/site/jacoco/index.html ]; then
fi
echo
echo "Jacoco coverage summary report for module: ${project.artifactId}"
echo "Jacoco coverage summary report:"
echo " See also target/site/jacoco/index.html"
echo " and https://www.jacoco.org/jacoco/trunk/doc/counters.html"
echo "------------------------------------------------------------"
# Parse Jacoco HTML coverage summary
if [ -f target/site/jacoco/index.html ]; then
echo -e "Instructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers
sed 's/<\/\w\+>/&\n/g' target/site/jacoco/index.html | grep -A 12 '<tfoot>' | grep '<td' | sed 's/<td class="\w\+\d*">\([^<]*\)<\/td>/\1/' | grep -v Total > /tmp/$$.values
if which xpath > /dev/null 2>&1; then
echo "Element\nInstructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers
xpath -n -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}'
rm /tmp/$$.headers /tmp/$$.values
else
echo "Jacoco coverage summary was not found.";
echo "xpath is not installed. Jacoco coverage summary will not be produced here...";
fi
echo "-----------------------------"
echo
echo "Untested classes, per Jacoco for module: ${project.artifactId}"
echo "-----------------------------"
# Parse Jacoco XML reports directly to find classes with 0% coverage
sed 's/<classs .*\?>/&\n/g;s/<\/class>/&\n/g' target/site/jacoco/jacoco.xml | grep -v 'counter type="CLASS" missed="0"' | sed 's/>.*//;s/.*\///;s/".*//'
echo "-----------------------------"
echo
if which html2text > /dev/null 2>&1; then
echo "Untested classes, per Jacoco:"
echo "-----------------------------"
for i in target/site/jacoco/*/index.html; do
html2text -width 500 -nobs $i | sed '1,/^Total/d;' | grep -v Created | sed 's/ \+/ /g' | sed 's/ [[:digit:]]$//' | grep -v 0$ | cut -d' ' -f1;
done;
echo
else
echo "html2text is not installed. Untested classes from Jacoco will not be printed here...";
fi
]]>
</argument>

View File

@ -121,11 +121,6 @@
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<dependency>
<groupId>org.commonmark</groupId>
<artifactId>commonmark</artifactId>
<version>0.25.0</version>
</dependency>
<!-- adding to help FastExcel -->
<dependency>

View File

@ -291,7 +291,6 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
/////////////////////////////
InsertInput insertInput = new InsertInput();
insertInput.setTableName("audit");
insertInput.setTransaction(input.getTransaction());
insertInput.setRecords(auditRecords);
InsertOutput insertOutput = new InsertAction().execute(insertInput);
@ -319,7 +318,6 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
{
insertInput = new InsertInput();
insertInput.setTableName("auditDetail");
insertInput.setTransaction(input.getTransaction());
insertInput.setRecords(auditDetailRecords);
new InsertAction().execute(insertInput);
}

View File

@ -124,7 +124,6 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
String contextSuffix = getContentSuffix(input);
AuditInput auditInput = new AuditInput();
auditInput.setTransaction(input.getTransaction());
if(auditLevel.equals(AuditLevel.RECORD) || (auditLevel.equals(AuditLevel.FIELD) && !dmlType.supportsFields))
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -1,38 +0,0 @@
/*
* 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.qqq.backend.core.actions.automation;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput;
/*******************************************************************************
** interface to be implemented by one that wishes to execute custom table triggers
*******************************************************************************/
public interface CustomTableTriggerRecordAutomationHandler extends RecordAutomationHandlerInterface
{
/***************************************************************************
**
***************************************************************************/
boolean handlesThisInput(RecordAutomationInput recordAutomationInput) throws QException;
}

View File

@ -22,11 +22,19 @@
package com.kingsrook.qqq.backend.core.actions.automation;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput;
/*******************************************************************************
** Base class for custom-codes to run as an automation action
*******************************************************************************/
@Deprecated(since = "0.26.0 - when RecordAutomationHandlerInterface was introduced")
public abstract class RecordAutomationHandler implements RecordAutomationHandlerInterface
public abstract class RecordAutomationHandler
{
/*******************************************************************************
**
*******************************************************************************/
public abstract void execute(RecordAutomationInput recordAutomationInput) throws QException;
}

View File

@ -1,40 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. 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.qqq.backend.core.actions.automation;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput;
/*******************************************************************************
** Interface for custom-codes to run as an automation action
*******************************************************************************/
public interface RecordAutomationHandlerInterface
{
/*******************************************************************************
**
*******************************************************************************/
void execute(RecordAutomationInput recordAutomationInput) throws QException;
}

View File

@ -1,89 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.qqq.backend.core.actions.automation;
import java.util.LinkedHashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** RecordAutomationHandler implementation that is called by automation runner
** that doesn't know to deal with a TableTrigger record that it received.
**
** e.g., if an app has altered that table (e.g., workflows-qbit).
*******************************************************************************/
public class RunCustomTableTriggerRecordAutomationHandler implements RecordAutomationHandlerInterface
{
private static final QLogger LOG = QLogger.getLogger(RunCustomTableTriggerRecordAutomationHandler.class);
private static Map<String, QCodeReference> handlers = new LinkedHashMap<>();
/***************************************************************************
**
***************************************************************************/
public static void registerHandler(String name, QCodeReference codeReference)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there's already a value mapped for this name, warn about it (unless it's for the same code reference) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(handlers.containsKey(name))
{
if(handlers.get(name).getName().equals(codeReference.getName()))
{
LOG.warn("Registering a CustomTableTriggerRecordAutomationHandler for a name that is already registered", logPair("name", name));
}
}
handlers.put(name, codeReference);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void execute(RecordAutomationInput recordAutomationInput) throws QException
{
for(QCodeReference codeReference : handlers.values())
{
CustomTableTriggerRecordAutomationHandler customHandler = QCodeLoader.getAdHoc(CustomTableTriggerRecordAutomationHandler.class, codeReference);
if(customHandler.handlesThisInput(recordAutomationInput))
{
customHandler.execute(recordAutomationInput);
return;
}
}
throw (new QException("No custom record automation handler was found for " + recordAutomationInput));
}
}

View File

@ -51,7 +51,7 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
**
*******************************************************************************/
public class RunRecordScriptAutomationHandler implements RecordAutomationHandlerInterface
public class RunRecordScriptAutomationHandler extends RecordAutomationHandler
{
private static final QLogger LOG = QLogger.getLogger(RunRecordScriptAutomationHandler.class);

View File

@ -33,9 +33,8 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandlerInterface;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
import com.kingsrook.qqq.backend.core.actions.automation.RunCustomTableTriggerRecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.automation.RunRecordScriptAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
@ -443,33 +442,15 @@ public class PollingAutomationPerTableRunner implements Runnable
}
}
TableAutomationAction tableAutomationAction = new TableAutomationAction()
rs.add(new TableAutomationAction()
.withName("Script:" + tableTrigger.getScriptId())
.withFilter(filter)
.withTriggerEvent(triggerEvent)
.withPriority(tableTrigger.getPriority())
.withIncludeRecordAssociations(true);
///////////////////////////////////////////////////////////////////////////////////////////////////////
// if the table trigger has a script id on it, then we know how to run that here in qqq-backend-core //
///////////////////////////////////////////////////////////////////////////////////////////////////////
if(tableTrigger.getScriptId() != null)
{
rs.add(tableAutomationAction
.withName("Script:" + tableTrigger.getScriptId())
.withValues(MapBuilder.of("scriptId", tableTrigger.getScriptId()))
.withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class)));
}
else
{
////////////////////////////////////////////////////////////////////////////////////////////////
// but - the app may have added an extension to the TableTrigger table (e.g., workflows qbit) //
// so, defer to RunCustomRecordAutomationHandler for unrecognized triggers //
////////////////////////////////////////////////////////////////////////////////////////////////
rs.add(tableAutomationAction
.withName("Custom Trigger:" + tableTrigger.getScriptId())
.withValues(MapBuilder.of("tableTriggerId", tableTrigger.getId()))
.withCodeReference(new QCodeReference(RunCustomTableTriggerRecordAutomationHandler.class)));
}
.withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class))
.withValues(MapBuilder.of("scriptId", tableTrigger.getScriptId()))
.withIncludeRecordAssociations(true)
);
}
catch(Exception e)
{
@ -545,7 +526,7 @@ public class PollingAutomationPerTableRunner implements Runnable
// note - this method - will re-query the objects, so we should have confidence that their data is fresh... //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> matchingQRecords = getRecordsMatchingActionFilter(table, records, action);
LOG.debug("Of the [" + records.size() + "] records that were pending automations, [" + matchingQRecords.size() + "] of them match the filter on the action:" + action);
LOG.debug("Of the [" + records.size() + "] records that were pending automations, [" + matchingQRecords.size() + "] of them match the filter on the action:" + action);
if(CollectionUtils.nullSafeHasContents(matchingQRecords))
{
LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
@ -668,7 +649,7 @@ public class PollingAutomationPerTableRunner implements Runnable
input.setRecordList(records);
input.setAction(action);
RecordAutomationHandlerInterface recordAutomationHandler = QCodeLoader.getAdHoc(RecordAutomationHandlerInterface.class, action.getCodeReference());
RecordAutomationHandler recordAutomationHandler = QCodeLoader.getAdHoc(RecordAutomationHandler.class, action.getCodeReference());
recordAutomationHandler.execute(input);
}
}

View File

@ -1,226 +0,0 @@
/*
* 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.qqq.backend.core.actions.customizers;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceWithProperties;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
/*******************************************************************************
** Implementation of TableCustomizerInterface that runs multiple other customizers
*******************************************************************************/
public class MultiCustomizer implements InitializableViaCodeReference, TableCustomizerInterface
{
private static final String KEY_CODE_REFERENCES = "codeReferences";
private List<TableCustomizerInterface> customizers = new ArrayList<>();
/***************************************************************************
* Factory method that builds a {@link QCodeReferenceWithProperties} that will
* allow this multi-customizer to be assigned to a table, and to track
* in that code ref's properties, the "sub" QCodeReferences to be used.
*
* Added to a table as in:
* <pre>
* table.withCustomizer(TableCustomizers.POST_INSERT_RECORD,
* MultiCustomizer.of(QCodeReference(x), QCodeReference(y)));
* </pre>
*
* @param codeReferences
* one or more {@link QCodeReference objects} to run when this customizer
* runs. note that they will run in the order provided in this list.
***************************************************************************/
public static QCodeReferenceWithProperties of(QCodeReference... codeReferences)
{
ArrayList<QCodeReference> list = new ArrayList<>(Arrays.stream(codeReferences).toList());
return (new QCodeReferenceWithProperties(MultiCustomizer.class, MapBuilder.of(KEY_CODE_REFERENCES, list)));
}
/***************************************************************************
* Add an additional table customizer code reference to an existing
* codeReference, e.g., constructed by the `of` factory method.
*
* @see #of(QCodeReference...)
***************************************************************************/
public static void addTableCustomizer(QCodeReferenceWithProperties existingMultiCustomizerCodeReference, QCodeReference codeReference)
{
ArrayList<QCodeReference> list = (ArrayList<QCodeReference>) existingMultiCustomizerCodeReference.getProperties().computeIfAbsent(KEY_CODE_REFERENCES, key -> new ArrayList<>());
list.add(codeReference);
}
/***************************************************************************
* When this class is instantiated by the QCodeLoader, initialize the
* sub-customizer objects.
***************************************************************************/
@Override
public void initialize(QCodeReference codeReference)
{
if(codeReference instanceof QCodeReferenceWithProperties codeReferenceWithProperties)
{
Serializable codeReferencesPropertyValue = codeReferenceWithProperties.getProperties().get(KEY_CODE_REFERENCES);
if(codeReferencesPropertyValue instanceof List<?> list)
{
for(Object o : list)
{
if(o instanceof QCodeReference reference)
{
TableCustomizerInterface customizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, reference);
customizers.add(customizer);
}
}
}
else
{
LOG.warn("Property KEY_CODE_REFERENCES [" + KEY_CODE_REFERENCES + "] must be a List<QCodeReference>.");
}
}
if(customizers.isEmpty())
{
LOG.info("No TableCustomizers were specified for MultiCustomizer.");
}
}
/***************************************************************************
* run postQuery method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> postQuery(QueryOrGetInputInterface queryInput, List<QRecord> records) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.postQuery(queryInput, records);
}
return records;
}
/***************************************************************************
* run preInsert method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.preInsert(insertInput, records, isPreview);
}
return records;
}
/***************************************************************************
* run postInsert method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> postInsert(InsertInput insertInput, List<QRecord> records) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.postInsert(insertInput, records);
}
return records;
}
/***************************************************************************
* run preUpdate method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.preUpdate(updateInput, records, isPreview, oldRecordList);
}
return records;
}
/***************************************************************************
* run postUpdate method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> postUpdate(UpdateInput updateInput, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.postUpdate(updateInput, records, oldRecordList);
}
return records;
}
/***************************************************************************
* run preDelete method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> preDelete(DeleteInput deleteInput, List<QRecord> records, boolean isPreview) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.preDelete(deleteInput, records, isPreview);
}
return records;
}
/***************************************************************************
* run postDelete method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.postDelete(deleteInput, records);
}
return records;
}
}

View File

@ -1,88 +0,0 @@
/*
* 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.qqq.backend.core.actions.customizers;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.utils.collections.TypeTolerantKeyMap;
/*******************************************************************************
** utility class to help table customizers working with the oldRecordList.
** Usage is just 2 lines:
** outside of loop-over-records:
** - OldRecordHelper oldRecordHelper = new OldRecordHelper(updateInput.getTableName(), oldRecordList);
** then inside the record loop:
** - Optional<QRecord> oldRecord = oldRecordHelper.getOldRecord(record);
*******************************************************************************/
public class OldRecordHelper
{
private String primaryKeyField;
private QFieldType primaryKeyType;
private Optional<List<QRecord>> oldRecordList;
private Map<Serializable, QRecord> oldRecordMap;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public OldRecordHelper(String tableName, Optional<List<QRecord>> oldRecordList)
{
this.primaryKeyField = QContext.getQInstance().getTable(tableName).getPrimaryKeyField();
this.primaryKeyType = QContext.getQInstance().getTable(tableName).getField(primaryKeyField).getType();
this.oldRecordList = oldRecordList;
}
/***************************************************************************
**
***************************************************************************/
public Optional<QRecord> getOldRecord(QRecord record)
{
if(oldRecordMap == null)
{
if(oldRecordList.isPresent())
{
oldRecordMap = new TypeTolerantKeyMap<>(primaryKeyType);
oldRecordList.get().forEach(r -> oldRecordMap.put(r.getValue(primaryKeyField), r));
}
else
{
oldRecordMap = Collections.emptyMap();
}
}
return (Optional.ofNullable(oldRecordMap.get(record.getValue(primaryKeyField))));
}
}

View File

@ -161,7 +161,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
public static String linkTableCreateWithDefaultValues(RenderWidgetInput input, String tableName, Map<String, Serializable> defaultValues) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/create#defaultValues=" + URLEncoder.encode(JsonUtils.toJson(defaultValues), StandardCharsets.UTF_8));
return (tablePath + "/create#defaultValues=" + URLEncoder.encode(JsonUtils.toJson(defaultValues), Charset.defaultCharset()));
}
@ -229,7 +229,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
}
filter = QQueryFilterDeduper.dedupeFilter(filter);
return (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), StandardCharsets.UTF_8));
return (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
}

View File

@ -25,14 +25,11 @@ package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.gson.reflect.TypeToken;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
@ -48,7 +45,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
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.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
@ -69,7 +65,6 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
import org.apache.commons.lang.BooleanUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -181,18 +176,6 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withOmitFieldNames(List<String> omitFieldNames)
{
ArrayList<String> arrayList = CollectionUtils.useOrWrap(omitFieldNames, new TypeToken<>() {});
widgetMetaData.withDefaultValue("omitFieldNames", arrayList);
return (this);
}
}
@ -212,25 +195,14 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
QTableMetaData leftTable = QContext.getQInstance().getTable(join.getLeftTable());
QTableMetaData rightTable = QContext.getQInstance().getTable(join.getRightTable());
Map<String, Serializable> widgetMetaDataDefaultValues = input.getWidgetMetaData().getDefaultValues();
List<String> omitFieldNames = (List<String>) widgetMetaDataDefaultValues.get("omitFieldNames");
if(omitFieldNames == null)
{
omitFieldNames = new ArrayList<>();
}
else
{
omitFieldNames = new MutableList<>(omitFieldNames);
}
Integer maxRows = null;
if(StringUtils.hasContent(input.getQueryParams().get("maxRows")))
{
maxRows = ValueUtils.getValueAsInteger(input.getQueryParams().get("maxRows"));
}
else if(widgetMetaDataDefaultValues.containsKey("maxRows"))
else if(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"))
{
maxRows = ValueUtils.getValueAsInteger(widgetMetaDataDefaultValues.get("maxRows"));
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().get("maxRows"));
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -262,19 +234,8 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
for(JoinOn joinOn : join.getJoinOns())
{
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, List.of(primaryRecord.getValue(joinOn.getLeftField()))));
omitFieldNames.add(joinOn.getRightField());
}
Serializable orderBy = widgetMetaDataDefaultValues.get("orderBy");
if(orderBy instanceof List orderByList && !orderByList.isEmpty() && orderByList.get(0) instanceof QFilterOrderBy)
{
filter.setOrderBys(orderByList);
}
else
{
filter.setOrderBys(join.getOrderBys());
}
filter.setOrderBys(join.getOrderBys());
filter.setLimit(maxRows);
QueryInput queryInput = new QueryInput();
@ -301,10 +262,9 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
}
String tablePath = QContext.getQInstance().getTablePath(rightTable.getName());
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), StandardCharsets.UTF_8));
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, rightTable, tablePath, viewAllLink, totalRows);
widgetData.setOmitFieldNames(omitFieldNames);
if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("canAddChildRecord"))))
{
@ -324,10 +284,11 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords);
if(widgetMetaDataDefaultValues.containsKey("disabledFieldsForNewChildRecords"))
Map<String, Serializable> widgetValues = input.getWidgetMetaData().getDefaultValues();
if(widgetValues.containsKey("disabledFieldsForNewChildRecords"))
{
@SuppressWarnings("unchecked")
Set<String> disabledFieldsForNewChildRecords = (Set<String>) widgetMetaDataDefaultValues.get("disabledFieldsForNewChildRecords");
Set<String> disabledFieldsForNewChildRecords = (Set<String>) widgetValues.get("disabledFieldsForNewChildRecords");
widgetData.setDisabledFieldsForNewChildRecords(disabledFieldsForNewChildRecords);
}
else
@ -347,10 +308,10 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
}
}
if(widgetMetaDataDefaultValues.containsKey("defaultValuesForNewChildRecordsFromParentFields"))
if(widgetValues.containsKey("defaultValuesForNewChildRecordsFromParentFields"))
{
@SuppressWarnings("unchecked")
Map<String, String> defaultValuesForNewChildRecordsFromParentFields = (Map<String, String>) widgetMetaDataDefaultValues.get("defaultValuesForNewChildRecordsFromParentFields");
Map<String, String> defaultValuesForNewChildRecordsFromParentFields = (Map<String, String>) widgetValues.get("defaultValuesForNewChildRecordsFromParentFields");
widgetData.setDefaultValuesForNewChildRecordsFromParentFields(defaultValuesForNewChildRecordsFromParentFields);
}
}

View File

@ -24,7 +24,6 @@ package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
@ -195,7 +194,7 @@ public class RecordListWidgetRenderer extends AbstractWidgetRenderer
}
String tablePath = QContext.getQInstance().getTablePath(tableName);
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), StandardCharsets.UTF_8));
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
ChildRecordListData widgetData = new ChildRecordListData(input.getQueryParams().get("widgetLabel"), queryOutput, table, tablePath, viewAllLink, totalRows);

View File

@ -299,8 +299,6 @@ public class MetaDataAction
metaDataOutput.setHelpContents(Objects.requireNonNullElse(QContext.getQInstance().getHelpContent(), Collections.emptyMap()));
metaDataOutput.setSupplementalInstanceMetaData(QContext.getQInstance().getSupplementalMetaData());
try
{
customizer.postProcess(metaDataOutput);
@ -331,25 +329,23 @@ public class MetaDataAction
if(metaDataActionCustomizerReference != null)
{
actionCustomizer = QCodeLoader.getAdHoc(MetaDataActionCustomizerInterface.class, metaDataActionCustomizerReference);
LOG.debug("Using new meta-data actionCustomizer of type: " + actionCustomizer.getClass().getSimpleName());
}
if(actionCustomizer == null)
{
/////////////////////////////////////////////////////////////////////////////////////
// check if QInstance is still using the now-deprecated getMetaDataFilter approach //
/////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("deprecation")
QCodeReference metaDataFilterReference = QContext.getQInstance().getMetaDataFilter();
if(metaDataFilterReference != null)
{
LOG.warn("QInstance.metaDataFilter is deprecated in favor of metaDataActionCustomizer.");
actionCustomizer = QCodeLoader.getAdHoc(MetaDataActionCustomizerInterface.class, metaDataFilterReference);
LOG.debug("Using new meta-data actionCustomizer (via metaDataFilter reference) of type: " + actionCustomizer.getClass().getSimpleName());
}
}
if(actionCustomizer == null)
{
actionCustomizer = new DefaultNoopMetaDataActionCustomizer();
LOG.debug("Using new default (allow-all) meta-data actionCustomizer");
}
return (actionCustomizer);

View File

@ -57,7 +57,7 @@ public class BulkTableActionProcessPermissionChecker implements CustomPermission
switch(bulkActionName)
{
case "bulkInsert" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.INSERT);
case "bulkEdit" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.EDIT);
case "bulkEdit", "bulkEditWithFile" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.EDIT);
case "bulkDelete" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.DELETE);
default -> LOG.warn("Unexpected bulk action name when checking permissions for process: " + processName);
}

View File

@ -198,10 +198,10 @@ public class RunBackendStepAction
//////////////////////////////////////////////////
// look for record ids in the input data values //
//////////////////////////////////////////////////
String recordIds = runBackendStepInput.getValueString("recordIds");
String recordIds = (String) runBackendStepInput.getValue("recordIds");
if(recordIds == null)
{
recordIds = runBackendStepInput.getValueString("recordId");
recordIds = (String) runBackendStepInput.getValue("recordId");
}
///////////////////////////////////////////////////////////

View File

@ -246,7 +246,6 @@ public class DeleteAction
{
DMLAuditInput dmlAuditInput = new DMLAuditInput()
.withTableActionInput(deleteInput)
.withTransaction(deleteInput.getTransaction())
.withAuditContext(deleteInput.getAuditContext());
oldRecordList.ifPresent(l -> dmlAuditInput.setRecordList(l));
new DMLAuditAction().execute(dmlAuditInput);
@ -402,7 +401,6 @@ public class DeleteAction
if(CollectionUtils.nullSafeHasContents(associatedKeys))
{
DeleteInput nextLevelDeleteInput = new DeleteInput();
nextLevelDeleteInput.setFlags(deleteInput.getFlags());
nextLevelDeleteInput.setTransaction(deleteInput.getTransaction());
nextLevelDeleteInput.setTableName(association.getAssociatedTableName());
nextLevelDeleteInput.setPrimaryKeys(associatedKeys);

View File

@ -34,6 +34,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
@ -53,7 +54,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
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.joins.JoinOn;
@ -157,7 +157,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
//////////////////////////////////////////////////
// insert any associations in the input records //
//////////////////////////////////////////////////
manageAssociations(table, insertOutput.getRecords(), insertInput);
manageAssociations(table, insertOutput.getRecords(), insertInput.getTransaction());
//////////////////
// do the audit //
@ -170,26 +170,13 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
{
new DMLAuditAction().execute(new DMLAuditInput()
.withTableActionInput(insertInput)
.withTransaction(insertInput.getTransaction())
.withAuditContext(insertInput.getAuditContext())
.withRecordList(insertOutput.getRecords()));
}
////////////////////////////////////////////////////////////////
// finally, run the post-insert customizers, if there are any //
////////////////////////////////////////////////////////////////
runPostInsertCustomizers(insertInput, table, insertOutput);
return insertOutput;
}
/***************************************************************************
**
***************************************************************************/
private static void runPostInsertCustomizers(InsertInput insertInput, QTableMetaData table, InsertOutput insertOutput)
{
//////////////////////////////////////////////////////////////
// finally, run the post-insert customizer, if there is one //
//////////////////////////////////////////////////////////////
Optional<TableCustomizerInterface> postInsertCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_INSERT_RECORD.getRole());
if(postInsertCustomizer.isPresent())
{
@ -206,25 +193,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
}
}
///////////////////////////////////////////////
// run all of the instance-level customizers //
///////////////////////////////////////////////
List<QCodeReference> tableCustomizerCodes = QContext.getQInstance().getTableCustomizers(TableCustomizers.POST_INSERT_RECORD);
for(QCodeReference tableCustomizerCode : tableCustomizerCodes)
{
try
{
TableCustomizerInterface tableCustomizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, tableCustomizerCode);
insertOutput.setRecords(tableCustomizer.postInsert(insertInput, insertOutput.getRecords()));
}
catch(Exception e)
{
for(QRecord record : insertOutput.getRecords())
{
record.addWarning(new QWarningMessage("An error occurred after the insert: " + e.getMessage()));
}
}
}
return insertOutput;
}
@ -339,19 +308,6 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
insertInput.setRecords(preInsertCustomizer.get().preInsert(insertInput, insertInput.getRecords(), isPreview));
}
}
///////////////////////////////////////////////
// run all of the instance-level customizers //
///////////////////////////////////////////////
List<QCodeReference> tableCustomizerCodes = QContext.getQInstance().getTableCustomizers(TableCustomizers.PRE_INSERT_RECORD);
for(QCodeReference tableCustomizerCode : tableCustomizerCodes)
{
TableCustomizerInterface tableCustomizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, tableCustomizerCode);
if(whenToRun.equals(tableCustomizer.whenToRunPreInsert(insertInput, isPreview)))
{
insertInput.setRecords(tableCustomizer.preInsert(insertInput, insertInput.getRecords(), isPreview));
}
}
}
@ -386,7 +342,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
/*******************************************************************************
**
*******************************************************************************/
private void manageAssociations(QTableMetaData table, List<QRecord> insertedRecords, InsertInput insertInput) throws QException
private void manageAssociations(QTableMetaData table, List<QRecord> insertedRecords, QBackendTransaction transaction) throws QException
{
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
@ -419,8 +375,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
if(CollectionUtils.nullSafeHasContents(nextLevelInserts))
{
InsertInput nextLevelInsertInput = new InsertInput();
nextLevelInsertInput.withFlags(insertInput.getFlags());
nextLevelInsertInput.setTransaction(insertInput.getTransaction());
nextLevelInsertInput.setTransaction(transaction);
nextLevelInsertInput.setTableName(association.getAssociatedTableName());
nextLevelInsertInput.setRecords(nextLevelInserts);
InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput);

View File

@ -126,7 +126,6 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
InsertInput insertInput = new InsertInput();
insertInput.setTableName(table.getName());
insertInput.setRecords(insertList);
insertInput.withFlags(input.getFlags());
insertInput.setTransaction(transaction);
insertInput.setOmitDmlAudit(input.getOmitDmlAudit());
InsertOutput insertOutput = new InsertAction().execute(insertInput);
@ -136,7 +135,6 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(table.getName());
updateInput.setRecords(updateList);
updateInput.withFlags(input.getFlags());
updateInput.setTransaction(transaction);
updateInput.setOmitDmlAudit(input.getOmitDmlAudit());
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
@ -153,7 +151,6 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(table.getName());
deleteInput.setQueryFilter(deleteFilter);
deleteInput.withFlags(input.getFlags());
deleteInput.setTransaction(transaction);
deleteInput.setOmitDmlAudit(input.getOmitDmlAudit());
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);

View File

@ -57,7 +57,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
@ -190,7 +189,6 @@ public class UpdateAction
else
{
DMLAuditInput dmlAuditInput = new DMLAuditInput()
.withTransaction(updateInput.getTransaction())
.withTableActionInput(updateInput)
.withRecordList(updateOutput.getRecords())
.withAuditContext(updateInput.getAuditContext());
@ -201,18 +199,6 @@ public class UpdateAction
//////////////////////////////////////////////////////////////
// finally, run the post-update customizer, if there is one //
//////////////////////////////////////////////////////////////
runPostUpdateCustomizers(updateInput, table, updateOutput, oldRecordList);
return updateOutput;
}
/***************************************************************************
**
***************************************************************************/
private static void runPostUpdateCustomizers(UpdateInput updateInput, QTableMetaData table, UpdateOutput updateOutput, Optional<List<QRecord>> oldRecordList)
{
Optional<TableCustomizerInterface> postUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_UPDATE_RECORD.getRole());
if(postUpdateCustomizer.isPresent())
{
@ -229,49 +215,7 @@ public class UpdateAction
}
}
///////////////////////////////////////////////
// run all of the instance-level customizers //
///////////////////////////////////////////////
List<QCodeReference> tableCustomizerCodes = QContext.getQInstance().getTableCustomizers(TableCustomizers.POST_UPDATE_RECORD);
for(QCodeReference tableCustomizerCode : tableCustomizerCodes)
{
try
{
TableCustomizerInterface tableCustomizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, tableCustomizerCode);
updateOutput.setRecords(tableCustomizer.postUpdate(updateInput, updateOutput.getRecords(), oldRecordList));
}
catch(Exception e)
{
for(QRecord record : updateOutput.getRecords())
{
record.addWarning(new QWarningMessage("An error occurred after the update: " + e.getMessage()));
}
}
}
}
/***************************************************************************
**
***************************************************************************/
private static void runPreUpdateCustomizers(UpdateInput updateInput, QTableMetaData table, Optional<List<QRecord>> oldRecordList, boolean isPreview) throws QException
{
Optional<TableCustomizerInterface> preUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
if(preUpdateCustomizer.isPresent())
{
updateInput.setRecords(preUpdateCustomizer.get().preUpdate(updateInput, updateInput.getRecords(), isPreview, oldRecordList));
}
///////////////////////////////////////////////
// run all of the instance-level customizers //
///////////////////////////////////////////////
List<QCodeReference> tableCustomizerCodes = QContext.getQInstance().getTableCustomizers(TableCustomizers.PRE_UPDATE_RECORD);
for(QCodeReference tableCustomizerCode : tableCustomizerCodes)
{
TableCustomizerInterface tableCustomizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, tableCustomizerCode);
updateInput.setRecords(tableCustomizer.preUpdate(updateInput, updateInput.getRecords(), isPreview, oldRecordList));
}
return updateOutput;
}
@ -334,7 +278,11 @@ public class UpdateAction
///////////////////////////////////////////////////////////////////////////
// after all validations, run the pre-update customizer, if there is one //
///////////////////////////////////////////////////////////////////////////
runPreUpdateCustomizers(updateInput, table, oldRecordList, isPreview);
Optional<TableCustomizerInterface> preUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
if(preUpdateCustomizer.isPresent())
{
updateInput.setRecords(preUpdateCustomizer.get().preUpdate(updateInput, updateInput.getRecords(), isPreview, oldRecordList));
}
}
@ -457,7 +405,7 @@ public class UpdateAction
QFieldType fieldType = table.getField(lock.getFieldName()).getType();
Serializable lockValue = ValueUtils.getValueAsFieldType(fieldType, oldRecord.getValue(lock.getFieldName()));
List<QErrorMessage> errors = ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE, Collections.emptyMap(), QContext.getQSession());
List<QErrorMessage> errors = ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE, Collections.emptyMap());
if(CollectionUtils.nullSafeHasContents(errors))
{
errors.forEach(e -> record.addError(e));
@ -606,7 +554,6 @@ public class UpdateAction
{
LOG.debug("Deleting associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", queryOutput.getRecords().size()));
DeleteInput deleteInput = new DeleteInput();
deleteInput.setFlags(updateInput.getFlags());
deleteInput.setTransaction(updateInput.getTransaction());
deleteInput.setTableName(association.getAssociatedTableName());
deleteInput.setPrimaryKeys(queryOutput.getRecords().stream().map(r -> r.getValue(associatedTable.getPrimaryKeyField())).collect(Collectors.toList()));
@ -619,7 +566,6 @@ public class UpdateAction
LOG.debug("Updating associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", nextLevelUpdates.size()));
UpdateInput nextLevelUpdateInput = new UpdateInput();
nextLevelUpdateInput.setTransaction(updateInput.getTransaction());
nextLevelUpdateInput.setFlags(updateInput.getFlags());
nextLevelUpdateInput.setTableName(association.getAssociatedTableName());
nextLevelUpdateInput.setRecords(nextLevelUpdates);
UpdateOutput nextLevelUpdateOutput = new UpdateAction().execute(nextLevelUpdateInput);
@ -630,7 +576,6 @@ public class UpdateAction
LOG.debug("Inserting associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", nextLevelUpdates.size()));
InsertInput nextLevelInsertInput = new InsertInput();
nextLevelInsertInput.setTransaction(updateInput.getTransaction());
nextLevelInsertInput.setFlags(updateInput.getFlags());
nextLevelInsertInput.setTableName(association.getAssociatedTableName());
nextLevelInsertInput.setRecords(nextLevelInserts);
InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput);

View File

@ -41,7 +41,6 @@ import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
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.QueryOrGetInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
@ -570,7 +569,7 @@ public class QueryActionCacheHelper
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
sourceQueryInput.setFilter(filter);
((QueryOrGetInputInterface) sourceQueryInput).setCommonParamsFrom(cacheQueryInput);
sourceQueryInput.setCommonParamsFrom(cacheQueryInput);
for(List<Serializable> uniqueKeyValue : uniqueKeyValues)
{

View File

@ -50,7 +50,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -103,7 +102,7 @@ public class ValidateRecordSecurityLockHelper
// actually check lock values //
////////////////////////////////
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>(), madeUpPrimaryKeys, transaction, QContext.getQSession());
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>(), madeUpPrimaryKeys, transaction);
/////////////////////////////////
// propagate errors to records //
@ -125,29 +124,6 @@ public class ValidateRecordSecurityLockHelper
/***************************************************************************
** return boolean if given session can read given record
***************************************************************************/
public static boolean allowedToReadRecord(QTableMetaData table, QRecord record, QSession qSession, QBackendTransaction transaction) throws QException
{
MultiRecordSecurityLock locksToCheck = getRecordSecurityLocks(table, Action.SELECT);
if(locksToCheck == null || CollectionUtils.nullSafeIsEmpty(locksToCheck.getLocks()))
{
return (true);
}
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
evaluateRecordLocks(table, List.of(record), Action.SELECT, locksToCheck, errorRecords, new ArrayList<>(), Collections.emptyMap(), transaction, qSession);
if(errorRecords.containsKey(record.getValue(table.getPrimaryKeyField())))
{
return (false);
}
return (true);
}
/*******************************************************************************
** For a list of `records` from a `table`, and a given `action`, evaluate a
** `recordSecurityLock` (which may be a multi-lock) - populating the input map
@ -166,7 +142,7 @@ public class ValidateRecordSecurityLockHelper
** BUT - WRITE locks - in their case, we read the record no matter what, and in
** here we need to verify we have a key that allows us to WRITE the record.
*******************************************************************************/
private static void evaluateRecordLocks(QTableMetaData table, List<QRecord> records, Action action, RecordSecurityLock recordSecurityLock, Map<Serializable, RecordWithErrors> errorRecords, List<Integer> treePosition, Map<Serializable, QRecord> madeUpPrimaryKeys, QBackendTransaction transaction, QSession qSession) throws QException
private static void evaluateRecordLocks(QTableMetaData table, List<QRecord> records, Action action, RecordSecurityLock recordSecurityLock, Map<Serializable, RecordWithErrors> errorRecords, List<Integer> treePosition, Map<Serializable, QRecord> madeUpPrimaryKeys, QBackendTransaction transaction) throws QException
{
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
{
@ -177,7 +153,7 @@ public class ValidateRecordSecurityLockHelper
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
{
treePosition.add(i);
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys, transaction, qSession);
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys, transaction);
treePosition.remove(treePosition.size() - 1);
i++;
}
@ -189,7 +165,7 @@ public class ValidateRecordSecurityLockHelper
// if this lock has an all-access key, and the user has that key, then there can't be any errors here, so return early //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && qSession.hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
{
return;
}
@ -217,7 +193,7 @@ public class ValidateRecordSecurityLockHelper
}
Serializable recordSecurityValue = record.getValue(field.getName());
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys, qSession);
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys);
if(CollectionUtils.nullSafeHasContents(recordErrors))
{
errorRecords.computeIfAbsent(record.getValue(primaryKeyField), (k) -> new RecordWithErrors(record)).addAll(recordErrors, treePosition);
@ -363,7 +339,7 @@ public class ValidateRecordSecurityLockHelper
for(QRecord inputRecord : inputRecords)
{
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys, qSession);
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys);
if(CollectionUtils.nullSafeHasContents(recordErrors))
{
errorRecords.computeIfAbsent(inputRecord.getValue(primaryKeyField), (k) -> new RecordWithErrors(inputRecord)).addAll(recordErrors, treePosition);
@ -470,7 +446,7 @@ public class ValidateRecordSecurityLockHelper
/*******************************************************************************
**
*******************************************************************************/
public static List<QErrorMessage> validateRecordSecurityValue(QTableMetaData table, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action, Map<Serializable, QRecord> madeUpPrimaryKeys, QSession qSession)
public static List<QErrorMessage> validateRecordSecurityValue(QTableMetaData table, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action, Map<Serializable, QRecord> madeUpPrimaryKeys)
{
if(recordSecurityValue == null || (madeUpPrimaryKeys != null && madeUpPrimaryKeys.containsKey(recordSecurityValue)))
{
@ -485,7 +461,7 @@ public class ValidateRecordSecurityLockHelper
}
else
{
if(!qSession.hasSecurityKeyValue(recordSecurityLock.getSecurityKeyType(), recordSecurityValue, fieldType))
if(!QContext.getQSession().hasSecurityKeyValue(recordSecurityLock.getSecurityKeyType(), recordSecurityValue, fieldType))
{
if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()))
{

View File

@ -47,12 +47,12 @@ public abstract class BasicCustomPossibleValueProvider<S, ID extends Serializabl
/***************************************************************************
**
***************************************************************************/
protected abstract S getSourceObject(Serializable id) throws QException;
protected abstract S getSourceObject(Serializable id);
/***************************************************************************
**
***************************************************************************/
protected abstract List<S> getAllSourceObjects() throws QException;
protected abstract List<S> getAllSourceObjects();
@ -60,7 +60,7 @@ public abstract class BasicCustomPossibleValueProvider<S, ID extends Serializabl
**
***************************************************************************/
@Override
public QPossibleValue<ID> getPossibleValue(Serializable idValue) throws QException
public QPossibleValue<ID> getPossibleValue(Serializable idValue)
{
S sourceObject = getSourceObject(idValue);
if(sourceObject == null)

View File

@ -45,7 +45,7 @@ public interface QCustomPossibleValueProvider<T extends Serializable>
/*******************************************************************************
**
*******************************************************************************/
QPossibleValue<T> getPossibleValue(Serializable idValue) throws QException;
QPossibleValue<T> getPossibleValue(Serializable idValue);
/*******************************************************************************
**

View File

@ -310,19 +310,6 @@ public class QValueFormatter
/*******************************************************************************
** For a list of records, set their recordLabels and display values - including
** record label (e.g., from the table meta data).
*******************************************************************************/
public static void setDisplayValuesInRecordsIncludingPossibleValueTranslations(QTableMetaData table, List<QRecord> records)
{
QPossibleValueTranslator possibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), QContext.getQSession());
possibleValueTranslator.translatePossibleValuesInRecords(table, records);
setDisplayValuesInRecords(table, records);
}
/*******************************************************************************
** For a list of records, set their recordLabels and display values - including
** record label (e.g., from the table meta data).

View File

@ -1,46 +0,0 @@
/*
* 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.qqq.backend.core.instances;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
/*******************************************************************************
* interface that can be added to a QSupplementalInstanceMetaData, to receive
* QHelpContent records during instance boot or upon updates in the help content
* table.
*******************************************************************************/
public interface QHelpContentPlugin
{
/***************************************************************************
* accept a single helpContent record, and apply its data to some data in the
* qInstance
*
* @param qInstance the active qInstance, that the content should be applied to
* @param helpContent entity with values from HelpContent table
* @param nameValuePairs parsed string -> string map from the help content key.
***************************************************************************/
void acceptHelpContent(QInstance qInstance, QHelpContent helpContent, Map<String, String> nameValuePairs);
}

View File

@ -31,9 +31,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@ -45,12 +43,11 @@ import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
import com.kingsrook.qqq.backend.core.actions.permissions.BulkTableActionProcessPermissionChecker;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.instances.enrichment.plugins.QInstanceEnricherPluginInterface;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.bulk.TableKeyFieldsPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
@ -59,7 +56,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueB
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
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.QSupplementalFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
@ -215,37 +211,6 @@ public class QInstanceEnricher
***************************************************************************/
private void enrichInstance()
{
////////////////////////////////////////////////////////////////////////////////////
// enriching some objects may cause additional ones to be added to the qInstance! //
// this caused concurrent modification exceptions, when we just iterated. //
// we could make a copy of the map and just process that, but then we wouldn't //
// enrich any new objects that do get added, so, use this technique instead. //
////////////////////////////////////////////////////////////////////////////////////
Set<QSupplementalInstanceMetaData> toEnrich = new LinkedHashSet<>(qInstance.getSupplementalMetaData().values());
Set<QSupplementalInstanceMetaData> enriched = new HashSet<>();
int count = 0;
while(!toEnrich.isEmpty())
{
Iterator<QSupplementalInstanceMetaData> iterator = toEnrich.iterator();
QSupplementalInstanceMetaData supplementalInstanceMetaData = iterator.next();
iterator.remove();
supplementalInstanceMetaData.enrich(qInstance);
enriched.add(supplementalInstanceMetaData);
for(QSupplementalInstanceMetaData possiblyNew : qInstance.getSupplementalMetaData().values())
{
if(!toEnrich.contains(possiblyNew) && !enriched.contains(possiblyNew))
{
if(count++ > 100)
{
throw (new QRuntimeException("Too many new QSupplementalInstanceMetaData objects were added while enriching others. This probably indicates a bug in enrichment code. Throwing to prevent infinite loop."));
}
toEnrich.add(possiblyNew);
}
}
}
runPlugins(QInstance.class, qInstance, qInstance);
}
@ -618,14 +583,6 @@ public class QInstanceEnricher
}
}
////////////////////////////////////////////////////
// enrich any supplemental meta data on the field //
////////////////////////////////////////////////////
for(QSupplementalFieldMetaData supplementalFieldMetaData : CollectionUtils.nonNullMap(field.getSupplementalMetaData()).values())
{
supplementalFieldMetaData.enrich(qInstance, field);
}
runPlugins(QFieldMetaData.class, field, qInstance);
}
@ -901,6 +858,11 @@ public class QInstanceEnricher
*******************************************************************************/
private void defineTableBulkProcesses(QInstance qInstance)
{
if(qInstance.getPossibleValueSource(TableKeyFieldsPossibleValueSource.NAME) == null)
{
qInstance.addPossibleValueSource(defineTableKeyFieldsPossibleValueSource());
}
for(QTableMetaData table : qInstance.getTables().values())
{
if(table.getFields() == null)
@ -924,6 +886,12 @@ public class QInstanceEnricher
defineTableBulkEdit(qInstance, table, bulkEditProcessName);
}
String bulkEditWithFileProcessName = table.getName() + ".bulkEditWithFile";
if(qInstance.getProcess(bulkEditWithFileProcessName) == null)
{
defineTableBulkEditWithFile(qInstance, table, bulkEditWithFileProcessName);
}
String bulkDeleteProcessName = table.getName() + ".bulkDelete";
if(qInstance.getProcess(bulkDeleteProcessName) == null)
{
@ -1097,13 +1065,129 @@ public class QInstanceEnricher
Fields whose switches are off will not be updated."""))
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.BULK_EDIT_FORM));
process.withStep(0, editScreen);
process.addStep(0, editScreen);
process.getFrontendStep("review").setRecordListFields(editableFields);
qInstance.addProcess(process);
}
/*******************************************************************************
**
*******************************************************************************/
public void defineTableBulkEditWithFile(QInstance qInstance, QTableMetaData table, String processName)
{
Map<String, Serializable> values = new HashMap<>();
values.put(StreamedETLWithFrontendProcess.FIELD_DESTINATION_TABLE, table.getName());
values.put(StreamedETLWithFrontendProcess.FIELD_PREVIEW_MESSAGE, "This is a preview of the records that will be updated.");
QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData(
BulkInsertExtractStep.class,
BulkInsertTransformStep.class,
BulkEditLoadStep.class,
values
)
.withName(processName)
.withLabel(table.getLabel() + " Bulk Edit With File")
.withTableName(table.getName())
.withIsHidden(true)
.withPermissionRules(qInstance.getDefaultPermissionRules().clone()
.withCustomPermissionChecker(new QCodeReference(BulkTableActionProcessPermissionChecker.class)));
List<QFieldMetaData> editableFields = table.getFields().values().stream()
.filter(QFieldMetaData::getIsEditable)
.filter(f -> !f.getType().equals(QFieldType.BLOB))
.toList();
QBackendStepMetaData prepareFileUploadStep = new QBackendStepMetaData()
.withName("prepareFileUpload")
.withCode(new QCodeReference(BulkInsertPrepareFileUploadStep.class));
QFrontendStepMetaData uploadScreen = new QFrontendStepMetaData()
.withName("upload")
.withLabel("Upload File")
.withFormField(new QFieldMetaData("theFile", QFieldType.BLOB)
.withFieldAdornment(FileUploadAdornment.newFieldAdornment()
.withValue(FileUploadAdornment.formatDragAndDrop())
.withValue(FileUploadAdornment.widthFull()))
.withLabel(table.getLabel() + " File")
.withIsRequired(true))
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.HTML))
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM));
QBackendStepMetaData prepareFileMappingStep = new QBackendStepMetaData()
.withName("prepareFileMapping")
.withCode(new QCodeReference(BulkInsertPrepareFileMappingStep.class));
QFrontendStepMetaData fileMappingScreen = new QFrontendStepMetaData()
.withName("fileMapping")
.withLabel("File Mapping")
.withBackStepName("prepareFileUpload")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_FILE_MAPPING_FORM))
.withFormField(new QFieldMetaData("hasHeaderRow", QFieldType.BOOLEAN))
.withFormField(new QFieldMetaData("layout", QFieldType.STRING)) // is actually PVS, but, this field is only added to help support helpContent, so :shrug:
.withFormField(new QFieldMetaData("tableKeyFields", QFieldType.STRING).withPossibleValueSourceName(TableKeyFieldsPossibleValueSource.NAME));
QBackendStepMetaData receiveFileMappingStep = new QBackendStepMetaData()
.withName("receiveFileMapping")
.withCode(new QCodeReference(BulkInsertReceiveFileMappingStep.class));
QBackendStepMetaData prepareValueMappingStep = new QBackendStepMetaData()
.withName("prepareValueMapping")
.withCode(new QCodeReference(BulkInsertPrepareValueMappingStep.class));
QFrontendStepMetaData valueMappingScreen = new QFrontendStepMetaData()
.withName("valueMapping")
.withLabel("Value Mapping")
.withBackStepName("prepareFileMapping")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_VALUE_MAPPING_FORM));
QBackendStepMetaData receiveValueMappingStep = new QBackendStepMetaData()
.withName("receiveValueMapping")
.withCode(new QCodeReference(BulkInsertReceiveValueMappingStep.class));
int i = 0;
process.withStep(i++, prepareFileUploadStep);
process.withStep(i++, uploadScreen);
process.withStep(i++, prepareFileMappingStep);
process.withStep(i++, fileMappingScreen);
process.withStep(i++, receiveFileMappingStep);
process.withStep(i++, prepareValueMappingStep);
process.withStep(i++, valueMappingScreen);
process.withStep(i++, receiveValueMappingStep);
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW).setRecordListFields(editableFields);
//////////////////////////////////////////////////////////////////////////////////////////
// put the bulk-load profile form (e.g., for saving it) on the review & result screens) //
//////////////////////////////////////////////////////////////////////////////////////////
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW)
.withBackStepName("prepareFileMapping")
.getComponents().add(0, new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_PROFILE_FORM));
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_RESULT)
.getComponents().add(0, new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_PROFILE_FORM));
qInstance.addProcess(process);
}
/*******************************************************************************
**
*******************************************************************************/
private QPossibleValueSource defineTableKeyFieldsPossibleValueSource()
{
return (new QPossibleValueSource()
.withName(TableKeyFieldsPossibleValueSource.NAME)
.withType(QPossibleValueSourceType.CUSTOM)
.withCustomCodeReference(new QCodeReference(TableKeyFieldsPossibleValueSource.class)));
}
/*******************************************************************************
**
*******************************************************************************/
@ -1448,10 +1532,10 @@ public class QInstanceEnricher
if(possibleValueSource.getIdType() == null)
{
QTableMetaData table = qInstance.getTable(possibleValueSource.getTableName());
if(table != null && table.getFields() != null)
if(table != null)
{
String primaryKeyField = table.getPrimaryKeyField();
QFieldMetaData primaryKeyFieldMetaData = CollectionUtils.nonNullMap(table.getFields()).get(primaryKeyField);
QFieldMetaData primaryKeyFieldMetaData = table.getFields().get(primaryKeyField);
if(primaryKeyFieldMetaData != null)
{
possibleValueSource.setIdType(primaryKeyFieldMetaData.getType());
@ -1485,7 +1569,7 @@ public class QInstanceEnricher
{
QCustomPossibleValueProvider<?> customPossibleValueProvider = QCodeLoader.getAdHoc(QCustomPossibleValueProvider.class, possibleValueSource.getCustomCodeReference());
Method getPossibleValueMethod = customPossibleValueProvider.getClass().getMethod("getPossibleValue", Serializable.class);
Method getPossibleValueMethod = customPossibleValueProvider.getClass().getDeclaredMethod("getPossibleValue", Serializable.class);
Type returnType = getPossibleValueMethod.getGenericReturnType();
Type idType = ((ParameterizedType) returnType).getActualTypeArguments()[0];
@ -1521,18 +1605,7 @@ public class QInstanceEnricher
if(enrichMethod.isPresent())
{
Class<?> parameterType = enrichMethod.get().getParameterTypes()[0];
Set<String> existingPluginIdentifiers = enricherPlugins.getOrDefault(parameterType, Collections.emptyList())
.stream().map(p -> p.getPluginIdentifier())
.collect(Collectors.toSet());
if(existingPluginIdentifiers.contains(plugin.getPluginIdentifier()))
{
LOG.debug("Enricher plugin is already registered - not re-adding it", logPair("pluginIdentifer", plugin.getPluginIdentifier()));
}
else
{
enricherPlugins.add(parameterType, plugin);
}
enricherPlugins.add(parameterType, plugin);
}
else
{
@ -1552,17 +1625,6 @@ public class QInstanceEnricher
/*******************************************************************************
** Getter for enricherPlugins
**
*******************************************************************************/
public static ListingHash<Class<?>, QInstanceEnricherPluginInterface<?>> getEnricherPlugins()
{
return enricherPlugins;
}
/***************************************************************************
** scan the classpath for classes in the specified package name which
** implement the QInstanceEnricherPluginInterface - any found get added

View File

@ -36,7 +36,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
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.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpFormat;
@ -164,23 +163,6 @@ public class QInstanceHelpContentManager
{
processHelpContentForInstance(qInstance, key, slotName, roles, helpContent);
}
else
{
for(QSupplementalInstanceMetaData supplementalInstanceMetaData : qInstance.getSupplementalMetaData().values())
{
if(supplementalInstanceMetaData instanceof QHelpContentPlugin helpContentPlugin)
{
try
{
helpContentPlugin.acceptHelpContent(qInstance, helpContent, nameValuePairs);
}
catch(Exception e)
{
LOG.warn("Error processing a helpContent record in a helpContentPlugin", e, logPair("pluginName", supplementalInstanceMetaData.getName()), logPair("id", record.getValue("id")));
}
}
}
}
}
catch(Exception e)
{

View File

@ -41,8 +41,7 @@ import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandlerInterface;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
@ -73,7 +72,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QSupplementalFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
@ -241,10 +239,8 @@ public class QInstanceValidator
/***************************************************************************
* this method still supports the deprecated MetaDataFilter (plus its
* replacement, MetaDataActionCustomizer
**
***************************************************************************/
@SuppressWarnings("deprecation")
private void validateInstanceAttributes(QInstance qInstance)
{
if(qInstance.getMetaDataFilter() != null)
@ -256,17 +252,6 @@ public class QInstanceValidator
{
validateSimpleCodeReference("Instance metaDataActionCustomizer ", qInstance.getMetaDataActionCustomizer(), MetaDataActionCustomizerInterface.class);
}
if(qInstance.getTableCustomizers() != null)
{
for(Map.Entry<String, List<QCodeReference>> entry : qInstance.getTableCustomizers().entrySet())
{
for(QCodeReference codeReference : CollectionUtils.nonNullList(entry.getValue()))
{
validateSimpleCodeReference("Instance tableCustomizer of type " + entry.getKey() + ": ", codeReference, TableCustomizerInterface.class);
}
}
}
}
@ -298,18 +283,7 @@ public class QInstanceValidator
if(validateMethod.isPresent())
{
Class<?> parameterType = validateMethod.get().getParameterTypes()[0];
Set<String> existingPluginIdentifiers = validatorPlugins.getOrDefault(parameterType, Collections.emptyList())
.stream().map(p -> p.getPluginIdentifier())
.collect(Collectors.toSet());
if(existingPluginIdentifiers.contains(plugin.getPluginIdentifier()))
{
LOG.debug("Validator plugin is already registered - not re-adding it", logPair("pluginIdentifer", plugin.getPluginIdentifier()));
}
else
{
validatorPlugins.add(parameterType, plugin);
}
validatorPlugins.add(parameterType, plugin);
}
else
{
@ -329,17 +303,6 @@ public class QInstanceValidator
/*******************************************************************************
** Getter for validatorPlugins
**
*******************************************************************************/
public static ListingHash<Class<?>, QInstanceValidatorPluginInterface<?>> getValidatorPlugins()
{
return validatorPlugins;
}
/*******************************************************************************
**
*******************************************************************************/
@ -1185,21 +1148,6 @@ public class QInstanceValidator
}
}
}
validateFieldSupplementalMetaData(field, qInstance);
}
/***************************************************************************
**
***************************************************************************/
public void validateFieldSupplementalMetaData(QFieldMetaData field, QInstance qInstance)
{
for(QSupplementalFieldMetaData supplementalFieldMetaData : CollectionUtils.nonNullMap(field.getSupplementalMetaData()).values())
{
supplementalFieldMetaData.validate(qInstance, field, this);
}
}
@ -1392,7 +1340,7 @@ public class QInstanceValidator
numberSet++;
if(preAssertionsForCodeReference(action.getCodeReference(), actionPrefix))
{
validateSimpleCodeReference(actionPrefix + "code reference: ", action.getCodeReference(), RecordAutomationHandlerInterface.class);
validateSimpleCodeReference(actionPrefix + "code reference: ", action.getCodeReference(), RecordAutomationHandler.class);
}
}
@ -1753,8 +1701,6 @@ public class QInstanceValidator
validateSimpleCodeReference("Process " + processName + " code reference:", codeReference, expectedClass);
}
validateFieldSupplementalMetaData(fieldMetaData, qInstance);
}
}
}
@ -2292,7 +2238,8 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
public void validateSimpleCodeReference(String prefix, QCodeReference codeReference, Class<?>... anyOfExpectedClasses)
@SafeVarargs
private void validateSimpleCodeReference(String prefix, QCodeReference codeReference, Class<?>... anyOfExpectedClasses)
{
if(!preAssertionsForCodeReference(codeReference, prefix))
{

View File

@ -1,37 +0,0 @@
/*
* 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.qqq.backend.core.instances.assessment;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
** marker for an object which can be processed by the QInstanceAssessor.
*******************************************************************************/
public interface Assessable
{
/***************************************************************************
**
***************************************************************************/
void assess(QInstanceAssessor qInstanceAssessor, QInstance qInstance);
}

View File

@ -1,219 +0,0 @@
/*
* 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.qqq.backend.core.instances.assessment;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** POC of a class that is meant to review meta-data for accuracy vs. real backends.
*******************************************************************************/
public class QInstanceAssessor
{
private static final QLogger LOG = QLogger.getLogger(QInstanceAssessor.class);
private final QInstance qInstance;
private List<String> errors = new ArrayList<>();
private List<String> warnings = new ArrayList<>();
private List<String> suggestions = new ArrayList<>();
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public QInstanceAssessor(QInstance qInstance)
{
this.qInstance = qInstance;
}
/*******************************************************************************
**
*******************************************************************************/
public void assess()
{
for(QBackendMetaData backend : qInstance.getBackends().values())
{
if(backend instanceof Assessable assessable)
{
assessable.assess(this, qInstance);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:AvoidEscapedUnicodeCharacters")
public String getSummary()
{
StringBuilder rs = new StringBuilder();
///////////////////////////
// print header & errors //
///////////////////////////
if(CollectionUtils.nullSafeIsEmpty(errors))
{
rs.append("Assessment passed with no errors! \uD83D\uDE0E\n");
}
else
{
rs.append("Assessment found the following ").append(StringUtils.plural(errors, "error", "errors")).append(": \uD83D\uDE32\n");
for(String error : errors)
{
rs.append(" - ").append(error).append("\n");
}
}
/////////////////////////////////////
// print warnings if there are any //
/////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(warnings))
{
rs.append("\nAssessment found the following ").append(StringUtils.plural(warnings, "warning", "warnings")).append(": \uD83E\uDD28\n");
for(String warning : warnings)
{
rs.append(" - ").append(warning).append("\n");
}
}
//////////////////////////////////////////
// print suggestions, if there were any //
//////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(suggestions))
{
rs.append("\nThe following ").append(StringUtils.plural(suggestions, "fix is", "fixes are")).append(" suggested: \uD83E\uDD13\n");
for(String suggestion : suggestions)
{
rs.append("\n").append(suggestion).append("\n\n");
}
}
return (rs.toString());
}
/*******************************************************************************
** Getter for qInstance
**
*******************************************************************************/
public QInstance getInstance()
{
return qInstance;
}
/*******************************************************************************
** Getter for errors
**
*******************************************************************************/
public List<String> getErrors()
{
return errors;
}
/*******************************************************************************
** Getter for warnings
**
*******************************************************************************/
public List<String> getWarnings()
{
return warnings;
}
/*******************************************************************************
**
*******************************************************************************/
public void addError(String errorMessage)
{
errors.add(errorMessage);
}
/*******************************************************************************
**
*******************************************************************************/
public void addWarning(String warningMessage)
{
warnings.add(warningMessage);
}
/*******************************************************************************
**
*******************************************************************************/
public void addError(String errorMessage, Exception e)
{
addError(errorMessage + " : " + e.getMessage());
}
/*******************************************************************************
**
*******************************************************************************/
public void addSuggestion(String message)
{
suggestions.add(message);
}
/*******************************************************************************
**
*******************************************************************************/
public int getExitCode()
{
if(CollectionUtils.nullSafeHasContents(errors))
{
return (1);
}
else
{
return (0);
}
}
}

View File

@ -37,13 +37,4 @@ public interface QInstanceEnricherPluginInterface<T>
*******************************************************************************/
void enrich(T object, QInstance qInstance);
/***************************************************************************
**
***************************************************************************/
default String getPluginIdentifier()
{
return getClass().getName();
}
}

View File

@ -38,13 +38,4 @@ public interface QInstanceValidatorPluginInterface<T>
*******************************************************************************/
void validate(T object, QInstance qInstance, QInstanceValidator qInstanceValidator);
/***************************************************************************
**
***************************************************************************/
default String getPluginIdentifier()
{
return getClass().getName();
}
}

View File

@ -25,7 +25,6 @@ package com.kingsrook.qqq.backend.core.model.actions.audits;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
@ -37,8 +36,6 @@ public class AuditInput extends AbstractActionInput implements Serializable
{
private List<AuditSingleInput> auditSingleInputList = new ArrayList<>();
private QBackendTransaction transaction;
/*******************************************************************************
@ -95,42 +92,4 @@ public class AuditInput extends AbstractActionInput implements Serializable
return (this);
}
/*******************************************************************************
* Getter for transaction
* @see #withTransaction(QBackendTransaction)
*******************************************************************************/
public QBackendTransaction getTransaction()
{
return (this.transaction);
}
/*******************************************************************************
* Setter for transaction
* @see #withTransaction(QBackendTransaction)
*******************************************************************************/
public void setTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
}
/*******************************************************************************
* Fluent setter for transaction
*
* @param transaction
* transaction upon which the audits will be inserted.
*
* @return this
*******************************************************************************/
public AuditInput withTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
return (this);
}
}

View File

@ -24,7 +24,6 @@ package com.kingsrook.qqq.backend.core.model.actions.audits;
import java.io.Serializable;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -39,8 +38,6 @@ public class DMLAuditInput extends AbstractActionInput implements Serializable
private List<QRecord> oldRecordList;
private AbstractTableActionInput tableActionInput;
private QBackendTransaction transaction;
private String auditContext = null;
@ -167,43 +164,4 @@ public class DMLAuditInput extends AbstractActionInput implements Serializable
return (this);
}
/*******************************************************************************
* Getter for transaction
* @see #withTransaction(QBackendTransaction)
*******************************************************************************/
public QBackendTransaction getTransaction()
{
return (this.transaction);
}
/*******************************************************************************
* Setter for transaction
* @see #withTransaction(QBackendTransaction)
*******************************************************************************/
public void setTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
}
/*******************************************************************************
* Fluent setter for transaction
*
* @param transaction
* transaction that will be used for inserting the audits, where (presumably)
* the DML against the record occurred as well
*
* @return this
*******************************************************************************/
public DMLAuditInput withTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
return (this);
}
}

View File

@ -25,7 +25,6 @@ package com.kingsrook.qqq.backend.core.model.actions.metadata;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData;
@ -42,13 +41,12 @@ import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
*******************************************************************************/
public class MetaDataOutput extends AbstractActionOutput
{
private Map<String, QFrontendTableMetaData> tables;
private Map<String, QFrontendProcessMetaData> processes;
private Map<String, QFrontendReportMetaData> reports;
private Map<String, QFrontendAppMetaData> apps;
private Map<String, QFrontendWidgetMetaData> widgets;
private Map<String, String> environmentValues;
private Map<String, QSupplementalInstanceMetaData> supplementalInstanceMetaData;
private Map<String, QFrontendTableMetaData> tables;
private Map<String, QFrontendProcessMetaData> processes;
private Map<String, QFrontendReportMetaData> reports;
private Map<String, QFrontendAppMetaData> apps;
private Map<String, QFrontendWidgetMetaData> widgets;
private Map<String, String> environmentValues;
private List<AppTreeNode> appTree;
private QBrandingMetaData branding;
@ -232,28 +230,6 @@ public class MetaDataOutput extends AbstractActionOutput
/*******************************************************************************
** Getter for supplementalInstanceMetaData
**
*******************************************************************************/
public Map<String, QSupplementalInstanceMetaData> getSupplementalInstanceMetaData()
{
return supplementalInstanceMetaData;
}
/*******************************************************************************
** Setter for supplementalInstanceMetaData
**
*******************************************************************************/
public void setSupplementalInstanceMetaData(Map<String, QSupplementalInstanceMetaData> supplementalInstanceMetaData)
{
this.supplementalInstanceMetaData = supplementalInstanceMetaData;
}
/*******************************************************************************
** Setter for helpContents
**

View File

@ -23,11 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -35,45 +31,6 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
*******************************************************************************/
public interface ProcessSummaryLineInterface extends Serializable
{
QLogger LOG = QLogger.getLogger(ProcessSummaryLineInterface.class);
/***************************************************************************
**
***************************************************************************/
static void log(String message, Serializable summaryLines, List<LogPair> additionalLogPairs)
{
try
{
if(summaryLines instanceof List)
{
List<ProcessSummaryLineInterface> list = (List<ProcessSummaryLineInterface>) summaryLines;
List<LogPair> logPairs = new ArrayList<>();
for(ProcessSummaryLineInterface processSummaryLineInterface : list)
{
LogPair logPair = processSummaryLineInterface.toLogPair();
logPair.setKey(logPair.getKey() + logPairs.size());
logPairs.add(logPair);
}
if(additionalLogPairs != null)
{
logPairs.addAll(0, additionalLogPairs);
}
logPairs.add(0, logPair("message", message));
LOG.info(logPairs);
}
else
{
LOG.info("Unrecognized type for summaryLines (expected List)", logPair("processSummary", summaryLines));
}
}
catch(Exception e)
{
LOG.info("Error logging a process summary", e, logPair("processSummary", summaryLines));
}
}
/*******************************************************************************
** Getter for status

View File

@ -1,35 +0,0 @@
/*
* 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.qqq.backend.core.model.actions.tables;
import java.io.Serializable;
/*******************************************************************************
** interface to mark enums (presumably classes too, but the original intent is
** enums) that can be added to insert/update/delete action inputs to flag behaviors
*******************************************************************************/
public interface ActionFlag extends Serializable
{
}

View File

@ -1,117 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.qqq.backend.core.model.actions.tables;
import java.util.EnumSet;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
/*******************************************************************************
** Common getters & setters, shared by both QueryInput and CountInput.
**
** Original impetus for this class is the setCommonParamsFrom() method - for cases
** where we need to change a Query to a Get, or vice-versa, and we want to copy over
** all of those input params.
*******************************************************************************/
public interface QueryOrCountInputInterface
{
/*******************************************************************************
** Set in THIS, the "common params" (e.g., common to both Query & Count inputs)
** from the parameter SOURCE object.
*******************************************************************************/
default void setCommonParamsFrom(QueryOrCountInputInterface source)
{
this.setTransaction(source.getTransaction());
this.setFilter(source.getFilter());
this.setTableName(source.getTableName());
this.setQueryJoins(source.getQueryJoins());
this.setTimeoutSeconds(source.getTimeoutSeconds());
this.setQueryHints(source.getQueryHints());
}
/*******************************************************************************
**
*******************************************************************************/
String getTableName();
/***************************************************************************
**
***************************************************************************/
void setTableName(String tableName);
/*******************************************************************************
**
*******************************************************************************/
QQueryFilter getFilter();
/***************************************************************************
**
***************************************************************************/
void setFilter(QQueryFilter filter);
/*******************************************************************************
** Getter for transaction
*******************************************************************************/
QBackendTransaction getTransaction();
/*******************************************************************************
** Setter for transaction
*******************************************************************************/
void setTransaction(QBackendTransaction transaction);
/*******************************************************************************
** Getter for queryJoins
*******************************************************************************/
List<QueryJoin> getQueryJoins();
/*******************************************************************************
** Setter for queryJoins
**
*******************************************************************************/
void setQueryJoins(List<QueryJoin> queryJoins);
/*******************************************************************************
**
*******************************************************************************/
Integer getTimeoutSeconds();
/***************************************************************************
**
***************************************************************************/
void setTimeoutSeconds(Integer timeoutSeconds);
/*******************************************************************************
** Getter for queryHints
*******************************************************************************/
EnumSet<QueryHint> getQueryHints();
/*******************************************************************************
** Setter for queryHints
*******************************************************************************/
void setQueryHints(EnumSet<QueryHint> queryHints);
}

View File

@ -25,7 +25,6 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.aggregate;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -38,8 +37,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
*******************************************************************************/
public class AggregateInput extends AbstractTableActionInput
{
private QBackendTransaction transaction;
private QQueryFilter filter;
private List<Aggregate> aggregates;
private List<GroupBy> groupBys = new ArrayList<>();
@ -407,35 +404,4 @@ public class AggregateInput extends AbstractTableActionInput
return (queryHints.contains(queryHint));
}
/*******************************************************************************
** Getter for transaction
*******************************************************************************/
public QBackendTransaction getTransaction()
{
return (this.transaction);
}
/*******************************************************************************
** Setter for transaction
*******************************************************************************/
public void setTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
}
/*******************************************************************************
** Fluent setter for transaction
*******************************************************************************/
public AggregateInput withTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
return (this);
}
}

View File

@ -25,10 +25,8 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.count;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrCountInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
@ -37,10 +35,9 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
** Input data for the Count action
**
*******************************************************************************/
public class CountInput extends AbstractTableActionInput implements QueryOrCountInputInterface
public class CountInput extends AbstractTableActionInput
{
private QBackendTransaction transaction;
private QQueryFilter filter;
private QQueryFilter filter;
private Integer timeoutSeconds;
@ -288,35 +285,4 @@ public class CountInput extends AbstractTableActionInput implements QueryOrCount
return (queryHints.contains(queryHint));
}
/*******************************************************************************
** Getter for transaction
*******************************************************************************/
public QBackendTransaction getTransaction()
{
return (this.transaction);
}
/*******************************************************************************
** Setter for transaction
*******************************************************************************/
public void setTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
}
/*******************************************************************************
** Fluent setter for transaction
*******************************************************************************/
public CountInput withTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
return (this);
}
}

View File

@ -24,12 +24,9 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.delete;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.ActionFlag;
import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -50,8 +47,6 @@ public class DeleteInput extends AbstractTableActionInput
private boolean omitDmlAudit = false;
private String auditContext = null;
private Set<ActionFlag> flags;
/*******************************************************************************
@ -300,65 +295,4 @@ public class DeleteInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for flags
*******************************************************************************/
public Set<ActionFlag> getFlags()
{
return (this.flags);
}
/*******************************************************************************
** Setter for flags
*******************************************************************************/
public void setFlags(Set<ActionFlag> flags)
{
this.flags = flags;
}
/*******************************************************************************
** Fluent setter for flags
*******************************************************************************/
public DeleteInput withFlags(Set<ActionFlag> flags)
{
this.flags = flags;
return (this);
}
/***************************************************************************
**
***************************************************************************/
public DeleteInput withFlag(ActionFlag flag)
{
if(this.flags == null)
{
this.flags = new HashSet<>();
}
this.flags.add(flag);
return (this);
}
/***************************************************************************
**
***************************************************************************/
public boolean hasFlag(ActionFlag flag)
{
if(this.flags == null)
{
return (false);
}
return (this.flags.contains(flag));
}
}

View File

@ -23,12 +23,9 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.insert;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.ActionFlag;
import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -51,8 +48,6 @@ public class InsertInput extends AbstractTableActionInput
private boolean omitDmlAudit = false;
private String auditContext = null;
private Set<ActionFlag> flags;
/*******************************************************************************
@ -321,65 +316,4 @@ public class InsertInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for flags
*******************************************************************************/
public Set<ActionFlag> getFlags()
{
return (this.flags);
}
/*******************************************************************************
** Setter for flags
*******************************************************************************/
public void setFlags(Set<ActionFlag> flags)
{
this.flags = flags;
}
/*******************************************************************************
** Fluent setter for flags
*******************************************************************************/
public InsertInput withFlags(Set<ActionFlag> flags)
{
this.flags = flags;
return (this);
}
/***************************************************************************
**
***************************************************************************/
public InsertInput withFlag(ActionFlag flag)
{
if(this.flags == null)
{
this.flags = new HashSet<>();
}
this.flags.add(flag);
return (this);
}
/***************************************************************************
**
***************************************************************************/
public boolean hasFlag(ActionFlag flag)
{
if(this.flags == null)
{
return (false);
}
return (this.flags.contains(flag));
}
}

View File

@ -659,21 +659,6 @@ public class QQueryFilter implements Serializable, Cloneable, QMetaDataObject
}
}
//////////////////////////////////////
// recursively process sub filters! //
//////////////////////////////////////
for(QQueryFilter subFilter : CollectionUtils.nonNullList(getSubFilters()))
{
try
{
subFilter.interpretValues(inputValues, useCase);
}
catch(Exception e)
{
caughtExceptions.add(e);
}
}
if(!caughtExceptions.isEmpty())
{
String message = "Error interpreting filter values: " + StringUtils.joinWithCommasAndAnd(caughtExceptions.stream().map(e -> e.getMessage()).toList());
@ -839,7 +824,6 @@ public class QQueryFilter implements Serializable, Cloneable, QMetaDataObject
}
/*******************************************************************************
** Getter for subFilterSetOperator
*******************************************************************************/
@ -870,7 +854,6 @@ public class QQueryFilter implements Serializable, Cloneable, QMetaDataObject
}
/***************************************************************************
**
***************************************************************************/

View File

@ -32,7 +32,6 @@ import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrCountInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
@ -43,7 +42,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterf
** CountInput, and AggregateInput}, with common attributes for all of these
** "read" operations (like, queryHints,
*******************************************************************************/
public class QueryInput extends AbstractTableActionInput implements QueryOrGetInputInterface, QueryOrCountInputInterface, Cloneable
public class QueryInput extends AbstractTableActionInput implements QueryOrGetInputInterface, Cloneable
{
private QBackendTransaction transaction;
private QQueryFilter filter;

View File

@ -22,12 +22,9 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.replace;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.ActionFlag;
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.tables.UniqueKey;
@ -42,14 +39,12 @@ public class ReplaceInput extends AbstractTableActionInput
private UniqueKey key;
private List<QRecord> records;
private QQueryFilter filter;
private boolean performDeletes = true;
private boolean allowNullKeyValuesToEqual = false;
private boolean setPrimaryKeyInInsertedRecords = false;
private boolean performDeletes = true;
private boolean allowNullKeyValuesToEqual = false;
private boolean setPrimaryKeyInInsertedRecords = false;
private boolean omitDmlAudit = false;
private Set<ActionFlag> flags;
/*******************************************************************************
@ -308,65 +303,4 @@ public class ReplaceInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for flags
*******************************************************************************/
public Set<ActionFlag> getFlags()
{
return (this.flags);
}
/*******************************************************************************
** Setter for flags
*******************************************************************************/
public void setFlags(Set<ActionFlag> flags)
{
this.flags = flags;
}
/*******************************************************************************
** Fluent setter for flags
*******************************************************************************/
public ReplaceInput withFlags(Set<ActionFlag> flags)
{
this.flags = flags;
return (this);
}
/***************************************************************************
**
***************************************************************************/
public ReplaceInput withFlag(ActionFlag flag)
{
if(this.flags == null)
{
this.flags = new HashSet<>();
}
this.flags.add(flag);
return (this);
}
/***************************************************************************
**
***************************************************************************/
public boolean hasFlag(ActionFlag flag)
{
if(this.flags == null)
{
return (false);
}
return (this.flags.contains(flag));
}
}

View File

@ -23,12 +23,9 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.update;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.ActionFlag;
import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -59,8 +56,6 @@ public class UpdateInput extends AbstractTableActionInput
private boolean omitModifyDateUpdate = false;
private String auditContext = null;
private Set<ActionFlag> flags;
/*******************************************************************************
@ -390,65 +385,4 @@ public class UpdateInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for flags
*******************************************************************************/
public Set<ActionFlag> getFlags()
{
return (this.flags);
}
/*******************************************************************************
** Setter for flags
*******************************************************************************/
public void setFlags(Set<ActionFlag> flags)
{
this.flags = flags;
}
/*******************************************************************************
** Fluent setter for flags
*******************************************************************************/
public UpdateInput withFlags(Set<ActionFlag> flags)
{
this.flags = flags;
return (this);
}
/***************************************************************************
**
***************************************************************************/
public UpdateInput withFlag(ActionFlag flag)
{
if(this.flags == null)
{
this.flags = new HashSet<>();
}
this.flags.add(flag);
return (this);
}
/***************************************************************************
**
***************************************************************************/
public boolean hasFlag(ActionFlag flag)
{
if(this.flags == null)
{
return (false);
}
return (this.flags.contains(flag));
}
}

View File

@ -35,13 +35,13 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
*******************************************************************************/
public class SearchPossibleValueSourceInput extends AbstractActionInput implements Cloneable
{
private String possibleValueSourceName;
private QQueryFilter defaultQueryFilter;
private String searchTerm;
private List<Serializable> idList;
private List<String> labelList;
private Map<String, Serializable> otherValues;
private String possibleValueSourceName;
private QQueryFilter defaultQueryFilter;
private String searchTerm;
private List<Serializable> idList;
private List<String> labelList;
private Map<String, String> pathParamMap;
private Map<String, List<String>> queryParamMap;
private Integer skip = 0;
private Integer limit = 250;
@ -320,31 +320,62 @@ public class SearchPossibleValueSourceInput extends AbstractActionInput implemen
/*******************************************************************************
** Getter for otherValues
** Getter for pathParamMap
*******************************************************************************/
public Map<String, Serializable> getOtherValues()
public Map<String, String> getPathParamMap()
{
return (this.otherValues);
return (this.pathParamMap);
}
/*******************************************************************************
** Setter for otherValues
** Setter for pathParamMap
*******************************************************************************/
public void setOtherValues(Map<String, Serializable> otherValues)
public void setPathParamMap(Map<String, String> pathParamMap)
{
this.otherValues = otherValues;
this.pathParamMap = pathParamMap;
}
/*******************************************************************************
** Fluent setter for otherValues
** Fluent setter for pathParamMap
*******************************************************************************/
public SearchPossibleValueSourceInput withOtherValues(Map<String, Serializable> otherValues)
public SearchPossibleValueSourceInput withPathParamMap(Map<String, String> pathParamMap)
{
this.otherValues = otherValues;
this.pathParamMap = pathParamMap;
return (this);
}
/*******************************************************************************
** Getter for queryParamMap
*******************************************************************************/
public Map<String, List<String>> getQueryParamMap()
{
return (this.queryParamMap);
}
/*******************************************************************************
** Setter for queryParamMap
*******************************************************************************/
public void setQueryParamMap(Map<String, List<String>> queryParamMap)
{
this.queryParamMap = queryParamMap;
}
/*******************************************************************************
** Fluent setter for queryParamMap
*******************************************************************************/
public SearchPossibleValueSourceInput withQueryParamMap(Map<String, List<String>> queryParamMap)
{
this.queryParamMap = queryParamMap;
return (this);
}

View File

@ -27,7 +27,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TablesSupportingAutomationsPossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.savedviews.SavedView;
import com.kingsrook.qqq.backend.core.model.scripts.Script;
@ -48,16 +48,16 @@ public class TableTrigger extends QRecordEntity
@QField(isEditable = false)
private Instant modifyDate;
@QField(possibleValueSourceName = TablesSupportingAutomationsPossibleValueSourceMetaDataProvider.NAME, isRequired = true)
@QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME)
private String tableName;
@QField(possibleValueSourceName = SavedView.TABLE_NAME)
private Integer filterId;
@QField(possibleValueSourceName = Script.TABLE_NAME, isRequired = true)
@QField(possibleValueSourceName = Script.TABLE_NAME)
private Integer scriptId;
@QField(defaultValue = "500")
@QField()
private Integer priority;
@QField()

View File

@ -0,0 +1,153 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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.qqq.backend.core.model.bulk;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
**
*******************************************************************************/
public class TableKeyFieldsPossibleValueSource implements QCustomPossibleValueProvider<String>
{
public static final String NAME = "tableKeyFields";
/*******************************************************************************
**
*******************************************************************************/
@Override
public QPossibleValue<String> getPossibleValue(Serializable tableAndKey)
{
QPossibleValue<String> possibleValue = null;
/////////////////////////////////////////////////////////////
// keys are in the format <tableName>-<key1>|<key2>|<key3> //
/////////////////////////////////////////////////////////////
String[] keyParts = tableAndKey.toString().split("-");
String tableName = keyParts[0];
String key = keyParts[1];
QTableMetaData table = QContext.getQInstance().getTable(tableName);
if(table.getPrimaryKeyField().equals(key))
{
String id = table.getPrimaryKeyField();
String label = table.getField(table.getPrimaryKeyField()).getLabel();
possibleValue = new QPossibleValue<>(id, label);
}
else
{
for(UniqueKey uniqueKey : table.getUniqueKeys())
{
String potentialMatch = getIdFromUniqueKey(uniqueKey);
if(potentialMatch.equals(key))
{
String id = potentialMatch;
String label = getLabelFromUniqueKey(table, uniqueKey);
possibleValue = new QPossibleValue<>(id, label);
break;
}
}
}
return (possibleValue);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QPossibleValue<String>> search(SearchPossibleValueSourceInput input) throws QException
{
List<QPossibleValue<String>> rs = new ArrayList<>();
if(!CollectionUtils.nonNullMap(input.getPathParamMap()).containsKey("processName") || input.getPathParamMap().get("processName") == null || input.getPathParamMap().get("processName").isEmpty())
{
throw (new QException("Path Param of processName was not found."));
}
////////////////////////////////////////////////////
// process name will be like tnt.bulkEditWithFile //
////////////////////////////////////////////////////
String processName = input.getPathParamMap().get("processName");
String tableName = processName.split("\\.")[0];
QTableMetaData table = QContext.getQInstance().getTable(tableName);
for(UniqueKey uniqueKey : CollectionUtils.nonNullList(table.getUniqueKeys()))
{
String id = getIdFromUniqueKey(uniqueKey);
String label = getLabelFromUniqueKey(table, uniqueKey);
if(!StringUtils.hasContent(input.getSearchTerm()) || input.getSearchTerm().equals(id))
{
rs.add(new QPossibleValue<>(id, label));
}
}
rs.sort(Comparator.comparing(QPossibleValue::getLabel));
///////////////////////////////
// put the primary key first //
///////////////////////////////
if(!StringUtils.hasContent(input.getSearchTerm()) || input.getSearchTerm().equals(table.getPrimaryKeyField()))
{
rs.add(0, new QPossibleValue<>(table.getPrimaryKeyField(), table.getField(table.getPrimaryKeyField()).getLabel()));
}
return rs;
}
/*******************************************************************************
**
*******************************************************************************/
private String getIdFromUniqueKey(UniqueKey uniqueKey)
{
return (StringUtils.join("|", uniqueKey.getFieldNames()));
}
/*******************************************************************************
**
*******************************************************************************/
private String getLabelFromUniqueKey(QTableMetaData tableMetaData, UniqueKey uniqueKey)
{
List<String> fieldLabels = new ArrayList<>(uniqueKey.getFieldNames().stream().map(f -> tableMetaData.getField(f).getLabel()).toList());
fieldLabels.sort(Comparator.naturalOrder());
return (StringUtils.joinWithCommasAndAnd(fieldLabels));
}
}

View File

@ -23,7 +23,6 @@ package com.kingsrook.qqq.backend.core.model.dashboard.widgets;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
@ -53,7 +52,6 @@ public class ChildRecordListData extends QWidgetData
private Map<String, Serializable> defaultValuesForNewChildRecords;
private Set<String> disabledFieldsForNewChildRecords;
private Map<String, String> defaultValuesForNewChildRecordsFromParentFields;
private List<String> omitFieldNames;
@ -557,37 +555,6 @@ public class ChildRecordListData extends QWidgetData
return (this);
}
/*******************************************************************************
** Getter for omitFieldNames
*******************************************************************************/
public List<String> getOmitFieldNames()
{
return (this.omitFieldNames);
}
/*******************************************************************************
** Setter for omitFieldNames
*******************************************************************************/
public void setOmitFieldNames(List<String> omitFieldNames)
{
this.omitFieldNames = omitFieldNames;
}
/*******************************************************************************
** Fluent setter for omitFieldNames
*******************************************************************************/
public ChildRecordListData withOmitFieldNames(List<String> omitFieldNames)
{
this.omitFieldNames = omitFieldNames;
return (this);
}
}

View File

@ -35,15 +35,7 @@ public class FilterAndColumnsSetupData extends QWidgetData
private Boolean allowVariables = false;
private Boolean hideColumns = false;
private Boolean hidePreview = false;
private Boolean hideSortBy = false;
private Boolean overrideIsEditable;
private List<String> filterDefaultFieldNames;
private List<String> omitExposedJoins;
private Boolean isApiVersioned = false;
private String apiName;
private String apiPath;
private String apiVersion;
private String filterFieldName = "queryFilterJson";
private String columnFieldName = "columnsJson";
@ -298,227 +290,4 @@ public class FilterAndColumnsSetupData extends QWidgetData
return (this);
}
/*******************************************************************************
** Getter for overrideIsEditable
*******************************************************************************/
public Boolean getOverrideIsEditable()
{
return (this.overrideIsEditable);
}
/*******************************************************************************
** Setter for overrideIsEditable
*******************************************************************************/
public void setOverrideIsEditable(Boolean overrideIsEditable)
{
this.overrideIsEditable = overrideIsEditable;
}
/*******************************************************************************
** Fluent setter for overrideIsEditable
*******************************************************************************/
public FilterAndColumnsSetupData withOverrideIsEditable(Boolean overrideIsEditable)
{
this.overrideIsEditable = overrideIsEditable;
return (this);
}
/*******************************************************************************
** Getter for hideSortBy
*******************************************************************************/
public Boolean getHideSortBy()
{
return (this.hideSortBy);
}
/*******************************************************************************
** Setter for hideSortBy
*******************************************************************************/
public void setHideSortBy(Boolean hideSortBy)
{
this.hideSortBy = hideSortBy;
}
/*******************************************************************************
** Fluent setter for hideSortBy
*******************************************************************************/
public FilterAndColumnsSetupData withHideSortBy(Boolean hideSortBy)
{
this.hideSortBy = hideSortBy;
return (this);
}
/*******************************************************************************
** Getter for isApiVersioned
*******************************************************************************/
public Boolean getIsApiVersioned()
{
return (this.isApiVersioned);
}
/*******************************************************************************
** Setter for isApiVersioned
*******************************************************************************/
public void setIsApiVersioned(Boolean isApiVersioned)
{
this.isApiVersioned = isApiVersioned;
}
/*******************************************************************************
** Fluent setter for isApiVersioned
*******************************************************************************/
public FilterAndColumnsSetupData withIsApiVersioned(Boolean isApiVersioned)
{
this.isApiVersioned = isApiVersioned;
return (this);
}
/*******************************************************************************
** Getter for apiName
*******************************************************************************/
public String getApiName()
{
return (this.apiName);
}
/*******************************************************************************
** Setter for apiName
*******************************************************************************/
public void setApiName(String apiName)
{
this.apiName = apiName;
}
/*******************************************************************************
** Fluent setter for apiName
*******************************************************************************/
public FilterAndColumnsSetupData withApiName(String apiName)
{
this.apiName = apiName;
return (this);
}
/*******************************************************************************
** Getter for apiPath
*******************************************************************************/
public String getApiPath()
{
return (this.apiPath);
}
/*******************************************************************************
** Setter for apiPath
*******************************************************************************/
public void setApiPath(String apiPath)
{
this.apiPath = apiPath;
}
/*******************************************************************************
** Fluent setter for apiPath
*******************************************************************************/
public FilterAndColumnsSetupData withApiPath(String apiPath)
{
this.apiPath = apiPath;
return (this);
}
/*******************************************************************************
** Getter for apiVersion
*******************************************************************************/
public String getApiVersion()
{
return (this.apiVersion);
}
/*******************************************************************************
** Setter for apiVersion
*******************************************************************************/
public void setApiVersion(String apiVersion)
{
this.apiVersion = apiVersion;
}
/*******************************************************************************
** Fluent setter for apiVersion
*******************************************************************************/
public FilterAndColumnsSetupData withApiVersion(String apiVersion)
{
this.apiVersion = apiVersion;
return (this);
}
/*******************************************************************************
* Getter for omitExposedJoins
* @see #withOmitExposedJoins(List)
*******************************************************************************/
public List<String> getOmitExposedJoins()
{
return (this.omitExposedJoins);
}
/*******************************************************************************
* Setter for omitExposedJoins
* @see #withOmitExposedJoins(List)
*******************************************************************************/
public void setOmitExposedJoins(List<String> omitExposedJoins)
{
this.omitExposedJoins = omitExposedJoins;
}
/*******************************************************************************
* Fluent setter for omitExposedJoins
*
* @param omitExposedJoins
* list of tableNames of exposed joins that shouldn't be available in the filter.
* @return this
*******************************************************************************/
public FilterAndColumnsSetupData withOmitExposedJoins(List<String> omitExposedJoins)
{
this.omitExposedJoins = omitExposedJoins;
return (this);
}
}

View File

@ -209,7 +209,6 @@ public abstract class QRecordEntity
try
{
QRecord qRecord = new QRecord();
qRecord.setTableName(tableName());
for(QRecordEntityField qRecordEntityField : getFieldList(this.getClass()))
{

View File

@ -23,7 +23,6 @@ package com.kingsrook.qqq.backend.core.model.data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -258,39 +257,4 @@ public class QRecordWithJoinedRecords extends QRecord
return (this);
}
/***************************************************************************
* Given an object of this type (`this`), add a list of join-records to it,
* producing a new list, which is `this` × joinRecordList.
* One may want to use this in a loop to build a larger cross product - more
* to come here in that spirit.
* @param joinTable name of the join table (e.g., to prefix the join fields)
* @param joinRecordList list of join records. may not be null. may be 0+ size.
* @return list of new QRecordWithJoinedRecords, based on `this` with each
* joinRecord added (e.g., output list is same size as joinRecordList).
* Note that does imply an 'inner' style join - where - if the joinRecordList
* is empty, you'll get back an empty list!
***************************************************************************/
public List<QRecordWithJoinedRecords> buildCrossProduct(String joinTable, List<QRecord> joinRecordList)
{
List<QRecordWithJoinedRecords> rs = new ArrayList<>();
for(QRecord joinRecord : joinRecordList)
{
/////////////////////////////////////////////////////////////
// essentially clone the existing QRecordWithJoinedRecords //
/////////////////////////////////////////////////////////////
QRecordWithJoinedRecords newRecord = new QRecordWithJoinedRecords(mainRecord);
components.forEach((k, v) -> newRecord.addJoinedRecordValues(k, v));
///////////////////////////////////////////
// now add the new join record to it too //
///////////////////////////////////////////
newRecord.addJoinedRecordValues(joinTable, joinRecord);
rs.add(newRecord);
}
return (rs);
}
}

View File

@ -177,18 +177,6 @@ public class MetaDataProducerHelper
/////////////////////////////////////////////////////////////////////////////////////////////
// sort them by sort order, then by the type that they return, as set up in the static map //
/////////////////////////////////////////////////////////////////////////////////////////////
sortMetaDataProducers(producers);
return (producers);
}
/***************************************************************************
**
***************************************************************************/
public static void sortMetaDataProducers(List<MetaDataProducerInterface<?>> producers)
{
producers.sort(Comparator
.comparing((MetaDataProducerInterface<?> p) -> p.getSortOrder())
.thenComparing((MetaDataProducerInterface<?> p) ->
@ -203,8 +191,9 @@ public class MetaDataProducerHelper
return (0);
}
}));
}
return (producers);
}
/*******************************************************************************
@ -428,7 +417,7 @@ public class MetaDataProducerHelper
return (null);
}
ChildJoinFromRecordEntityGenericMetaDataProducer producer = new ChildJoinFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, possibleValueFieldName, childTable.childJoin().orderBy(), childTable.childJoin().isOneToOne());
ChildJoinFromRecordEntityGenericMetaDataProducer producer = new ChildJoinFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, possibleValueFieldName, childTable.childJoin().orderBy());
producer.setSourceClass(entityClass);
return producer;
}

View File

@ -86,7 +86,7 @@ public class MetaDataProducerMultiOutput implements MetaDataProducerOutput, Sour
{
List<T> rs = new ArrayList<>();
for(MetaDataProducerOutput content : CollectionUtils.nonNullList(contents))
for(MetaDataProducerOutput content : contents)
{
if(content instanceof MetaDataProducerMultiOutput multiOutput)
{
@ -145,36 +145,4 @@ public class MetaDataProducerMultiOutput implements MetaDataProducerOutput, Sour
setSourceQBitName(sourceQBitName);
return this;
}
/***************************************************************************
* get a typed and named meta-data object out of this output container.
*
* @param <C> the type of the object to return, e.g., QTableMetaData
* @param outputClass the class for the type to return
* @param name the name of the object, e.g., a table or process name.
* @return the requested TopLevelMetaDataInterface object (in the requested
* type), or null if not found.
***************************************************************************/
public <C extends TopLevelMetaDataInterface> C get(Class<C> outputClass, String name)
{
for(MetaDataProducerOutput content : CollectionUtils.nonNullList(contents))
{
if(content instanceof MetaDataProducerMultiOutput multiOutput)
{
C c = multiOutput.get(outputClass, name);
if(c != null)
{
return (c);
}
}
else if(outputClass.isInstance(content) && name.equals(((TopLevelMetaDataInterface)content).getName()))
{
return (C) content;
}
}
return null;
}
}

View File

@ -23,7 +23,6 @@ package com.kingsrook.qqq.backend.core.model.metadata;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@ -31,7 +30,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@ -67,7 +65,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.scheduler.schedulable.SchedulableType;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import io.github.cdimascio.dotenv.Dotenv;
import io.github.cdimascio.dotenv.DotenvEntry;
@ -119,8 +116,6 @@ public class QInstance
private QPermissionRules defaultPermissionRules = QPermissionRules.defaultInstance();
private QAuditRules defaultAuditRules = QAuditRules.defaultInstanceLevelNone();
private ListingHash<String, QCodeReference> tableCustomizers;
@Deprecated(since = "migrated to metaDataCustomizer")
private QCodeReference metaDataFilter = null;
@ -1628,76 +1623,4 @@ public class QInstance
return (this);
}
/*******************************************************************************
** Getter for tableCustomizers
*******************************************************************************/
public ListingHash<String, QCodeReference> getTableCustomizers()
{
return (this.tableCustomizers);
}
/*******************************************************************************
** Setter for tableCustomizers
*******************************************************************************/
public void setTableCustomizers(ListingHash<String, QCodeReference> tableCustomizers)
{
this.tableCustomizers = tableCustomizers;
}
/*******************************************************************************
** Fluent setter for tableCustomizers
*******************************************************************************/
public QInstance withTableCustomizers(ListingHash<String, QCodeReference> tableCustomizers)
{
this.tableCustomizers = tableCustomizers;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public QInstance withTableCustomizer(String role, QCodeReference customizer)
{
if(this.tableCustomizers == null)
{
this.tableCustomizers = new ListingHash<>();
}
this.tableCustomizers.add(role, customizer);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public QInstance withTableCustomizer(TableCustomizers tableCustomizer, QCodeReference customizer)
{
return (withTableCustomizer(tableCustomizer.getRole(), customizer));
}
/*******************************************************************************
** Getter for tableCustomizers
*******************************************************************************/
public List<QCodeReference> getTableCustomizers(TableCustomizers tableCustomizer)
{
if(this.tableCustomizers == null)
{
return (Collections.emptyList());
}
return (this.tableCustomizers.getOrDefault(tableCustomizer.getRole(), Collections.emptyList()));
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.metadata;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
@ -36,7 +37,7 @@ public interface QSupplementalInstanceMetaData extends TopLevelMetaDataInterface
/*******************************************************************************
**
*******************************************************************************/
default void enrich(QInstance qInstance)
default void enrich(QTableMetaData table)
{
////////////////////////
// noop in base class //

View File

@ -22,10 +22,6 @@
package com.kingsrook.qqq.backend.core.model.metadata.fields;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
** Base-class for field-level meta-data defined by some supplemental module, etc,
** outside of qqq core
@ -33,43 +29,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
public abstract class QSupplementalFieldMetaData
{
/*******************************************************************************
**
*******************************************************************************/
public boolean includeInFrontendMetaData()
{
return (false);
}
/*******************************************************************************
** Getter for type
*******************************************************************************/
public abstract String getType();
/***************************************************************************
**
***************************************************************************/
public void enrich(QInstance qInstance, QFieldMetaData fieldMetaData)
{
////////////////////////
// noop in base class //
////////////////////////
}
/*******************************************************************************
**
*******************************************************************************/
public void validate(QInstance qInstance, QFieldMetaData fieldMetaData, QInstanceValidator qInstanceValidator)
{
////////////////////////
// noop in base class //
////////////////////////
}
}

View File

@ -24,18 +24,14 @@ package com.kingsrook.qqq.backend.core.model.metadata.frontend;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehaviorForFrontend;
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.QSupplementalFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -55,7 +51,6 @@ public class QFrontendFieldMetaData implements Serializable
private boolean isRequired;
private boolean isEditable;
private boolean isHeavy;
private boolean isHidden;
private Integer gridColumns;
private String possibleValueSourceName;
private String displayFormat;
@ -65,10 +60,8 @@ public class QFrontendFieldMetaData implements Serializable
private List<FieldAdornment> adornments;
private List<QHelpContent> helpContents;
private QPossibleValueSource inlinePossibleValueSource;
private QQueryFilter possibleValueSourceFilter;
private List<FieldBehaviorForFrontend> behaviors;
private Map<String, QSupplementalFieldMetaData> supplementalFieldMetaData;
private List<FieldBehaviorForFrontend> behaviors;
//////////////////////////////////////////////////////////////////////////////////
// do not add setters. take values from the source-object in the constructor!! //
@ -86,7 +79,6 @@ public class QFrontendFieldMetaData implements Serializable
this.isRequired = fieldMetaData.getIsRequired();
this.isEditable = fieldMetaData.getIsEditable();
this.isHeavy = fieldMetaData.getIsHeavy();
this.isHidden = fieldMetaData.getIsHidden();
this.gridColumns = fieldMetaData.getGridColumns();
this.possibleValueSourceName = fieldMetaData.getPossibleValueSourceName();
this.displayFormat = fieldMetaData.getDisplayFormat();
@ -95,7 +87,6 @@ public class QFrontendFieldMetaData implements Serializable
this.helpContents = fieldMetaData.getHelpContents();
this.inlinePossibleValueSource = fieldMetaData.getInlinePossibleValueSource();
this.maxLength = fieldMetaData.getMaxLength();
this.possibleValueSourceFilter = fieldMetaData.getPossibleValueSourceFilter();
for(FieldBehavior<?> behavior : CollectionUtils.nonNullCollection(fieldMetaData.getBehaviors()))
{
@ -108,19 +99,6 @@ public class QFrontendFieldMetaData implements Serializable
behaviors.add(fbff);
}
}
for(Map.Entry<String, QSupplementalFieldMetaData> entry : CollectionUtils.nonNullMap(fieldMetaData.getSupplementalMetaData()).entrySet())
{
QSupplementalFieldMetaData supplementalFieldMetaData = entry.getValue();
if(supplementalFieldMetaData.includeInFrontendMetaData())
{
if(this.supplementalFieldMetaData == null)
{
this.supplementalFieldMetaData = new HashMap<>();
}
this.supplementalFieldMetaData.put(entry.getKey(), supplementalFieldMetaData);
}
}
}
@ -191,17 +169,6 @@ public class QFrontendFieldMetaData implements Serializable
/*******************************************************************************
** Getter for isHidden
**
*******************************************************************************/
public boolean getIsHidden()
{
return isHidden;
}
/*******************************************************************************
** Getter for gridColumns
**
@ -287,37 +254,4 @@ public class QFrontendFieldMetaData implements Serializable
{
return behaviors;
}
/*******************************************************************************
** Getter for supplementalFieldMetaData
**
*******************************************************************************/
public Map<String, QSupplementalFieldMetaData> getSupplementalFieldMetaData()
{
return supplementalFieldMetaData;
}
/*******************************************************************************
** Getter for maxLength
**
*******************************************************************************/
public Integer getMaxLength()
{
return maxLength;
}
/*******************************************************************************
** Getter for possibleValueSourceFilter
**
*******************************************************************************/
public QQueryFilter getPossibleValueSourceFilter()
{
return possibleValueSourceFilter;
}
}

View File

@ -85,35 +85,23 @@ public class QFrontendTableMetaData
// do not add setters. take values from the source-object in the constructor!! //
//////////////////////////////////////////////////////////////////////////////////
/***************************************************************************
** standard constructor - uses all fields on the table.
***************************************************************************/
public QFrontendTableMetaData(AbstractActionInput actionInput, QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFullMetaData, boolean includeJoins)
{
this(actionInput, backendForTable, tableMetaData, includeFullMetaData, includeJoins, tableMetaData.getFields());
}
/*******************************************************************************
** alternative constructor - takes a map of fields to use (e.g., for an old
** api version of the table w/ different fields!)
**
*******************************************************************************/
public QFrontendTableMetaData(AbstractActionInput actionInput, QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFullMetaData, boolean includeJoins, Map<String, QFieldMetaData> overrideFields)
public QFrontendTableMetaData(AbstractActionInput actionInput, QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFullMetaData, boolean includeJoins)
{
this.name = tableMetaData.getName();
this.label = tableMetaData.getLabel();
this.isHidden = tableMetaData.getIsHidden();
Map<String, QFieldMetaData> inputFields = overrideFields == null ? tableMetaData.getFields() : overrideFields;
if(includeFullMetaData)
{
this.primaryKeyField = tableMetaData.getPrimaryKeyField();
this.fields = new HashMap<>();
for(String fieldName : inputFields.keySet())
for(String fieldName : tableMetaData.getFields().keySet())
{
QFieldMetaData field = inputFields.get(fieldName);
QFieldMetaData field = tableMetaData.getField(fieldName);
if(!field.getIsHidden())
{
this.fields.put(fieldName, new QFrontendFieldMetaData(field));

View File

@ -26,13 +26,10 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
/*******************************************************************************
** meta-data definition of "Help Content" to show to a user - for use in
** meta-data defintion of "Help Content" to show to a user - for use in
** a specific "role" (e.g., insert screens but not view screens), and in a
** particular "format" (e.g., plain text, html, markdown).
**
@ -51,12 +48,6 @@ public class QHelpContent implements QMetaDataObject
private HelpFormat format;
private Set<HelpRole> roles;
////////////////////////////////////
// these appear to be thread safe //
////////////////////////////////////
private static Parser commonMarkParser = Parser.builder().build();
private static HtmlRenderer commonMarkRenderer = HtmlRenderer.builder().build();
/*******************************************************************************
@ -80,38 +71,6 @@ public class QHelpContent implements QMetaDataObject
/***************************************************************************
* Return the content as html string, based on its format.
* Only MARKDOWN actually gets processed (via commonmark) - but TEXT and
* HTML come out as-is.
***************************************************************************/
public String getContentAsHtml()
{
if(content == null)
{
return (null);
}
if(HelpFormat.MARKDOWN.equals(this.format))
{
//////////////////////////////
// convert markdown to HTML //
//////////////////////////////
Node document = commonMarkParser.parse(content);
String html = commonMarkRenderer.render(document);
return (html);
}
else
{
///////////////////////////////////////////////////
// other formats (html & text) just output as-is //
///////////////////////////////////////////////////
return (content);
}
}
/*******************************************************************************
** Getter for content
*******************************************************************************/

View File

@ -213,7 +213,7 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
{
if(stepList != null)
{
stepList.forEach(this::withStep);
stepList.forEach(this::addStep);
}
return (this);
@ -231,7 +231,7 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
{
index = this.stepList.size();
}
withStep(index, step);
addStep(index, step);
return (this);
}

View File

@ -26,15 +26,12 @@ import java.util.Objects;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildJoin;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProductionContext;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
@ -65,7 +62,6 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
private String childTableName; // e.g., lineItem
private String parentTableName; // e.g., order
private String foreignKeyFieldName; // e.g., orderId
private boolean isOneToOne;
private ChildJoin.OrderBy[] orderBys;
@ -76,7 +72,7 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
/***************************************************************************
**
***************************************************************************/
public ChildJoinFromRecordEntityGenericMetaDataProducer(String childTableName, String parentTableName, String foreignKeyFieldName, ChildJoin.OrderBy[] orderBys, boolean isOneToOne)
public ChildJoinFromRecordEntityGenericMetaDataProducer(String childTableName, String parentTableName, String foreignKeyFieldName, ChildJoin.OrderBy[] orderBys)
{
Objects.requireNonNull(childTableName, "childTableName cannot be null");
Objects.requireNonNull(parentTableName, "parentTableName cannot be null");
@ -86,7 +82,6 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
this.parentTableName = parentTableName;
this.foreignKeyFieldName = foreignKeyFieldName;
this.orderBys = orderBys;
this.isOneToOne = isOneToOne;
}
@ -97,14 +92,23 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
@Override
public QJoinMetaData produce(QInstance qInstance) throws QException
{
QTableMetaData parentTable = getTable(qInstance, parentTableName);
QTableMetaData childTable = getTable(qInstance, childTableName);
QTableMetaData parentTable = qInstance.getTable(parentTableName);
if(parentTable == null)
{
throw (new QException("Could not find tableMetaData " + parentTableName));
}
QTableMetaData childTable = qInstance.getTable(childTableName);
if(childTable == null)
{
throw (new QException("Could not find tableMetaData " + childTable));
}
QJoinMetaData join = new QJoinMetaData()
.withLeftTable(parentTableName)
.withRightTable(childTableName)
.withInferredName()
.withType(isOneToOne ? JoinType.ONE_TO_ONE : JoinType.ONE_TO_MANY)
.withType(JoinType.ONE_TO_MANY)
.withJoinOn(new JoinOn(parentTable.getPrimaryKeyField(), foreignKeyFieldName));
if(orderBys != null && orderBys.length > 0)
@ -127,41 +131,6 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
/***************************************************************************
*
***************************************************************************/
private QTableMetaData getTable(QInstance qInstance, String tableName) throws QException
{
QTableMetaData table = qInstance.getTable(tableName);
if(table == null)
{
///////////////////////////////////////////////////////////////////////////////
// in case we're producing a QBit, and it's added a table to a multi-output, //
// but not yet the instance, see if we can get table from there //
///////////////////////////////////////////////////////////////////////////////
for(MetaDataProducerMultiOutput metaDataProducerMultiOutput : QBitProductionContext.getReadOnlyViewOfMetaDataProducerMultiOutputStack())
{
table = CollectionUtils.nonNullList(metaDataProducerMultiOutput.getEach(QTableMetaData.class)).stream()
.filter(t -> t.getName().equals(tableName))
.findFirst().orElse(null);
if(table != null)
{
break;
}
}
}
if(table == null)
{
throw (new QException("Could not find tableMetaData: " + table));
}
return table;
}
/*******************************************************************************
** Getter for sourceClass
**

View File

@ -25,13 +25,10 @@ package com.kingsrook.qqq.backend.core.model.metadata.producers;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildRecordListWidget;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProductionContext;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -92,26 +89,6 @@ public class ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer implem
String name = QJoinMetaData.makeInferredJoinName(parentTableName, childTableName);
QJoinMetaData join = qInstance.getJoin(name);
if(join == null)
{
for(MetaDataProducerMultiOutput metaDataProducerMultiOutput : QBitProductionContext.getReadOnlyViewOfMetaDataProducerMultiOutputStack())
{
join = CollectionUtils.nonNullList(metaDataProducerMultiOutput.getEach(QJoinMetaData.class)).stream()
.filter(t -> t.getName().equals(name))
.findFirst().orElse(null);
if(join != null)
{
break;
}
}
}
if(join == null)
{
throw (new QException("Could not find joinMetaData: " + name));
}
QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(join)
.withName(name)
.withLabel(childRecordListWidget.label())

View File

@ -38,8 +38,6 @@ public @interface ChildJoin
OrderBy[] orderBy() default { };
boolean isOneToOne() default false;
/***************************************************************************
**
***************************************************************************/

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.qbits;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput;
@ -32,7 +33,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput;
** Specifically exists to accept the QBitConfig as a type parameter and a value,
** easily accessed in the producer's methods as getQBitConfig()
*******************************************************************************/
public abstract class QBitComponentMetaDataProducer<T extends MetaDataProducerOutput, C extends QBitConfig> implements QBitComponentMetaDataProducerInterface<T, C>
public abstract class QBitComponentMetaDataProducer<T extends MetaDataProducerOutput, C extends QBitConfig> implements MetaDataProducerInterface<T>
{
private C qBitConfig = null;
@ -41,7 +42,6 @@ public abstract class QBitComponentMetaDataProducer<T extends MetaDataProducerOu
/*******************************************************************************
** Getter for qBitConfig
*******************************************************************************/
@Override
public C getQBitConfig()
{
return (this.qBitConfig);
@ -52,7 +52,6 @@ public abstract class QBitComponentMetaDataProducer<T extends MetaDataProducerOu
/*******************************************************************************
** Setter for qBitConfig
*******************************************************************************/
@Override
public void setQBitConfig(C qBitConfig)
{
this.qBitConfig = qBitConfig;

View File

@ -1,50 +0,0 @@
/*
* 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.qqq.backend.core.model.metadata.qbits;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput;
/*******************************************************************************
** extension of MetaDataProducerInterface, designed for producing meta data
** within a (java-defined, at this time) QBit.
**
** Specifically exists to accept the QBitConfig as a type parameter and a value,
** easily accessed in the producer's methods as getQBitConfig()
*******************************************************************************/
public interface QBitComponentMetaDataProducerInterface<T extends MetaDataProducerOutput, C extends QBitConfig> extends MetaDataProducerInterface<T>
{
/*******************************************************************************
** Getter for qBitConfig
*******************************************************************************/
C getQBitConfig();
/*******************************************************************************
** Setter for qBitConfig
*******************************************************************************/
void setQBitConfig(C qBitConfig);
}

View File

@ -107,14 +107,4 @@ public interface QBitConfig extends Serializable
{
return (null);
}
/***************************************************************************
*
***************************************************************************/
default String getDefaultBackendNameForTables()
{
return (null);
}
}

View File

@ -1,192 +0,0 @@
/*
* 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.qqq.backend.core.model.metadata.qbits;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** 2nd generation interface for top-level meta-data production classes that make
** a qbit (evolution over QBitProducer).
**
*******************************************************************************/
public interface QBitMetaDataProducer<C extends QBitConfig> extends MetaDataProducerInterface<MetaDataProducerMultiOutput>
{
QLogger LOG = QLogger.getLogger(QBitMetaDataProducer.class);
/***************************************************************************
**
***************************************************************************/
C getQBitConfig();
/***************************************************************************
**
***************************************************************************/
QBitMetaData getQBitMetaData();
/***************************************************************************
**
***************************************************************************/
default String getNamespace()
{
return (null);
}
/***************************************************************************
**
***************************************************************************/
default void postProduceActions(MetaDataProducerMultiOutput metaDataProducerMultiOutput, QInstance qinstance) throws QException
{
/////////////////////
// noop by default //
/////////////////////
}
/***************************************************************************
**
***************************************************************************/
default String getPackageNameForFindingMetaDataProducers()
{
Class<?> clazz = getClass();
////////////////////////////////////////////////////////////////
// Walk up the hierarchy until we find the direct implementer //
////////////////////////////////////////////////////////////////
while(clazz != null)
{
Class<?>[] interfaces = clazz.getInterfaces();
for(Class<?> interfaze : interfaces)
{
if(interfaze == QBitMetaDataProducer.class)
{
return clazz.getPackageName();
}
}
clazz = clazz.getSuperclass();
}
throw (new QRuntimeException("Unable to find packageName for QBitMetaDataProducer. You may need to implement getPackageName yourself..."));
}
/***************************************************************************
**
***************************************************************************/
@Override
default MetaDataProducerMultiOutput produce(QInstance qInstance) throws QException
{
MetaDataProducerMultiOutput rs = new MetaDataProducerMultiOutput();
QBitMetaData qBitMetaData = getQBitMetaData();
C qBitConfig = getQBitConfig();
qInstance.addQBit(qBitMetaData);
QBitProductionContext.pushQBitConfig(qBitConfig);
QBitProductionContext.pushMetaDataProducerMultiOutput(rs);
try
{
qBitConfig.validate(qInstance);
List<MetaDataProducerInterface<?>> producers = MetaDataProducerHelper.findProducers(getPackageNameForFindingMetaDataProducers());
MetaDataProducerHelper.sortMetaDataProducers(producers);
for(MetaDataProducerInterface<?> producer : producers)
{
if(producer.getClass().equals(this.getClass()))
{
/////////////////////////////////////////////
// avoid recursive processing of ourselves //
/////////////////////////////////////////////
continue;
}
////////////////////////////////////////////////////////////////////////////
// todo is this deprecated in favor of QBitProductionContext's stack... ? //
////////////////////////////////////////////////////////////////////////////
if(producer instanceof QBitComponentMetaDataProducerInterface<?, ?>)
{
QBitComponentMetaDataProducerInterface<?, C> qBitComponentMetaDataProducer = (QBitComponentMetaDataProducerInterface<?, C>) producer;
qBitComponentMetaDataProducer.setQBitConfig(qBitConfig);
}
if(!producer.isEnabled())
{
LOG.debug("Not using producer which is not enabled", logPair("producer", producer.getClass().getSimpleName()));
continue;
}
MetaDataProducerOutput subProducerOutput = producer.produce(qInstance);
/////////////////////////////////////////////////
// apply some things from the config to tables //
/////////////////////////////////////////////////
if(subProducerOutput instanceof QTableMetaData table)
{
if(qBitConfig.getTableMetaDataCustomizer() != null)
{
subProducerOutput = qBitConfig.getTableMetaDataCustomizer().customizeMetaData(qInstance, table);
}
if(!StringUtils.hasContent(table.getBackendName()) && StringUtils.hasContent(qBitConfig.getDefaultBackendNameForTables()))
{
table.setBackendName(qBitConfig.getDefaultBackendNameForTables());
}
}
////////////////////////////////////////////////////////////
// set source qbit, if subProducerOutput is aware of such //
////////////////////////////////////////////////////////////
if(subProducerOutput instanceof SourceQBitAware sourceQBitAware)
{
sourceQBitAware.setSourceQBitName(qBitMetaData.getName());
}
rs.add(subProducerOutput);
}
postProduceActions(rs, qInstance);
return (rs);
}
finally
{
QBitProductionContext.popQBitConfig();
QBitProductionContext.popMetaDataProducerMultiOutput();
}
}
}

View File

@ -76,11 +76,14 @@ public interface QBitProducer
{
qBitConfig.validate(qInstance);
///////////////////////////////
// todo - move to base class //
///////////////////////////////
for(MetaDataProducerInterface<?> producer : producers)
{
if(producer instanceof QBitComponentMetaDataProducerInterface<?,?>)
if(producer instanceof QBitComponentMetaDataProducer<?, ?>)
{
QBitComponentMetaDataProducerInterface<?,C> qBitComponentMetaDataProducer = (QBitComponentMetaDataProducerInterface<?,C>) producer;
QBitComponentMetaDataProducer<?, C> qBitComponentMetaDataProducer = (QBitComponentMetaDataProducer<?, C>) producer;
qBitComponentMetaDataProducer.setQBitConfig(qBitConfig);
}

View File

@ -1,136 +0,0 @@
/*
* 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.qqq.backend.core.model.metadata.qbits;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
/*******************************************************************************
** While a qbit is being produced, track the context of the current config
** and metaDataProducerMultiOutput that is being used. also, in case one
** qbit produces another, push these contextual objects on a stack.
*******************************************************************************/
public class QBitProductionContext
{
private static final QLogger LOG = QLogger.getLogger(QBitProductionContext.class);
private static Stack<QBitConfig> qbitConfigStack = new Stack<>();
private static Stack<MetaDataProducerMultiOutput> metaDataProducerMultiOutputStack = new Stack<>();
/***************************************************************************
**
***************************************************************************/
public static void pushQBitConfig(QBitConfig qBitConfig)
{
qbitConfigStack.push(qBitConfig);
}
/***************************************************************************
**
***************************************************************************/
public static QBitConfig peekQBitConfig()
{
if(qbitConfigStack.isEmpty())
{
LOG.warn("Request to peek at empty QBitProductionContext configStack - returning null");
return (null);
}
return qbitConfigStack.peek();
}
/***************************************************************************
**
***************************************************************************/
public static void popQBitConfig()
{
if(qbitConfigStack.isEmpty())
{
LOG.warn("Request to pop empty QBitProductionContext configStack - returning with noop");
return;
}
qbitConfigStack.pop();
}
/***************************************************************************
**
***************************************************************************/
public static void pushMetaDataProducerMultiOutput(MetaDataProducerMultiOutput metaDataProducerMultiOutput)
{
metaDataProducerMultiOutputStack.push(metaDataProducerMultiOutput);
}
/***************************************************************************
**
***************************************************************************/
public static MetaDataProducerMultiOutput peekMetaDataProducerMultiOutput()
{
if(metaDataProducerMultiOutputStack.isEmpty())
{
LOG.warn("Request to peek at empty QBitProductionContext configStack - returning null");
return (null);
}
return metaDataProducerMultiOutputStack.peek();
}
/***************************************************************************
**
***************************************************************************/
public static List<MetaDataProducerMultiOutput> getReadOnlyViewOfMetaDataProducerMultiOutputStack()
{
return Collections.unmodifiableList(metaDataProducerMultiOutputStack);
}
/***************************************************************************
**
***************************************************************************/
public static void popMetaDataProducerMultiOutput()
{
if(metaDataProducerMultiOutputStack.isEmpty())
{
LOG.warn("Request to pop empty QBitProductionContext metaDataProducerMultiOutput - returning with noop");
return;
}
metaDataProducerMultiOutputStack.pop();
}
}

View File

@ -44,7 +44,7 @@ public class MultiRecordSecurityLock extends RecordSecurityLock implements Clone
**
*******************************************************************************/
@Override
public MultiRecordSecurityLock clone()
protected MultiRecordSecurityLock clone() throws CloneNotSupportedException
{
MultiRecordSecurityLock clone = (MultiRecordSecurityLock) super.clone();

View File

@ -57,27 +57,20 @@ public class RecordSecurityLock implements Cloneable
**
*******************************************************************************/
@Override
public RecordSecurityLock clone()
protected RecordSecurityLock clone() throws CloneNotSupportedException
{
try
{
RecordSecurityLock clone = (RecordSecurityLock) super.clone();
RecordSecurityLock clone = (RecordSecurityLock) super.clone();
/////////////////////////
// deep-clone the list //
/////////////////////////
if(joinNameChain != null)
{
clone.joinNameChain = new ArrayList<>();
clone.joinNameChain.addAll(joinNameChain);
}
return (clone);
}
catch(CloneNotSupportedException e)
/////////////////////////
// deep-clone the list //
/////////////////////////
if(joinNameChain != null)
{
throw (new RuntimeException("Could not clone", e));
clone.joinNameChain = new ArrayList<>();
clone.joinNameChain.addAll(joinNameChain);
}
return (clone);
}

View File

@ -85,7 +85,7 @@ public class TablesCustomPossibleValueProvider extends BasicCustomPossibleValueP
/***************************************************************************
**
***************************************************************************/
protected boolean isTableAllowed(QTableMetaData table)
private boolean isTableAllowed(QTableMetaData table)
{
if(table == null)
{

View File

@ -1,47 +0,0 @@
/*
* 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.qqq.backend.core.model.metadata.tables.automation;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesCustomPossibleValueProvider;
/*******************************************************************************
** subset of the tables PVS custom provider, to only include tables support automations
*******************************************************************************/
public class TablesSupportingAutomationsCustomPossibleValueProvider extends TablesCustomPossibleValueProvider
{
/***************************************************************************
**
***************************************************************************/
@Override
protected boolean isTableAllowed(QTableMetaData table)
{
if(table.getAutomationDetails() == null)
{
return (false);
}
return super.isTableAllowed(table);
}
}

View File

@ -1,57 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.qqq.backend.core.model.metadata.tables.automation;
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.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
/*******************************************************************************
** subset of the tables PVS, to only include tables support automations
*******************************************************************************/
public class TablesSupportingAutomationsPossibleValueSourceMetaDataProvider
{
public static final String NAME = "tablesSupportingAutomations";
/*******************************************************************************
**
*******************************************************************************/
public static QPossibleValueSource defineTablesPossibleValueSource(QInstance qInstance)
{
QPossibleValueSource possibleValueSource = new QPossibleValueSource()
.withName(NAME)
.withIdType(QFieldType.STRING)
.withType(QPossibleValueSourceType.CUSTOM)
.withCustomCodeReference(new QCodeReference(TablesSupportingAutomationsCustomPossibleValueProvider.class))
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY);
return (possibleValueSource);
}
}

View File

@ -28,7 +28,6 @@ import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -52,7 +51,7 @@ public class BackendVariantsUtil
String variantTypeKey = backendMetaData.getBackendVariantsConfig().getVariantTypeKey();
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(variantTypeKey))
{
throw (new QUserFacingException("Could not find Backend Variant information in session under key '" + variantTypeKey + "' for Backend '" + backendMetaData.getName() + "'"));
throw (new QException("Could not find Backend Variant information in session under key '" + variantTypeKey + "' for Backend '" + backendMetaData.getName() + "'"));
}
Serializable variantId = session.getBackendVariants().get(variantTypeKey);
return variantId;
@ -84,9 +83,6 @@ public class BackendVariantsUtil
}
else
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// note, we'll consider this a programmer-error, not a user-facing one (e.g., bad submitted data), so not throw user-facing //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
throw (new QException("Backend Variant's recordLookupFunction is not of any expected type (should have been caught by instance validation??)"));
}
}
@ -103,7 +99,7 @@ public class BackendVariantsUtil
if(record == null)
{
throw (new QUserFacingException("Could not find Backend Variant in table " + backendMetaData.getBackendVariantsConfig().getOptionsTableName() + " with id '" + variantId + "'"));
throw (new QException("Could not find Backend Variant in table " + backendMetaData.getBackendVariantsConfig().getOptionsTableName() + " with id '" + variantId + "'"));
}
return record;
}

View File

@ -60,6 +60,9 @@ public class SavedBulkLoadProfile extends QRecordEntity
@QField(label = "Mapping JSON")
private String mappingJson;
@QField()
private Boolean isBulkEdit;
/*******************************************************************************
@ -251,7 +254,6 @@ public class SavedBulkLoadProfile extends QRecordEntity
/*******************************************************************************
** Getter for mappingJson
*******************************************************************************/
@ -282,4 +284,34 @@ public class SavedBulkLoadProfile extends QRecordEntity
}
/*******************************************************************************
** Getter for isBulkEdit
*******************************************************************************/
public Boolean getIsBulkEdit()
{
return (this.isBulkEdit);
}
/*******************************************************************************
** Setter for isBulkEdit
*******************************************************************************/
public void setIsBulkEdit(Boolean isBulkEdit)
{
this.isBulkEdit = isBulkEdit;
}
/*******************************************************************************
** Fluent setter for isBulkEdit
*******************************************************************************/
public SavedBulkLoadProfile withIsBulkEdit(Boolean isBulkEdit)
{
this.isBulkEdit = isBulkEdit;
return (this);
}
}

View File

@ -113,7 +113,7 @@ public class SavedBulkLoadProfileMetaDataProvider
.withFieldsFromEntity(SavedBulkLoadProfile.class)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD))
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "label", "tableName")))
.withSection(new QFieldSection("details", new QIcon().withName("text_snippet"), Tier.T2, List.of("userId", "mappingJson")))
.withSection(new QFieldSection("details", new QIcon().withName("text_snippet"), Tier.T2, List.of("userId", "mappingJson", "isBulkEdit")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
table.getField("mappingJson").withBehavior(SavedBulkLoadProfileJsonFieldDisplayValueFormatter.getInstance());

View File

@ -62,7 +62,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TablesSupportingAutomationsPossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.scripts.LoadScriptTestDetailsProcessStep;
import com.kingsrook.qqq.backend.core.processes.implementations.scripts.RunRecordScriptExtractStep;
@ -98,7 +97,6 @@ public class ScriptsMetaDataProvider
defineStandardScriptsJoins(instance);
defineStandardScriptsWidgets(instance);
instance.addPossibleValueSource(TablesPossibleValueSourceMetaDataProvider.defineTablesPossibleValueSource(instance));
instance.addPossibleValueSource(TablesSupportingAutomationsPossibleValueSourceMetaDataProvider.defineTablesPossibleValueSource(instance));
instance.addProcess(defineStoreScriptRevisionProcess());
instance.addProcess(defineTestScriptProcess());
instance.addProcess(defineLoadScriptTestDetailsProcess());
@ -176,7 +174,7 @@ public class ScriptsMetaDataProvider
.withLoadStepClass(RunRecordScriptLoadStep.class)
.getProcessMetaData();
processMetaData.withStep(0, new QFrontendStepMetaData()
processMetaData.addStep(0, new QFrontendStepMetaData()
.withName("input")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
.withFormField(new QFieldMetaData("scriptId", QFieldType.INTEGER).withPossibleValueSourceName(Script.TABLE_NAME)

View File

@ -1,123 +0,0 @@
/*
* 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.qqq.backend.core.model.tables;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.values.BasicCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** possible-value source provider for the `QQQ Table` PVS - a list of all tables
** in an application/qInstance (that you have permission to see)
*******************************************************************************/
public class QQQTableCustomPossibleValueProvider extends BasicCustomPossibleValueProvider<QRecord, Integer>
{
/***************************************************************************
**
***************************************************************************/
@Override
protected QPossibleValue<Integer> makePossibleValue(QRecord sourceObject)
{
return (new QPossibleValue<>(sourceObject.getValueInteger("id"), sourceObject.getValueString("label")));
}
/***************************************************************************
**
***************************************************************************/
@Override
protected QRecord getSourceObject(Serializable id) throws QException
{
QRecord qqqTableRecord = GetAction.execute(QQQTable.TABLE_NAME, id);
if(qqqTableRecord == null)
{
return (null);
}
QTableMetaData table = QContext.getQInstance().getTable(qqqTableRecord.getValueString("name"));
return isTableAllowed(table) ? qqqTableRecord : null;
}
/***************************************************************************
**
***************************************************************************/
@Override
protected List<QRecord> getAllSourceObjects() throws QException
{
List<QRecord> records = QueryAction.execute(QQQTable.TABLE_NAME, null);
ArrayList<QRecord> rs = new ArrayList<>();
for(QRecord record : records)
{
QTableMetaData table = QContext.getQInstance().getTable(record.getValueString("name"));
if(isTableAllowed(table))
{
rs.add(record);
}
}
return rs;
}
/***************************************************************************
**
***************************************************************************/
private boolean isTableAllowed(QTableMetaData table)
{
if(table == null)
{
return (false);
}
if(table.getIsHidden())
{
return (false);
}
PermissionCheckResult permissionCheckResult = PermissionsHelper.getPermissionCheckResult(new QueryInput(table.getName()), table);
if(!PermissionCheckResult.ALLOW.equals(permissionCheckResult))
{
return (false);
}
return (true);
}
}

View File

@ -22,29 +22,18 @@
package com.kingsrook.qqq.backend.core.model.tables;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
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.QueryOrGetInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
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.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.processes.utils.GeneralProcessUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -100,41 +89,4 @@ public class QQQTableTableManager
return getOutput.getRecord().getValueInteger("id");
}
/***************************************************************************
**
***************************************************************************/
public static List<QRecord> setRecordLinksToRecordsFromTableDynamicForPostQuery(QueryOrGetInputInterface queryInput, List<QRecord> records, String tableIdField, String recordIdField) throws QException
{
/////////////////////////////////////////////////////////////////////////////////////////
// note, this is a second copy of this logic (first being in standard process traces). //
// let the rule of 3 apply if we find ourselves copying it again //
/////////////////////////////////////////////////////////////////////////////////////////
if(queryInput.getShouldGenerateDisplayValues())
{
///////////////////////////////////////////////////////////////////////////////////////////
// for records with a table id value - look up that table name, then set a display-value //
// for the Link type adornment, to the inputRecordId record within that table. //
///////////////////////////////////////////////////////////////////////////////////////////
Set<Serializable> tableIds = records.stream().map(r -> r.getValue(tableIdField)).filter(Objects::nonNull).collect(Collectors.toSet());
if(!tableIds.isEmpty())
{
Map<Serializable, QRecord> tableMap = GeneralProcessUtils.loadTableToMap(QQQTable.TABLE_NAME, "id", new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.IN, tableIds)));
for(QRecord record : records)
{
QRecord qqqTableRecord = tableMap.get(record.getValue(tableIdField));
if(qqqTableRecord != null && record.getValue(recordIdField) != null)
{
record.setDisplayValue(recordIdField + ":" + AdornmentType.LinkValues.TO_RECORD_FROM_TABLE_DYNAMIC, qqqTableRecord.getValueString("name"));
}
}
}
}
return (records);
}
}

View File

@ -27,9 +27,6 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
@ -128,11 +125,10 @@ public class QQQTablesMetaDataProvider
public QPossibleValueSource defineQQQTablePossibleValueSource()
{
return (new QPossibleValueSource()
.withType(QPossibleValueSourceType.TABLE)
.withName(QQQTable.TABLE_NAME)
.withType(QPossibleValueSourceType.CUSTOM)
.withIdType(QFieldType.INTEGER)
.withCustomCodeReference(new QCodeReference(QQQTableCustomPossibleValueProvider.class))
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY));
.withTableName(QQQTable.TABLE_NAME))
.withOrderByField("label");
}
}

View File

@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
@ -42,7 +43,14 @@ public class MemoryCountAction implements CountInterface
{
try
{
CountOutput countOutput = MemoryRecordStore.getInstance().count(countInput);
if(CollectionUtils.nullSafeHasContents(countInput.getQueryJoins()))
{
throw (new UnsupportedOperationException("Performing counts on tables with exposed joins is currently not supported by the Memory Backend."));
}
CountOutput countOutput = new CountOutput();
countOutput.setCount(MemoryRecordStore.getInstance().count(countInput));
countOutput.setDistinctCount(countOutput.getCount());
return (countOutput);
}
catch(Exception e)

View File

@ -1,35 +0,0 @@
/*
* 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.qqq.backend.core.modules.backend.implementations.memory;
import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantSetting;
/*******************************************************************************
** since some settings are required for a variant, if you're using memory backend
** with variants, this is a setting you can use.
*******************************************************************************/
public enum MemoryModuleBackendVariantSetting implements BackendVariantSetting
{
PRIMARY_KEY
}

View File

@ -33,12 +33,10 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
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 java.util.function.Function;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.DateTimeGroupBy;
@ -56,7 +54,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.GroupBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByAggregate;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByGroupBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext;
@ -66,22 +63,17 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
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.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantsConfig;
import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantsUtil;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang3.BooleanUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -93,11 +85,8 @@ public class MemoryRecordStore
private static MemoryRecordStore instance;
//////////////////////////////////////////////////////////
// these maps are: BackendIdentifier > tableName > data //
//////////////////////////////////////////////////////////
private Map<BackendIdentifier, Map<String, Map<Serializable, QRecord>>> data;
private Map<BackendIdentifier, Map<String, Integer>> nextSerials;
private Map<String, Map<Serializable, QRecord>> data;
private Map<String, Integer> nextSerials;
private static boolean collectStatistics = false;
@ -161,31 +150,13 @@ public class MemoryRecordStore
/*******************************************************************************
**
*******************************************************************************/
private Map<Serializable, QRecord> getTableData(QTableMetaData table) throws QException
private Map<Serializable, QRecord> getTableData(QTableMetaData table)
{
BackendIdentifier backendIdentifier = getBackendIdentifier(table);
Map<String, Map<Serializable, QRecord>> dataForBackend = data.computeIfAbsent(backendIdentifier, k -> new HashMap<>());
return (dataForBackend.computeIfAbsent(table.getName(), k -> new HashMap<>()));
}
/***************************************************************************
**
***************************************************************************/
private BackendIdentifier getBackendIdentifier(QTableMetaData table) throws QException
{
BackendIdentifier backendIdentifier = NonVariant.getInstance();
QBackendMetaData backendMetaData = QContext.getQInstance().getBackend(table.getBackendName());
BackendVariantsConfig backendVariantsConfig = backendMetaData.getBackendVariantsConfig();
if(backendVariantsConfig != null)
if(!data.containsKey(table.getName()))
{
String variantType = backendMetaData.getBackendVariantsConfig().getVariantTypeKey();
QRecord variantRecord = BackendVariantsUtil.getVariantRecord(backendMetaData);
Serializable variantId = variantRecord.getValue(QContext.getQInstance().getTable(variantRecord.getTableName()).getPrimaryKeyField());
backendIdentifier = new Variant(variantType, variantId);
data.put(table.getName(), new HashMap<>());
}
return backendIdentifier;
return (data.get(table.getName()));
}
@ -340,55 +311,17 @@ public class MemoryRecordStore
/*******************************************************************************
**
*******************************************************************************/
public CountOutput count(CountInput input) throws QException
public Integer count(CountInput input) throws QException
{
////////////////////////////////////////////////////////////////////////////////////////////
// set up a query input - we'll implement count by counting the records in a query output //
////////////////////////////////////////////////////////////////////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTableName(input.getTableName());
if(input.getFilter() != null)
{
queryInput.setFilter(input.getFilter().clone().withSkip(null).withLimit(null));
}
if(input.getQueryJoins() != null)
{
queryInput.setQueryJoins(new ArrayList<>());
for(QueryJoin queryJoin : input.getQueryJoins())
{
queryInput.getQueryJoins().add(queryJoin.clone());
}
}
///////////////////
// run the query //
///////////////////
List<QRecord> queryResult = query(queryInput);
////////////////////////
// build count output //
////////////////////////
CountOutput countOutput = new CountOutput();
countOutput.setCount(queryResult.size());
//////////////////////////////////////
// figure out distinct if requested //
//////////////////////////////////////
if(BooleanUtils.isTrue(input.getIncludeDistinctCount()))
{
QTableMetaData table = QContext.getQInstance().getTable(input.getTableName());
String primaryKeyField = table.getPrimaryKeyField();
Set<Serializable> distinctValues = new HashSet<>();
for(QRecord record : queryResult)
{
distinctValues.add(record.getValue(primaryKeyField));
}
countOutput.setDistinctCount(distinctValues.size());
}
return (countOutput);
return (queryResult.size());
}
@ -396,7 +329,7 @@ public class MemoryRecordStore
/*******************************************************************************
**
*******************************************************************************/
public List<QRecord> insert(InsertInput input, boolean returnInsertedRecords) throws QException
public List<QRecord> insert(InsertInput input, boolean returnInsertedRecords)
{
incrementStatistic(input);
@ -411,7 +344,7 @@ public class MemoryRecordStore
////////////////////////////////////////
// grab the next unique serial to use //
////////////////////////////////////////
Integer nextSerial = getNextSerial(table);
Integer nextSerial = nextSerials.get(table.getName());
if(nextSerial == null)
{
nextSerial = 1;
@ -431,9 +364,6 @@ public class MemoryRecordStore
// differently from other backends, because of having the same record variable in the backend store and in the user-code. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QRecord recordToInsert = new QRecord(record);
makeValueTypesMatchFieldTypes(table, recordToInsert);
if(CollectionUtils.nullSafeHasContents(recordToInsert.getErrors()))
{
outputRecords.add(recordToInsert);
@ -477,65 +407,17 @@ public class MemoryRecordStore
}
}
setNextSerial(table, nextSerial);
nextSerials.put(table.getName(), nextSerial);
return (outputRecords);
}
/***************************************************************************
**
***************************************************************************/
private void setNextSerial(QTableMetaData table, Integer nextSerial) throws QException
{
BackendIdentifier backendIdentifier = getBackendIdentifier(table);
Map<String, Integer> nextSerialsForBackend = nextSerials.computeIfAbsent(backendIdentifier, (k) -> new HashMap<>());
nextSerialsForBackend.put(table.getName(), nextSerial);
}
/***************************************************************************
**
***************************************************************************/
private Integer getNextSerial(QTableMetaData table) throws QException
{
BackendIdentifier backendIdentifier = getBackendIdentifier(table);
Map<String, Integer> nextSerialsForBackend = nextSerials.computeIfAbsent(backendIdentifier, (k) -> new HashMap<>());
return (nextSerialsForBackend.get(table.getName()));
}
/***************************************************************************
**
***************************************************************************/
private static void makeValueTypesMatchFieldTypes(QTableMetaData table, QRecord recordToInsert)
{
for(QFieldMetaData field : table.getFields().values())
{
Serializable value = recordToInsert.getValue(field.getName());
if(value != null)
{
try
{
recordToInsert.setValue(field.getName(), ValueUtils.getValueAsFieldType(field.getType(), value));
}
catch(Exception e)
{
LOG.info("Error converting value to field's type", e, logPair("fieldName", field.getName()), logPair("value", value));
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
public List<QRecord> update(UpdateInput input, boolean returnUpdatedRecords) throws QException
public List<QRecord> update(UpdateInput input, boolean returnUpdatedRecords)
{
if(input.getRecords() == null)
{
@ -562,19 +444,7 @@ public class MemoryRecordStore
QRecord recordToUpdate = tableData.get(primaryKeyValue);
for(Map.Entry<String, Serializable> valueEntry : record.getValues().entrySet())
{
String fieldName = valueEntry.getKey();
try
{
///////////////////////////////////////////////
// try to make field values match field type //
///////////////////////////////////////////////
recordToUpdate.setValue(fieldName, ValueUtils.getValueAsFieldType(table.getField(fieldName).getType(), valueEntry.getValue()));
}
catch(Exception e)
{
LOG.info("Error converting value to field's type", e, logPair("fieldName", fieldName), logPair("value", valueEntry.getValue()));
recordToUpdate.setValue(fieldName, valueEntry.getValue());
}
recordToUpdate.setValue(valueEntry.getKey(), valueEntry.getValue());
}
if(returnUpdatedRecords)
@ -592,7 +462,7 @@ public class MemoryRecordStore
/*******************************************************************************
**
*******************************************************************************/
public int delete(DeleteInput input) throws QException
public int delete(DeleteInput input)
{
if(input.getPrimaryKeys() == null)
{
@ -1057,58 +927,4 @@ public class MemoryRecordStore
return (filter.clone());
}
}
/***************************************************************************
** key for the internal maps of this class - either for a non-variant version
** of the memory backend, or for one based on variants.
***************************************************************************/
private sealed interface BackendIdentifier permits NonVariant, Variant
{
}
/***************************************************************************
** singleton, representing non-variant instance of memory backend.
***************************************************************************/
private static final class NonVariant implements BackendIdentifier
{
private static NonVariant nonVariant = null;
/*******************************************************************************
** Singleton constructor
*******************************************************************************/
private NonVariant()
{
}
/*******************************************************************************
** Singleton accessor
*******************************************************************************/
public static NonVariant getInstance()
{
if(nonVariant == null)
{
nonVariant = new NonVariant();
}
return (nonVariant);
}
}
/***************************************************************************
** record representing a variant type & id
***************************************************************************/
private record Variant(String type, Serializable id) implements BackendIdentifier
{
}
}

View File

@ -27,14 +27,11 @@ import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
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.CriteriaOption;
@ -44,12 +41,9 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAndJoinTable;
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.NotImplementedException;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -203,7 +197,7 @@ public class BackendQueryFilterUtils
{
String fieldName = field == null ? "__unknownField" : field.getName();
ListIterator<Serializable> valueListIterator = CollectionUtils.nonNullList(criterion.getValues()).listIterator();
ListIterator<Serializable> valueListIterator = criterion.getValues().listIterator();
while(valueListIterator.hasNext())
{
Serializable criteriaValue = valueListIterator.next();
@ -727,50 +721,4 @@ public class BackendQueryFilterUtils
return regex.toString();
}
/***************************************************************************
*
***************************************************************************/
public static Set<String> identifyJoinTablesInFilter(String mainTableName, QQueryFilter filter) throws QException
{
Set<String> rs = new HashSet<>();
QTableMetaData mainTable = QContext.getQInstance().getTable(mainTableName);
for(QFilterCriteria criteria : CollectionUtils.nonNullList(filter.getCriteria()))
{
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(mainTable, criteria.getFieldName());
if(!fieldAndJoinTable.joinTable().getName().equals(mainTableName))
{
rs.add(fieldAndJoinTable.joinTable().getName());
}
if(StringUtils.hasContent(criteria.getOtherFieldName()))
{
FieldAndJoinTable otherFieldAndJoinTable = FieldAndJoinTable.get(mainTable, criteria.getOtherFieldName());
if(!otherFieldAndJoinTable.joinTable().getName().equals(mainTableName))
{
rs.add(otherFieldAndJoinTable.joinTable().getName());
}
}
}
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(filter.getOrderBys()))
{
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(mainTable, orderBy.getFieldName());
if(!fieldAndJoinTable.joinTable().getName().equals(mainTableName))
{
rs.add(fieldAndJoinTable.joinTable().getName());
}
}
for(QQueryFilter subFilter : CollectionUtils.nonNullList(filter.getSubFilters()))
{
rs.addAll(identifyJoinTablesInFilter(mainTableName, subFilter));
}
return (rs);
}
}

View File

@ -36,10 +36,12 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaUpdateStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ProcessSummaryProviderInterface;
import com.kingsrook.qqq.backend.core.processes.implementations.general.ProcessSummaryWarningsAndErrorsRollup;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.commons.lang.BooleanUtils;
import static com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep.buildInfoSummaryLines;
@ -53,6 +55,9 @@ public class BulkEditLoadStep extends LoadViaUpdateStep implements ProcessSummar
private ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK);
private List<ProcessSummaryLine> infoSummaries = new ArrayList<>();
private Serializable firstInsertedPrimaryKey = null;
private Serializable lastInsertedPrimaryKey = null;
private ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = ProcessSummaryWarningsAndErrorsRollup.build("edited");
private String tableLabel;
@ -106,7 +111,15 @@ public class BulkEditLoadStep extends LoadViaUpdateStep implements ProcessSummar
tableLabel = table.getLabel();
}
buildInfoSummaryLines(runBackendStepInput, table, infoSummaries, true);
boolean isBulkEdit = BooleanUtils.isTrue(runBackendStepInput.getValueBoolean("isBulkEdit"));
if(isBulkEdit)
{
buildBulkUpdateWithFileInfoSummaryLines(runBackendStepOutput, table);
}
else
{
buildInfoSummaryLines(runBackendStepInput, table, infoSummaries, true);
}
}
@ -146,4 +159,83 @@ public class BulkEditLoadStep extends LoadViaUpdateStep implements ProcessSummar
}
}
/***************************************************************************
**
***************************************************************************/
private void buildBulkUpdateWithFileInfoSummaryLines(RunBackendStepOutput runBackendStepOutput, QTableMetaData table)
{
/////////////////////////////////////////////////////////////////////////////////////////////
// the transform step builds summary lines that it predicts will update successfully. //
// but those lines don't have ids, which we'd like to have (e.g., for a process trace that //
// might link to the built record). also, it's possible that there was a fail that only //
// happened in the actual update, so, basically, re-do the summary here //
/////////////////////////////////////////////////////////////////////////////////////////////
BulkInsertTransformStep transformStep = (BulkInsertTransformStep) getTransformStep();
ProcessSummaryLine okSummary = transformStep.okSummary;
okSummary.setCount(0);
okSummary.setPrimaryKeys(new ArrayList<>());
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// but - since errors from the transform step don't even make it through to us here in the load step, //
// do re-use the ProcessSummaryWarningsAndErrorsRollup from transform step as follows: //
// clear out its warnings - we'll completely rebuild them here (with primary keys) //
// and add new error lines, e.g., in case of errors that only happened past the validation if possible. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = transformStep.processSummaryWarningsAndErrorsRollup;
processSummaryWarningsAndErrorsRollup.resetWarnings();
List<QRecord> updatedRecords = runBackendStepOutput.getRecords();
for(QRecord updatedRecord : updatedRecords)
{
Serializable primaryKey = updatedRecord.getValue(table.getPrimaryKeyField());
if(CollectionUtils.nullSafeIsEmpty(updatedRecord.getErrors()) && primaryKey != null)
{
/////////////////////////////////////////////////////////////////////////
// if the record had no errors, and we have a primary key for it, then //
// keep track of the range of primary keys (first and last) //
/////////////////////////////////////////////////////////////////////////
if(firstInsertedPrimaryKey == null)
{
firstInsertedPrimaryKey = primaryKey;
}
lastInsertedPrimaryKey = primaryKey;
if(!CollectionUtils.nullSafeIsEmpty(updatedRecord.getWarnings()))
{
////////////////////////////////////////////////////////////////////////////
// if there were warnings on the updated record, put it in a warning line //
////////////////////////////////////////////////////////////////////////////
String message = updatedRecord.getWarnings().get(0).getMessage();
processSummaryWarningsAndErrorsRollup.addWarning(message, primaryKey);
}
else
{
///////////////////////////////////////////////////////////////////////
// if no warnings for the updated record, then put it in the OK line //
///////////////////////////////////////////////////////////////////////
okSummary.incrementCountAndAddPrimaryKey(primaryKey);
}
}
else
{
//////////////////////////////////////////////////////////////////////
// else if there were errors or no primary key, build an error line //
//////////////////////////////////////////////////////////////////////
String message = "Failed to update";
if(!CollectionUtils.nullSafeIsEmpty(updatedRecord.getErrors()))
{
//////////////////////////////////////////////////////////
// use the error message from the record if we have one //
//////////////////////////////////////////////////////////
message = updatedRecord.getErrors().get(0).getMessage();
}
processSummaryWarningsAndErrorsRollup.addError(message, primaryKey);
}
}
okSummary.pickMessage(true);
}
}

View File

@ -48,6 +48,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.BooleanUtils;
import org.json.JSONObject;
@ -65,9 +66,11 @@ public class BulkInsertPrepareFileMappingStep implements BackendStep
{
buildFileDetailsForMappingStep(runBackendStepInput, runBackendStepOutput);
boolean isBulkEdit = BooleanUtils.isTrue(runBackendStepInput.getValueBoolean("isBulkEdit"));
String tableName = runBackendStepInput.getValueString("tableName");
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(tableName);
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(tableName, isBulkEdit);
runBackendStepOutput.addValue("tableStructure", tableStructure);
runBackendStepOutput.addValue("isBulkEdit", isBulkEdit);
boolean needSuggestedMapping = true;
if(runBackendStepOutput.getProcessState().getIsStepBack())
@ -81,7 +84,7 @@ public class BulkInsertPrepareFileMappingStep implements BackendStep
{
@SuppressWarnings("unchecked")
List<String> headerValues = (List<String>) runBackendStepOutput.getValue("headerValues");
buildSuggestedMapping(headerValues, getPrepopulatedValues(runBackendStepInput), tableStructure, runBackendStepOutput);
buildSuggestedMapping(isBulkEdit, headerValues, getPrepopulatedValues(runBackendStepInput), tableStructure, runBackendStepOutput);
}
}
@ -95,8 +98,8 @@ public class BulkInsertPrepareFileMappingStep implements BackendStep
String prepopulatedValuesJson = runBackendStepInput.getValueString("prepopulatedValues");
if(StringUtils.hasContent(prepopulatedValuesJson))
{
Map<String, Serializable> rs = new LinkedHashMap<>();
JSONObject jsonObject = JsonUtils.toJSONObject(prepopulatedValuesJson);
Map<String, Serializable> rs = new LinkedHashMap<>();
JSONObject jsonObject = JsonUtils.toJSONObject(prepopulatedValuesJson);
for(String key : jsonObject.keySet())
{
rs.put(key, jsonObject.optString(key, null));
@ -112,16 +115,16 @@ public class BulkInsertPrepareFileMappingStep implements BackendStep
/***************************************************************************
**
***************************************************************************/
private void buildSuggestedMapping(List<String> headerValues, Map<String, Serializable> prepopulatedValues, BulkLoadTableStructure tableStructure, RunBackendStepOutput runBackendStepOutput)
private void buildSuggestedMapping(boolean isBulkEdit, List<String> headerValues, Map<String, Serializable> prepopulatedValues, BulkLoadTableStructure tableStructure, RunBackendStepOutput runBackendStepOutput)
{
BulkLoadMappingSuggester bulkLoadMappingSuggester = new BulkLoadMappingSuggester();
BulkLoadProfile bulkLoadProfile = bulkLoadMappingSuggester.suggestBulkLoadMappingProfile(tableStructure, headerValues);
BulkLoadProfile bulkLoadProfile = bulkLoadMappingSuggester.suggestBulkLoadMappingProfile(tableStructure, headerValues, isBulkEdit);
if(CollectionUtils.nullSafeHasContents(prepopulatedValues))
{
for(Map.Entry<String, Serializable> entry : prepopulatedValues.entrySet())
{
String fieldName = entry.getKey();
String fieldName = entry.getKey();
boolean foundFieldInProfile = false;
for(BulkLoadProfileField bulkLoadProfileField : bulkLoadProfile.getFieldList())

View File

@ -65,10 +65,12 @@ public class BulkInsertPrepareFileUploadStep implements BackendStep
runBackendStepOutput.addValue("theFile", null);
}
boolean isBulkEdit = runBackendStepInput.getProcessName().endsWith("EditWithFile");
String tableName = runBackendStepInput.getValueString("tableName");
QTableMetaData table = QContext.getQInstance().getTable(tableName);
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(tableName);
runBackendStepOutput.addValue("tableStructure", tableStructure);
runBackendStepOutput.addValue("isBulkEdit", isBulkEdit);
List<QFieldMetaData> requiredFields = new ArrayList<>();
List<QFieldMetaData> additionalFields = new ArrayList<>();
@ -84,6 +86,14 @@ public class BulkInsertPrepareFileUploadStep implements BackendStep
}
}
/////////////////////////////////////////////
// bulk edit allows primary key as a field //
/////////////////////////////////////////////
if(isBulkEdit)
{
requiredFields.add(0, table.getField(table.getPrimaryKeyField()));
}
StringBuilder html;
String childTableLabels = "";
@ -96,11 +106,11 @@ public class BulkInsertPrepareFileUploadStep implements BackendStep
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
boolean listFieldsInHelpText = false;
if(!CollectionUtils.nullSafeHasContents(tableStructure.getAssociations()))
if(isBulkEdit || !CollectionUtils.nullSafeHasContents(tableStructure.getAssociations()))
{
html = new StringBuilder("""
<p>Upload either a CSV or Excel (.xlsx) file, with one row for each record you want to
insert in the ${tableLabel} table.</p><br />
${action} in the ${tableLabel} table.</p><br />
<p>Your file can contain any number of columns. You will be prompted to map fields from
the ${tableLabel} table to columns from your file or default values for all records that
@ -204,6 +214,7 @@ public class BulkInsertPrepareFileUploadStep implements BackendStep
finishCSV(flatCSV);
String htmlString = html.toString()
.replace("${action}", (isBulkEdit ? "edit" : "insert"))
.replace("${tableLabel}", table.getLabel())
.replace("${childTableLabels}", childTableLabels)
.replace("${flatCSV}", Base64.getEncoder().encodeToString(flatCSV.toString().getBytes(StandardCharsets.UTF_8)))

View File

@ -113,6 +113,8 @@ public class BulkInsertStepUtils
{
String layout = runBackendStepInput.getValueString("layout");
Boolean hasHeaderRow = runBackendStepInput.getValueBoolean("hasHeaderRow");
String keyFields = runBackendStepInput.getValueString("keyFields");
Boolean isBulkEdit = runBackendStepInput.getValueBoolean("isBulkEdit");
ArrayList<BulkLoadProfileField> fieldList = new ArrayList<>();
@ -127,6 +129,7 @@ public class BulkInsertStepUtils
bulkLoadProfileField.setColumnIndex(jsonObject.has("columnIndex") ? jsonObject.getInt("columnIndex") : null);
bulkLoadProfileField.setDefaultValue((Serializable) jsonObject.opt("defaultValue"));
bulkLoadProfileField.setDoValueMapping(jsonObject.optBoolean("doValueMapping"));
bulkLoadProfileField.setClearIfEmpty(jsonObject.optBoolean("clearIfEmpty"));
if(BooleanUtils.isTrue(bulkLoadProfileField.getDoValueMapping()) && jsonObject.has("valueMappings"))
{
@ -140,6 +143,8 @@ public class BulkInsertStepUtils
}
BulkLoadProfile bulkLoadProfile = new BulkLoadProfile()
.withIsBulkEdit(isBulkEdit)
.withKeyFields(keyFields)
.withVersion(version)
.withFieldList(fieldList)
.withHasHeaderRow(hasHeaderRow)
@ -213,7 +218,7 @@ public class BulkInsertStepUtils
{
return (processTracerKeyRecordMessage);
}
return (null);
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
@ -32,12 +33,13 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer.WhenToRun;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@ -48,6 +50,11 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutp
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
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.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
@ -68,6 +75,9 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.BooleanUtils;
import org.json.JSONArray;
import org.json.JSONObject;
/*******************************************************************************
@ -75,9 +85,9 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
*******************************************************************************/
public class BulkInsertTransformStep extends AbstractTransformStep
{
ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK);
public ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK);
ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = ProcessSummaryWarningsAndErrorsRollup.build("inserted")
public ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = ProcessSummaryWarningsAndErrorsRollup.build("inserted")
.withDoReplaceSingletonCountLinesWithSuffixOnly(false);
private ListingHash<String, RowValue> errorToExampleRowValueMap = new ListingHash<>();
@ -190,6 +200,252 @@ public class BulkInsertTransformStep extends AbstractTransformStep
QTableMetaData table = QContext.getQInstance().getTable(runBackendStepInput.getTableName());
List<QRecord> records = runBackendStepInput.getRecords();
if(BooleanUtils.isTrue(runBackendStepInput.getValueBoolean("isBulkEdit")))
{
handleBulkEdit(runBackendStepInput, runBackendStepOutput, records, table);
runBackendStepOutput.addValue("isBulkEdit", true);
}
else
{
handleBulkLoad(runBackendStepInput, runBackendStepOutput, records, table);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void handleBulkEdit(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput, List<QRecord> records, QTableMetaData table) throws QException
{
///////////////////////////////////////////
// get the key fields for this bulk edit //
///////////////////////////////////////////
String keyFieldsString = runBackendStepInput.getValueString("keyFields");
List<String> keyFields = Arrays.asList(keyFieldsString.split("\\|"));
//////////////////////////////////////////////////////////////////////////
// if the key field is the primary key, then just look up those records //
//////////////////////////////////////////////////////////////////////////
List<QRecord> nonMatchingRecords = new ArrayList<>();
List<QRecord> oldRecords = new ArrayList<>();
List<QRecord> recordsToUpdate = new ArrayList<>();
if(keyFields.size() == 1 && table.getPrimaryKeyField().equals(keyFields.get(0)))
{
recordsToUpdate = records;
String primaryKeyName = table.getPrimaryKeyField();
List<Serializable> primaryKeys = records.stream().map(record -> record.getValue(primaryKeyName)).toList();
oldRecords = new QueryAction().execute(new QueryInput(table.getName()).withFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, primaryKeys)))).getRecords();
///////////////////////////////////////////
// get a set of old records primary keys //
///////////////////////////////////////////
Set<Serializable> matchedPrimaryKeys = oldRecords.stream()
.map(r -> r.getValue(table.getPrimaryKeyField()))
.collect(java.util.stream.Collectors.toSet());
////////////////////////////////////////////////////////////////////////////////////////////////////
// iterate over file records and if primary keys dont match, add to the non matching records list //
////////////////////////////////////////////////////////////////////////////////////////////////////
for(QRecord record : records)
{
Serializable recordKey = record.getValue(table.getPrimaryKeyField());
if(!matchedPrimaryKeys.contains(recordKey))
{
nonMatchingRecords.add(record);
}
}
}
else
{
Set<Serializable> uniqueIds = new HashSet<>();
List<QRecord> potentialRecords = new ArrayList<>();
////////////////////////////////////////////////////////////////////////////////////////////////////
// if not using the primary key, then we will look up all records for each part of the unique key //
// and for each found, if all unique parts match we will add to our list of database records //
////////////////////////////////////////////////////////////////////////////////////////////////////
for(String uniqueKeyPart : keyFields)
{
List<Serializable> values = records.stream().map(record -> record.getValue(uniqueKeyPart)).toList();
for(QRecord databaseRecord : new QueryAction().execute(new QueryInput(table.getName()).withFilter(new QQueryFilter(new QFilterCriteria(uniqueKeyPart, QCriteriaOperator.IN, values)))).getRecords())
{
if(!uniqueIds.contains(databaseRecord.getValue(table.getPrimaryKeyField())))
{
potentialRecords.add(databaseRecord);
uniqueIds.add(databaseRecord.getValue(table.getPrimaryKeyField()));
}
}
}
///////////////////////////////////////////////////////////////////////////////
// now iterate over all of the potential records checking each unique fields //
///////////////////////////////////////////////////////////////////////////////
fileRecordLoop:
for(QRecord fileRecord : records)
{
for(QRecord databaseRecord : potentialRecords)
{
boolean allMatch = true;
for(String uniqueKeyPart : keyFields)
{
if(!Objects.equals(fileRecord.getValue(uniqueKeyPart), databaseRecord.getValue(uniqueKeyPart)))
{
allMatch = false;
}
}
//////////////////////////////////////////////////////////////////////////////////////
// if we get here with all matching, update the record from the file's primary key, //
// add it to the list to update, and continue looping over file records //
//////////////////////////////////////////////////////////////////////////////////////
if(allMatch)
{
oldRecords.add(databaseRecord);
fileRecord.setValue(table.getPrimaryKeyField(), databaseRecord.getValue(table.getPrimaryKeyField()));
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// iterate over the fields in the bulk load profile, if the value for that field is empty and the value //
// of 'clear if empty' is set to true, then update the record to update with the old record's value //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
JSONArray array = new JSONArray(runBackendStepInput.getValueString("fieldListJSON"));
for(int i = 0; i < array.length(); i++)
{
JSONObject jsonObject = array.getJSONObject(i);
String fieldName = jsonObject.optString("fieldName");
boolean clearIfEmpty = jsonObject.optBoolean("clearIfEmpty");
if(fileRecord.getValue(fieldName) == null)
{
if(clearIfEmpty)
{
fileRecord.setValue(fieldName, null);
}
else
{
fileRecord.setValue(fieldName, databaseRecord.getValue(fieldName));
}
}
}
recordsToUpdate.add(fileRecord);
continue fileRecordLoop;
}
}
///////////////////////////////////////////////////////////////////////////////////////
// if we make it here, that means the record was not found, keep for logging warning //
///////////////////////////////////////////////////////////////////////////////////////
nonMatchingRecords.add(fileRecord);
}
}
for(QRecord missingRecord : CollectionUtils.nonNullList(nonMatchingRecords))
{
String message = "Did not have a matching existing record.";
processSummaryWarningsAndErrorsRollup.addError(message, null);
addToErrorToExampleRowMap(message, missingRecord);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// set up an insert-input, which will be used as input to the pre-customizer as well as for additional validations //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
UpdateInput updateInput = new UpdateInput(table.getName());
updateInput.setInputSource(QInputSource.USER);
updateInput.setRecords(recordsToUpdate);
//////////////////////////////////////////////////////////////////////
// load the pre-insert customizer and set it up, if there is one //
// then we'll run it based on its WhenToRun value //
// we do this, in case it needs to, for example, adjust values that //
// are part of a unique key //
//////////////////////////////////////////////////////////////////////
boolean didAlreadyRunCustomizer = false;
Optional<TableCustomizerInterface> preUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
if(preUpdateCustomizer.isPresent())
{
List<QRecord> recordsAfterCustomizer = preUpdateCustomizer.get().preUpdate(updateInput, records, true, Optional.of(oldRecords));
runBackendStepInput.setRecords(recordsAfterCustomizer);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// so we used to have a comment here asking "do we care if the customizer runs both now, and in the validation below?" //
// when implementing Bulk Load V2, we were seeing that some customizers were adding errors to records, both now, and //
// when they ran below. so, at that time, we added this boolean, to track and avoid the double-run... //
// we could also imagine this being a setting on the pre-insert customizer, similar to its whenToRun attribute... //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
didAlreadyRunCustomizer = true;
}
/////////////////////////////////////////////////////////////////////////////////
// run all validation from the insert action - in Preview mode (boolean param) //
/////////////////////////////////////////////////////////////////////////////////
updateInput.setRecords(recordsToUpdate);
UpdateAction updateAction = new UpdateAction();
updateAction.performValidations(updateInput, Optional.of(recordsToUpdate), didAlreadyRunCustomizer);
List<QRecord> validationResultRecords = updateInput.getRecords();
/////////////////////////////////////////////////////////////////
// look at validation results to build process summary results //
/////////////////////////////////////////////////////////////////
List<QRecord> outputRecords = new ArrayList<>();
for(QRecord record : validationResultRecords)
{
List<QErrorMessage> errorsFromAssociations = getErrorsFromAssociations(record);
if(CollectionUtils.nullSafeHasContents(errorsFromAssociations))
{
List<QErrorMessage> recordErrors = Objects.requireNonNullElseGet(record.getErrors(), () -> new ArrayList<>());
recordErrors.addAll(errorsFromAssociations);
record.setErrors(recordErrors);
}
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
{
for(QErrorMessage error : record.getErrors())
{
if(error instanceof AbstractBulkLoadRollableValueError rollableValueError)
{
processSummaryWarningsAndErrorsRollup.addError(rollableValueError.getMessageToUseAsProcessSummaryRollupKey(), null);
addToErrorToExampleRowValueMap(rollableValueError, record);
}
else
{
processSummaryWarningsAndErrorsRollup.addError(error.getMessage(), null);
addToErrorToExampleRowMap(error.getMessage(), record);
}
}
}
else if(CollectionUtils.nullSafeHasContents(record.getWarnings()))
{
String message = record.getWarnings().get(0).getMessage();
processSummaryWarningsAndErrorsRollup.addWarning(message, null);
outputRecords.add(record);
}
else
{
okSummary.incrementCountAndAddPrimaryKey(null);
outputRecords.add(record);
for(Map.Entry<String, List<QRecord>> entry : CollectionUtils.nonNullMap(record.getAssociatedRecords()).entrySet())
{
String associationName = entry.getKey();
ProcessSummaryLine associationToInsertLine = associationsToInsertSummaries.computeIfAbsent(associationName, x -> new ProcessSummaryLine(Status.OK));
associationToInsertLine.incrementCount(CollectionUtils.nonNullList(entry.getValue()).size());
}
}
}
runBackendStepOutput.setRecords(outputRecords);
this.rowsProcessed += records.size();
}
/*******************************************************************************
**
*******************************************************************************/
private void handleBulkLoad(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput, List<QRecord> records, QTableMetaData table) throws QException
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// set up an insert-input, which will be used as input to the pre-customizer as well as for additional validations //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -209,7 +465,7 @@ public class BulkInsertTransformStep extends AbstractTransformStep
Optional<TableCustomizerInterface> preInsertCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_INSERT_RECORD.getRole());
if(preInsertCustomizer.isPresent())
{
AbstractPreInsertCustomizer.WhenToRun whenToRun = preInsertCustomizer.get().whenToRunPreInsert(insertInput, true);
WhenToRun whenToRun = preInsertCustomizer.get().whenToRunPreInsert(insertInput, true);
if(WhenToRun.BEFORE_ALL_VALIDATIONS.equals(whenToRun) || WhenToRun.BEFORE_UNIQUE_KEY_CHECKS.equals(whenToRun))
{
List<QRecord> recordsAfterCustomizer = preInsertCustomizer.get().preInsert(insertInput, records, true);
@ -485,11 +741,13 @@ public class BulkInsertTransformStep extends AbstractTransformStep
recordsProcessedLine.withPluralFutureMessage("records were");
recordsProcessedLine.withPluralPastMessage("records were");
String noWarningsSuffix = processSummaryWarningsAndErrorsRollup.countWarnings() == 0 ? "" : " with no warnings";
okSummary.setSingularFutureMessage(tableLabel + " record will be inserted" + noWarningsSuffix + ".");
okSummary.setPluralFutureMessage(tableLabel + " records will be inserted" + noWarningsSuffix + ".");
okSummary.setSingularPastMessage(tableLabel + " record was inserted" + noWarningsSuffix + ".");
okSummary.setPluralPastMessage(tableLabel + " records were inserted" + noWarningsSuffix + ".");
boolean isBulkEdit = BooleanUtils.isTrue(runBackendStepOutput.getValueBoolean("isBulkEdit"));
String action = isBulkEdit ? "updated" : "inserted";
String noWarningsSuffix = processSummaryWarningsAndErrorsRollup.countWarnings() == 0 ? "" : " with no warnings";
okSummary.setSingularFutureMessage(tableLabel + " record will be " + action + noWarningsSuffix + ".");
okSummary.setPluralFutureMessage(tableLabel + " records will be " + action + noWarningsSuffix + ".");
okSummary.setSingularPastMessage(tableLabel + " record was " + action + noWarningsSuffix + ".");
okSummary.setPluralPastMessage(tableLabel + " records were " + action + noWarningsSuffix + ".");
okSummary.pickMessage(isForResultScreen);
okSummary.addSelfToListIfAnyCount(rs);
@ -502,10 +760,10 @@ public class BulkInsertTransformStep extends AbstractTransformStep
String associationLabel = associationTable.getLabel();
ProcessSummaryLine line = entry.getValue();
line.setSingularFutureMessage(associationLabel + " record will be inserted.");
line.setPluralFutureMessage(associationLabel + " records will be inserted.");
line.setSingularPastMessage(associationLabel + " record was inserted.");
line.setPluralPastMessage(associationLabel + " records were inserted.");
line.setSingularFutureMessage(associationLabel + " record will be " + action + ".");
line.setPluralFutureMessage(associationLabel + " records will be " + action + ".");
line.setSingularPastMessage(associationLabel + " record was " + action + ".");
line.setPluralPastMessage(associationLabel + " records were " + action + ".");
line.pickMessage(isForResultScreen);
line.addSelfToListIfAnyCount(rs);
}
@ -518,8 +776,8 @@ public class BulkInsertTransformStep extends AbstractTransformStep
ukErrorSummary
.withMessageSuffix(" inserted, because of duplicate values in a unique key on the fields (" + uniqueKey.getDescription(table) + "), with values"
+ (ukErrorSummary.areThereMoreSampleValues ? " such as: " : ": ")
+ StringUtils.joinWithCommasAndAnd(new ArrayList<>(ukErrorSummary.sampleValues)))
+ (ukErrorSummary.areThereMoreSampleValues ? " such as: " : ": ")
+ StringUtils.joinWithCommasAndAnd(new ArrayList<>(ukErrorSummary.sampleValues)))
.withSingularFutureMessage(" record will not be")
.withPluralFutureMessage(" records will not be")

View File

@ -54,7 +54,7 @@ public class BulkLoadMappingSuggester
/***************************************************************************
**
***************************************************************************/
public BulkLoadProfile suggestBulkLoadMappingProfile(BulkLoadTableStructure tableStructure, List<String> headerRow)
public BulkLoadProfile suggestBulkLoadMappingProfile(BulkLoadTableStructure tableStructure, List<String> headerRow, boolean isBulkEdit)
{
massagedHeadersWithoutNumbersToIndexMap = new LinkedHashMap<>();
for(int i = 0; i < headerRow.size(); i++)
@ -90,6 +90,7 @@ public class BulkLoadMappingSuggester
.withVersion("v1")
.withLayout(layout)
.withHasHeaderRow(true)
.withIsBulkEdit(isBulkEdit)
.withFieldList(fieldList);
return (bulkLoadProfile);

Some files were not shown because too many files have changed in this diff Show More