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 3852c18e..172d9c60 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java +++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java @@ -14,34 +14,43 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; 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.TableMetaDataAction; import com.kingsrook.qqq.backend.core.adapters.CsvToQRecordAdapter; import com.kingsrook.qqq.backend.core.adapters.JsonToQFieldMappingAdapter; import com.kingsrook.qqq.backend.core.adapters.JsonToQRecordAdapter; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.actions.AbstractQFieldMapping; -import com.kingsrook.qqq.backend.core.model.actions.DeleteRequest; -import com.kingsrook.qqq.backend.core.model.actions.DeleteResult; -import com.kingsrook.qqq.backend.core.model.actions.InsertRequest; -import com.kingsrook.qqq.backend.core.model.actions.InsertResult; -import com.kingsrook.qqq.backend.core.model.actions.MetaDataRequest; -import com.kingsrook.qqq.backend.core.model.actions.MetaDataResult; -import com.kingsrook.qqq.backend.core.model.actions.QCriteriaOperator; -import com.kingsrook.qqq.backend.core.model.actions.QFilterCriteria; -import com.kingsrook.qqq.backend.core.model.actions.QQueryFilter; -import com.kingsrook.qqq.backend.core.model.actions.QueryRequest; -import com.kingsrook.qqq.backend.core.model.actions.QueryResult; -import com.kingsrook.qqq.backend.core.model.actions.TableMetaDataRequest; -import com.kingsrook.qqq.backend.core.model.actions.TableMetaDataResult; +import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; +import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteRequest; +import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteResult; +import com.kingsrook.qqq.backend.core.model.actions.insert.InsertRequest; +import com.kingsrook.qqq.backend.core.model.actions.insert.InsertResult; +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.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.query.QueryRequest; +import com.kingsrook.qqq.backend.core.model.actions.query.QueryResult; +import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.AbstractQFieldMapping; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.modules.QAuthenticationModuleDispatcher; +import com.kingsrook.qqq.backend.core.modules.interfaces.QAuthenticationModuleInterface; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import org.apache.commons.io.FileUtils; import picocli.CommandLine; @@ -65,6 +74,7 @@ public class QPicoCliImplementation public static final int DEFAULT_LIMIT = 20; private static QInstance qInstance; + private static QSession session; @@ -115,6 +125,7 @@ public class QPicoCliImplementation ** my-app-cli $table delete (--primaryKey=|--$uc=...) ** my-app-cli $table insert (--body=|--$field=...) ** my-app-cli $table update (--primaryKey=|--$uc=...) (--body=|--$field=...) + ** my-app-cli $table process $process ... ** *******************************************************************************/ public int runCli(String name, String[] args, PrintStream out, PrintStream err) @@ -148,6 +159,12 @@ public class QPicoCliImplementation tableCommand.addSubcommand("query", defineQueryCommand(table)); tableCommand.addSubcommand("insert", defineInsertCommand(table)); tableCommand.addSubcommand("delete", defineDeleteCommand(table)); + + List processes = qInstance.getProcessesForTable(tableName); + if(CollectionUtils.nullSafeHasContents(processes)) + { + tableCommand.addSubcommand("process", defineTableProcessesCommand(table, processes)); + } }); CommandLine commandLine = new CommandLine(topCommandSpec); @@ -156,6 +173,9 @@ public class QPicoCliImplementation try { + setupSession(args); + // todo - think about, do some tables get turned off based on authentication? + ParseResult parseResult = commandLine.parseArgs(args); /////////////////////////////////////////// @@ -202,6 +222,22 @@ public class QPicoCliImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private static void setupSession(String[] args) throws QModuleDispatchException + { + QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); + QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication()); + + // todo - does this need some per-provider logic actually? mmm... + Map authenticationContext = new HashMap<>(); + authenticationContext.put("sessionId", System.getenv("sessionId")); + session = authenticationModule.createSession(authenticationContext); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -285,6 +321,24 @@ public class QPicoCliImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private CommandSpec defineTableProcessesCommand(QTableMetaData table, List processes) + { + CommandSpec processesCommand = CommandSpec.create(); + + for(QProcessMetaData process : processes) + { + CommandSpec processCommand = CommandSpec.create(); + processesCommand.addSubcommand(process.getName(), processCommand); + } + + return (processesCommand); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -334,7 +388,8 @@ public class QPicoCliImplementation if(tableParseResult.hasSubcommand()) { ParseResult subParseResult = tableParseResult.subcommand(); - switch(subParseResult.commandSpec().name()) + String subCommandName = subParseResult.commandSpec().name(); + switch(subCommandName) { case "meta-data": { @@ -352,9 +407,14 @@ public class QPicoCliImplementation { return runTableDelete(commandLine, tableName, subParseResult); } + case "process": + { + CommandLine subCommandLine = commandLine.getSubcommands().get(subCommandName); + return runTableProcess(subCommandLine, tableName, subParseResult); + } default: { - commandLine.getErr().println("Unknown command: " + subParseResult.commandSpec().name()); + commandLine.getErr().println("Unknown command: " + subCommandName); commandLine.usage(commandLine.getOut()); return commandLine.getCommandSpec().exitCodeOnUsageHelp(); } @@ -369,12 +429,48 @@ public class QPicoCliImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private int runTableProcess(CommandLine commandLine, String tableName, ParseResult subParseResult) + { + if(!subParseResult.hasSubcommand()) + { + commandLine.usage(commandLine.getOut()); + return commandLine.getCommandSpec().exitCodeOnUsageHelp(); + } + else + { + String subCommandName = subParseResult.subcommand().commandSpec().name(); + CommandLine subCommandLine = commandLine.getSubcommands().get(subCommandName); + return runTableProcessLevelCommand(subCommandLine, tableName, subParseResult.subcommand()); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private int runTableProcessLevelCommand(CommandLine subCommandLine, String tableName, ParseResult processParseResult) + { + String processName = processParseResult.commandSpec().name(); + QTableMetaData table = qInstance.getTable(tableName); + QProcessMetaData process = qInstance.getProcess(processName); + RunFunctionAction runFunctionAction = new RunFunctionAction(); + // todo! + return 0; + } + + + /******************************************************************************* ** *******************************************************************************/ private int runTableMetaData(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException { TableMetaDataRequest tableMetaDataRequest = new TableMetaDataRequest(qInstance); + tableMetaDataRequest.setSession(session); tableMetaDataRequest.setTableName(tableName); TableMetaDataAction tableMetaDataAction = new TableMetaDataAction(); TableMetaDataResult tableMetaDataResult = tableMetaDataAction.execute(tableMetaDataRequest); @@ -390,6 +486,7 @@ public class QPicoCliImplementation private int runTableQuery(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException { QueryRequest queryRequest = new QueryRequest(qInstance); + queryRequest.setSession(session); queryRequest.setTableName(tableName); queryRequest.setSkip(subParseResult.matchedOptionValue("skip", null)); queryRequest.setLimit(subParseResult.matchedOptionValue("limit", DEFAULT_LIMIT)); @@ -423,6 +520,7 @@ public class QPicoCliImplementation private int runTableInsert(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException { InsertRequest insertRequest = new InsertRequest(qInstance); + insertRequest.setSession(session); insertRequest.setTableName(tableName); QTableMetaData table = qInstance.getTable(tableName); @@ -510,6 +608,7 @@ public class QPicoCliImplementation private int runTableDelete(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException { DeleteRequest deleteRequest = new DeleteRequest(qInstance); + deleteRequest.setSession(session); deleteRequest.setTableName(tableName); ///////////////////////////////////////////// @@ -535,6 +634,7 @@ public class QPicoCliImplementation if(parseResult.hasMatchedOption("--meta-data")) { MetaDataRequest metaDataRequest = new MetaDataRequest(qInstance); + metaDataRequest.setSession(session); MetaDataAction metaDataAction = new MetaDataAction(); MetaDataResult metaDataResult = metaDataAction.execute(metaDataRequest); commandLine.getOut().println(JsonUtils.toPrettyJson(metaDataResult)); 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 7341102f..9569757a 100644 --- a/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java @@ -11,6 +11,7 @@ import java.io.IOException; import java.io.PrintStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.HashMap; import java.util.UUID; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.utils.JsonUtils; @@ -19,10 +20,12 @@ 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; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; /******************************************************************************* @@ -56,7 +59,7 @@ class QPicoCliImplementationTest public void test_noArgs() { TestOutput testOutput = testCli(); - assertTrue(testOutput.getOutput().contains("Usage: " + CLI_NAME)); + assertTestOutputContains(testOutput, "Usage: " + CLI_NAME); } @@ -69,8 +72,8 @@ class QPicoCliImplementationTest public void test_help() { TestOutput testOutput = testCli("--help"); - assertTrue(testOutput.getOutput().contains("Usage: " + CLI_NAME)); - assertTrue(testOutput.getOutput().matches("(?s).*Commands:.*person.*")); + assertTestOutputContains(testOutput, "Usage: " + CLI_NAME); + assertTestOutputContains(testOutput, "Commands:.*person"); } @@ -83,7 +86,7 @@ class QPicoCliImplementationTest public void test_version() { TestOutput testOutput = testCli("--version"); - assertTrue(testOutput.getOutput().contains(CLI_NAME + " v1.0")); + assertTestOutputContains(testOutput, CLI_NAME + " v1.0"); } @@ -97,8 +100,8 @@ class QPicoCliImplementationTest { String badOption = "--asdf"; TestOutput testOutput = testCli(badOption); - assertTrue(testOutput.getError().contains("Unknown option: '" + badOption + "'")); - assertTrue(testOutput.getError().contains("Usage: " + CLI_NAME)); + assertTestErrorContains(testOutput, "Unknown option: '" + badOption + "'"); + assertTestErrorContains(testOutput, "Usage: " + CLI_NAME); } @@ -131,8 +134,17 @@ class QPicoCliImplementationTest public void test_table() { TestOutput testOutput = testCli("person"); - assertTrue(testOutput.getOutput().contains("Usage: " + CLI_NAME + " person [COMMAND]")); - assertTrue(testOutput.getOutput().matches("(?s).*Commands:.*query.*")); + assertTestOutputContains(testOutput, "Usage: " + CLI_NAME + " person \\[COMMAND\\]"); + assertTestOutputContains(testOutput, "Commands:.*query.*process"); + + /////////////////////////////////////////////////////// + // make sure that if there are no processes for the // + // table, that the processes sub-command isn't given // + /////////////////////////////////////////////////////// + QInstance qInstanceWithoutProcesses = TestUtils.defineInstance(); + qInstanceWithoutProcesses.setProcesses(new HashMap<>()); + testOutput = testCli(qInstanceWithoutProcesses, "person"); + assertTestOutputDoesNotContain(testOutput, "process"); } @@ -146,8 +158,8 @@ class QPicoCliImplementationTest { String badCommand = "qwuijibo"; TestOutput testOutput = testCli("person", badCommand); - assertTrue(testOutput.getError().contains("Unmatched argument at index 1: '" + badCommand + "'")); - assertTrue(testOutput.getError().contains("Usage: " + CLI_NAME + " person [COMMAND]")); + assertTestErrorContains(testOutput, "Unmatched argument at index 1: '" + badCommand + "'"); + assertTestErrorContains(testOutput, "Usage: " + CLI_NAME + " person \\[COMMAND\\]"); } @@ -202,7 +214,7 @@ class QPicoCliImplementationTest public void test_tableInsertNoFieldsPrintsUsage() { TestOutput testOutput = testCli("person", "insert"); - assertTrue(testOutput.getOutput().contains("Usage: " + CLI_NAME + " person insert")); + assertTestOutputContains(testOutput, "Usage: " + CLI_NAME + " person insert"); } @@ -299,7 +311,7 @@ class QPicoCliImplementationTest String csvContents = """ "Louis","P","Willikers",1024, "Nestle","G","Crunch",1701, - + """; File file = new File("/tmp/" + UUID.randomUUID() + ".csv"); @@ -347,12 +359,111 @@ class QPicoCliImplementationTest + /******************************************************************************* + ** test requesting the list of processes for a table + ** + *******************************************************************************/ + @Test + public void test_tableProcess() throws Exception + { + TestOutput testOutput = testCli("person", "process"); + + //////////////////////////////////////////////// + // should list the processes under this table // + //////////////////////////////////////////////// + assertTestOutputContains(testOutput, "Commands.*greet"); + } + + + + /******************************************************************************* + ** test trying to run a process, but giving an invalid name. + ** + *******************************************************************************/ + @Test + public void test_tableProcessUnknownName() throws Exception + { + 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\\]"); + } + + + + /******************************************************************************* + ** test running a process on a table + ** + *******************************************************************************/ + @Test + @Disabled // not yet done. + public void test_tableProcessGreet() throws Exception + { + TestOutput testOutput = testCli("person", "process", "greet"); + + fail("Assertion not written..."); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void assertTestOutputContains(TestOutput testOutput, String expectedRegexSubstring) + { + if(!testOutput.getOutput().matches("(?s).*" + expectedRegexSubstring + ".*")) + { + fail("Expected output to contain this regex pattern:\n" + expectedRegexSubstring + + "\nBut it did not. The full output was:\n" + testOutput.getOutput()); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void assertTestOutputDoesNotContain(TestOutput testOutput, String expectedRegexSubstring) + { + if(testOutput.getOutput().matches("(?s).*" + expectedRegexSubstring + ".*")) + { + fail("Expected output to not contain this regex pattern:\n" + expectedRegexSubstring + + "\nBut it did. The full output was:\n" + testOutput.getOutput()); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void assertTestErrorContains(TestOutput testOutput, String expectedRegexSubstring) + { + if(!testOutput.getError().matches("(?s).*" + expectedRegexSubstring + ".*")) + { + fail("Expected error-output to contain this regex pattern:\n" + expectedRegexSubstring + + "\nBut it did not. The full error-output was:\n" + testOutput.getOutput()); + } + } + + + /******************************************************************************* ** *******************************************************************************/ private TestOutput testCli(String... args) { QInstance qInstance = TestUtils.defineInstance(); + return testCli(qInstance, args); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private TestOutput testCli(QInstance qInstance, String... args) + { QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 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 70b8535a..12c55e79 100644 --- a/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java @@ -8,12 +8,23 @@ package com.kingsrook.qqq.frontend.picocli; import java.io.InputStream; import java.sql.Connection; import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.QCodeType; +import com.kingsrook.qqq.backend.core.model.metadata.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; -import com.kingsrook.qqq.backend.module.rdbms.RDBSMBackendMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData; +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.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; import org.apache.commons.io.IOUtils; @@ -35,7 +46,7 @@ public class TestUtils public static void primeTestDatabase() throws Exception { ConnectionManager connectionManager = new ConnectionManager(); - Connection connection = connectionManager.getConnection(new RDBSMBackendMetaData(TestUtils.defineBackend())); + Connection connection = connectionManager.getConnection(new RDBMSBackendMetaData(TestUtils.defineBackend())); InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql"); assertNotNull(primeTestDatabaseSqlStream); List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream); @@ -55,7 +66,7 @@ public class TestUtils public static void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception { ConnectionManager connectionManager = new ConnectionManager(); - Connection connection = connectionManager.getConnection(new RDBSMBackendMetaData(defineBackend())); + Connection connection = connectionManager.getConnection(new RDBMSBackendMetaData(defineBackend())); QueryManager.executeStatement(connection, sql, resultSetProcessor); } @@ -68,13 +79,28 @@ public class TestUtils public static QInstance defineInstance() { QInstance qInstance = new QInstance(); + qInstance.setAuthentication(defineAuthentication()); qInstance.addBackend(defineBackend()); qInstance.addTable(defineTablePerson()); + qInstance.addProcess(defineProcessGreetPeople()); return (qInstance); } + /******************************************************************************* + ** Define the authentication used in standard tests - using 'mock' type. + ** + *******************************************************************************/ + private static QAuthenticationMetaData defineAuthentication() + { + return new QAuthenticationMetaData() + .withName("mock") + .withType("mock"); + } + + + /******************************************************************************* ** Define the h2 rdbms backend ** @@ -113,4 +139,38 @@ public class TestUtils .withField(new QFieldMetaData("email", QFieldType.STRING)); } + + + /******************************************************************************* + ** Define the 'greet people' process + *******************************************************************************/ + private static QProcessMetaData defineProcessGreetPeople() + { + return new QProcessMetaData() + .withName("greet") + .withTableName("person") + .addFunction(new QFunctionMetaData() + .withName("prepare") + .withCode(new QCodeReference() + .withName("com.kingsrook.qqq.backend.core.interfaces.mock.MockFunctionBody") + .withCodeType(QCodeType.JAVA) + .withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context? + .withInputData(new QFunctionInputMetaData() + .withRecordListMetaData(new QRecordListMetaData().withTableName("person")) + .withFieldList(List.of( + new QFieldMetaData("greetingPrefix", QFieldType.STRING), + new QFieldMetaData("greetingSuffix", QFieldType.STRING) + ))) + .withOutputMetaData(new QFunctionOutputMetaData() + .withRecordListMetaData(new QRecordListMetaData() + .withTableName("person") + .addField(new QFieldMetaData("fullGreeting", QFieldType.STRING)) + ) + .withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING)))) + .withOutputView(new QOutputView() + .withMessageField("outputMessage") + .withRecordListView(new QRecordListView().withFieldNames(List.of("id", "firstName", "lastName", "fullGreeting")))) + ); + } + }