From 5ce9310e5499ed1588080190a5a2c6b6fbb25c94 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 14 Jul 2022 10:08:23 -0500 Subject: [PATCH 01/12] Update for next development version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5061715e..3d07d298 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ com.kingsrook.qqq qqq-middleware-picocli - 0.1.0 + 0.2.0-SNAPSHOT scm:git:git@github.com:Kingsrook/qqq-middleware-picocli.git From e3a762b2e4903979accdf36e6d5bc245a35f4242 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 14 Jul 2022 10:11:39 -0500 Subject: [PATCH 02/12] Removing overly-agressive assertion --- .../qqq/frontend/picocli/QPicoCliImplementationTest.java | 1 - 1 file changed, 1 deletion(-) 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 90ea165f..8e14abd8 100644 --- a/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java @@ -202,7 +202,6 @@ class QPicoCliImplementationTest assertNotNull(metaData); assertEquals(1, metaData.keySet().size(), "Number of top-level keys"); JSONObject table = metaData.getJSONObject("table"); - assertEquals(4, table.keySet().size(), "Number of mid-level keys"); assertEquals("person", table.getString("name")); assertEquals("Person", table.getString("label")); assertEquals("id", table.getString("primaryKeyField")); From 4b80bd589c210f2ddf9a8867a3d344c8afd97e32 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 14 Jul 2022 13:06:25 -0500 Subject: [PATCH 03/12] Reorganize packages; rename Request to Input and Response to Output --- pom.xml | 10 +- .../picocli/PicoCliProcessCallback.java | 6 +- .../qqq/frontend/picocli/QCommandBuilder.java | 4 +- .../picocli/QPicoCliImplementation.java | 144 +++++++++--------- .../qqq/frontend/picocli/TestUtils.java | 14 +- 5 files changed, 89 insertions(+), 89 deletions(-) diff --git a/pom.xml b/pom.xml index 3d07d298..59ce218d 100644 --- a/pom.xml +++ b/pom.xml @@ -53,20 +53,20 @@ com.kingsrook.qqq qqq-backend-core - 0.1.0 + 0.2.0-SNAPSHOT com.kingsrook.qqq qqq-backend-module-rdbms - 0.1.0 + 0.2.0-SNAPSHOT test - info.picocli - picocli - 4.6.1 + info.picocli + picocli + 4.6.1 com.h2database 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..c949c9fd 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; 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..8177b6df 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java +++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java @@ -32,48 +32,48 @@ 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 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.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.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.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.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.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.QAuthenticationModuleDispatcher; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import org.apache.commons.io.FileUtils; import picocli.CommandLine; @@ -367,7 +367,7 @@ public class QPicoCliImplementation { String processName = processParseResult.commandSpec().name(); QProcessMetaData process = qInstance.getProcess(processName); - RunProcessRequest request = new RunProcessRequest(qInstance); + RunProcessInput request = new RunProcessInput(qInstance); request.setSession(session); request.setProcessName(processName); @@ -384,7 +384,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 +414,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,14 +430,14 @@ 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(); } @@ -448,16 +448,16 @@ public class QPicoCliImplementation *******************************************************************************/ 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", DEFAULT_LIMIT)); + 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(); } @@ -492,9 +492,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; @@ -565,11 +565,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,9 +580,9 @@ 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<>(); @@ -616,10 +616,10 @@ public class QPicoCliImplementation return commandLine.getCommandSpec().exitCodeOnUsageHelp(); } - updateRequest.setRecords(recordList); + updateInput.setRecords(recordList); 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 +631,19 @@ 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)); + deleteInput.setPrimaryKeys(Arrays.asList(primaryKeyValues)); 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,11 +657,11 @@ 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(); } 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..926cc972 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,15 @@ 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.processes.implementations.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.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; From 84858b1eb463a1265130c2f9e32263558df6080e Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 19 Jul 2022 12:18:47 -0500 Subject: [PATCH 04/12] QQQ-26 update backend-core, queryOutput interface; also add 'get' command, and move log4j output to log file --- .circleci/config.yml | 2 + .gitignore | 1 + pom.xml | 10 +- .../qqq/frontend/picocli/QCommandBuilder.java | 69 +++- .../picocli/QPicoCliImplementation.java | 172 +++++++- src/main/resources/qqq-picocli-log4j2.xml | 18 + .../picocli/QPicoCliImplementationTest.java | 386 ++++++++++++------ .../qqq/frontend/picocli/TestOutput.java | 136 ++++++ 8 files changed, 656 insertions(+), 138 deletions(-) create mode 100644 src/main/resources/qqq-picocli-log4j2.xml create mode 100644 src/test/java/com/kingsrook/qqq/frontend/picocli/TestOutput.java 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..b65cb041 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,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 59ce218d..292d8a30 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-SNAPSHOT + 0.2.0-20220719.154219-3 com.kingsrook.qqq qqq-backend-module-rdbms - 0.2.0-SNAPSHOT + 0.2.0-20220719.155012-5 test @@ -97,6 +97,12 @@ 5.8.1 test + + org.assertj + assertj-core + 3.23.1 + test + 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 c949c9fd..970034b4 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java +++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java @@ -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,71 @@ 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()); + exportCommand.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria") + .type(String[].class) + .description("Query filter criteria for the export. May be given multiple times. Use format: $fieldName $operator $value. e.g., id EQUALS 42") + .build()); + + // todo - add the fields as explicit params? + + return exportCommand; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + 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(); + countCommand.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria") + .type(String[].class) + .build()); + + // todo - add the fields as explicit params? + + return countCommand; + } + + + /******************************************************************************* ** *******************************************************************************/ 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 8177b6df..e34de75d 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; @@ -35,6 +37,7 @@ import java.util.Map; 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; @@ -46,12 +49,16 @@ import com.kingsrook.qqq.backend.core.adapters.JsonToQRecordAdapter; import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; +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.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; @@ -75,7 +82,9 @@ import com.kingsrook.qqq.backend.core.model.session.QSession; 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 org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.core.config.Configurator; import picocli.CommandLine; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Model.OptionSpec; @@ -94,10 +103,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 +121,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 +145,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; } @@ -232,7 +249,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<>(); @@ -254,7 +271,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 +302,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 +313,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 +378,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 +391,9 @@ public class QPicoCliImplementation *******************************************************************************/ private int runActualProcess(CommandLine subCommandLine, ParseResult processParseResult) { - String processName = processParseResult.commandSpec().name(); - QProcessMetaData process = qInstance.getProcess(processName); - RunProcessInput request = new RunProcessInput(qInstance); + String processName = processParseResult.commandSpec().name(); + QProcessMetaData process = qInstance.getProcess(processName); + RunProcessInput request = new RunProcessInput(qInstance); request.setSession(session); request.setProcessName(processName); @@ -443,6 +469,49 @@ public class QPicoCliImplementation + /******************************************************************************* + ** + *******************************************************************************/ + 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)); + queryInput.setLimit(subParseResult.matchedOptionValue("limit", DEFAULT_QUERY_LIMIT)); + 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(); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -452,7 +521,7 @@ public class QPicoCliImplementation queryInput.setSession(session); queryInput.setTableName(tableName); queryInput.setSkip(subParseResult.matchedOptionValue("skip", null)); - queryInput.setLimit(subParseResult.matchedOptionValue("limit", DEFAULT_LIMIT)); + queryInput.setLimit(subParseResult.matchedOptionValue("limit", DEFAULT_QUERY_LIMIT)); queryInput.setFilter(generateQueryFilter(subParseResult)); QueryAction queryAction = new QueryAction(); @@ -463,6 +532,77 @@ public class QPicoCliImplementation + /******************************************************************************* + ** + *******************************************************************************/ + 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 +614,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])); @@ -532,7 +672,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) @@ -589,7 +729,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) { @@ -638,7 +778,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(","); deleteInput.setPrimaryKeys(Arrays.asList(primaryKeyValues)); 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..9f0f53fb 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 ** @@ -422,6 +467,9 @@ class QPicoCliImplementationTest + /******************************************************************************* + ** + *******************************************************************************/ private void assertRowValueById(String tableName, String columnName, String value, Integer id) throws Exception { TestUtils.runTestSql("SELECT " + columnName + " FROM " + tableName + " WHERE id=" + id, (rs -> { @@ -470,7 +518,7 @@ class QPicoCliImplementationTest ** *******************************************************************************/ @Test - public void test_tableProcess() throws Exception + public void test_tableProcess() { TestOutput testOutput = testCli("person", "process"); @@ -487,7 +535,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 +550,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 +559,216 @@ 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 +819,16 @@ class QPicoCliImplementationTest + /******************************************************************************* + ** delete a file, asserting that we did so. + *******************************************************************************/ + private void deleteFile(File file) + { + assertTrue(file.delete()); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -628,118 +890,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; + } +} From b14f96ef6c5a77eaa85c4f38c445d80bd299142e Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Tue, 19 Jul 2022 18:28:09 -0500 Subject: [PATCH 05/12] QQQ-27: updates to allow Auth0 to be an authentication model in picocli --- pom.xml | 11 +++ .../picocli/QPicoCliImplementation.java | 69 +++++++++++++++++-- .../qqq/frontend/picocli/TestUtils.java | 5 +- 3 files changed, 77 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 59ce218d..9cf6787a 100644 --- a/pom.xml +++ b/pom.xml @@ -62,12 +62,23 @@ test + info.picocli picocli 4.6.1 + + info.picocli + picocli-shell-jline3 + 4.6.3 + + + io.github.cdimascio + java-dotenv + 5.2.2 + com.h2database h2 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 8177b6df..feecd243 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java +++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java @@ -32,6 +32,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +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; @@ -44,6 +45,7 @@ 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.metadata.MetaDataInput; @@ -72,10 +74,15 @@ 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.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 io.github.cdimascio.dotenv.Dotenv; import org.apache.commons.io.FileUtils; +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; @@ -229,15 +236,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()); - // todo - does this need some per-provider logic actually? mmm... - Map authenticationContext = new HashMap<>(); - authenticationContext.put("sessionId", System.getenv("sessionId")); - session = authenticationModule.createSession(authenticationContext); + 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); + } + } @@ -367,7 +424,7 @@ public class QPicoCliImplementation { String processName = processParseResult.commandSpec().name(); QProcessMetaData process = qInstance.getProcess(processName); - RunProcessInput request = new RunProcessInput(qInstance); + RunProcessInput request = new RunProcessInput(qInstance); request.setSession(session); request.setProcessName(processName); 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 926cc972..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,8 +25,9 @@ package com.kingsrook.qqq.frontend.picocli; import java.io.InputStream; import java.sql.Connection; import java.util.List; +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.model.metadata.QAuthenticationMetaData; +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; @@ -112,7 +113,7 @@ public class TestUtils { return new QAuthenticationMetaData() .withName("mock") - .withType("mock"); + .withType(QAuthenticationType.MOCK); } From 59211717a02d328ddc16cf13014762812da3b8c2 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Thu, 21 Jul 2022 11:54:13 -0500 Subject: [PATCH 06/12] QQQ-27: upped qqq-backend-core / rdbms versions --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 9cf6787a..60c9fb95 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-SNAPSHOT + 0.2.0-20220721.162748-8 com.kingsrook.qqq qqq-backend-module-rdbms - 0.2.0-SNAPSHOT + 0.2.0-20220721.162748-8 test From 984a650d8c16e453a75352e6776237232857dbc8 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 26 Jul 2022 16:49:42 -0500 Subject: [PATCH 07/12] Update deps --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 82a38d84..0001bcf5 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-20220725.183211-13 + 0.2.0-20220726.214150-15 com.kingsrook.qqq qqq-backend-module-rdbms - 0.2.0-20220725.183409-11 + 0.2.0-20220726.214633-12 test From f17514c608ca80ff8dd5b832bfe44bed45cbfb3f Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 27 Jul 2022 09:15:47 -0500 Subject: [PATCH 08/12] QQQ-28 implement bulk edit, delete, insert --- .../qqq/frontend/picocli/QCommandBuilder.java | 54 +++++-- .../picocli/QPicoCliImplementation.java | 134 ++++++++++++++---- .../picocli/QPicoCliImplementationTest.java | 81 ++++++++++- 3 files changed, 230 insertions(+), 39 deletions(-) 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 6d2fdff1..415922c3 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java +++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java @@ -179,10 +179,7 @@ public class QCommandBuilder .type(int.class) .description("Optional limit on the max number of records to include in the export.") .build()); - exportCommand.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria") - .type(String[].class) - .description("Query filter criteria for the export. May be given multiple times. Use format: $fieldName $operator $value. e.g., id EQUALS 42") - .build()); + addCriteriaOption(exportCommand); // todo - add the fields as explicit params? @@ -191,6 +188,38 @@ public class QCommandBuilder + /******************************************************************************* + ** 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()); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -214,9 +243,7 @@ public class QCommandBuilder private CommandLine.Model.CommandSpec defineCountCommand(QTableMetaData table) { CommandLine.Model.CommandSpec countCommand = CommandLine.Model.CommandSpec.create(); - countCommand.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria") - .type(String[].class) - .build()); + addCriteriaOption(countCommand); // todo - add the fields as explicit params? @@ -233,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()); @@ -251,10 +279,7 @@ public class QCommandBuilder */ 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()); + addPrimaryKeyOrKeysOption(updateCommand, "update"); for(QFieldMetaData field : table.getFields().values()) { @@ -262,9 +287,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; } @@ -315,6 +345,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; } 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 5dd1835f..e518355a 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java +++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java @@ -62,6 +62,7 @@ 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.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; @@ -265,7 +266,7 @@ public class QPicoCliImplementation Log.info("No session information found in environment"); } - return(dotenvOptional); + return (dotenvOptional); } @@ -283,8 +284,8 @@ public class QPicoCliImplementation //////////////////////////////////// // look for .env environment file // //////////////////////////////////// - String sessionId = null; - Optional dotenv = loadDotEnv(); + String sessionId = null; + Optional dotenv = loadDotEnv(); if(dotenv.isPresent()) { sessionId = dotenv.get().get("SESSION_ID"); @@ -293,8 +294,8 @@ public class QPicoCliImplementation 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..."); + 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()) { @@ -701,6 +702,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 // @@ -782,38 +791,81 @@ public class QPicoCliImplementation 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(); - } - - updateInput.setRecords(recordList); + updateInput.setRecords(recordsToUpdate); UpdateAction updateAction = new UpdateAction(); UpdateOutput updateResult = updateAction.execute(updateInput); @@ -835,9 +887,24 @@ public class QPicoCliImplementation ///////////////////////////////////////////// // get the pKeys that the user specified // ///////////////////////////////////////////// - String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", ""); - Serializable[] primaryKeyValues = primaryKeyOption.split(","); - deleteInput.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(); DeleteOutput deleteResult = deleteAction.execute(deleteInput); @@ -865,4 +932,21 @@ public class QPicoCliImplementation 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/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java b/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java index cc70e5e5..73671010 100644 --- a/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java @@ -447,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", @@ -467,6 +480,55 @@ 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-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); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -486,6 +548,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 ** @@ -717,7 +791,6 @@ class QPicoCliImplementationTest - /******************************************************************************* ** test exporting a table ** @@ -736,6 +809,8 @@ class QPicoCliImplementationTest deleteFile(file); } + + /******************************************************************************* ** test exporting a table ** From e5ea13c2e07559f6fb8c8d948dbf0ec9248d1c4f Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Wed, 27 Jul 2022 16:57:27 -0500 Subject: [PATCH 09/12] QQQ-27: fixed bug when a primary key not specified --- .../kingsrook/qqq/frontend/picocli/QCommandBuilder.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 415922c3..505f1340 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java +++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java @@ -278,8 +278,12 @@ public class QCommandBuilder .build()); */ - QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField()); - addPrimaryKeyOrKeysOption(updateCommand, "update"); + QFieldMetaData primaryKeyField = null; + if(table.getPrimaryKeyField() != null) + { + primaryKeyField = table.getField(table.getPrimaryKeyField()); + addPrimaryKeyOrKeysOption(updateCommand, "update"); + } for(QFieldMetaData field : table.getFields().values()) { From 2079c82101ef8c2a07e03acfa8ef286ed2290353 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Wed, 27 Jul 2022 23:45:14 -0500 Subject: [PATCH 10/12] QQQ-27: gitignore .env --- .gitignore | 1 + pom.xml | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index b65cb041..2c7054c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ target/ *.iml +.env ############################################# diff --git a/pom.xml b/pom.xml index 0001bcf5..f5d10a42 100644 --- a/pom.xml +++ b/pom.xml @@ -74,11 +74,6 @@ picocli-shell-jline3 4.6.3 - - io.github.cdimascio - java-dotenv - 5.2.2 - com.h2database h2 From a7e7122fdef1fde920aaf2c565e88997e81d505e Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 28 Jul 2022 09:13:14 -0500 Subject: [PATCH 11/12] Removing limit on query; updating to qqq-backend 0.2.0 --- pom.xml | 4 ++-- .../qqq/frontend/picocli/QPicoCliImplementation.java | 3 +-- .../qqq/frontend/picocli/QPicoCliImplementationTest.java | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index f5d10a42..2c577f31 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-20220726.214150-15 + 0.2.0 com.kingsrook.qqq qqq-backend-module-rdbms - 0.2.0-20220726.214633-12 + 0.2.0 test 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 e518355a..e83fe394 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java +++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java @@ -536,7 +536,6 @@ public class QPicoCliImplementation queryInput.setSession(session); queryInput.setTableName(tableName); queryInput.setSkip(subParseResult.matchedOptionValue("skip", null)); - queryInput.setLimit(subParseResult.matchedOptionValue("limit", DEFAULT_QUERY_LIMIT)); String primaryKeyValue = subParseResult.matchedPositionalValue(0, null); if(primaryKeyValue == null) @@ -579,7 +578,7 @@ public class QPicoCliImplementation queryInput.setSession(session); queryInput.setTableName(tableName); queryInput.setSkip(subParseResult.matchedOptionValue("skip", null)); - queryInput.setLimit(subParseResult.matchedOptionValue("limit", DEFAULT_QUERY_LIMIT)); + queryInput.setLimit(subParseResult.matchedOptionValue("limit", null)); queryInput.setFilter(generateQueryFilter(subParseResult)); QueryAction queryAction = new QueryAction(); 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 73671010..12d30a86 100644 --- a/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java @@ -491,6 +491,7 @@ class QPicoCliImplementationTest 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()); From c2aa82cd8df7c64f2884b94f7ff263274524d9ab Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 28 Jul 2022 09:18:57 -0500 Subject: [PATCH 12/12] Update versions for release --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2c577f31..df0fcb18 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ com.kingsrook.qqq qqq-middleware-picocli - 0.2.0-SNAPSHOT + 0.2.0 scm:git:git@github.com:Kingsrook/qqq-middleware-picocli.git