diff --git a/.circleci/config.yml b/.circleci/config.yml
index fe4c371c..658f75d1 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -24,6 +24,8 @@ commands:
name: Run Maven
command: |
mvn -s .circleci/mvn-settings.xml << parameters.maven_subcommand >>
+ - store_artifacts:
+ path: target/site/jacoco
- run:
name: Save test results
command: |
diff --git a/.gitignore b/.gitignore
index 39736a21..2c7054c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
target/
*.iml
+.env
#############################################
@@ -28,3 +29,4 @@ target/
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
+.DS_Store
diff --git a/pom.xml b/pom.xml
index 5061715e..df0fcb18 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,7 +25,7 @@
com.kingsrook.qqq
qqq-middleware-picocli
- 0.1.0
+ 0.2.0
scm:git:git@github.com:Kingsrook/qqq-middleware-picocli.git
@@ -53,20 +53,26 @@
com.kingsrook.qqq
qqq-backend-core
- 0.1.0
+ 0.2.0
com.kingsrook.qqq
qqq-backend-module-rdbms
- 0.1.0
+ 0.2.0
test
+
- info.picocli
- picocli
- 4.6.1
+ info.picocli
+ picocli
+ 4.6.1
+
+
+ info.picocli
+ picocli-shell-jline3
+ 4.6.3
com.h2database
@@ -97,6 +103,12 @@
5.8.1
test
+
+ org.assertj
+ assertj-core
+ 3.23.1
+ test
+
diff --git a/src/main/java/com/kingsrook/qqq/frontend/picocli/PicoCliProcessCallback.java b/src/main/java/com/kingsrook/qqq/frontend/picocli/PicoCliProcessCallback.java
index b4cf0a70..9d6f4555 100644
--- a/src/main/java/com/kingsrook/qqq/frontend/picocli/PicoCliProcessCallback.java
+++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/PicoCliProcessCallback.java
@@ -27,9 +27,9 @@ 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 com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import picocli.CommandLine;
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 d2fbed7b..505f1340 100644
--- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java
+++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java
@@ -29,10 +29,10 @@ 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.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
+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 picocli.CommandLine;
@@ -90,11 +90,13 @@ public class QCommandBuilder
// add table-specific sub-commands for the table //
///////////////////////////////////////////////////
tableCommand.addSubcommand("meta-data", defineMetaDataCommand(table));
- tableCommand.addSubcommand("count", defineQueryCommand(table));
+ tableCommand.addSubcommand("count", defineCountCommand(table));
+ tableCommand.addSubcommand("get", defineGetCommand(table));
tableCommand.addSubcommand("query", defineQueryCommand(table));
tableCommand.addSubcommand("insert", defineInsertCommand(table));
tableCommand.addSubcommand("update", defineUpdateCommand(table));
tableCommand.addSubcommand("delete", defineDeleteCommand(table));
+ tableCommand.addSubcommand("export", defineExportCommand(table));
List processes = qInstance.getProcessesForTable(tableName);
if(CollectionUtils.nullSafeHasContents(processes))
@@ -158,6 +160,98 @@ public class QCommandBuilder
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private CommandLine.Model.CommandSpec defineExportCommand(QTableMetaData table)
+ {
+ CommandLine.Model.CommandSpec exportCommand = CommandLine.Model.CommandSpec.create();
+ exportCommand.addOption(CommandLine.Model.OptionSpec.builder("-f", "--filename")
+ .type(String.class)
+ .description("File name (including path) to write to. File extension will be used to determine the report format. Supported formats are: csv, xlsx.")
+ .required(true)
+ .build());
+ exportCommand.addOption(CommandLine.Model.OptionSpec.builder("-e", "--fieldNames")
+ .type(String.class)
+ .description("Comma-separated list of field names (e.g., from table meta-data) to include in the export. If not given, then all fields in the table are included.")
+ .build());
+ exportCommand.addOption(CommandLine.Model.OptionSpec.builder("-l", "--limit")
+ .type(int.class)
+ .description("Optional limit on the max number of records to include in the export.")
+ .build());
+ addCriteriaOption(exportCommand);
+
+ // todo - add the fields as explicit params?
+
+ return exportCommand;
+ }
+
+
+
+ /*******************************************************************************
+ ** add the standard '--criteria' option
+ *******************************************************************************/
+ private void addCriteriaOption(CommandLine.Model.CommandSpec commandSpec)
+ {
+ commandSpec.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria")
+ .type(String[].class)
+ .description("""
+ Query filter criteria. May be given multiple times.
+ Use format: "$fieldName $operator $value".
+ e.g., "id EQUALS 42\"""")
+ .build());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void addPrimaryKeyOrKeysOption(CommandLine.Model.CommandSpec updateCommand, String verbForDescription)
+ {
+ updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--primaryKey")
+ // type(getClassForField(primaryKeyField))
+ .type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type?
+ .description("""
+ Primary Key(s) for the records to %s.
+ May provide multiple values, separated by commas""".formatted(verbForDescription))
+ .build());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private CommandLine.Model.CommandSpec defineGetCommand(QTableMetaData table)
+ {
+ CommandLine.Model.CommandSpec getCommand = CommandLine.Model.CommandSpec.create();
+ getCommand.addPositional(CommandLine.Model.PositionalParamSpec.builder()
+ .index("0")
+ // .type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type?
+ .description("Primary key value from the table")
+ .build());
+
+ return getCommand;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private CommandLine.Model.CommandSpec defineCountCommand(QTableMetaData table)
+ {
+ CommandLine.Model.CommandSpec countCommand = CommandLine.Model.CommandSpec.create();
+ addCriteriaOption(countCommand);
+
+ // todo - add the fields as explicit params?
+
+ return countCommand;
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -166,6 +260,7 @@ public class QCommandBuilder
CommandLine.Model.CommandSpec updateCommand = CommandLine.Model.CommandSpec.create();
/*
+ todo - future may accept files, similar to (bulk) insert
updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--jsonBody")
.type(String.class)
.build());
@@ -183,11 +278,12 @@ public class QCommandBuilder
.build());
*/
- QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
- updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--primaryKey")
- // type(getClassForField(primaryKeyField))
- .type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type?
- .build());
+ QFieldMetaData primaryKeyField = null;
+ if(table.getPrimaryKeyField() != null)
+ {
+ primaryKeyField = table.getField(table.getPrimaryKeyField());
+ addPrimaryKeyOrKeysOption(updateCommand, "update");
+ }
for(QFieldMetaData field : table.getFields().values())
{
@@ -195,9 +291,14 @@ public class QCommandBuilder
{
updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--field-" + field.getName())
.type(getClassForField(field))
+ .description("""
+ Value to set for the field %s""".formatted(field.getName()))
.build());
}
}
+
+ addCriteriaOption(updateCommand);
+
return updateCommand;
}
@@ -248,6 +349,8 @@ public class QCommandBuilder
.type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type?
.build());
+ addCriteriaOption(deleteCommand);
+
return deleteCommand;
}
@@ -306,6 +409,7 @@ public class QCommandBuilder
case DATE -> LocalDate.class;
// case TIME -> LocalTime.class;
case DATE_TIME -> LocalDateTime.class;
+ case BLOB -> byte[].class;
};
// @formatter:on
}
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 56cd335a..e83fe394 100644
--- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java
+++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java
@@ -23,7 +23,9 @@ package com.kingsrook.qqq.frontend.picocli;
import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Serializable;
@@ -32,50 +34,65 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import com.kingsrook.qqq.backend.core.actions.CountAction;
-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.RunProcessAction;
-import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction;
-import com.kingsrook.qqq.backend.core.actions.UpdateAction;
+import java.util.Optional;
+import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
+import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction;
+import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
+import com.kingsrook.qqq.backend.core.actions.reporting.ReportAction;
+import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
+import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
+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.adapters.CsvToQRecordAdapter;
import com.kingsrook.qqq.backend.core.adapters.JsonToQFieldMappingAdapter;
import com.kingsrook.qqq.backend.core.adapters.JsonToQRecordAdapter;
import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter;
+import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
-import com.kingsrook.qqq.backend.core.model.actions.count.CountRequest;
-import com.kingsrook.qqq.backend.core.model.actions.count.CountResult;
-import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteRequest;
-import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteResult;
-import com.kingsrook.qqq.backend.core.model.actions.insert.InsertRequest;
-import com.kingsrook.qqq.backend.core.model.actions.insert.InsertResult;
-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;
-import com.kingsrook.qqq.backend.core.model.actions.query.QueryRequest;
-import com.kingsrook.qqq.backend.core.model.actions.query.QueryResult;
+import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
+import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
+import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
+import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput;
+import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput;
import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.AbstractQFieldMapping;
-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.actions.shared.mapping.QKeyBasedFieldMapping;
+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.delete.DeleteOutput;
+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.actions.tables.query.QueryInput;
+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.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.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
+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.modules.QAuthenticationModuleDispatcher;
-import com.kingsrook.qqq.backend.core.modules.interfaces.QAuthenticationModuleInterface;
+import com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import io.github.cdimascio.dotenv.Dotenv;
import org.apache.commons.io.FileUtils;
+import org.apache.logging.log4j.core.config.Configurator;
+import org.jline.reader.LineReader;
+import org.jline.reader.LineReaderBuilder;
+import org.jline.utils.Log;
import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Model.OptionSpec;
@@ -94,10 +111,10 @@ import picocli.CommandLine.UnmatchedArgumentException;
*******************************************************************************/
public class QPicoCliImplementation
{
- public static final int DEFAULT_LIMIT = 20;
+ public static final int DEFAULT_QUERY_LIMIT = 20;
private static QInstance qInstance;
- private static QSession session;
+ private static QSession session;
@@ -112,14 +129,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
@@ -136,6 +153,14 @@ public class QPicoCliImplementation
*******************************************************************************/
public QPicoCliImplementation(QInstance qInstance)
{
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // use the qqq-picocli log4j config, less the system property log4j.configurationFile was set by the runner //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(System.getProperty("log4j.configurationFile") == null)
+ {
+ Configurator.initialize(null, "qqq-picocli-log4j2.xml");
+ }
+
QPicoCliImplementation.qInstance = qInstance;
}
@@ -229,15 +254,65 @@ public class QPicoCliImplementation
/*******************************************************************************
**
*******************************************************************************/
- private static void setupSession(String[] args) throws QModuleDispatchException
+ private static Optional loadDotEnv()
+ {
+ Optional dotenvOptional = Optional.empty();
+ try
+ {
+ dotenvOptional = Optional.of(Dotenv.configure().load());
+ }
+ catch(Exception e)
+ {
+ Log.info("No session information found in environment");
+ }
+
+ return (dotenvOptional);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void setupSession(String[] args) throws QModuleDispatchException, QAuthenticationException
{
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
- QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication());
+ QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication());
+
+ try
+ {
+ ////////////////////////////////////
+ // look for .env environment file //
+ ////////////////////////////////////
+ String sessionId = null;
+ Optional dotenv = loadDotEnv();
+ if(dotenv.isPresent())
+ {
+ sessionId = dotenv.get().get("SESSION_ID");
+ }
+
+ Map authenticationContext = new HashMap<>();
+ if(sessionId == null && authenticationModule instanceof Auth0AuthenticationModule)
+ {
+ LineReader lr = LineReaderBuilder.builder().build();
+ String tokenId = lr.readLine("Create a .env file with the contents of the Auth0 JWT Id Token in the variable 'SESSION_ID': \nPress enter once complete...");
+ dotenv = loadDotEnv();
+ if(dotenv.isPresent())
+ {
+ sessionId = dotenv.get().get("SESSION_ID");
+ }
+ }
+
+ authenticationContext.put("sessionId", sessionId);
+
+ // todo - does this need some per-provider logic actually? mmm...
+ session = authenticationModule.createSession(qInstance, authenticationContext);
+ }
+ catch(QAuthenticationException qae)
+ {
+ throw (qae);
+ }
- // todo - does this need some per-provider logic actually? mmm...
- Map authenticationContext = new HashMap<>();
- authenticationContext.put("sessionId", System.getenv("sessionId"));
- session = authenticationModule.createSession(authenticationContext);
}
@@ -254,7 +329,7 @@ public class QPicoCliImplementation
else
{
ParseResult subParseResult = parseResult.subcommand();
- String subCommandName = subParseResult.commandSpec().name();
+ String subCommandName = subParseResult.commandSpec().name();
CommandLine subCommandLine = commandLine.getSubcommands().get(subCommandName);
switch(subCommandName)
{
@@ -285,7 +360,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":
@@ -296,10 +371,19 @@ public class QPicoCliImplementation
{
return runTableCount(commandLine, tableName, subParseResult);
}
+ case "get":
+ {
+ CommandLine subCommandLine = commandLine.getSubcommands().get(subCommandName);
+ return runTableGet(commandLine, tableName, subParseResult, subCommandLine);
+ }
case "query":
{
return runTableQuery(commandLine, tableName, subParseResult);
}
+ case "export":
+ {
+ return runTableExport(commandLine, tableName, subParseResult);
+ }
case "insert":
{
return runTableInsert(commandLine, tableName, subParseResult);
@@ -352,7 +436,7 @@ public class QPicoCliImplementation
///////////////////////////////////////////
// move on to running the actual process //
///////////////////////////////////////////
- String subCommandName = subParseResult.subcommand().commandSpec().name();
+ String subCommandName = subParseResult.subcommand().commandSpec().name();
CommandLine subCommandLine = commandLine.getSubcommands().get(subCommandName);
return runActualProcess(subCommandLine, subParseResult.subcommand());
}
@@ -365,9 +449,9 @@ public class QPicoCliImplementation
*******************************************************************************/
private int runActualProcess(CommandLine subCommandLine, ParseResult processParseResult)
{
- String processName = processParseResult.commandSpec().name();
- QProcessMetaData process = qInstance.getProcess(processName);
- RunProcessRequest request = new RunProcessRequest(qInstance);
+ String processName = processParseResult.commandSpec().name();
+ QProcessMetaData process = qInstance.getProcess(processName);
+ RunProcessInput request = new RunProcessInput(qInstance);
request.setSession(session);
request.setProcessName(processName);
@@ -384,7 +468,7 @@ public class QPicoCliImplementation
try
{
- RunProcessResult result = new RunProcessAction().execute(request);
+ RunProcessOutput result = new RunProcessAction().execute(request);
subCommandLine.getOut().println("Process Results: "); // todo better!!
for(QFieldMetaData outputField : process.getOutputFields())
{
@@ -414,12 +498,12 @@ public class QPicoCliImplementation
*******************************************************************************/
private int runTableMetaData(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException
{
- TableMetaDataRequest tableMetaDataRequest = new TableMetaDataRequest(qInstance);
- tableMetaDataRequest.setSession(session);
- tableMetaDataRequest.setTableName(tableName);
+ TableMetaDataInput tableMetaDataInput = new TableMetaDataInput(qInstance);
+ tableMetaDataInput.setSession(session);
+ tableMetaDataInput.setTableName(tableName);
TableMetaDataAction tableMetaDataAction = new TableMetaDataAction();
- TableMetaDataResult tableMetaDataResult = tableMetaDataAction.execute(tableMetaDataRequest);
- commandLine.getOut().println(JsonUtils.toPrettyJson(tableMetaDataResult));
+ TableMetaDataOutput tableMetaDataOutput = tableMetaDataAction.execute(tableMetaDataInput);
+ commandLine.getOut().println(JsonUtils.toPrettyJson(tableMetaDataOutput));
return commandLine.getCommandSpec().exitCodeOnSuccess();
}
@@ -430,39 +514,152 @@ public class QPicoCliImplementation
*******************************************************************************/
private int runTableCount(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException
{
- CountRequest countRequest = new CountRequest(qInstance);
- countRequest.setSession(session);
- countRequest.setTableName(tableName);
- countRequest.setFilter(generateQueryFilter(subParseResult));
+ CountInput countInput = new CountInput(qInstance);
+ countInput.setSession(session);
+ countInput.setTableName(tableName);
+ countInput.setFilter(generateQueryFilter(subParseResult));
CountAction countAction = new CountAction();
- CountResult countResult = countAction.execute(countRequest);
- commandLine.getOut().println(JsonUtils.toPrettyJson(countResult));
+ CountOutput countOutput = countAction.execute(countInput);
+ commandLine.getOut().println(JsonUtils.toPrettyJson(countOutput));
return commandLine.getCommandSpec().exitCodeOnSuccess();
}
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private int runTableGet(CommandLine commandLine, String tableName, ParseResult subParseResult, CommandLine subCommandLine) throws QException
+ {
+ QueryInput queryInput = new QueryInput(qInstance);
+ queryInput.setSession(session);
+ queryInput.setTableName(tableName);
+ queryInput.setSkip(subParseResult.matchedOptionValue("skip", null));
+ String primaryKeyValue = subParseResult.matchedPositionalValue(0, null);
+
+ if(primaryKeyValue == null)
+ {
+ subCommandLine.usage(commandLine.getOut());
+ return commandLine.getCommandSpec().exitCodeOnUsageHelp();
+ }
+
+ QTableMetaData table = queryInput.getTable();
+ QQueryFilter filter = new QQueryFilter()
+ .withCriteria(new QFilterCriteria()
+ .withFieldName(table.getPrimaryKeyField())
+ .withOperator(QCriteriaOperator.EQUALS)
+ .withValues(List.of(primaryKeyValue)));
+ queryInput.setFilter(filter);
+
+ QueryAction queryAction = new QueryAction();
+ QueryOutput queryOutput = queryAction.execute(queryInput);
+ List records = queryOutput.getRecords();
+ if(records.isEmpty())
+ {
+ commandLine.getOut().println("No " + table.getLabel() + " found for " + table.getField(table.getPrimaryKeyField()).getLabel() + ": " + primaryKeyValue);
+ return commandLine.getCommandSpec().exitCodeOnInvalidInput();
+ }
+ else
+ {
+ commandLine.getOut().println(JsonUtils.toPrettyJson(records.get(0)));
+ return commandLine.getCommandSpec().exitCodeOnSuccess();
+ }
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
private int runTableQuery(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException
{
- QueryRequest queryRequest = new QueryRequest(qInstance);
- queryRequest.setSession(session);
- queryRequest.setTableName(tableName);
- queryRequest.setSkip(subParseResult.matchedOptionValue("skip", null));
- queryRequest.setLimit(subParseResult.matchedOptionValue("limit", DEFAULT_LIMIT));
- queryRequest.setFilter(generateQueryFilter(subParseResult));
+ QueryInput queryInput = new QueryInput(qInstance);
+ queryInput.setSession(session);
+ queryInput.setTableName(tableName);
+ queryInput.setSkip(subParseResult.matchedOptionValue("skip", null));
+ queryInput.setLimit(subParseResult.matchedOptionValue("limit", null));
+ queryInput.setFilter(generateQueryFilter(subParseResult));
QueryAction queryAction = new QueryAction();
- QueryResult queryResult = queryAction.execute(queryRequest);
- commandLine.getOut().println(JsonUtils.toPrettyJson(queryResult));
+ QueryOutput queryOutput = queryAction.execute(queryInput);
+ commandLine.getOut().println(JsonUtils.toPrettyJson(queryOutput));
return commandLine.getCommandSpec().exitCodeOnSuccess();
}
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private int runTableExport(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException
+ {
+ String filename = subParseResult.matchedOptionValue("--filename", "");
+
+ /////////////////////////////////////////////////////////////////////////////////////////
+ // if a format query param wasn't given, then try to get file extension from file name //
+ /////////////////////////////////////////////////////////////////////////////////////////
+ ReportFormat reportFormat;
+ if(filename.contains("."))
+ {
+ reportFormat = ReportFormat.fromString(filename.substring(filename.lastIndexOf(".") + 1));
+ }
+ else
+ {
+ throw (new QUserFacingException("File name did not contain an extension, so report format could not be inferred."));
+ }
+
+ OutputStream outputStream;
+ try
+ {
+ outputStream = new FileOutputStream(filename);
+ }
+ catch(Exception e)
+ {
+ throw (new QException("Error opening report file: " + e.getMessage(), e));
+ }
+
+ try
+ {
+ /////////////////////////////////////////////
+ // set up the report action's input object //
+ /////////////////////////////////////////////
+ ReportInput reportInput = new ReportInput(qInstance);
+ reportInput.setSession(session);
+ reportInput.setTableName(tableName);
+ reportInput.setReportFormat(reportFormat);
+ reportInput.setFilename(filename);
+ reportInput.setReportOutputStream(outputStream);
+ reportInput.setLimit(subParseResult.matchedOptionValue("limit", null));
+
+ reportInput.setQueryFilter(generateQueryFilter(subParseResult));
+
+ String fieldNames = subParseResult.matchedOptionValue("--fieldNames", "");
+ if(StringUtils.hasContent(fieldNames))
+ {
+ reportInput.setFieldNames(Arrays.asList(fieldNames.split(",")));
+ }
+
+ ReportOutput reportOutput = new ReportAction().execute(reportInput);
+
+ commandLine.getOut().println("Wrote " + reportOutput.getRecordCount() + " records to file " + filename);
+ return commandLine.getCommandSpec().exitCodeOnSuccess();
+ }
+ finally
+ {
+ try
+ {
+ outputStream.close();
+ }
+ catch(IOException e)
+ {
+ throw (new QException("Error closing report file", e));
+ }
+ }
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -474,7 +671,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]));
@@ -492,9 +689,9 @@ public class QPicoCliImplementation
*******************************************************************************/
private int runTableInsert(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException
{
- InsertRequest insertRequest = new InsertRequest(qInstance);
- insertRequest.setSession(session);
- insertRequest.setTableName(tableName);
+ InsertInput insertInput = new InsertInput(qInstance);
+ insertInput.setSession(session);
+ insertInput.setTableName(tableName);
QTableMetaData table = qInstance.getTable(tableName);
AbstractQFieldMapping> mapping = null;
@@ -504,6 +701,14 @@ public class QPicoCliImplementation
String json = subParseResult.matchedOptionValue("--mapping", "");
mapping = new JsonToQFieldMappingAdapter().buildMappingFromJson(json);
}
+ else
+ {
+ mapping = new QKeyBasedFieldMapping();
+ for(Map.Entry entry : table.getFields().entrySet())
+ {
+ ((QKeyBasedFieldMapping) mapping).addMapping(entry.getKey(), entry.getValue().getLabel());
+ }
+ }
/////////////////////////////////////////////
// get the records that the user specified //
@@ -532,7 +737,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)
@@ -565,11 +770,11 @@ public class QPicoCliImplementation
}
}
- insertRequest.setRecords(recordList);
+ insertInput.setRecords(recordList);
InsertAction insertAction = new InsertAction();
- InsertResult insertResult = insertAction.execute(insertRequest);
- commandLine.getOut().println(JsonUtils.toPrettyJson(insertResult));
+ InsertOutput insertOutput = insertAction.execute(insertInput);
+ commandLine.getOut().println(JsonUtils.toPrettyJson(insertOutput));
return commandLine.getCommandSpec().exitCodeOnSuccess();
}
@@ -580,46 +785,89 @@ public class QPicoCliImplementation
*******************************************************************************/
private int runTableUpdate(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException
{
- UpdateRequest updateRequest = new UpdateRequest(qInstance);
- updateRequest.setSession(session);
- updateRequest.setTableName(tableName);
+ UpdateInput updateInput = new UpdateInput(qInstance);
+ updateInput.setSession(session);
+ updateInput.setTableName(tableName);
QTableMetaData table = qInstance.getTable(tableName);
- List recordList = new ArrayList<>();
+ List recordsToUpdate = new ArrayList<>();
+ boolean anyFields = false;
- boolean anyFields = false;
+ String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
+ String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] {});
- String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
- Serializable[] primaryKeyValues = primaryKeyOption.split(",");
- for(Serializable primaryKeyValue : primaryKeyValues)
+ if(StringUtils.hasContent(primaryKeyOption))
{
- QRecord record = new QRecord();
+ //////////////////////////////////////////////////////////////////////////////////////
+ // if the primaryKey option was given, split it up and seed the recordToUpdate list //
+ //////////////////////////////////////////////////////////////////////////////////////
+ Serializable[] primaryKeyValues = primaryKeyOption.split(",");
+ for(Serializable primaryKeyValue : primaryKeyValues)
+ {
+ recordsToUpdate.add(new QRecord().withValue(table.getPrimaryKeyField(), primaryKeyValue));
+ }
+ }
+ else if(criteria.length > 0)
+ {
+ //////////////////////////////////////////////////////////////////////////////////////
+ // else if criteria were given, execute the query for the lsit of records to update //
+ //////////////////////////////////////////////////////////////////////////////////////
+ for(QRecord qRecord : executeQuery(tableName, subParseResult))
+ {
+ recordsToUpdate.add(new QRecord().withValue(table.getPrimaryKeyField(), qRecord.getValue(table.getPrimaryKeyField())));
+ }
+ }
+ else
+ {
+ commandLine.getErr().println("Error: Either primaryKey or criteria must be specified.");
+ CommandLine subCommandLine = commandLine.getSubcommands().get("update");
+ subCommandLine.usage(commandLine.getOut());
+ return commandLine.getCommandSpec().exitCodeOnUsageHelp();
+ }
- recordList.add(record);
- record.setValue(table.getPrimaryKeyField(), primaryKeyValue);
+ ///////////////////////////////////////////////////
+ // make sure at least one --field- arg was given //
+ ///////////////////////////////////////////////////
+ for(OptionSpec matchedOption : subParseResult.matchedOptions())
+ {
+ if(matchedOption.longestName().startsWith("--field-"))
+ {
+ anyFields = true;
+ }
+ }
+ if(!anyFields)
+ {
+ commandLine.getErr().println("Error: At least one field to update must be specified.");
+ CommandLine subCommandLine = commandLine.getSubcommands().get("update");
+ subCommandLine.usage(commandLine.getOut());
+ return commandLine.getCommandSpec().exitCodeOnUsageHelp();
+ }
+
+ if(recordsToUpdate.isEmpty())
+ {
+ commandLine.getErr().println("No rows to update were found.");
+ CommandLine subCommandLine = commandLine.getSubcommands().get("update");
+ subCommandLine.usage(commandLine.getOut());
+ return commandLine.getCommandSpec().exitCodeOnUsageHelp();
+ }
+
+ for(QRecord record : recordsToUpdate)
+ {
for(OptionSpec matchedOption : subParseResult.matchedOptions())
{
if(matchedOption.longestName().startsWith("--field-"))
{
- anyFields = true;
String fieldName = matchedOption.longestName().substring(8);
record.setValue(fieldName, matchedOption.getValue());
}
}
}
- if(!anyFields || recordList.isEmpty())
- {
- CommandLine subCommandLine = commandLine.getSubcommands().get("update");
- subCommandLine.usage(commandLine.getOut());
- return commandLine.getCommandSpec().exitCodeOnUsageHelp();
- }
-
- updateRequest.setRecords(recordList);
+ updateInput.setRecords(recordsToUpdate);
UpdateAction updateAction = new UpdateAction();
- UpdateResult updateResult = updateAction.execute(updateRequest);
+ UpdateOutput updateResult = updateAction.execute(updateInput);
commandLine.getOut().println(JsonUtils.toPrettyJson(updateResult));
return commandLine.getCommandSpec().exitCodeOnSuccess();
}
@@ -631,19 +879,34 @@ public class QPicoCliImplementation
*******************************************************************************/
private int runTableDelete(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException
{
- DeleteRequest deleteRequest = new DeleteRequest(qInstance);
- deleteRequest.setSession(session);
- deleteRequest.setTableName(tableName);
+ DeleteInput deleteInput = new DeleteInput(qInstance);
+ deleteInput.setSession(session);
+ deleteInput.setTableName(tableName);
/////////////////////////////////////////////
// get the pKeys that the user specified //
/////////////////////////////////////////////
- String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
- Serializable[] primaryKeyValues = primaryKeyOption.split(",");
- deleteRequest.setPrimaryKeys(Arrays.asList(primaryKeyValues));
+ String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
+ String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] {});
+
+ if(StringUtils.hasContent(primaryKeyOption))
+ {
+ deleteInput.setPrimaryKeys(Arrays.asList(primaryKeyOption.split(",")));
+ }
+ else if(criteria.length > 0)
+ {
+ deleteInput.setQueryFilter(generateQueryFilter(subParseResult));
+ }
+ else
+ {
+ commandLine.getErr().println("Error: Either primaryKey or criteria must be specified.");
+ CommandLine subCommandLine = commandLine.getSubcommands().get("delete");
+ subCommandLine.usage(commandLine.getOut());
+ return commandLine.getCommandSpec().exitCodeOnUsageHelp();
+ }
DeleteAction deleteAction = new DeleteAction();
- DeleteResult deleteResult = deleteAction.execute(deleteRequest);
+ DeleteOutput deleteResult = deleteAction.execute(deleteInput);
commandLine.getOut().println(JsonUtils.toPrettyJson(deleteResult));
return commandLine.getCommandSpec().exitCodeOnSuccess();
}
@@ -657,15 +920,32 @@ public class QPicoCliImplementation
{
if(parseResult.hasMatchedOption("--meta-data"))
{
- MetaDataRequest metaDataRequest = new MetaDataRequest(qInstance);
- metaDataRequest.setSession(session);
+ MetaDataInput metaDataInput = new MetaDataInput(qInstance);
+ metaDataInput.setSession(session);
MetaDataAction metaDataAction = new MetaDataAction();
- MetaDataResult metaDataResult = metaDataAction.execute(metaDataRequest);
- commandLine.getOut().println(JsonUtils.toPrettyJson(metaDataResult));
+ MetaDataOutput metaDataOutput = metaDataAction.execute(metaDataInput);
+ commandLine.getOut().println(JsonUtils.toPrettyJson(metaDataOutput));
return commandLine.getCommandSpec().exitCodeOnSuccess();
}
commandLine.usage(commandLine.getOut());
return commandLine.getCommandSpec().exitCodeOnUsageHelp();
}
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private List executeQuery(String tableName, ParseResult subParseResult) throws QException
+ {
+ QueryInput queryInput = new QueryInput(qInstance);
+ queryInput.setSession(session);
+ queryInput.setTableName(tableName);
+ queryInput.setFilter(generateQueryFilter(subParseResult));
+
+ QueryAction queryAction = new QueryAction();
+ QueryOutput queryOutput = queryAction.execute(queryInput);
+ return (queryOutput.getRecords());
+ }
}
diff --git a/src/main/resources/qqq-picocli-log4j2.xml b/src/main/resources/qqq-picocli-log4j2.xml
new file mode 100644
index 00000000..5b03a388
--- /dev/null
+++ b/src/main/resources/qqq-picocli-log4j2.xml
@@ -0,0 +1,18 @@
+
+
+
+ %date{ISO8601} | %relative | %level | %threadName{1} | %logger{1}.%method | %message%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 8e14abd8..12d30a86 100644
--- a/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java
+++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java
@@ -31,6 +31,7 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
@@ -40,6 +41,7 @@ import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -226,7 +228,7 @@ class QPicoCliImplementationTest
int count = countResult.getInt("count");
assertEquals(4, count);
- testOutput = testCli("person", "count", "--criteria", "id EQUALS 3");
+ testOutput = testCli("person", "count", "--criteria", "id EQUALS 3");
countResult = JsonUtils.toJSONObject(testOutput.getOutput());
assertNotNull(countResult);
count = countResult.getInt("count");
@@ -254,6 +256,49 @@ class QPicoCliImplementationTest
+ /*******************************************************************************
+ ** test running a "get single record" action (singleton query) on a table
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableGetNoIdGiven()
+ {
+ TestOutput testOutput = testCli("person", "get");
+ assertTestOutputContains(testOutput, "Usage: " + CLI_NAME + " person get PARAM");
+ assertTestOutputContains(testOutput, "Primary key value from the table");
+ }
+
+
+
+ /*******************************************************************************
+ ** test running a "get single record" action (singleton query) on a table
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableGet()
+ {
+ TestOutput testOutput = testCli("person", "get", "1");
+ JSONObject getResult = JsonUtils.toJSONObject(testOutput.getOutput());
+ assertNotNull(getResult);
+ assertEquals(1, getResult.getJSONObject("values").getInt("id"));
+ assertEquals("Darin", getResult.getJSONObject("values").getString("firstName"));
+ }
+
+
+
+ /*******************************************************************************
+ ** test running a "get single record" action (singleton query) on a table
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableGetMissingId()
+ {
+ TestOutput testOutput = testCli("person", "get", "1976");
+ assertTestOutputContains(testOutput, "No Person found for Id: 1976");
+ }
+
+
+
/*******************************************************************************
** test running an insert w/o specifying any fields, prints usage
**
@@ -402,11 +447,24 @@ class QPicoCliImplementationTest
/*******************************************************************************
- ** test running an update w/ fields as arguments
+ ** test running an update w/o specifying any pkeys or criteria, prints usage
**
*******************************************************************************/
@Test
- public void test_tableUpdateFieldArguments() throws Exception
+ public void test_tableUpdateNoRecordsPrintsUsage()
+ {
+ TestOutput testOutput = testCli("person", "update", "--field-firstName=Lucy");
+ assertTestOutputContains(testOutput, "Usage: " + CLI_NAME + " person update");
+ }
+
+
+
+ /*******************************************************************************
+ ** test running an update w/ fields as arguments and one primary key
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableUpdateFieldArgumentsOnePrimaryKey() throws Exception
{
assertRowValueById("person", "first_name", "Garret", 5);
TestOutput testOutput = testCli("person", "update",
@@ -422,6 +480,59 @@ class QPicoCliImplementationTest
+ /*******************************************************************************
+ ** test running an update w/ fields as arguments and multiple primary keys
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableUpdateFieldArgumentsManyPrimaryKeys() throws Exception
+ {
+ assertRowValueById("person", "first_name", "Tyler", 4);
+ assertRowValueById("person", "first_name", "Garret", 5);
+ TestOutput testOutput = testCli("person", "update",
+ "--primaryKey=4,5",
+ "--field-birthDate=1980-05-31",
+ "--field-firstName=Lucy",
+ "--field-lastName=Lu");
+ JSONObject updateResult = JsonUtils.toJSONObject(testOutput.getOutput());
+ assertNotNull(updateResult);
+ assertEquals(2, updateResult.getJSONArray("records").length());
+ assertEquals(4, updateResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getInt("id"));
+ assertEquals(5, updateResult.getJSONArray("records").getJSONObject(1).getJSONObject("values").getInt("id"));
+ assertRowValueById("person", "first_name", "Lucy", 4);
+ assertRowValueById("person", "first_name", "Lucy", 5);
+ }
+
+
+
+ /*******************************************************************************
+ ** test running an update w/ fields as arguments and a criteria
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableUpdateFieldArgumentsCriteria() throws Exception
+ {
+ assertRowValueById("person", "first_name", "Tyler", 4);
+ assertRowValueById("person", "first_name", "Garret", 5);
+ TestOutput testOutput = testCli("person", "update",
+ "--criteria",
+ "id GREATER_THAN_OR_EQUALS 4",
+ "--field-firstName=Lucy",
+ "--field-lastName=Lu");
+ JSONObject updateResult = JsonUtils.toJSONObject(testOutput.getOutput());
+ assertNotNull(updateResult);
+ assertEquals(2, updateResult.getJSONArray("records").length());
+ assertEquals(4, updateResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getInt("id"));
+ assertEquals(5, updateResult.getJSONArray("records").getJSONObject(1).getJSONObject("values").getInt("id"));
+ assertRowValueById("person", "first_name", "Lucy", 4);
+ assertRowValueById("person", "first_name", "Lucy", 5);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
private void assertRowValueById(String tableName, String columnName, String value, Integer id) throws Exception
{
TestUtils.runTestSql("SELECT " + columnName + " FROM " + tableName + " WHERE id=" + id, (rs -> {
@@ -438,6 +549,18 @@ class QPicoCliImplementationTest
+ /*******************************************************************************
+ ** test running a delete without enough args
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableDeleteWithoutArgs() throws Exception
+ {
+ TestOutput testOutput = testCli("person", "delete");
+ assertTestOutputContains(testOutput, "Usage: " + CLI_NAME + " person delete");
+ }
+
+
/*******************************************************************************
** test running a delete against a table
**
@@ -448,10 +571,7 @@ class QPicoCliImplementationTest
TestOutput testOutput = testCli("person", "delete", "--primaryKey", "2,4");
JSONObject deleteResult = JsonUtils.toJSONObject(testOutput.getOutput());
assertNotNull(deleteResult);
- JSONArray records = deleteResult.getJSONArray("records");
- assertEquals(2, records.length());
- assertEquals(2, records.getJSONObject(0).getJSONObject("values").getInt("id"));
- assertEquals(4, records.getJSONObject(1).getJSONObject("values").getInt("id"));
+ assertEquals(2, deleteResult.getInt("deletedRecordCount"));
TestUtils.runTestSql("SELECT id FROM person", (rs -> {
int rowsFound = 0;
while(rs.next())
@@ -470,7 +590,7 @@ class QPicoCliImplementationTest
**
*******************************************************************************/
@Test
- public void test_tableProcess() throws Exception
+ public void test_tableProcess()
{
TestOutput testOutput = testCli("person", "process");
@@ -487,7 +607,7 @@ class QPicoCliImplementationTest
**
*******************************************************************************/
@Test
- public void test_tableProcessUnknownName() throws Exception
+ public void test_tableProcessUnknownName()
{
String badProcessName = "not-a-process";
TestOutput testOutput = testCli("person", "process", badProcessName);
@@ -502,7 +622,7 @@ class QPicoCliImplementationTest
**
*******************************************************************************/
@Test
- public void test_tableProcessGreetUsingCallbackForFields() throws Exception
+ public void test_tableProcessGreetUsingCallbackForFields()
{
setStandardInputLines("Hi", "How are you?");
TestOutput testOutput = testCli("person", "process", "greet");
@@ -511,12 +631,217 @@ class QPicoCliImplementationTest
}
+
+ /*******************************************************************************
+ ** test exporting a table
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableExportNoArgsExcel()
+ {
+ String filename = "/tmp/" + UUID.randomUUID() + ".xlsx";
+ TestOutput testOutput = testCli("person", "export", "--filename=" + filename);
+ assertTestOutputContains(testOutput, "Wrote 5 records to file " + filename);
+
+ File file = new File(filename);
+ assertTrue(file.exists());
+
+ // todo - some day when we learn to read Excel, assert that we wrote as expected.
+
+ deleteFile(file);
+ }
+
+
+
+ /*******************************************************************************
+ ** test exporting a table
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableExportWithLimit() throws Exception
+ {
+ String filename = "/tmp/" + UUID.randomUUID() + ".csv";
+ TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--limit=3");
+ assertTestOutputContains(testOutput, "Wrote 3 records to file " + filename);
+
+ File file = new File(filename);
+ @SuppressWarnings("unchecked")
+ List list = FileUtils.readLines(file);
+ assertEquals(4, list.size());
+ assertThat(list.get(0)).contains("""
+ "Id","Create Date","Modify Date\"""");
+ assertThat(list.get(1)).matches("""
+ ^"1",.*"Darin.*""");
+ assertThat(list.get(3)).matches("""
+ ^"3",.*"Tim.*""");
+
+ deleteFile(file);
+ }
+
+
+
+ /*******************************************************************************
+ ** test exporting a table
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableExportWithCriteria() throws Exception
+ {
+ String filename = "/tmp/" + UUID.randomUUID() + ".csv";
+ TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--criteria", "id NOT_EQUALS 3");
+ assertTestOutputContains(testOutput, "Wrote 4 records to file " + filename);
+
+ File file = new File(filename);
+ @SuppressWarnings("unchecked")
+ List list = FileUtils.readLines(file);
+ assertEquals(5, list.size());
+ assertThat(list.get(0)).contains("""
+ "Id","Create Date","Modify Date\"""");
+ assertThat(list.get(1)).matches("^\"1\",.*");
+ assertThat(list.get(2)).matches("^\"2\",.*");
+ assertThat(list.get(3)).matches("^\"4\",.*");
+ assertThat(list.get(4)).matches("^\"5\",.*");
+
+ deleteFile(file);
+ }
+
+
+
+ /*******************************************************************************
+ ** test exporting a table
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableExportWithoutFilename()
+ {
+ TestOutput testOutput = testCli("person", "export");
+ assertTestErrorContains(testOutput, "Missing required option: '--filename=PARAM'");
+ assertTestErrorContains(testOutput, "Usage: " + CLI_NAME + " person export");
+ assertTestErrorContains(testOutput, "-f=PARAM");
+ }
+
+
+
+ /*******************************************************************************
+ ** test exporting a table
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableExportNoFileExtension()
+ {
+ String filename = "/tmp/" + UUID.randomUUID();
+ TestOutput testOutput = testCli("person", "export", "--filename=" + filename);
+ assertTestErrorContains(testOutput, "File name did not contain an extension");
+ }
+
+
+
+ /*******************************************************************************
+ ** test exporting a table
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableExportBadFileType()
+ {
+ String filename = "/tmp/" + UUID.randomUUID() + ".docx";
+ TestOutput testOutput = testCli("person", "export", "--filename=" + filename);
+ assertTestErrorContains(testOutput, "Unsupported report format: docx.");
+ }
+
+
+
+ /*******************************************************************************
+ ** test exporting a table
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableExportBadFilePath()
+ {
+ String filename = "/no-such/directory/" + UUID.randomUUID() + "report.csv";
+ TestOutput testOutput = testCli("person", "export", "--filename=" + filename);
+ assertTestErrorContains(testOutput, "No such file or directory");
+ }
+
+
+
+ /*******************************************************************************
+ ** test exporting a table
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableExportBadFieldNams()
+ {
+ String filename = "/tmp/" + UUID.randomUUID() + ".csv";
+ TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--fieldNames=foo");
+ assertTestErrorContains(testOutput, "Field name foo was not found on the Person table");
+ }
+
+
+
+ /*******************************************************************************
+ ** test exporting a table
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableExportBadFieldNames()
+ {
+ String filename = "/tmp/" + UUID.randomUUID() + ".csv";
+ TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--fieldNames=foo,bar,baz");
+ assertTestErrorContains(testOutput, "Fields names foo, bar, and baz were not found on the Person table");
+ }
+
+
+
+ /*******************************************************************************
+ ** test exporting a table
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableExportGoodFieldNamesXslx() throws IOException
+ {
+ String filename = "/tmp/" + UUID.randomUUID() + ".xlsx";
+ TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--fieldNames=id,lastName,birthDate");
+
+ File file = new File(filename);
+ assertTrue(file.exists());
+
+ // todo - some day when we learn to read Excel, assert that we wrote as expected (with 3 columns)
+
+ deleteFile(file);
+ }
+
+
+
+ /*******************************************************************************
+ ** test exporting a table
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableExportGoodFieldNamesCSV() throws IOException
+ {
+ String filename = "/tmp/" + UUID.randomUUID() + ".csv";
+ TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--fieldNames=id,lastName,birthDate");
+
+ File file = new File(filename);
+ @SuppressWarnings("unchecked")
+ List list = FileUtils.readLines(file);
+ assertEquals(6, list.size());
+ assertThat(list.get(0)).isEqualTo("""
+ "Id","Last Name","Birth Date\"""");
+ assertThat(list.get(1)).isEqualTo("""
+ "1","Kelkhoff","1980-05-31\"""");
+
+ deleteFile(file);
+ }
+
+
+
/*******************************************************************************
** test running a process on a table
**
*******************************************************************************/
@Test
- public void test_tableProcessGreetUsingOptionsForFields() throws Exception
+ public void test_tableProcessGreetUsingOptionsForFields()
{
TestOutput testOutput = testCli("person", "process", "greet", "--field-greetingPrefix=Hello", "--field-greetingSuffix=World");
assertTestOutputDoesNotContain(testOutput, "Please supply a value for the field");
@@ -567,6 +892,16 @@ class QPicoCliImplementationTest
+ /*******************************************************************************
+ ** delete a file, asserting that we did so.
+ *******************************************************************************/
+ private void deleteFile(File file)
+ {
+ assertTrue(file.delete());
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -628,118 +963,4 @@ class QPicoCliImplementationTest
System.setIn(stdin);
}
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private static class TestOutput
- {
- private String output;
- private String[] outputLines;
- private String error;
- private String[] errorLines;
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- public TestOutput(String output, String error)
- {
- this.output = output;
- this.error = error;
-
- this.outputLines = output.split("\n");
- this.errorLines = error.split("\n");
- }
-
-
-
- /*******************************************************************************
- ** Getter for output
- **
- *******************************************************************************/
- public String getOutput()
- {
- return output;
- }
-
-
-
- /*******************************************************************************
- ** Setter for output
- **
- *******************************************************************************/
- public void setOutput(String output)
- {
- this.output = output;
- }
-
-
-
- /*******************************************************************************
- ** Getter for outputLines
- **
- *******************************************************************************/
- public String[] getOutputLines()
- {
- return outputLines;
- }
-
-
-
- /*******************************************************************************
- ** Setter for outputLines
- **
- *******************************************************************************/
- public void setOutputLines(String[] outputLines)
- {
- this.outputLines = outputLines;
- }
-
-
-
- /*******************************************************************************
- ** Getter for error
- **
- *******************************************************************************/
- public String getError()
- {
- return error;
- }
-
-
-
- /*******************************************************************************
- ** Setter for error
- **
- *******************************************************************************/
- public void setError(String error)
- {
- this.error = error;
- }
-
-
-
- /*******************************************************************************
- ** Getter for errorLines
- **
- *******************************************************************************/
- public String[] getErrorLines()
- {
- return errorLines;
- }
-
-
-
- /*******************************************************************************
- ** Setter for errorLines
- **
- *******************************************************************************/
- public void setErrorLines(String[] errorLines)
- {
- this.errorLines = errorLines;
- }
- }
}
diff --git a/src/test/java/com/kingsrook/qqq/frontend/picocli/TestOutput.java b/src/test/java/com/kingsrook/qqq/frontend/picocli/TestOutput.java
new file mode 100644
index 00000000..fcbb76d8
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/TestOutput.java
@@ -0,0 +1,136 @@
+/*
+ * 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;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+class TestOutput
+{
+ private String output;
+ private String[] outputLines;
+ private String error;
+ private String[] errorLines;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public TestOutput(String output, String error)
+ {
+ this.output = output;
+ this.error = error;
+
+ this.outputLines = output.split("\n");
+ this.errorLines = error.split("\n");
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for output
+ **
+ *******************************************************************************/
+ public String getOutput()
+ {
+ return output;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for output
+ **
+ *******************************************************************************/
+ public void setOutput(String output)
+ {
+ this.output = output;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for outputLines
+ **
+ *******************************************************************************/
+ public String[] getOutputLines()
+ {
+ return outputLines;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for outputLines
+ **
+ *******************************************************************************/
+ public void setOutputLines(String[] outputLines)
+ {
+ this.outputLines = outputLines;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for error
+ **
+ *******************************************************************************/
+ public String getError()
+ {
+ return error;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for error
+ **
+ *******************************************************************************/
+ public void setError(String error)
+ {
+ this.error = error;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for errorLines
+ **
+ *******************************************************************************/
+ public String[] getErrorLines()
+ {
+ return errorLines;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for errorLines
+ **
+ *******************************************************************************/
+ public void setErrorLines(String[] errorLines)
+ {
+ this.errorLines = 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 c72c140b..434b9d39 100644
--- a/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java
+++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java
@@ -25,15 +25,16 @@ 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.MockBackendStep;
-import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData;
-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;
-import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
-import com.kingsrook.qqq.backend.core.model.metadata.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
+import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep;
+import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
+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.QInstance;
-import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData;
@@ -112,7 +113,7 @@ public class TestUtils
{
return new QAuthenticationMetaData()
.withName("mock")
- .withType("mock");
+ .withType(QAuthenticationType.MOCK);
}