diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 00000000..0ef02745
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,77 @@
+version: 2.1
+
+executors:
+ java17:
+ docker:
+ - image: 'cimg/openjdk:17.0'
+ resource_class: small
+
+orbs:
+ slack: circleci/slack@4.10.1
+
+commands:
+ run_maven:
+ parameters:
+ maven_subcommand:
+ default: test
+ type: string
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - v1-dependencies-{{ checksum "pom.xml" }}
+ - run:
+ name: Run Maven
+ command: |
+ mvn -s .circleci/mvn-settings.xml << parameters.maven_subcommand >>
+ - run:
+ name: Save test results
+ command: |
+ mkdir -p ~/test-results/junit/
+ find . -type f -regex ".*/target/surefire-reports/.*xml" -exec cp {} ~/test-results/junit/ \;
+ when: always
+ - store_test_results:
+ path: ~/test-results
+ - save_cache:
+ paths:
+ - ~/.m2
+ key: v1-dependencies-{{ checksum "pom.xml" }}
+
+jobs:
+ mvn_test:
+ executor: java17
+ steps:
+ - run_maven:
+ maven_subcommand: test
+ - slack/notify:
+ event: fail
+
+ mvn_deploy:
+ executor: java17
+ steps:
+ - run_maven:
+ maven_subcommand: deploy
+ - slack/notify:
+ event: always
+
+workflows:
+ test_only:
+ jobs:
+ - mvn_test:
+ context: [ qqq-maven-registry-credentials, kingsrook-slack ]
+ filters:
+ branches:
+ ignore: /dev/
+ tags:
+ ignore: /(version|snapshot)-.*/
+
+ deploy:
+ jobs:
+ - mvn_deploy:
+ context: [ qqq-maven-registry-credentials, kingsrook-slack ]
+ filters:
+ branches:
+ only: /dev/
+ tags:
+ only: /(version|snapshot)-.*/
+
diff --git a/.circleci/mvn-settings.xml b/.circleci/mvn-settings.xml
new file mode 100644
index 00000000..b2a345f0
--- /dev/null
+++ b/.circleci/mvn-settings.xml
@@ -0,0 +1,9 @@
+
+
+
+ github-qqq-maven-registry
+ ${env.QQQ_MAVEN_REGISTRY_USERNAME}
+ ${env.QQQ_MAVEN_REGISTRY_PASSWORD}
+
+
+
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
deleted file mode 100644
index 80d00dfd..00000000
--- a/.github/workflows/maven.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
-# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
-
-name: Java CI with Maven
-
-on:
- push:
- branches: [ main ]
- pull_request:
- branches: [ main ]
-
-jobs:
- build:
-
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v2
- - name: Set up JDK 17
- uses: actions/setup-java@v2
- with:
- java-version: '17'
- distribution: 'adopt'
- cache: maven
- - name: maven-settings-xml-action
- uses: whelk-io/maven-settings-xml-action@v20
- with:
- servers: '[{ "id": "github-qqq-maven-registry", "username": "${{ secrets.QQQ_MAVEN_REGISTRY_USERNAME }}", "password": "${{ secrets.QQQ_MAVEN_REGISTRY_PASSWORD }}" }]'
- repositories: '[{ "id": "github-qqq-maven-registry", "url": "https://maven.pkg.github.com/Kingsrook/qqq-maven-registry", "snapshots": { "enabled": "true" }}]'
- - name: Build with Maven
- run: mvn -B package --file pom.xml
- - name: Publish to GitHub Packages Apache Maven
- run: mvn deploy
- env:
- GITHUB_TOKEN: ${{ github.token }}
diff --git a/README.md b/README.md
index fac125e2..672d87d7 100644
--- a/README.md
+++ b/README.md
@@ -6,8 +6,7 @@ This is a qqq middleware module, providing [picocli](https://picocli.info) acces
QQQ - Low-code Application Framework for Engineers. \
Copyright (C) 2022. Kingsrook, LLC \
651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States \
-contact@kingsrook.com
-https://github.com/Kingsrook/intellij-commentator-plugin
+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
diff --git a/checkstyle.xml b/checkstyle.xml
index dbaa3479..76f872ed 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -46,6 +46,7 @@
-->
+
@@ -171,7 +172,7 @@
-
+
-
+
4.0.0
com.kingsrook.qqq
qqq-middleware-picocli
- 0.0-SNAPSHOT
+ 0.0.0
+
+
+ scm:git:git@github.com:Kingsrook/qqq-middleware-picocli.git
+ scm:git:git@github.com:Kingsrook/qqq-middleware-picocli.git
+ HEAD
+
@@ -47,12 +51,12 @@
com.kingsrook.qqq
qqq-backend-core
- 0.0-SNAPSHOT
+ 0.0.0
com.kingsrook.qqq
qqq-backend-module-rdbms
- 0.0-SNAPSHOT
+ 0.0.0
test
@@ -65,7 +69,7 @@
com.h2database
h2
- 1.4.197
+ 2.1.210
test
@@ -78,12 +82,12 @@
org.apache.logging.log4j
log4j-api
- 2.15.0
+ 2.17.1
org.apache.logging.log4j
log4j-core
- 2.15.0
+ 2.17.1
org.junit.jupiter
@@ -156,12 +160,28 @@
+
+ com.amashchenko.maven.plugin
+ gitflow-maven-plugin
+ 1.18.0
+
+
+ main
+ dev
+ version-
+
+ true
+ install
+ true
+ 1
+
+
- github
+ github-qqq-maven-registry
GitHub QQQ Maven Registry
https://maven.pkg.github.com/Kingsrook/qqq-maven-registry
diff --git a/src/main/java/com/kingsrook/qqq/frontend/picocli/PicoCliProcessCallback.java b/src/main/java/com/kingsrook/qqq/frontend/picocli/PicoCliProcessCallback.java
new file mode 100644
index 00000000..b4cf0a70
--- /dev/null
+++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/PicoCliProcessCallback.java
@@ -0,0 +1,87 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.frontend.picocli;
+
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Scanner;
+import com.kingsrook.qqq.backend.core.callbacks.QProcessCallback;
+import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
+import picocli.CommandLine;
+
+
+/*******************************************************************************
+ ** Define how a PicoCLI process gets data back to a QProcess.
+ *******************************************************************************/
+public class PicoCliProcessCallback implements QProcessCallback
+{
+ private final CommandLine commandLine;
+
+
+
+ /*******************************************************************************
+ ** Constructor that takes the picocli CommandLine object
+ *******************************************************************************/
+ public PicoCliProcessCallback(CommandLine commandLine)
+ {
+ this.commandLine = commandLine;
+ }
+
+
+
+ /*******************************************************************************
+ ** Get the filter query for this callback.
+ *******************************************************************************/
+ @Override
+ public QQueryFilter getQueryFilter()
+ {
+ return null;
+ }
+
+
+
+ /*******************************************************************************
+ ** Get the field values for this callback.
+ *******************************************************************************/
+ @Override
+ public Map getFieldValues(List fields)
+ {
+ Map rs = new HashMap<>();
+ final Scanner scanner = new Scanner(System.in);
+
+ ///////////////////////////////////
+ // todo - only if "interactive?" //
+ ///////////////////////////////////
+ for(QFieldMetaData field : fields)
+ {
+ commandLine.getOut().println("Please supply a value for the field: [" + field.getLabel() + "]:");
+ rs.put(field.getName(), scanner.nextLine());
+ }
+
+ return (rs);
+ }
+
+}
diff --git a/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java b/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java
index 3f5f0538..9d2525df 100644
--- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java
+++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java
@@ -25,12 +25,16 @@ package com.kingsrook.qqq.frontend.picocli;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
import picocli.CommandLine;
@@ -94,9 +98,27 @@ public class QCommandBuilder
List processes = qInstance.getProcessesForTable(tableName);
if(CollectionUtils.nullSafeHasContents(processes))
{
- tableCommand.addSubcommand("process", defineTableProcessesCommand(table, processes));
+ tableCommand.addSubcommand("process", defineProcessesCommand(processes));
}
});
+
+ ///////////////////////////////////////////////////////////////////////////
+ // add all orphan processes (e.g., ones without tables) to the top-level //
+ ///////////////////////////////////////////////////////////////////////////
+ List orphanProcesses = new ArrayList<>();
+ for(QProcessMetaData process : qInstance.getProcesses().values())
+ {
+ if(!StringUtils.hasContent(process.getTableName()))
+ {
+ orphanProcesses.add(process);
+ }
+ }
+
+ if(!orphanProcesses.isEmpty())
+ {
+ topCommandSpec.addSubcommand("processes", defineProcessesCommand(orphanProcesses));
+ }
+
return topCommandSpec;
}
@@ -233,14 +255,34 @@ public class QCommandBuilder
/*******************************************************************************
**
*******************************************************************************/
- private CommandLine.Model.CommandSpec defineTableProcessesCommand(QTableMetaData table, List processes)
+ private CommandLine.Model.CommandSpec defineProcessesCommand(List processes)
{
CommandLine.Model.CommandSpec processesCommand = CommandLine.Model.CommandSpec.create();
for(QProcessMetaData process : processes)
{
+ ///////////////////////////////////////////
+ // add the sub-command to run the proces //
+ ///////////////////////////////////////////
CommandLine.Model.CommandSpec processCommand = CommandLine.Model.CommandSpec.create();
processesCommand.addSubcommand(process.getName(), processCommand);
+
+ //////////////////////////////////////////////////////////////////////////////////
+ // add all (distinct, by name) input fields to the command as --field-* options //
+ //////////////////////////////////////////////////////////////////////////////////
+ Map inputFieldMap = new LinkedHashMap<>();
+ for(QFieldMetaData inputField : process.getInputFields())
+ {
+ inputFieldMap.put(inputField.getName(), inputField);
+ }
+
+ for(QFieldMetaData field : inputFieldMap.values())
+ {
+ processCommand.addOption(CommandLine.Model.OptionSpec.builder("--field-" + field.getName())
+ .type(getClassForField(field))
+ .build());
+ }
+
}
return (processesCommand);
diff --git a/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java
index fe97a991..1302cb94 100644
--- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java
+++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java
@@ -36,7 +36,7 @@ import com.kingsrook.qqq.backend.core.actions.DeleteAction;
import com.kingsrook.qqq.backend.core.actions.InsertAction;
import com.kingsrook.qqq.backend.core.actions.MetaDataAction;
import com.kingsrook.qqq.backend.core.actions.QueryAction;
-import com.kingsrook.qqq.backend.core.actions.RunFunctionAction;
+import com.kingsrook.qqq.backend.core.actions.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction;
import com.kingsrook.qqq.backend.core.actions.UpdateAction;
import com.kingsrook.qqq.backend.core.adapters.CsvToQRecordAdapter;
@@ -53,6 +53,8 @@ import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataRequest;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataResult;
import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataRequest;
import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataResult;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessRequest;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessResult;
import com.kingsrook.qqq.backend.core.model.actions.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter;
@@ -62,6 +64,7 @@ import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.AbstractQFiel
import com.kingsrook.qqq.backend.core.model.actions.update.UpdateRequest;
import com.kingsrook.qqq.backend.core.model.actions.update.UpdateResult;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
@@ -91,7 +94,7 @@ public class QPicoCliImplementation
public static final int DEFAULT_LIMIT = 20;
private static QInstance qInstance;
- private static QSession session;
+ private static QSession session;
@@ -106,14 +109,14 @@ public class QPicoCliImplementation
// parse args to look up metaData and prime instance
if(args.length > 0 && args[0].startsWith("--qInstanceJsonFile="))
{
- String filePath = args[0].replaceFirst("--.*=", "");
+ String filePath = args[0].replaceFirst("--.*=", "");
String qInstanceJson = FileUtils.readFileToString(new File(filePath));
qInstance = new QInstanceAdapter().jsonToQInstanceIncludingBackends(qInstanceJson);
String[] subArgs = Arrays.copyOfRange(args, 1, args.length);
QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance);
- int exitCode = qPicoCliImplementation.runCli("qapi", subArgs);
+ int exitCode = qPicoCliImplementation.runCli("qapi", subArgs);
System.exit(exitCode);
}
else
@@ -226,7 +229,7 @@ public class QPicoCliImplementation
private static void setupSession(String[] args) throws QModuleDispatchException
{
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
- QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication());
+ QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication());
// todo - does this need some per-provider logic actually? mmm...
Map authenticationContext = new HashMap<>();
@@ -247,9 +250,23 @@ public class QPicoCliImplementation
}
else
{
- String subCommandName = parseResult.subcommand().commandSpec().name();
+ ParseResult subParseResult = parseResult.subcommand();
+ String subCommandName = subParseResult.commandSpec().name();
CommandLine subCommandLine = commandLine.getSubcommands().get(subCommandName);
- return runTableLevelCommand(subCommandLine, parseResult.subcommand());
+ switch(subCommandName)
+ {
+ case "processes":
+ {
+ return runProcessCommand(subCommandLine, subParseResult);
+ }
+ default:
+ {
+ /////////////////////////////////////////////////////////
+ // by default, assume the command here is a table name //
+ /////////////////////////////////////////////////////////
+ return runTableLevelCommand(subCommandLine, subParseResult);
+ }
+ }
}
}
@@ -265,7 +282,7 @@ public class QPicoCliImplementation
if(tableParseResult.hasSubcommand())
{
ParseResult subParseResult = tableParseResult.subcommand();
- String subCommandName = subParseResult.commandSpec().name();
+ String subCommandName = subParseResult.commandSpec().name();
switch(subCommandName)
{
case "meta-data":
@@ -291,7 +308,7 @@ public class QPicoCliImplementation
case "process":
{
CommandLine subCommandLine = commandLine.getSubcommands().get(subCommandName);
- return runTableProcess(subCommandLine, tableName, subParseResult);
+ return runProcessCommand(subCommandLine, subParseResult);
}
default:
{
@@ -311,35 +328,74 @@ public class QPicoCliImplementation
/*******************************************************************************
- **
+ ** Handle a command up to the point where 'process' was given
*******************************************************************************/
- private int runTableProcess(CommandLine commandLine, String tableName, ParseResult subParseResult)
+ private int runProcessCommand(CommandLine commandLine, ParseResult subParseResult)
{
if(!subParseResult.hasSubcommand())
{
+ ////////////////////////////////////////////////////////////////
+ // process name must be a sub-command, so, error if not given //
+ ////////////////////////////////////////////////////////////////
commandLine.usage(commandLine.getOut());
return commandLine.getCommandSpec().exitCodeOnUsageHelp();
}
else
{
- String subCommandName = subParseResult.subcommand().commandSpec().name();
+ ///////////////////////////////////////////
+ // move on to running the actual process //
+ ///////////////////////////////////////////
+ String subCommandName = subParseResult.subcommand().commandSpec().name();
CommandLine subCommandLine = commandLine.getSubcommands().get(subCommandName);
- return runTableProcessLevelCommand(subCommandLine, tableName, subParseResult.subcommand());
+ return runActualProcess(subCommandLine, subParseResult.subcommand());
}
}
/*******************************************************************************
- **
+ ** actually run a process (the process name should be at the start of the sub-command line)
*******************************************************************************/
- private int runTableProcessLevelCommand(CommandLine subCommandLine, String tableName, ParseResult processParseResult)
+ private int runActualProcess(CommandLine subCommandLine, ParseResult processParseResult)
{
- String processName = processParseResult.commandSpec().name();
- QTableMetaData table = qInstance.getTable(tableName);
- QProcessMetaData process = qInstance.getProcess(processName);
- RunFunctionAction runFunctionAction = new RunFunctionAction();
- // todo!
+ String processName = processParseResult.commandSpec().name();
+ QProcessMetaData process = qInstance.getProcess(processName);
+ RunProcessRequest request = new RunProcessRequest(qInstance);
+
+ request.setSession(session);
+ request.setProcessName(processName);
+ request.setCallback(new PicoCliProcessCallback(subCommandLine));
+
+ for(OptionSpec matchedOption : processParseResult.matchedOptions())
+ {
+ if(matchedOption.longestName().startsWith("--field-"))
+ {
+ String fieldName = matchedOption.longestName().substring(8);
+ request.addValue(fieldName, matchedOption.getValue());
+ }
+ }
+
+ try
+ {
+ RunProcessResult result = new RunProcessAction().execute(request);
+ subCommandLine.getOut().println("Process Results: "); // todo better!!
+ for(QFieldMetaData outputField : process.getOutputFields())
+ {
+ subCommandLine.getOut().format(" %s: %s\n", outputField.getLabel(), result.getValues().get(outputField.getName()));
+ }
+
+ if(result.getError() != null)
+ {
+ subCommandLine.getOut().println("Process Error message: " + result.getError());
+ }
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ subCommandLine.getOut().println("Caught Exception running process. See stack trace above for details.");
+ return 1;
+ }
+
return 0;
}
@@ -379,7 +435,7 @@ public class QPicoCliImplementation
for(String criterion : criteria)
{
// todo - parse!
- String[] parts = criterion.split(" ");
+ String[] parts = criterion.split(" ");
QFilterCriteria qQueryCriteria = new QFilterCriteria();
qQueryCriteria.setFieldName(parts[0]);
qQueryCriteria.setOperator(QCriteriaOperator.valueOf(parts[1]));
@@ -440,7 +496,7 @@ public class QPicoCliImplementation
try
{
String path = subParseResult.matchedOptionValue("--csvFile", "");
- String csv = FileUtils.readFileToString(new File(path));
+ String csv = FileUtils.readFileToString(new File(path));
recordList = new CsvToQRecordAdapter().buildRecordsFromCsv(csv, table, mapping);
}
catch(IOException e)
@@ -497,7 +553,7 @@ public class QPicoCliImplementation
boolean anyFields = false;
- String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
+ String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
Serializable[] primaryKeyValues = primaryKeyOption.split(",");
for(Serializable primaryKeyValue : primaryKeyValues)
{
@@ -546,7 +602,7 @@ public class QPicoCliImplementation
/////////////////////////////////////////////
// get the pKeys that the user specified //
/////////////////////////////////////////////
- String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
+ String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
Serializable[] primaryKeyValues = primaryKeyOption.split(",");
deleteRequest.setPrimaryKeys(Arrays.asList(primaryKeyValues));
diff --git a/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java b/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java
index 306b537f..4f298522 100644
--- a/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java
+++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java
@@ -22,10 +22,12 @@
package com.kingsrook.qqq.frontend.picocli;
+import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
+import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
@@ -37,7 +39,6 @@ import org.apache.commons.io.FileUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -51,8 +52,8 @@ import static org.junit.jupiter.api.Assertions.fail;
*******************************************************************************/
class QPicoCliImplementationTest
{
- private static final boolean VERBOSE = true;
- private static final String CLI_NAME = "cli-unit-test";
+ private static final boolean VERBOSE = true;
+ private static final String CLI_NAME = "cli-unit-test";
@@ -115,7 +116,7 @@ class QPicoCliImplementationTest
@Test
public void test_badOption()
{
- String badOption = "--asdf";
+ String badOption = "--asdf";
TestOutput testOutput = testCli(badOption);
assertTestErrorContains(testOutput, "Unknown option: '" + badOption + "'");
assertTestErrorContains(testOutput, "Usage: " + CLI_NAME);
@@ -131,14 +132,22 @@ class QPicoCliImplementationTest
public void test_metaData()
{
TestOutput testOutput = testCli("--meta-data");
- JSONObject metaData = JsonUtils.toJSONObject(testOutput.getOutput());
+ JSONObject metaData = JsonUtils.toJSONObject(testOutput.getOutput());
assertNotNull(metaData);
- assertEquals(1, metaData.keySet().size(), "Number of top-level keys");
+ assertEquals(2, metaData.keySet().size(), "Number of top-level keys");
+
assertTrue(metaData.has("tables"));
- JSONObject tables = metaData.getJSONObject("tables");
+ JSONObject tables = metaData.getJSONObject("tables");
JSONObject personTable = tables.getJSONObject("person");
assertEquals("person", personTable.getString("name"));
assertEquals("Person", personTable.getString("label"));
+
+ assertTrue(metaData.has("processes"));
+ JSONObject processes = metaData.getJSONObject("processes");
+ JSONObject greetProcess = processes.getJSONObject("greet");
+ assertEquals("greet", greetProcess.getString("name"));
+ assertEquals("Greet", greetProcess.getString("label"));
+ assertEquals("person", greetProcess.getString("tableName"));
}
@@ -173,7 +182,7 @@ class QPicoCliImplementationTest
@Test
public void test_tableUnknownCommand()
{
- String badCommand = "qwuijibo";
+ String badCommand = "qwuijibo";
TestOutput testOutput = testCli("person", badCommand);
assertTestErrorContains(testOutput, "Unmatched argument at index 1: '" + badCommand + "'");
assertTestErrorContains(testOutput, "Usage: " + CLI_NAME + " person \\[COMMAND\\]");
@@ -189,7 +198,7 @@ class QPicoCliImplementationTest
public void test_tableMetaData()
{
TestOutput testOutput = testCli("person", "meta-data");
- JSONObject metaData = JsonUtils.toJSONObject(testOutput.getOutput());
+ JSONObject metaData = JsonUtils.toJSONObject(testOutput.getOutput());
assertNotNull(metaData);
assertEquals(1, metaData.keySet().size(), "Number of top-level keys");
JSONObject table = metaData.getJSONObject("table");
@@ -212,7 +221,7 @@ class QPicoCliImplementationTest
@Test
public void test_tableQuery()
{
- TestOutput testOutput = testCli("person", "query", "--skip=1", "--limit=2", "--criteria", "id NOT_EQUALS 3");
+ TestOutput testOutput = testCli("person", "query", "--skip=1", "--limit=2", "--criteria", "id NOT_EQUALS 3");
JSONObject queryResult = JsonUtils.toJSONObject(testOutput.getOutput());
assertNotNull(queryResult);
JSONArray records = queryResult.getJSONArray("records");
@@ -246,7 +255,8 @@ class QPicoCliImplementationTest
{
TestOutput testOutput = testCli("person", "insert",
"--field-firstName=Lucy",
- "--field-lastName=Lu");
+ "--field-lastName=Lu",
+ "--field-email=lucy@kingsrook.com");
JSONObject insertResult = JsonUtils.toJSONObject(testOutput.getOutput());
assertNotNull(insertResult);
assertEquals(1, insertResult.getJSONArray("records").length());
@@ -263,20 +273,21 @@ class QPicoCliImplementationTest
public void test_tableInsertJsonObjectArgumentWithMapping()
{
String mapping = """
- --mapping={"firstName":"first","lastName":"ln"}
+ --mapping={"firstName":"first","lastName":"ln","email":"email"}
""";
String jsonBody = """
- --jsonBody={"first":"Chester","ln":"Cheese"}
+ --jsonBody={"first":"Chester","ln":"Cheese","email":"chester@kingsrook.com"}
""";
- TestOutput testOutput = testCli("person", "insert", mapping, jsonBody);
+ TestOutput testOutput = testCli("person", "insert", mapping, jsonBody);
JSONObject insertResult = JsonUtils.toJSONObject(testOutput.getOutput());
assertNotNull(insertResult);
assertEquals(1, insertResult.getJSONArray("records").length());
assertEquals(6, insertResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getInt("id"));
assertEquals("Chester", insertResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getString("firstName"));
assertEquals("Cheese", insertResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getString("lastName"));
+ assertEquals("chester@kingsrook.com", insertResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getString("email"));
}
@@ -289,18 +300,21 @@ class QPicoCliImplementationTest
public void test_tableInsertJsonArrayFileWithMapping() throws IOException
{
String mapping = """
- --mapping={"firstName":"first","lastName":"ln"}
+ --mapping={"firstName":"first","lastName":"ln","email":"email"}
""";
String jsonContents = """
- [{"first":"Charlie","ln":"Bear"},{"first":"Coco","ln":"Bean"}]
+ [
+ {"first":"Charlie","ln":"Bear","email":"charlie-bear@kingsrook.com"},
+ {"first":"Coco","ln":"Bean","email":"coco-bean@kingsrook.com"}
+ ]
""";
File file = new File("/tmp/" + UUID.randomUUID() + ".json");
file.deleteOnExit();
FileUtils.writeStringToFile(file, jsonContents);
- TestOutput testOutput = testCli("person", "insert", mapping, "--jsonFile=" + file.getAbsolutePath());
+ TestOutput testOutput = testCli("person", "insert", mapping, "--jsonFile=" + file.getAbsolutePath());
JSONObject insertResult = JsonUtils.toJSONObject(testOutput.getOutput());
assertNotNull(insertResult);
JSONArray records = insertResult.getJSONArray("records");
@@ -309,8 +323,10 @@ class QPicoCliImplementationTest
assertEquals(7, insertResult.getJSONArray("records").getJSONObject(1).getJSONObject("values").getInt("id"));
assertEquals("Charlie", records.getJSONObject(0).getJSONObject("values").getString("firstName"));
assertEquals("Bear", records.getJSONObject(0).getJSONObject("values").getString("lastName"));
+ assertEquals("charlie-bear@kingsrook.com", records.getJSONObject(0).getJSONObject("values").getString("email"));
assertEquals("Coco", records.getJSONObject(1).getJSONObject("values").getString("firstName"));
assertEquals("Bean", records.getJSONObject(1).getJSONObject("values").getString("lastName"));
+ assertEquals("coco-bean@kingsrook.com", records.getJSONObject(1).getJSONObject("values").getString("email"));
}
@@ -323,12 +339,12 @@ class QPicoCliImplementationTest
public void test_tableInsertCsvFileWithIndexMapping() throws IOException
{
String mapping = """
- --mapping={"firstName":1,"lastName":3}
+ --mapping={"firstName":1,"lastName":3,"email":5}
""";
String csvContents = """
- "Louis","P","Willikers",1024,
- "Nestle","G","Crunch",1701,
+ "Louis","P","Willikers",1024,"louis@kingsrook.com",
+ "Nestle","G","Crunch",1701,"nestle@kingsrook.com",
""";
@@ -336,7 +352,7 @@ class QPicoCliImplementationTest
file.deleteOnExit();
FileUtils.writeStringToFile(file, csvContents);
- TestOutput testOutput = testCli("person", "insert", mapping, "--csvFile=" + file.getAbsolutePath());
+ TestOutput testOutput = testCli("person", "insert", mapping, "--csvFile=" + file.getAbsolutePath());
JSONObject insertResult = JsonUtils.toJSONObject(testOutput.getOutput());
assertNotNull(insertResult);
JSONArray records = insertResult.getJSONArray("records");
@@ -408,7 +424,7 @@ class QPicoCliImplementationTest
@Test
public void test_tableDelete() throws Exception
{
- TestOutput testOutput = testCli("person", "delete", "--primaryKey", "2,4");
+ TestOutput testOutput = testCli("person", "delete", "--primaryKey", "2,4");
JSONObject deleteResult = JsonUtils.toJSONObject(testOutput.getOutput());
assertNotNull(deleteResult);
JSONArray records = deleteResult.getJSONArray("records");
@@ -452,8 +468,8 @@ class QPicoCliImplementationTest
@Test
public void test_tableProcessUnknownName() throws Exception
{
- String badProcessName = "not-a-process";
- TestOutput testOutput = testCli("person", "process", badProcessName);
+ String badProcessName = "not-a-process";
+ TestOutput testOutput = testCli("person", "process", badProcessName);
assertTestErrorContains(testOutput, "Unmatched argument at index 2: '" + badProcessName + "'");
assertTestErrorContains(testOutput, "Usage: " + CLI_NAME + " person process \\[COMMAND\\]");
}
@@ -465,12 +481,25 @@ class QPicoCliImplementationTest
**
*******************************************************************************/
@Test
- @Disabled // not yet done.
- public void test_tableProcessGreet() throws Exception
+ public void test_tableProcessGreetUsingCallbackForFields() throws Exception
{
+ setStandardInputLines("Hi", "How are you?");
TestOutput testOutput = testCli("person", "process", "greet");
+ assertTestOutputContains(testOutput, "Please supply a value for the field.*Greeting Prefix");
+ assertTestOutputContains(testOutput, "Hi X How are you?");
+ }
- fail("Assertion not written...");
+
+ /*******************************************************************************
+ ** test running a process on a table
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableProcessGreetUsingOptionsForFields() throws Exception
+ {
+ TestOutput testOutput = testCli("person", "process", "greet", "--field-greetingPrefix=Hello", "--field-greetingSuffix=There");
+ assertTestOutputDoesNotContain(testOutput, "Please supply a value for the field");
+ assertTestOutputContains(testOutput, "Hello X There");
}
@@ -536,7 +565,7 @@ class QPicoCliImplementationTest
QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
+ ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
if(VERBOSE)
{
@@ -546,7 +575,7 @@ class QPicoCliImplementationTest
qPicoCliImplementation.runCli(CLI_NAME, args, new PrintStream(outputStream, true), new PrintStream(errorStream, true));
String output = outputStream.toString(StandardCharsets.UTF_8);
- String error = errorStream.toString(StandardCharsets.UTF_8);
+ String error = errorStream.toString(StandardCharsets.UTF_8);
if(VERBOSE)
{
@@ -560,14 +589,34 @@ class QPicoCliImplementationTest
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void setStandardInputLines(String... lines)
+ {
+ StringBuilder stringBuilder = new StringBuilder();
+ for(String line : lines)
+ {
+ stringBuilder.append(line);
+ if(!line.endsWith("\n"))
+ {
+ stringBuilder.append("\n");
+ }
+ }
+ ByteArrayInputStream stdin = new ByteArrayInputStream(stringBuilder.toString().getBytes(Charset.defaultCharset()));
+ System.setIn(stdin);
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
private static class TestOutput
{
- private String output;
+ private String output;
private String[] outputLines;
- private String error;
+ private String error;
private String[] errorLines;
diff --git a/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java b/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java
index 13835afb..7940bbcc 100644
--- a/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java
+++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java
@@ -25,8 +25,8 @@ package com.kingsrook.qqq.frontend.picocli;
import java.io.InputStream;
import java.sql.Connection;
import java.util.List;
+import com.kingsrook.qqq.backend.core.interfaces.mock.MockFunctionBody;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData;
-import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.QCodeUsage;
@@ -41,7 +41,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QOutputView;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListView;
-import com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendMetaData;
+import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import org.apache.commons.io.IOUtils;
@@ -62,11 +62,12 @@ public class TestUtils
@SuppressWarnings("unchecked")
public static void primeTestDatabase() throws Exception
{
- ConnectionManager connectionManager = new ConnectionManager();
- Connection connection = connectionManager.getConnection(new RDBMSBackendMetaData(TestUtils.defineBackend()));
- InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql");
+ ConnectionManager connectionManager = new ConnectionManager();
+ Connection connection = connectionManager.getConnection(TestUtils.defineBackend());
+ InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql");
assertNotNull(primeTestDatabaseSqlStream);
List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream);
+ lines = lines.stream().filter(line -> !line.startsWith("-- ")).toList();
String joinedSQL = String.join("\n", lines);
for(String sql : joinedSQL.split(";"))
{
@@ -83,7 +84,7 @@ public class TestUtils
public static void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception
{
ConnectionManager connectionManager = new ConnectionManager();
- Connection connection = connectionManager.getConnection(new RDBMSBackendMetaData(defineBackend()));
+ Connection connection = connectionManager.getConnection(defineBackend());
QueryManager.executeStatement(connection, sql, resultSetProcessor);
}
@@ -122,16 +123,15 @@ public class TestUtils
** Define the h2 rdbms backend
**
*******************************************************************************/
- public static QBackendMetaData defineBackend()
+ public static RDBMSBackendMetaData defineBackend()
{
- return new QBackendMetaData()
- .withName("default")
- .withType("rdbms")
- .withValue("vendor", "h2")
- .withValue("hostName", "mem")
- .withValue("databaseName", "test_database")
- .withValue("username", "sa")
- .withValue("password", "");
+ return (new RDBMSBackendMetaData()
+ .withVendor("h2")
+ .withHostName("mem")
+ .withDatabaseName("test_database")
+ .withUsername("sa")
+ .withPassword("")
+ .withName("default"));
}
@@ -169,7 +169,7 @@ public class TestUtils
.addFunction(new QFunctionMetaData()
.withName("prepare")
.withCode(new QCodeReference()
- .withName("com.kingsrook.qqq.backend.core.interfaces.mock.MockFunctionBody")
+ .withName(MockFunctionBody.class.getName())
.withCodeType(QCodeType.JAVA)
.withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context?
.withInputData(new QFunctionInputMetaData()
diff --git a/src/test/resources/prime-test-database.sql b/src/test/resources/prime-test-database.sql
index 6227f249..be858987 100644
--- a/src/test/resources/prime-test-database.sql
+++ b/src/test/resources/prime-test-database.sql
@@ -1,28 +1,28 @@
-/*
- * 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 .
- */
+--
+-- 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 .
+--
DROP TABLE IF EXISTS person;
CREATE TABLE person
(
- id SERIAL,
+ id INT AUTO_INCREMENT,
create_date TIMESTAMP DEFAULT now(),
modify_date TIMESTAMP DEFAULT now(),