mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 05:01:07 +00:00
QQQ-28 implement bulk edit, delete, insert
This commit is contained in:
@ -179,10 +179,7 @@ public class QCommandBuilder
|
||||
.type(int.class)
|
||||
.description("Optional limit on the max number of records to include in the export.")
|
||||
.build());
|
||||
exportCommand.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria")
|
||||
.type(String[].class)
|
||||
.description("Query filter criteria for the export. May be given multiple times. Use format: $fieldName $operator $value. e.g., id EQUALS 42")
|
||||
.build());
|
||||
addCriteriaOption(exportCommand);
|
||||
|
||||
// todo - add the fields as explicit params?
|
||||
|
||||
@ -191,6 +188,38 @@ public class QCommandBuilder
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** add the standard '--criteria' option
|
||||
*******************************************************************************/
|
||||
private void addCriteriaOption(CommandLine.Model.CommandSpec commandSpec)
|
||||
{
|
||||
commandSpec.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria")
|
||||
.type(String[].class)
|
||||
.description("""
|
||||
Query filter criteria. May be given multiple times.
|
||||
Use format: "$fieldName $operator $value".
|
||||
e.g., "id EQUALS 42\"""")
|
||||
.build());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void addPrimaryKeyOrKeysOption(CommandLine.Model.CommandSpec updateCommand, String verbForDescription)
|
||||
{
|
||||
updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--primaryKey")
|
||||
// type(getClassForField(primaryKeyField))
|
||||
.type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type?
|
||||
.description("""
|
||||
Primary Key(s) for the records to %s.
|
||||
May provide multiple values, separated by commas""".formatted(verbForDescription))
|
||||
.build());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -214,9 +243,7 @@ public class QCommandBuilder
|
||||
private CommandLine.Model.CommandSpec defineCountCommand(QTableMetaData table)
|
||||
{
|
||||
CommandLine.Model.CommandSpec countCommand = CommandLine.Model.CommandSpec.create();
|
||||
countCommand.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria")
|
||||
.type(String[].class)
|
||||
.build());
|
||||
addCriteriaOption(countCommand);
|
||||
|
||||
// todo - add the fields as explicit params?
|
||||
|
||||
@ -233,6 +260,7 @@ public class QCommandBuilder
|
||||
CommandLine.Model.CommandSpec updateCommand = CommandLine.Model.CommandSpec.create();
|
||||
|
||||
/*
|
||||
todo - future may accept files, similar to (bulk) insert
|
||||
updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--jsonBody")
|
||||
.type(String.class)
|
||||
.build());
|
||||
@ -251,10 +279,7 @@ public class QCommandBuilder
|
||||
*/
|
||||
|
||||
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
|
||||
updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--primaryKey")
|
||||
// type(getClassForField(primaryKeyField))
|
||||
.type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type?
|
||||
.build());
|
||||
addPrimaryKeyOrKeysOption(updateCommand, "update");
|
||||
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
{
|
||||
@ -262,9 +287,14 @@ public class QCommandBuilder
|
||||
{
|
||||
updateCommand.addOption(CommandLine.Model.OptionSpec.builder("--field-" + field.getName())
|
||||
.type(getClassForField(field))
|
||||
.description("""
|
||||
Value to set for the field %s""".formatted(field.getName()))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
addCriteriaOption(updateCommand);
|
||||
|
||||
return updateCommand;
|
||||
}
|
||||
|
||||
@ -315,6 +345,8 @@ public class QCommandBuilder
|
||||
.type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type?
|
||||
.build());
|
||||
|
||||
addCriteriaOption(deleteCommand);
|
||||
|
||||
return deleteCommand;
|
||||
}
|
||||
|
||||
|
@ -62,6 +62,7 @@ import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.AbstractQFieldMapping;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.QKeyBasedFieldMapping;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
@ -265,7 +266,7 @@ public class QPicoCliImplementation
|
||||
Log.info("No session information found in environment");
|
||||
}
|
||||
|
||||
return(dotenvOptional);
|
||||
return (dotenvOptional);
|
||||
}
|
||||
|
||||
|
||||
@ -283,8 +284,8 @@ public class QPicoCliImplementation
|
||||
////////////////////////////////////
|
||||
// look for .env environment file //
|
||||
////////////////////////////////////
|
||||
String sessionId = null;
|
||||
Optional<Dotenv> dotenv = loadDotEnv();
|
||||
String sessionId = null;
|
||||
Optional<Dotenv> dotenv = loadDotEnv();
|
||||
if(dotenv.isPresent())
|
||||
{
|
||||
sessionId = dotenv.get().get("SESSION_ID");
|
||||
@ -293,8 +294,8 @@ public class QPicoCliImplementation
|
||||
Map<String, String> authenticationContext = new HashMap<>();
|
||||
if(sessionId == null && authenticationModule instanceof Auth0AuthenticationModule)
|
||||
{
|
||||
LineReader lr = LineReaderBuilder.builder().build();
|
||||
String tokenId = lr.readLine("Create a .env file with the contents of the Auth0 JWT Id Token in the variable 'SESSION_ID': \nPress enter once complete...");
|
||||
LineReader lr = LineReaderBuilder.builder().build();
|
||||
String tokenId = lr.readLine("Create a .env file with the contents of the Auth0 JWT Id Token in the variable 'SESSION_ID': \nPress enter once complete...");
|
||||
dotenv = loadDotEnv();
|
||||
if(dotenv.isPresent())
|
||||
{
|
||||
@ -701,6 +702,14 @@ public class QPicoCliImplementation
|
||||
String json = subParseResult.matchedOptionValue("--mapping", "");
|
||||
mapping = new JsonToQFieldMappingAdapter().buildMappingFromJson(json);
|
||||
}
|
||||
else
|
||||
{
|
||||
mapping = new QKeyBasedFieldMapping();
|
||||
for(Map.Entry<String, QFieldMetaData> entry : table.getFields().entrySet())
|
||||
{
|
||||
((QKeyBasedFieldMapping) mapping).addMapping(entry.getKey(), entry.getValue().getLabel());
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// get the records that the user specified //
|
||||
@ -782,38 +791,81 @@ public class QPicoCliImplementation
|
||||
updateInput.setTableName(tableName);
|
||||
QTableMetaData table = qInstance.getTable(tableName);
|
||||
|
||||
List<QRecord> recordList = new ArrayList<>();
|
||||
List<QRecord> recordsToUpdate = new ArrayList<>();
|
||||
boolean anyFields = false;
|
||||
|
||||
boolean anyFields = false;
|
||||
String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
|
||||
String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] {});
|
||||
|
||||
String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
|
||||
Serializable[] primaryKeyValues = primaryKeyOption.split(",");
|
||||
for(Serializable primaryKeyValue : primaryKeyValues)
|
||||
if(StringUtils.hasContent(primaryKeyOption))
|
||||
{
|
||||
QRecord record = new QRecord();
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the primaryKey option was given, split it up and seed the recordToUpdate list //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
Serializable[] primaryKeyValues = primaryKeyOption.split(",");
|
||||
for(Serializable primaryKeyValue : primaryKeyValues)
|
||||
{
|
||||
recordsToUpdate.add(new QRecord().withValue(table.getPrimaryKeyField(), primaryKeyValue));
|
||||
}
|
||||
}
|
||||
else if(criteria.length > 0)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// else if criteria were given, execute the query for the lsit of records to update //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QRecord qRecord : executeQuery(tableName, subParseResult))
|
||||
{
|
||||
recordsToUpdate.add(new QRecord().withValue(table.getPrimaryKeyField(), qRecord.getValue(table.getPrimaryKeyField())));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
commandLine.getErr().println("Error: Either primaryKey or criteria must be specified.");
|
||||
CommandLine subCommandLine = commandLine.getSubcommands().get("update");
|
||||
subCommandLine.usage(commandLine.getOut());
|
||||
return commandLine.getCommandSpec().exitCodeOnUsageHelp();
|
||||
}
|
||||
|
||||
recordList.add(record);
|
||||
record.setValue(table.getPrimaryKeyField(), primaryKeyValue);
|
||||
///////////////////////////////////////////////////
|
||||
// make sure at least one --field- arg was given //
|
||||
///////////////////////////////////////////////////
|
||||
for(OptionSpec matchedOption : subParseResult.matchedOptions())
|
||||
{
|
||||
if(matchedOption.longestName().startsWith("--field-"))
|
||||
{
|
||||
anyFields = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!anyFields)
|
||||
{
|
||||
commandLine.getErr().println("Error: At least one field to update must be specified.");
|
||||
CommandLine subCommandLine = commandLine.getSubcommands().get("update");
|
||||
subCommandLine.usage(commandLine.getOut());
|
||||
return commandLine.getCommandSpec().exitCodeOnUsageHelp();
|
||||
}
|
||||
|
||||
if(recordsToUpdate.isEmpty())
|
||||
{
|
||||
commandLine.getErr().println("No rows to update were found.");
|
||||
CommandLine subCommandLine = commandLine.getSubcommands().get("update");
|
||||
subCommandLine.usage(commandLine.getOut());
|
||||
return commandLine.getCommandSpec().exitCodeOnUsageHelp();
|
||||
}
|
||||
|
||||
for(QRecord record : recordsToUpdate)
|
||||
{
|
||||
for(OptionSpec matchedOption : subParseResult.matchedOptions())
|
||||
{
|
||||
if(matchedOption.longestName().startsWith("--field-"))
|
||||
{
|
||||
anyFields = true;
|
||||
String fieldName = matchedOption.longestName().substring(8);
|
||||
record.setValue(fieldName, matchedOption.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!anyFields || recordList.isEmpty())
|
||||
{
|
||||
CommandLine subCommandLine = commandLine.getSubcommands().get("update");
|
||||
subCommandLine.usage(commandLine.getOut());
|
||||
return commandLine.getCommandSpec().exitCodeOnUsageHelp();
|
||||
}
|
||||
|
||||
updateInput.setRecords(recordList);
|
||||
updateInput.setRecords(recordsToUpdate);
|
||||
|
||||
UpdateAction updateAction = new UpdateAction();
|
||||
UpdateOutput updateResult = updateAction.execute(updateInput);
|
||||
@ -835,9 +887,24 @@ public class QPicoCliImplementation
|
||||
/////////////////////////////////////////////
|
||||
// get the pKeys that the user specified //
|
||||
/////////////////////////////////////////////
|
||||
String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
|
||||
Serializable[] primaryKeyValues = primaryKeyOption.split(",");
|
||||
deleteInput.setPrimaryKeys(Arrays.asList(primaryKeyValues));
|
||||
String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
|
||||
String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] {});
|
||||
|
||||
if(StringUtils.hasContent(primaryKeyOption))
|
||||
{
|
||||
deleteInput.setPrimaryKeys(Arrays.asList(primaryKeyOption.split(",")));
|
||||
}
|
||||
else if(criteria.length > 0)
|
||||
{
|
||||
deleteInput.setQueryFilter(generateQueryFilter(subParseResult));
|
||||
}
|
||||
else
|
||||
{
|
||||
commandLine.getErr().println("Error: Either primaryKey or criteria must be specified.");
|
||||
CommandLine subCommandLine = commandLine.getSubcommands().get("delete");
|
||||
subCommandLine.usage(commandLine.getOut());
|
||||
return commandLine.getCommandSpec().exitCodeOnUsageHelp();
|
||||
}
|
||||
|
||||
DeleteAction deleteAction = new DeleteAction();
|
||||
DeleteOutput deleteResult = deleteAction.execute(deleteInput);
|
||||
@ -865,4 +932,21 @@ public class QPicoCliImplementation
|
||||
commandLine.usage(commandLine.getOut());
|
||||
return commandLine.getCommandSpec().exitCodeOnUsageHelp();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private List<QRecord> executeQuery(String tableName, ParseResult subParseResult) throws QException
|
||||
{
|
||||
QueryInput queryInput = new QueryInput(qInstance);
|
||||
queryInput.setSession(session);
|
||||
queryInput.setTableName(tableName);
|
||||
queryInput.setFilter(generateQueryFilter(subParseResult));
|
||||
|
||||
QueryAction queryAction = new QueryAction();
|
||||
QueryOutput queryOutput = queryAction.execute(queryInput);
|
||||
return (queryOutput.getRecords());
|
||||
}
|
||||
}
|
||||
|
@ -447,11 +447,24 @@ class QPicoCliImplementationTest
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test running an update w/ fields as arguments
|
||||
** test running an update w/o specifying any pkeys or criteria, prints usage
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_tableUpdateFieldArguments() throws Exception
|
||||
public void test_tableUpdateNoRecordsPrintsUsage()
|
||||
{
|
||||
TestOutput testOutput = testCli("person", "update", "--field-firstName=Lucy");
|
||||
assertTestOutputContains(testOutput, "Usage: " + CLI_NAME + " person update");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test running an update w/ fields as arguments and one primary key
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_tableUpdateFieldArgumentsOnePrimaryKey() throws Exception
|
||||
{
|
||||
assertRowValueById("person", "first_name", "Garret", 5);
|
||||
TestOutput testOutput = testCli("person", "update",
|
||||
@ -467,6 +480,55 @@ class QPicoCliImplementationTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test running an update w/ fields as arguments and multiple primary keys
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_tableUpdateFieldArgumentsManyPrimaryKeys() throws Exception
|
||||
{
|
||||
assertRowValueById("person", "first_name", "Tyler", 4);
|
||||
assertRowValueById("person", "first_name", "Garret", 5);
|
||||
TestOutput testOutput = testCli("person", "update",
|
||||
"--primaryKey=4,5",
|
||||
"--field-firstName=Lucy",
|
||||
"--field-lastName=Lu");
|
||||
JSONObject updateResult = JsonUtils.toJSONObject(testOutput.getOutput());
|
||||
assertNotNull(updateResult);
|
||||
assertEquals(2, updateResult.getJSONArray("records").length());
|
||||
assertEquals(4, updateResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getInt("id"));
|
||||
assertEquals(5, updateResult.getJSONArray("records").getJSONObject(1).getJSONObject("values").getInt("id"));
|
||||
assertRowValueById("person", "first_name", "Lucy", 4);
|
||||
assertRowValueById("person", "first_name", "Lucy", 5);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test running an update w/ fields as arguments and a criteria
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_tableUpdateFieldArgumentsCriteria() throws Exception
|
||||
{
|
||||
assertRowValueById("person", "first_name", "Tyler", 4);
|
||||
assertRowValueById("person", "first_name", "Garret", 5);
|
||||
TestOutput testOutput = testCli("person", "update",
|
||||
"--criteria",
|
||||
"id GREATER_THAN_OR_EQUALS 4",
|
||||
"--field-firstName=Lucy",
|
||||
"--field-lastName=Lu");
|
||||
JSONObject updateResult = JsonUtils.toJSONObject(testOutput.getOutput());
|
||||
assertNotNull(updateResult);
|
||||
assertEquals(2, updateResult.getJSONArray("records").length());
|
||||
assertEquals(4, updateResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getInt("id"));
|
||||
assertEquals(5, updateResult.getJSONArray("records").getJSONObject(1).getJSONObject("values").getInt("id"));
|
||||
assertRowValueById("person", "first_name", "Lucy", 4);
|
||||
assertRowValueById("person", "first_name", "Lucy", 5);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -486,6 +548,18 @@ class QPicoCliImplementationTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test running a delete without enough args
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test_tableDeleteWithoutArgs() throws Exception
|
||||
{
|
||||
TestOutput testOutput = testCli("person", "delete");
|
||||
assertTestOutputContains(testOutput, "Usage: " + CLI_NAME + " person delete");
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test running a delete against a table
|
||||
**
|
||||
@ -717,7 +791,6 @@ class QPicoCliImplementationTest
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test exporting a table
|
||||
**
|
||||
@ -736,6 +809,8 @@ class QPicoCliImplementationTest
|
||||
deleteFile(file);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test exporting a table
|
||||
**
|
||||
|
Reference in New Issue
Block a user