Merge branch 'feature/QQQ-26-exports-poc' into feature/sprint-7-integration

This commit is contained in:
2022-07-26 08:17:38 -05:00
8 changed files with 658 additions and 142 deletions

View File

@ -24,6 +24,8 @@ commands:
name: Run Maven name: Run Maven
command: | command: |
mvn -s .circleci/mvn-settings.xml << parameters.maven_subcommand >> mvn -s .circleci/mvn-settings.xml << parameters.maven_subcommand >>
- store_artifacts:
path: target/site/jacoco
- run: - run:
name: Save test results name: Save test results
command: | command: |

1
.gitignore vendored
View File

@ -28,3 +28,4 @@ target/
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid* hs_err_pid*
.DS_Store

10
pom.xml
View File

@ -53,12 +53,12 @@
<dependency> <dependency>
<groupId>com.kingsrook.qqq</groupId> <groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId> <artifactId>qqq-backend-core</artifactId>
<version>0.2.0-20220721.162748-8</version> <version>0.2.0-20220725.183211-13</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.kingsrook.qqq</groupId> <groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-module-rdbms</artifactId> <artifactId>qqq-backend-module-rdbms</artifactId>
<version>0.2.0-20220721.162748-8</version> <version>0.2.0-20220725.183409-11</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
@ -108,6 +108,12 @@
<version>5.8.1</version> <version>5.8.1</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.23.1</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -90,11 +90,13 @@ public class QCommandBuilder
// add table-specific sub-commands for the table // // add table-specific sub-commands for the table //
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
tableCommand.addSubcommand("meta-data", defineMetaDataCommand(table)); tableCommand.addSubcommand("meta-data", defineMetaDataCommand(table));
tableCommand.addSubcommand("count", defineQueryCommand(table)); tableCommand.addSubcommand("count", defineCountCommand(table));
tableCommand.addSubcommand("get", defineGetCommand(table));
tableCommand.addSubcommand("query", defineQueryCommand(table)); tableCommand.addSubcommand("query", defineQueryCommand(table));
tableCommand.addSubcommand("insert", defineInsertCommand(table)); tableCommand.addSubcommand("insert", defineInsertCommand(table));
tableCommand.addSubcommand("update", defineUpdateCommand(table)); tableCommand.addSubcommand("update", defineUpdateCommand(table));
tableCommand.addSubcommand("delete", defineDeleteCommand(table)); tableCommand.addSubcommand("delete", defineDeleteCommand(table));
tableCommand.addSubcommand("export", defineExportCommand(table));
List<QProcessMetaData> processes = qInstance.getProcessesForTable(tableName); List<QProcessMetaData> processes = qInstance.getProcessesForTable(tableName);
if(CollectionUtils.nullSafeHasContents(processes)) if(CollectionUtils.nullSafeHasContents(processes))
@ -158,6 +160,71 @@ public class QCommandBuilder
/*******************************************************************************
**
*******************************************************************************/
private CommandLine.Model.CommandSpec defineExportCommand(QTableMetaData table)
{
CommandLine.Model.CommandSpec exportCommand = CommandLine.Model.CommandSpec.create();
exportCommand.addOption(CommandLine.Model.OptionSpec.builder("-f", "--filename")
.type(String.class)
.description("File name (including path) to write to. File extension will be used to determine the report format. Supported formats are: csv, xlsx.")
.required(true)
.build());
exportCommand.addOption(CommandLine.Model.OptionSpec.builder("-e", "--fieldNames")
.type(String.class)
.description("Comma-separated list of field names (e.g., from table meta-data) to include in the export. If not given, then all fields in the table are included.")
.build());
exportCommand.addOption(CommandLine.Model.OptionSpec.builder("-l", "--limit")
.type(int.class)
.description("Optional limit on the max number of records to include in the export.")
.build());
exportCommand.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria")
.type(String[].class)
.description("Query filter criteria for the export. May be given multiple times. Use format: $fieldName $operator $value. e.g., id EQUALS 42")
.build());
// todo - add the fields as explicit params?
return exportCommand;
}
/*******************************************************************************
**
*******************************************************************************/
private CommandLine.Model.CommandSpec defineGetCommand(QTableMetaData table)
{
CommandLine.Model.CommandSpec getCommand = CommandLine.Model.CommandSpec.create();
getCommand.addPositional(CommandLine.Model.PositionalParamSpec.builder()
.index("0")
// .type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type?
.description("Primary key value from the table")
.build());
return getCommand;
}
/*******************************************************************************
**
*******************************************************************************/
private CommandLine.Model.CommandSpec defineCountCommand(QTableMetaData table)
{
CommandLine.Model.CommandSpec countCommand = CommandLine.Model.CommandSpec.create();
countCommand.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria")
.type(String[].class)
.build());
// todo - add the fields as explicit params?
return countCommand;
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -306,6 +373,7 @@ public class QCommandBuilder
case DATE -> LocalDate.class; case DATE -> LocalDate.class;
// case TIME -> LocalTime.class; // case TIME -> LocalTime.class;
case DATE_TIME -> LocalDateTime.class; case DATE_TIME -> LocalDateTime.class;
case BLOB -> byte[].class;
}; };
// @formatter:on // @formatter:on
} }

