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