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 **