View File

@ -23,7 +23,9 @@ package com.kingsrook.qqq.frontend.picocli;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream; import java.io.PrintStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.Serializable; import java.io.Serializable;
@ -36,6 +38,7 @@ import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.reporting.ReportAction;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
@ -48,12 +51,16 @@ import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput; import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput;
import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.AbstractQFieldMapping; import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.AbstractQFieldMapping;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; 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.count.CountOutput;
@ -78,8 +85,10 @@ import com.kingsrook.qqq.backend.core.modules.authentication.Auth0Authentication
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import io.github.cdimascio.dotenv.Dotenv; import io.github.cdimascio.dotenv.Dotenv;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.core.config.Configurator;
import org.jline.reader.LineReader; import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder; import org.jline.reader.LineReaderBuilder;
import org.jline.utils.Log; import org.jline.utils.Log;
@ -101,7 +110,7 @@ import picocli.CommandLine.UnmatchedArgumentException;
*******************************************************************************/ *******************************************************************************/
public class QPicoCliImplementation public class QPicoCliImplementation
{ {
public static final int DEFAULT_LIMIT = 20; public static final int DEFAULT_QUERY_LIMIT = 20;
private static QInstance qInstance; private static QInstance qInstance;
private static QSession session; private static QSession session;
@ -143,6 +152,14 @@ public class QPicoCliImplementation
*******************************************************************************/ *******************************************************************************/
public QPicoCliImplementation(QInstance qInstance) public QPicoCliImplementation(QInstance qInstance)
{ {
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// use the qqq-picocli log4j config, less the system property log4j.configurationFile was set by the runner //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(System.getProperty("log4j.configurationFile") == null)
{
Configurator.initialize(null, "qqq-picocli-log4j2.xml");
}
QPicoCliImplementation.qInstance = qInstance; QPicoCliImplementation.qInstance = qInstance;
} }
@ -353,10 +370,19 @@ public class QPicoCliImplementation
{ {
return runTableCount(commandLine, tableName, subParseResult); return runTableCount(commandLine, tableName, subParseResult);
} }
case "get":
{
CommandLine subCommandLine = commandLine.getSubcommands().get(subCommandName);
return runTableGet(commandLine, tableName, subParseResult, subCommandLine);
}
case "query": case "query":
{ {
return runTableQuery(commandLine, tableName, subParseResult); return runTableQuery(commandLine, tableName, subParseResult);
} }
case "export":
{
return runTableExport(commandLine, tableName, subParseResult);
}
case "insert": case "insert":
{ {
return runTableInsert(commandLine, tableName, subParseResult); return runTableInsert(commandLine, tableName, subParseResult);
@ -500,6 +526,49 @@ public class QPicoCliImplementation
/*******************************************************************************
**
*******************************************************************************/
private int runTableGet(CommandLine commandLine, String tableName, ParseResult subParseResult, CommandLine subCommandLine) throws QException
{
QueryInput queryInput = new QueryInput(qInstance);
queryInput.setSession(session);
queryInput.setTableName(tableName);
queryInput.setSkip(subParseResult.matchedOptionValue("skip", null));
queryInput.setLimit(subParseResult.matchedOptionValue("limit", DEFAULT_QUERY_LIMIT));
String primaryKeyValue = subParseResult.matchedPositionalValue(0, null);
if(primaryKeyValue == null)
{
subCommandLine.usage(commandLine.getOut());
return commandLine.getCommandSpec().exitCodeOnUsageHelp();
}
QTableMetaData table = queryInput.getTable();
QQueryFilter filter = new QQueryFilter()
.withCriteria(new QFilterCriteria()
.withFieldName(table.getPrimaryKeyField())
.withOperator(QCriteriaOperator.EQUALS)
.withValues(List.of(primaryKeyValue)));
queryInput.setFilter(filter);
QueryAction queryAction = new QueryAction();
QueryOutput queryOutput = queryAction.execute(queryInput);
List<QRecord> records = queryOutput.getRecords();
if(records.isEmpty())
{
commandLine.getOut().println("No " + table.getLabel() + " found for " + table.getField(table.getPrimaryKeyField()).getLabel() + ": " + primaryKeyValue);
return commandLine.getCommandSpec().exitCodeOnInvalidInput();
}
else
{
commandLine.getOut().println(JsonUtils.toPrettyJson(records.get(0)));
return commandLine.getCommandSpec().exitCodeOnSuccess();
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -509,7 +578,7 @@ public class QPicoCliImplementation
queryInput.setSession(session); queryInput.setSession(session);
queryInput.setTableName(tableName); queryInput.setTableName(tableName);
queryInput.setSkip(subParseResult.matchedOptionValue("skip", null)); queryInput.setSkip(subParseResult.matchedOptionValue("skip", null));
queryInput.setLimit(subParseResult.matchedOptionValue("limit", DEFAULT_LIMIT)); queryInput.setLimit(subParseResult.matchedOptionValue("limit", DEFAULT_QUERY_LIMIT));
queryInput.setFilter(generateQueryFilter(subParseResult)); queryInput.setFilter(generateQueryFilter(subParseResult));
QueryAction queryAction = new QueryAction(); QueryAction queryAction = new QueryAction();
@ -520,6 +589,77 @@ public class QPicoCliImplementation
/*******************************************************************************
**
*******************************************************************************/
private int runTableExport(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException
{
String filename = subParseResult.matchedOptionValue("--filename", "");
/////////////////////////////////////////////////////////////////////////////////////////
// if a format query param wasn't given, then try to get file extension from file name //
/////////////////////////////////////////////////////////////////////////////////////////
ReportFormat reportFormat;
if(filename.contains("."))
{
reportFormat = ReportFormat.fromString(filename.substring(filename.lastIndexOf(".") + 1));
}
else
{
throw (new QUserFacingException("File name did not contain an extension, so report format could not be inferred."));
}
OutputStream outputStream;
try
{
outputStream = new FileOutputStream(filename);
}
catch(Exception e)
{
throw (new QException("Error opening report file: " + e.getMessage(), e));
}
try
{
/////////////////////////////////////////////
// set up the report action's input object //
/////////////////////////////////////////////
ReportInput reportInput = new ReportInput(qInstance);
reportInput.setSession(session);
reportInput.setTableName(tableName);
reportInput.setReportFormat(reportFormat);
reportInput.setFilename(filename);
reportInput.setReportOutputStream(outputStream);
reportInput.setLimit(subParseResult.matchedOptionValue("limit", null));
reportInput.setQueryFilter(generateQueryFilter(subParseResult));
String fieldNames = subParseResult.matchedOptionValue("--fieldNames", "");
if(StringUtils.hasContent(fieldNames))
{
reportInput.setFieldNames(Arrays.asList(fieldNames.split(",")));
}
ReportOutput reportOutput = new ReportAction().execute(reportInput);
commandLine.getOut().println("Wrote " + reportOutput.getRecordCount() + " records to file " + filename);
return commandLine.getCommandSpec().exitCodeOnSuccess();
}
finally
{
try
{
outputStream.close();
}
catch(IOException e)
{
throw (new QException("Error closing report file", e));
}
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Properties>
<Property name="LOG_PATTERN">%date{ISO8601} | %relative | %level | %threadName{1} | %logger{1}.%method | %message%n</Property>
</Properties>
<Appenders>
<File name="LogFile" fileName="log/qqq-picocli.log">
<PatternLayout pattern="${LOG_PATTERN}"/>
</File>
</Appenders>
<Loggers>
<Logger name="org.apache.log4j.xml" additivity="false">
</Logger>
<Root level="all">
<AppenderRef ref="LogFile"/>
</Root>
</Loggers>
</Configuration>

View File

@ -31,6 +31,7 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils;
@ -40,6 +41,7 @@ import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -254,6 +256,49 @@ class QPicoCliImplementationTest
/*******************************************************************************
** test running a "get single record" action (singleton query) on a table
**
*******************************************************************************/
@Test
public void test_tableGetNoIdGiven()
{
TestOutput testOutput = testCli("person", "get");
assertTestOutputContains(testOutput, "Usage: " + CLI_NAME + " person get PARAM");
assertTestOutputContains(testOutput, "Primary key value from the table");
}
/*******************************************************************************
** test running a "get single record" action (singleton query) on a table
**
*******************************************************************************/
@Test
public void test_tableGet()
{
TestOutput testOutput = testCli("person", "get", "1");
JSONObject getResult = JsonUtils.toJSONObject(testOutput.getOutput());
assertNotNull(getResult);
assertEquals(1, getResult.getJSONObject("values").getInt("id"));
assertEquals("Darin", getResult.getJSONObject("values").getString("firstName"));
}
/*******************************************************************************
** test running a "get single record" action (singleton query) on a table
**
*******************************************************************************/
@Test
public void test_tableGetMissingId()
{
TestOutput testOutput = testCli("person", "get", "1976");
assertTestOutputContains(testOutput, "No Person found for Id: 1976");
}
/******************************************************************************* /*******************************************************************************
** test running an insert w/o specifying any fields, prints usage ** test running an insert w/o specifying any fields, prints usage
** **
@ -422,6 +467,9 @@ class QPicoCliImplementationTest
/*******************************************************************************
**
*******************************************************************************/
private void assertRowValueById(String tableName, String columnName, String value, Integer id) throws Exception private void assertRowValueById(String tableName, String columnName, String value, Integer id) throws Exception
{ {
TestUtils.runTestSql("SELECT " + columnName + " FROM " + tableName + " WHERE id=" + id, (rs -> { TestUtils.runTestSql("SELECT " + columnName + " FROM " + tableName + " WHERE id=" + id, (rs -> {
@ -448,10 +496,7 @@ class QPicoCliImplementationTest
TestOutput testOutput = testCli("person", "delete", "--primaryKey", "2,4"); TestOutput testOutput = testCli("person", "delete", "--primaryKey", "2,4");
JSONObject deleteResult = JsonUtils.toJSONObject(testOutput.getOutput()); JSONObject deleteResult = JsonUtils.toJSONObject(testOutput.getOutput());
assertNotNull(deleteResult); assertNotNull(deleteResult);
JSONArray records = deleteResult.getJSONArray("records"); assertEquals(2, deleteResult.getInt("deletedRecordCount"));
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 -> { TestUtils.runTestSql("SELECT id FROM person", (rs -> {
int rowsFound = 0; int rowsFound = 0;
while(rs.next()) while(rs.next())
@ -470,7 +515,7 @@ class QPicoCliImplementationTest
** **
*******************************************************************************/ *******************************************************************************/
@Test @Test
public void test_tableProcess() throws Exception public void test_tableProcess()
{ {
TestOutput testOutput = testCli("person", "process"); TestOutput testOutput = testCli("person", "process");
@ -487,7 +532,7 @@ class QPicoCliImplementationTest
** **
*******************************************************************************/ *******************************************************************************/
@Test @Test
public void test_tableProcessUnknownName() throws Exception public void test_tableProcessUnknownName()
{ {
String badProcessName = "not-a-process"; String badProcessName = "not-a-process";
TestOutput testOutput = testCli("person", "process", badProcessName); TestOutput testOutput = testCli("person", "process", badProcessName);
@ -502,7 +547,7 @@ class QPicoCliImplementationTest
** **
*******************************************************************************/ *******************************************************************************/
@Test @Test
public void test_tableProcessGreetUsingCallbackForFields() throws Exception public void test_tableProcessGreetUsingCallbackForFields()
{ {
setStandardInputLines("Hi", "How are you?"); setStandardInputLines("Hi", "How are you?");
TestOutput testOutput = testCli("person", "process", "greet"); TestOutput testOutput = testCli("person", "process", "greet");
@ -511,12 +556,216 @@ class QPicoCliImplementationTest
} }
/*******************************************************************************
** test exporting a table
**
*******************************************************************************/
@Test
public void test_tableExportNoArgsExcel()
{
String filename = "/tmp/" + UUID.randomUUID() + ".xlsx";
TestOutput testOutput = testCli("person", "export", "--filename=" + filename);
assertTestOutputContains(testOutput, "Wrote 5 records to file " + filename);
File file = new File(filename);
assertTrue(file.exists());
// todo - some day when we learn to read Excel, assert that we wrote as expected.
deleteFile(file);
}
/*******************************************************************************
** test exporting a table
**
*******************************************************************************/
@Test
public void test_tableExportWithLimit() throws Exception
{
String filename = "/tmp/" + UUID.randomUUID() + ".csv";
TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--limit=3");
assertTestOutputContains(testOutput, "Wrote 3 records to file " + filename);
File file = new File(filename);
@SuppressWarnings("unchecked")
List<String> list = FileUtils.readLines(file);
assertEquals(4, list.size());
assertThat(list.get(0)).contains("""
"Id","Create Date","Modify Date\"""");
assertThat(list.get(1)).matches("""
^"1",.*"Darin.*""");
assertThat(list.get(3)).matches("""
^"3",.*"Tim.*""");
deleteFile(file);
}
/*******************************************************************************
** test exporting a table
**
*******************************************************************************/
@Test
public void test_tableExportWithCriteria() throws Exception
{
String filename = "/tmp/" + UUID.randomUUID() + ".csv";
TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--criteria", "id NOT_EQUALS 3");
assertTestOutputContains(testOutput, "Wrote 4 records to file " + filename);
File file = new File(filename);
@SuppressWarnings("unchecked")
List<String> list = FileUtils.readLines(file);
assertEquals(5, list.size());
assertThat(list.get(0)).contains("""
"Id","Create Date","Modify Date\"""");
assertThat(list.get(1)).matches("^\"1\",.*");
assertThat(list.get(2)).matches("^\"2\",.*");
assertThat(list.get(3)).matches("^\"4\",.*");
assertThat(list.get(4)).matches("^\"5\",.*");
deleteFile(file);
}
/*******************************************************************************
** test exporting a table
**
*******************************************************************************/
@Test
public void test_tableExportWithoutFilename()
{
TestOutput testOutput = testCli("person", "export");
assertTestErrorContains(testOutput, "Missing required option: '--filename=PARAM'");
assertTestErrorContains(testOutput, "Usage: " + CLI_NAME + " person export");
assertTestErrorContains(testOutput, "-f=PARAM");
}
/*******************************************************************************
** test exporting a table
**
*******************************************************************************/
@Test
public void test_tableExportNoFileExtension()
{
String filename = "/tmp/" + UUID.randomUUID();
TestOutput testOutput = testCli("person", "export", "--filename=" + filename);
assertTestErrorContains(testOutput, "File name did not contain an extension");
}
/*******************************************************************************
** test exporting a table
**
*******************************************************************************/
@Test
public void test_tableExportBadFileType()
{
String filename = "/tmp/" + UUID.randomUUID() + ".docx";
TestOutput testOutput = testCli("person", "export", "--filename=" + filename);
assertTestErrorContains(testOutput, "Unsupported report format: docx.");
}
/*******************************************************************************
** test exporting a table
**
*******************************************************************************/
@Test
public void test_tableExportBadFilePath()
{
String filename = "/no-such/directory/" + UUID.randomUUID() + "report.csv";
TestOutput testOutput = testCli("person", "export", "--filename=" + filename);
assertTestErrorContains(testOutput, "No such file or directory");
}
/*******************************************************************************
** test exporting a table
**
*******************************************************************************/
@Test
public void test_tableExportBadFieldNams()
{
String filename = "/tmp/" + UUID.randomUUID() + ".csv";
TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--fieldNames=foo");
assertTestErrorContains(testOutput, "Field name foo was not found on the Person table");
}
/*******************************************************************************
** test exporting a table
**
*******************************************************************************/
@Test
public void test_tableExportBadFieldNames()
{
String filename = "/tmp/" + UUID.randomUUID() + ".csv";
TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--fieldNames=foo,bar,baz");
assertTestErrorContains(testOutput, "Fields names foo, bar, and baz were not found on the Person table");
}
/*******************************************************************************
** test exporting a table
**
*******************************************************************************/
@Test
public void test_tableExportGoodFieldNamesXslx() throws IOException
{
String filename = "/tmp/" + UUID.randomUUID() + ".xlsx";
TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--fieldNames=id,lastName,birthDate");
File file = new File(filename);
assertTrue(file.exists());
// todo - some day when we learn to read Excel, assert that we wrote as expected (with 3 columns)
deleteFile(file);
}
/*******************************************************************************
** test exporting a table
**
*******************************************************************************/
@Test
public void test_tableExportGoodFieldNamesCSV() throws IOException
{
String filename = "/tmp/" + UUID.randomUUID() + ".csv";
TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--fieldNames=id,lastName,birthDate");
File file = new File(filename);
@SuppressWarnings("unchecked")
List<String> list = FileUtils.readLines(file);
assertEquals(6, list.size());
assertThat(list.get(0)).isEqualTo("""
"Id","Last Name","Birth Date\"""");
assertThat(list.get(1)).isEqualTo("""
"1","Kelkhoff","1980-05-31\"""");
deleteFile(file);
}
/******************************************************************************* /*******************************************************************************
** test running a process on a table ** test running a process on a table
** **
*******************************************************************************/ *******************************************************************************/
@Test @Test
public void test_tableProcessGreetUsingOptionsForFields() throws Exception public void test_tableProcessGreetUsingOptionsForFields()
{ {
TestOutput testOutput = testCli("person", "process", "greet", "--field-greetingPrefix=Hello", "--field-greetingSuffix=World"); TestOutput testOutput = testCli("person", "process", "greet", "--field-greetingPrefix=Hello", "--field-greetingSuffix=World");
assertTestOutputDoesNotContain(testOutput, "Please supply a value for the field"); assertTestOutputDoesNotContain(testOutput, "Please supply a value for the field");
@ -567,6 +816,16 @@ class QPicoCliImplementationTest
/*******************************************************************************
** delete a file, asserting that we did so.
*******************************************************************************/
private void deleteFile(File file)
{
assertTrue(file.delete());
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -628,118 +887,4 @@ class QPicoCliImplementationTest
System.setIn(stdin); System.setIn(stdin);
} }
/*******************************************************************************
**
*******************************************************************************/
private static class TestOutput
{
private String output;
private String[] outputLines;
private String error;
private String[] errorLines;
/*******************************************************************************
**
*******************************************************************************/
public TestOutput(String output, String error)
{
this.output = output;
this.error = error;
this.outputLines = output.split("\n");
this.errorLines = error.split("\n");
}
/*******************************************************************************
** Getter for output
**
*******************************************************************************/
public String getOutput()
{
return output;
}
/*******************************************************************************
** Setter for output
**
*******************************************************************************/
public void setOutput(String output)
{
this.output = output;
}
/*******************************************************************************
** Getter for outputLines
**
*******************************************************************************/
public String[] getOutputLines()
{
return outputLines;
}
/*******************************************************************************
** Setter for outputLines
**
*******************************************************************************/
public void setOutputLines(String[] outputLines)
{
this.outputLines = outputLines;
}
/*******************************************************************************
** Getter for error
**
*******************************************************************************/
public String getError()
{
return error;
}
/*******************************************************************************
** Setter for error
**
*******************************************************************************/
public void setError(String error)
{
this.error = error;
}
/*******************************************************************************
** Getter for errorLines
**
*******************************************************************************/
public String[] getErrorLines()
{
return errorLines;
}
/*******************************************************************************
** Setter for errorLines
**
*******************************************************************************/
public void setErrorLines(String[] errorLines)
{
this.errorLines = errorLines;
}
}
} }

View File

@ -0,0 +1,136 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.frontend.picocli;
/*******************************************************************************
**
*******************************************************************************/
class TestOutput
{
private String output;
private String[] outputLines;
private String error;
private String[] errorLines;
/*******************************************************************************
**
*******************************************************************************/
public TestOutput(String output, String error)
{
this.output = output;
this.error = error;
this.outputLines = output.split("\n");
this.errorLines = error.split("\n");
}
/*******************************************************************************
** Getter for output
**
*******************************************************************************/
public String getOutput()
{
return output;
}
/*******************************************************************************
** Setter for output
**
*******************************************************************************/
public void setOutput(String output)
{
this.output = output;
}
/*******************************************************************************
** Getter for outputLines
**
*******************************************************************************/
public String[] getOutputLines()
{
return outputLines;
}
/*******************************************************************************
** Setter for outputLines
**
*******************************************************************************/
public void setOutputLines(String[] outputLines)
{
this.outputLines = outputLines;
}
/*******************************************************************************
** Getter for error
**
*******************************************************************************/
public String getError()
{
return error;
}
/*******************************************************************************
** Setter for error
**
*******************************************************************************/
public void setError(String error)
{
this.error = error;
}
/*******************************************************************************
** Getter for errorLines
**
*******************************************************************************/
public String[] getErrorLines()
{
return errorLines;
}
/*******************************************************************************
** Setter for errorLines
**
*******************************************************************************/
public void setErrorLines(String[] errorLines)
{
this.errorLines = errorLines;
}
}