From 154468bdf340a42edd0a2f3b2bff5363151f32e2 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 1 Mar 2022 18:29:59 -0600 Subject: [PATCH] Checkpoint: Added Update (edit) action; Load definition from JSON --- pom.xml | 14 +- .../qqq/frontend/picocli/QCommandBuilder.java | 253 +++++++++++++++++ .../picocli/QPicoCliImplementation.java | 257 ++++++------------ .../picocli/QPicoCliImplementationTest.java | 76 +++++- .../qqq/frontend/picocli/TestUtils.java | 4 +- src/test/resources/personQInstance.json | 156 +++++++++++ .../personQInstanceIncludingBackend.json | 163 +++++++++++ ...TrekDatabaseQInstanceIncludingBackend.json | 70 +++++ 8 files changed, 807 insertions(+), 186 deletions(-) create mode 100644 src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java create mode 100644 src/test/resources/personQInstance.json create mode 100644 src/test/resources/personQInstanceIncludingBackend.json create mode 100644 src/test/resources/starTrekDatabaseQInstanceIncludingBackend.json diff --git a/pom.xml b/pom.xml index f887c5c3..7099f797 100644 --- a/pom.xml +++ b/pom.xml @@ -79,7 +79,19 @@ - + + maven-assembly-plugin + + + jar-with-dependencies + + + + com.kingsrook.qqq.frontend.picocli.QPicoCliImplementation + + + + diff --git a/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java b/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java new file mode 100644 index 00000000..4a3f84b5 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java @@ -0,0 +1,253 @@ +/* + * Copyright © 2021-2022. Kingsrook LLC . All Rights Reserved. + */ + +package com.kingsrook.qqq.frontend.picocli; + + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import picocli.CommandLine; + + +/******************************************************************************* + ** Helper class for QPicCliImplementation to build the Command + ** + *******************************************************************************/ +public class QCommandBuilder +{ + private final QInstance qInstance; + + + + /******************************************************************************* + ** Constructor. + ** + *******************************************************************************/ + public QCommandBuilder(QInstance qInstance) + { + this.qInstance = qInstance; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + CommandLine.Model.CommandSpec buildCommandSpec(String topCommandName) + { + ////////////////////////////////// + // define the top-level command // + ////////////////////////////////// + CommandLine.Model.CommandSpec topCommandSpec = CommandLine.Model.CommandSpec.create(); + topCommandSpec.name(topCommandName); + topCommandSpec.version(topCommandName + " v1.0"); // todo... uh? + topCommandSpec.mixinStandardHelpOptions(true); // usageHelp and versionHelp options + topCommandSpec.addOption(CommandLine.Model.OptionSpec.builder("-m", "--meta-data") + .type(boolean.class) + .description("Output the meta-data for this CLI") + .build()); + + ///////////////////////////////////// + // add each table as a sub-command // + ///////////////////////////////////// + qInstance.getTables().keySet().stream().sorted().forEach(tableName -> + { + QTableMetaData table = qInstance.getTable(tableName); + + CommandLine.Model.CommandSpec tableCommand = CommandLine.Model.CommandSpec.create(); + topCommandSpec.addSubcommand(table.getName(), tableCommand); + + /////////////////////////////////////////////////// + // add table-specific sub-commands for the table // + /////////////////////////////////////////////////// + tableCommand.addSubcommand("meta-data", defineMetaDataCommand(table)); + tableCommand.addSubcommand("query", defineQueryCommand(table)); + tableCommand.addSubcommand("insert", defineInsertCommand(table)); + tableCommand.addSubcommand("update", defineUpdateCommand(table)); + tableCommand.addSubcommand("delete", defineDeleteCommand(table)); + + List processes = qInstance.getProcessesForTable(tableName); + if(CollectionUtils.nullSafeHasContents(processes)) + { + tableCommand.addSubcommand("process", defineTableProcessesCommand(table, processes)); + } + }); + return topCommandSpec; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private CommandLine.Model.CommandSpec defineMetaDataCommand(QTableMetaData table) + { + return CommandLine.Model.CommandSpec.create(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private CommandLine.Model.CommandSpec defineQueryCommand(QTableMetaData table) + { + CommandLine.Model.CommandSpec queryCommand = CommandLine.Model.CommandSpec.create(); + queryCommand.addOption(CommandLine.Model.OptionSpec.builder("-l", "--limit") + .type(int.class) + .build()); + queryCommand.addOption(CommandLine.Model.OptionSpec.builder("-s", "--skip") + .type(int.class) + .build()); + queryCommand.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria") + .type(String[].class) + .build()); + + // todo - add the fields as explicit params? + + return queryCommand; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private CommandLine.Model.CommandSpec defineUpdateCommand(QTableMetaData table) + { + CommandLine.Model.CommandSpec updateCommand = CommandLine.Model.CommandSpec.create(); + + /* + updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--jsonBody") + .type(String.class) + .build()); + + updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--jsonFile") + .type(String.class) + .build()); + + updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--csvFile") + .type(String.class) + .build()); + + updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--mapping") + .type(String.class) + .build()); + */ + + QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField()); + updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--primaryKey") + // type(getClassForField(primaryKeyField)) + .type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type? + .build()); + + for(QFieldMetaData field : table.getFields().values()) + { + if(!field.equals(primaryKeyField)) + { + updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--field-" + field.getName()) + .type(getClassForField(field)) + .build()); + } + } + return updateCommand; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private CommandLine.Model.CommandSpec defineInsertCommand(QTableMetaData table) + { + CommandLine.Model.CommandSpec insertCommand = CommandLine.Model.CommandSpec.create(); + + insertCommand.addOption(CommandLine.Model.OptionSpec.builder("--jsonBody") + .type(String.class) + .build()); + + insertCommand.addOption(CommandLine.Model.OptionSpec.builder("--jsonFile") + .type(String.class) + .build()); + + insertCommand.addOption(CommandLine.Model.OptionSpec.builder("--csvFile") + .type(String.class) + .build()); + + insertCommand.addOption(CommandLine.Model.OptionSpec.builder("--mapping") + .type(String.class) + .build()); + + for(QFieldMetaData field : table.getFields().values()) + { + insertCommand.addOption(CommandLine.Model.OptionSpec.builder("--field-" + field.getName()) + .type(getClassForField(field)) + .build()); + } + return insertCommand; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private CommandLine.Model.CommandSpec defineDeleteCommand(QTableMetaData table) + { + CommandLine.Model.CommandSpec deleteCommand = CommandLine.Model.CommandSpec.create(); + + deleteCommand.addOption(CommandLine.Model.OptionSpec.builder("--primaryKey") + .type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type? + .build()); + + return deleteCommand; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private CommandLine.Model.CommandSpec defineTableProcessesCommand(QTableMetaData table, List processes) + { + CommandLine.Model.CommandSpec processesCommand = CommandLine.Model.CommandSpec.create(); + + for(QProcessMetaData process : processes) + { + CommandLine.Model.CommandSpec processCommand = CommandLine.Model.CommandSpec.create(); + processesCommand.addSubcommand(process.getName(), processCommand); + } + + return (processesCommand); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("checkstyle:Indentation") + private Class getClassForField(QFieldMetaData field) + { + // @formatter:off // IJ can't do new-style switch correctly yet... + return switch(field.getType()) + { + case STRING, TEXT, HTML, PASSWORD -> String.class; + case INTEGER -> Integer.class; + case DECIMAL -> BigDecimal.class; + case DATE -> LocalDate.class; + // case TIME -> LocalTime.class; + case DATE_TIME -> LocalDateTime.class; + }; + // @formatter:on + } + +} diff --git a/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java index 172d9c60..62e92c54 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java +++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java @@ -9,9 +9,7 @@ import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.io.PrintWriter; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.LocalDateTime; +import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -23,9 +21,11 @@ import com.kingsrook.qqq.backend.core.actions.MetaDataAction; import com.kingsrook.qqq.backend.core.actions.QueryAction; import com.kingsrook.qqq.backend.core.actions.RunFunctionAction; import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction; +import com.kingsrook.qqq.backend.core.actions.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.delete.DeleteRequest; @@ -42,15 +42,15 @@ 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.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.data.QRecord; -import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; 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.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import org.apache.commons.io.FileUtils; import picocli.CommandLine; @@ -81,17 +81,29 @@ public class QPicoCliImplementation /******************************************************************************* ** *******************************************************************************/ - public static void main(String[] args) + public static void main(String[] args) throws IOException { - QInstance qInstance = new QInstance(); - - // todo - parse args to look up metaData and prime instance // todo - authentication // qInstance.addBackend(QMetaDataProvider.getQBackend()); - QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance); - int exitCode = qPicoCliImplementation.runCli("qapi", args); - System.exit(exitCode); + // parse args to look up metaData and prime instance + if(args.length > 0 && args[0].startsWith("--qInstanceJsonFile=")) + { + 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); + System.exit(exitCode); + } + else + { + System.err.println("To run this main class directly, you must specify: --qInstanceJsonFile=path/to/qInstance.json"); + System.exit(1); + } } @@ -107,7 +119,8 @@ public class QPicoCliImplementation /******************************************************************************* - ** + ** Driver method that uses System out & err streams. + * *******************************************************************************/ public int runCli(String name, String[] args) { @@ -117,6 +130,8 @@ public class QPicoCliImplementation /******************************************************************************* + ** Actual driver methods that takes streams as params. + * ** examples - todo, make docs complete! ** my-app-cli [--all] [--format=] ** my-app-cli $table meta-data [--format=] @@ -130,42 +145,7 @@ public class QPicoCliImplementation *******************************************************************************/ public int runCli(String name, String[] args, PrintStream out, PrintStream err) { - ////////////////////////////////// - // define the top-level command // - ////////////////////////////////// - CommandSpec topCommandSpec = CommandSpec.create(); - topCommandSpec.name(name); - topCommandSpec.version(name + " v1.0"); // todo... uh? - topCommandSpec.mixinStandardHelpOptions(true); // usageHelp and versionHelp options - topCommandSpec.addOption(OptionSpec.builder("-m", "--meta-data") - .type(boolean.class) - .description("Output the meta-data for this CLI") - .build()); - - ///////////////////////////////////// - // add each table as a sub-command // - ///////////////////////////////////// - qInstance.getTables().keySet().stream().sorted().forEach(tableName -> - { - QTableMetaData table = qInstance.getTable(tableName); - - CommandSpec tableCommand = CommandSpec.create(); - topCommandSpec.addSubcommand(table.getName(), tableCommand); - - /////////////////////////////////////////////////// - // add table-specific sub-commands for the table // - /////////////////////////////////////////////////// - tableCommand.addSubcommand("meta-data", defineMetaDataCommand(table)); - tableCommand.addSubcommand("query", defineQueryCommand(table)); - tableCommand.addSubcommand("insert", defineInsertCommand(table)); - tableCommand.addSubcommand("delete", defineDeleteCommand(table)); - - List processes = qInstance.getProcessesForTable(tableName); - if(CollectionUtils.nullSafeHasContents(processes)) - { - tableCommand.addSubcommand("process", defineTableProcessesCommand(table, processes)); - } - }); + CommandSpec topCommandSpec = new QCommandBuilder(qInstance).buildCommandSpec(name); CommandLine commandLine = new CommandLine(topCommandSpec); commandLine.setOut(new PrintWriter(out, true)); @@ -215,6 +195,7 @@ public class QPicoCliImplementation /////////////////////////////////////////// // handle exceptions from business logic // /////////////////////////////////////////// + ex.printStackTrace(); commandLine.getErr().println("Error: " + ex.getMessage()); return (commandLine.getCommandSpec().exitCodeOnExecutionException()); } @@ -238,127 +219,6 @@ public class QPicoCliImplementation - /******************************************************************************* - ** - *******************************************************************************/ - private CommandSpec defineMetaDataCommand(QTableMetaData table) - { - return CommandSpec.create(); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private CommandSpec defineQueryCommand(QTableMetaData table) - { - CommandSpec queryCommand = CommandSpec.create(); - queryCommand.addOption(OptionSpec.builder("-l", "--limit") - .type(int.class) - .build()); - queryCommand.addOption(OptionSpec.builder("-s", "--skip") - .type(int.class) - .build()); - queryCommand.addOption(OptionSpec.builder("-c", "--criteria") - .type(String[].class) - .build()); - - // todo - add the fields as explicit params? - - return queryCommand; - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private CommandSpec defineInsertCommand(QTableMetaData table) - { - CommandSpec insertCommand = CommandSpec.create(); - - insertCommand.addOption(OptionSpec.builder("--jsonBody") - .type(String.class) - .build()); - - insertCommand.addOption(OptionSpec.builder("--jsonFile") - .type(String.class) - .build()); - - insertCommand.addOption(OptionSpec.builder("--csvFile") - .type(String.class) - .build()); - - insertCommand.addOption(OptionSpec.builder("--mapping") - .type(String.class) - .build()); - - for(QFieldMetaData field : table.getFields().values()) - { - insertCommand.addOption(OptionSpec.builder("--field-" + field.getName()) - .type(getClassForField(field)) - .build()); - } - return insertCommand; - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private CommandSpec defineDeleteCommand(QTableMetaData table) - { - CommandSpec deleteCommand = CommandSpec.create(); - - deleteCommand.addOption(OptionSpec.builder("--primaryKey") - .type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type? - .build()); - - return deleteCommand; - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private CommandSpec defineTableProcessesCommand(QTableMetaData table, List processes) - { - CommandSpec processesCommand = CommandSpec.create(); - - for(QProcessMetaData process : processes) - { - CommandSpec processCommand = CommandSpec.create(); - processesCommand.addSubcommand(process.getName(), processCommand); - } - - return (processesCommand); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private Class getClassForField(QFieldMetaData field) - { - // @formatter:off // IJ can't do new-style switch correctly yet... - return switch(field.getType()) - { - case STRING, TEXT, HTML, PASSWORD -> String.class; - case INTEGER -> Integer.class; - case DECIMAL -> BigDecimal.class; - case DATE -> LocalDate.class; - // case TIME -> LocalTime.class; - case DATE_TIME -> LocalDateTime.class; - }; - // @formatter:on - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -403,6 +263,10 @@ public class QPicoCliImplementation { return runTableInsert(commandLine, tableName, subParseResult); } + case "update": + { + return runTableUpdate(commandLine, tableName, subParseResult); + } case "delete": { return runTableDelete(commandLine, tableName, subParseResult); @@ -602,6 +466,57 @@ 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); + QTableMetaData table = qInstance.getTable(tableName); + + List recordList = new ArrayList<>(); + + boolean anyFields = false; + + String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", ""); + Serializable[] primaryKeyValues = primaryKeyOption.split(","); + for(Serializable primaryKeyValue : primaryKeyValues) + { + QRecord record = new QRecord(); + + recordList.add(record); + record.setValue(table.getPrimaryKeyField(), primaryKeyValue); + + for(OptionSpec matchedOption : subParseResult.matchedOptions()) + { + if(matchedOption.longestName().startsWith("--field-")) + { + anyFields = true; + String fieldName = matchedOption.longestName().substring(8); + record.setValue(fieldName, matchedOption.getValue()); + } + } + } + + if(!anyFields || recordList.isEmpty()) + { + CommandLine subCommandLine = commandLine.getSubcommands().get("update"); + subCommandLine.usage(commandLine.getOut()); + return commandLine.getCommandSpec().exitCodeOnUsageHelp(); + } + + updateRequest.setRecords(recordList); + + UpdateAction updateAction = new UpdateAction(); + UpdateResult updateResult = updateAction.execute(updateRequest); + commandLine.getOut().println(JsonUtils.toPrettyJson(updateResult)); + return commandLine.getCommandSpec().exitCodeOnSuccess(); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -615,7 +530,7 @@ public class QPicoCliImplementation // get the pKeys that the user specified // ///////////////////////////////////////////// String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", ""); - String[] primaryKeyValues = primaryKeyOption.split(","); + Serializable[] primaryKeyValues = primaryKeyOption.split(","); deleteRequest.setPrimaryKeys(Arrays.asList(primaryKeyValues)); DeleteAction deleteAction = new DeleteAction(); 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 9569757a..a2fc1b17 100644 --- a/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java @@ -198,10 +198,11 @@ class QPicoCliImplementationTest TestOutput testOutput = testCli("person", "query", "--skip=1", "--limit=2", "--criteria", "id NOT_EQUALS 3"); JSONObject queryResult = JsonUtils.toJSONObject(testOutput.getOutput()); assertNotNull(queryResult); - assertEquals(2, queryResult.getJSONArray("records").length()); + JSONArray records = queryResult.getJSONArray("records"); + assertEquals(2, records.length()); // query for id != 3, and skipping 1, expect to get back rows 2 & 4 - assertEquals(2, queryResult.getJSONArray("records").getJSONObject(0).getInt("primaryKey")); - assertEquals(4, queryResult.getJSONArray("records").getJSONObject(1).getInt("primaryKey")); + assertEquals(2, records.getJSONObject(0).getJSONObject("values").getInt("id")); + assertEquals(4, records.getJSONObject(1).getJSONObject("values").getInt("id")); } @@ -232,7 +233,7 @@ class QPicoCliImplementationTest JSONObject insertResult = JsonUtils.toJSONObject(testOutput.getOutput()); assertNotNull(insertResult); assertEquals(1, insertResult.getJSONArray("records").length()); - assertEquals(6, insertResult.getJSONArray("records").getJSONObject(0).getInt("primaryKey")); + assertEquals(6, insertResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getInt("id")); } @@ -256,7 +257,7 @@ class QPicoCliImplementationTest JSONObject insertResult = JsonUtils.toJSONObject(testOutput.getOutput()); assertNotNull(insertResult); assertEquals(1, insertResult.getJSONArray("records").length()); - assertEquals(6, insertResult.getJSONArray("records").getJSONObject(0).getInt("primaryKey")); + assertEquals(6, insertResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getInt("id")); assertEquals("Chester", insertResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getString("firstName")); assertEquals("Cheese", insertResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getString("lastName")); } @@ -287,8 +288,8 @@ class QPicoCliImplementationTest assertNotNull(insertResult); JSONArray records = insertResult.getJSONArray("records"); assertEquals(2, records.length()); - assertEquals(6, records.getJSONObject(0).getInt("primaryKey")); - assertEquals(7, records.getJSONObject(1).getInt("primaryKey")); + assertEquals(6, insertResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getInt("id")); + assertEquals(7, insertResult.getJSONArray("records").getJSONObject(1).getJSONObject("values").getInt("id")); assertEquals("Charlie", records.getJSONObject(0).getJSONObject("values").getString("firstName")); assertEquals("Bear", records.getJSONObject(0).getJSONObject("values").getString("lastName")); assertEquals("Coco", records.getJSONObject(1).getJSONObject("values").getString("firstName")); @@ -323,8 +324,8 @@ class QPicoCliImplementationTest assertNotNull(insertResult); JSONArray records = insertResult.getJSONArray("records"); assertEquals(2, records.length()); - assertEquals(6, records.getJSONObject(0).getInt("primaryKey")); - assertEquals(7, records.getJSONObject(1).getInt("primaryKey")); + assertEquals(6, insertResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getInt("id")); + assertEquals(7, insertResult.getJSONArray("records").getJSONObject(1).getJSONObject("values").getInt("id")); assertEquals("Louis", records.getJSONObject(0).getJSONObject("values").getString("firstName")); assertEquals("Willikers", records.getJSONObject(0).getJSONObject("values").getString("lastName")); assertEquals("Nestle", records.getJSONObject(1).getJSONObject("values").getString("firstName")); @@ -333,6 +334,56 @@ class QPicoCliImplementationTest + /******************************************************************************* + ** test running an update w/o specifying any fields, prints usage + ** + *******************************************************************************/ + @Test + public void test_tableUpdateNoFieldsPrintsUsage() + { + TestOutput testOutput = testCli("person", "update"); + assertTestOutputContains(testOutput, "Usage: " + CLI_NAME + " person update"); + } + + + + /******************************************************************************* + ** test running an update w/ fields as arguments + ** + *******************************************************************************/ + @Test + public void test_tableUpdateFieldArguments() throws Exception + { + assertRowValueById("person", "first_name", "Garret", 5); + TestOutput testOutput = testCli("person", "update", + "--primaryKey=5", + "--field-firstName=Lucy", + "--field-lastName=Lu"); + JSONObject updateResult = JsonUtils.toJSONObject(testOutput.getOutput()); + assertNotNull(updateResult); + assertEquals(1, updateResult.getJSONArray("records").length()); + assertEquals(5, updateResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getInt("id")); + assertRowValueById("person", "first_name", "Lucy", 5); + } + + + + private void assertRowValueById(String tableName, String columnName, String value, Integer id) throws Exception + { + TestUtils.runTestSql("SELECT " + columnName + " FROM " + tableName + " WHERE id=" + id, (rs -> { + if(rs.next()) + { + assertEquals(value, rs.getString(1)); + } + else + { + fail("Row not found"); + } + })); + } + + + /******************************************************************************* ** test running a delete against a table ** @@ -343,9 +394,10 @@ class QPicoCliImplementationTest TestOutput testOutput = testCli("person", "delete", "--primaryKey", "2,4"); JSONObject deleteResult = JsonUtils.toJSONObject(testOutput.getOutput()); assertNotNull(deleteResult); - assertEquals(2, deleteResult.getJSONArray("records").length()); - assertEquals(2, deleteResult.getJSONArray("records").getJSONObject(0).getInt("primaryKey")); - assertEquals(4, deleteResult.getJSONArray("records").getJSONObject(1).getInt("primaryKey")); + JSONArray records = deleteResult.getJSONArray("records"); + assertEquals(2, records.length()); + assertEquals(2, records.getJSONObject(0).getJSONObject("values").getInt("id")); + assertEquals(4, records.getJSONObject(1).getJSONObject("values").getInt("id")); TestUtils.runTestSql("SELECT id FROM person", (rs -> { int rowsFound = 0; while(rs.next()) 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 12c55e79..7250b132 100644 --- a/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java @@ -9,13 +9,13 @@ import java.io.InputStream; import java.sql.Connection; import java.util.List; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.QCodeUsage; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; 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.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionMetaData; diff --git a/src/test/resources/personQInstance.json b/src/test/resources/personQInstance.json new file mode 100644 index 00000000..250e5f57 --- /dev/null +++ b/src/test/resources/personQInstance.json @@ -0,0 +1,156 @@ +{ + "authentication": { + "name": "mock", + "type": "mock", + "values": null + }, + "tables": { + "person": { + "name": "person", + "label": "Person", + "backendName": "default", + "primaryKeyField": "id", + "fields": { + "id": { + "name": "id", + "label": null, + "backendName": null, + "type": "INTEGER", + "possibleValueSourceName": null + }, + "createDate": { + "name": "createDate", + "label": null, + "backendName": null, + "type": "DATE_TIME", + "possibleValueSourceName": null + }, + "modifyDate": { + "name": "modifyDate", + "label": null, + "backendName": null, + "type": "DATE_TIME", + "possibleValueSourceName": null + }, + "firstName": { + "name": "firstName", + "label": null, + "backendName": null, + "type": "STRING", + "possibleValueSourceName": null + }, + "lastName": { + "name": "lastName", + "label": null, + "backendName": null, + "type": "STRING", + "possibleValueSourceName": null + }, + "birthDate": { + "name": "birthDate", + "label": null, + "backendName": null, + "type": "DATE", + "possibleValueSourceName": null + }, + "email": { + "name": "email", + "label": null, + "backendName": null, + "type": "STRING", + "possibleValueSourceName": null + }, + "homeState": { + "name": "homeState", + "label": null, + "backendName": null, + "type": "STRING", + "possibleValueSourceName": "state" + } + } + } + }, + "possibleValueSources": { + "state": { + "name": "state", + "type": "ENUM", + "enumValues": [ + "IL", + "MO" + ] + } + }, + "processes": { + "greet": { + "name": "greet", + "tableName": "person", + "functionList": [ + { + "name": "prepare", + "label": null, + "inputMetaData": { + "recordListMetaData": { + "tableName": "person", + "fields": null + }, + "fieldList": [ + { + "name": "greetingPrefix", + "label": null, + "backendName": null, + "type": "STRING", + "possibleValueSourceName": null + }, + { + "name": "greetingSuffix", + "label": null, + "backendName": null, + "type": "STRING", + "possibleValueSourceName": null + } + ] + }, + "outputMetaData": { + "recordListMetaData": { + "tableName": "person", + "fields": { + "fullGreeting": { + "name": "fullGreeting", + "label": null, + "backendName": null, + "type": "STRING", + "possibleValueSourceName": null + } + } + }, + "fieldList": [ + { + "name": "outputMessage", + "label": null, + "backendName": null, + "type": "STRING", + "possibleValueSourceName": null + } + ] + }, + "code": { + "name": "com.kingsrook.qqq.backend.core.interfaces.mock.MockFunctionBody", + "codeType": "JAVA", + "codeUsage": "FUNCTION" + }, + "outputView": { + "messageField": "outputMessage", + "recordListView": { + "fieldNames": [ + "id", + "firstName", + "lastName", + "fullGreeting" + ] + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/src/test/resources/personQInstanceIncludingBackend.json b/src/test/resources/personQInstanceIncludingBackend.json new file mode 100644 index 00000000..74095833 --- /dev/null +++ b/src/test/resources/personQInstanceIncludingBackend.json @@ -0,0 +1,163 @@ +{ + "tables": { + "person": { + "primaryKeyField": "id", + "name": "person", + "backendName": "default", + "label": "Person", + "fields": { + "firstName": { + "name": "firstName", + "backendName": null, + "label": null, + "type": "STRING", + "possibleValueSourceName": null + }, + "lastName": { + "name": "lastName", + "backendName": null, + "label": null, + "type": "STRING", + "possibleValueSourceName": null + }, + "modifyDate": { + "name": "modifyDate", + "backendName": null, + "label": null, + "type": "DATE_TIME", + "possibleValueSourceName": null + }, + "homeState": { + "name": "homeState", + "backendName": null, + "label": null, + "type": "STRING", + "possibleValueSourceName": "state" + }, + "id": { + "name": "id", + "backendName": null, + "label": null, + "type": "INTEGER", + "possibleValueSourceName": null + }, + "birthDate": { + "name": "birthDate", + "backendName": null, + "label": null, + "type": "DATE", + "possibleValueSourceName": null + }, + "email": { + "name": "email", + "backendName": null, + "label": null, + "type": "STRING", + "possibleValueSourceName": null + }, + "createDate": { + "name": "createDate", + "backendName": null, + "label": null, + "type": "DATE_TIME", + "possibleValueSourceName": null + } + } + } + }, + "processes": { + "greet": { + "functionList": [ + { + "code": { + "codeUsage": "FUNCTION", + "codeType": "JAVA", + "name": "com.kingsrook.qqq.backend.core.interfaces.mock.MockFunctionBody" + }, + "inputMetaData": { + "recordListMetaData": { + "fields": null, + "tableName": "person" + }, + "fieldList": [ + { + "name": "greetingPrefix", + "backendName": null, + "label": null, + "type": "STRING", + "possibleValueSourceName": null + }, + { + "name": "greetingSuffix", + "backendName": null, + "label": null, + "type": "STRING", + "possibleValueSourceName": null + } + ] + }, + "outputMetaData": { + "recordListMetaData": { + "fields": { + "fullGreeting": { + "name": "fullGreeting", + "backendName": null, + "label": null, + "type": "STRING", + "possibleValueSourceName": null + } + }, + "tableName": "person" + }, + "fieldList": [ + { + "name": "outputMessage", + "backendName": null, + "label": null, + "type": "STRING", + "possibleValueSourceName": null + } + ] + }, + "outputView": { + "messageField": "outputMessage", + "recordListView": { + "fieldNames": [ + "id", + "firstName", + "lastName", + "fullGreeting" + ] + } + }, + "name": "prepare", + "label": null + } + ], + "name": "greet", + "tableName": "person" + } + }, + "possibleValueSources": { + "state": { + "name": "state", + "type": "ENUM", + "enumValues": [ + "IL", + "MO" + ] + } + }, + "backends": { + "default": { + "values": null, + "name": "default", + "type": "mock" + } + }, + "authentication": { + "values": null, + "name": "mock", + "type": "mock" + } +} \ No newline at end of file diff --git a/src/test/resources/starTrekDatabaseQInstanceIncludingBackend.json b/src/test/resources/starTrekDatabaseQInstanceIncludingBackend.json new file mode 100644 index 00000000..69d0389a --- /dev/null +++ b/src/test/resources/starTrekDatabaseQInstanceIncludingBackend.json @@ -0,0 +1,70 @@ +{ + "tables": { + "series": { + "primaryKeyField": "id", + "name": "series", + "backendName": "wherenooneMysql", + "label": "Person", + "fields": { + "name": { + "name": "name", + "backendName": null, + "label": null, + "type": "STRING", + "possibleValueSourceName": null + }, + "abbreviation": { + "name": "abbreviation", + "backendName": null, + "label": null, + "type": "STRING", + "possibleValueSourceName": null + }, + "id": { + "name": "id", + "backendName": null, + "label": null, + "type": "INTEGER", + "possibleValueSourceName": null + }, + "createDate": { + "name": "createDate", + "backendName": "create_date", + "label": "Create Date", + "type": "DATE_TIME", + "possibleValueSourceName": null + }, + "modifyDate": { + "name": "modifyDate", + "backendName": "modify_date", + "label": "Modify Date", + "type": "DATE_TIME", + "possibleValueSourceName": null + } + } + } + }, + "processes": { + }, + "possibleValueSources": { + }, + "backends": { + "wherenooneMysql": { + "values": { + "vendor": "mysql", + "hostName": "localhost", + "port": "3306", + "databaseName": "wherenoone", + "username": "root", + "password": "password" + }, + "name": "wherenooneMysql", + "type": "rdbms" + } + }, + "authentication": { + "values": null, + "name": "mock", + "type": "mock" + } +}