diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java
new file mode 100644
index 00000000..258cd856
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java
@@ -0,0 +1,40 @@
+/*
+ * 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.backend.core.actions.interfaces;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+
+
+/*******************************************************************************
+ ** Interface for the Get action.
+ **
+ *******************************************************************************/
+public interface GetInterface
+{
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ GetOutput execute(GetInput getInput) throws QException;
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java
new file mode 100644
index 00000000..da923b7b
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java
@@ -0,0 +1,161 @@
+/*
+ * 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.backend.core.actions.tables;
+
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import com.kingsrook.qqq.backend.core.actions.ActionHelper;
+import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
+import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
+import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
+import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
+import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
+import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
+
+
+/*******************************************************************************
+ ** Action to run a get against a table.
+ **
+ *******************************************************************************/
+public class GetAction
+{
+ private Optional> postGetRecordCustomizer;
+
+ private GetInput getInput;
+ private QValueFormatter qValueFormatter;
+ private QPossibleValueTranslator qPossibleValueTranslator;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public GetOutput execute(GetInput getInput) throws QException
+ {
+ ActionHelper.validateSession(getInput);
+
+ postGetRecordCustomizer = QCodeLoader.getTableCustomizerFunction(getInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole());
+ this.getInput = getInput;
+
+ QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
+ QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(getInput.getBackend());
+ // todo pre-customization - just get to modify the request?
+
+ GetInterface getInterface = null;
+ try
+ {
+ getInterface = qModule.getGetInterface();
+ }
+ catch(IllegalStateException ise)
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ // if a module doesn't implement Get directly - try to do a Get by a Query by the primary key //
+ // see below. //
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ }
+
+ GetOutput getOutput;
+ if(getInterface != null)
+ {
+ getOutput = getInterface.execute(getInput);
+ }
+ else
+ {
+ getOutput = performGetViaQuery(getInput);
+ }
+
+ if(getOutput.getRecord() != null)
+ {
+ getOutput.setRecord(postRecordActions(getOutput.getRecord()));
+ }
+
+ return getOutput;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private GetOutput performGetViaQuery(GetInput getInput) throws QException
+ {
+ QueryInput queryInput = new QueryInput(getInput.getInstance());
+ queryInput.setSession(getInput.getSession());
+ queryInput.setTableName(getInput.getTableName());
+ queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, List.of(getInput.getPrimaryKey()))));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ GetOutput getOutput = new GetOutput();
+ if(!queryOutput.getRecords().isEmpty())
+ {
+ getOutput.setRecord(queryOutput.getRecords().get(0));
+ }
+ return (getOutput);
+ }
+
+
+
+ /*******************************************************************************
+ ** Run the necessary actions on a record. This may include setting display values,
+ ** translating possible values, and running post-record customizations.
+ *******************************************************************************/
+ public QRecord postRecordActions(QRecord record)
+ {
+ QRecord returnRecord = record;
+ if(this.postGetRecordCustomizer.isPresent())
+ {
+ returnRecord = postGetRecordCustomizer.get().apply(record);
+ }
+
+ if(getInput.getShouldTranslatePossibleValues())
+ {
+ if(qPossibleValueTranslator == null)
+ {
+ qPossibleValueTranslator = new QPossibleValueTranslator(getInput.getInstance(), getInput.getSession());
+ }
+ qPossibleValueTranslator.translatePossibleValuesInRecords(getInput.getTable(), List.of(returnRecord));
+ }
+
+ if(getInput.getShouldGenerateDisplayValues())
+ {
+ if(qValueFormatter == null)
+ {
+ qValueFormatter = new QValueFormatter();
+ }
+ qValueFormatter.setDisplayValuesInRecords(getInput.getTable(), List.of(returnRecord));
+ }
+
+ return (returnRecord);
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java
new file mode 100644
index 00000000..e08e3f22
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java
@@ -0,0 +1,186 @@
+/*
+ * 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.backend.core.model.actions.tables.get;
+
+
+import java.io.Serializable;
+import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+
+
+/*******************************************************************************
+ ** Input data for the Get action
+ **
+ *******************************************************************************/
+public class GetInput extends AbstractTableActionInput
+{
+ private QBackendTransaction transaction;
+ private Serializable primaryKey;
+
+ private boolean shouldTranslatePossibleValues = false;
+ private boolean shouldGenerateDisplayValues = false;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public GetInput()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public GetInput(QInstance instance)
+ {
+ super(instance);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public GetInput(QInstance instance, QSession session)
+ {
+ super(instance);
+ setSession(session);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for primaryKey
+ **
+ *******************************************************************************/
+ public Serializable getPrimaryKey()
+ {
+ return primaryKey;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for primaryKey
+ **
+ *******************************************************************************/
+ public void setPrimaryKey(Serializable primaryKey)
+ {
+ this.primaryKey = primaryKey;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for primaryKey
+ **
+ *******************************************************************************/
+ public GetInput withPrimaryKey(Serializable primaryKey)
+ {
+ this.primaryKey = primaryKey;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for shouldTranslatePossibleValues
+ **
+ *******************************************************************************/
+ public boolean getShouldTranslatePossibleValues()
+ {
+ return shouldTranslatePossibleValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for shouldTranslatePossibleValues
+ **
+ *******************************************************************************/
+ public void setShouldTranslatePossibleValues(boolean shouldTranslatePossibleValues)
+ {
+ this.shouldTranslatePossibleValues = shouldTranslatePossibleValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for shouldGenerateDisplayValues
+ **
+ *******************************************************************************/
+ public boolean getShouldGenerateDisplayValues()
+ {
+ return shouldGenerateDisplayValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for shouldGenerateDisplayValues
+ **
+ *******************************************************************************/
+ public void setShouldGenerateDisplayValues(boolean shouldGenerateDisplayValues)
+ {
+ this.shouldGenerateDisplayValues = shouldGenerateDisplayValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for transaction
+ **
+ *******************************************************************************/
+ public QBackendTransaction getTransaction()
+ {
+ return transaction;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for transaction
+ **
+ *******************************************************************************/
+ public void setTransaction(QBackendTransaction transaction)
+ {
+ this.transaction = transaction;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for transaction
+ **
+ *******************************************************************************/
+ public GetInput withTransaction(QBackendTransaction transaction)
+ {
+ this.transaction = transaction;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetOutput.java
new file mode 100644
index 00000000..2f9cada6
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetOutput.java
@@ -0,0 +1,72 @@
+/*
+ * 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.backend.core.model.actions.tables.get;
+
+
+import java.io.Serializable;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+
+
+/*******************************************************************************
+ ** Output for a Get action
+ **
+ *******************************************************************************/
+public class GetOutput extends AbstractActionOutput implements Serializable
+{
+ private QRecord record;
+
+
+
+ /*******************************************************************************
+ ** Getter for record
+ **
+ *******************************************************************************/
+ public QRecord getRecord()
+ {
+ return record;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for record
+ **
+ *******************************************************************************/
+ public void setRecord(QRecord record)
+ {
+ this.record = record;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for record
+ **
+ *******************************************************************************/
+ public GetOutput withRecord(QRecord record)
+ {
+ this.record = record;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java
index 13e6347a..3c3c0eb6 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java
@@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.modules.backend;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
+import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
@@ -76,6 +77,15 @@ public interface QBackendModuleInterface
return null;
}
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default GetInterface getGetInterface()
+ {
+ throwNotImplemented("Get");
+ return null;
+ }
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java
index 88f01258..4e5a3624 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java
@@ -23,9 +23,9 @@ package com.kingsrook.qqq.backend.core.utils;
import java.io.IOException;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -233,31 +233,54 @@ public class JsonUtils
public static QRecord parseQRecord(JSONObject jsonObject, Map fields)
{
QRecord record = new QRecord();
+
+ FIELDS_LOOP:
for(String fieldName : fields.keySet())
{
QFieldMetaData metaData = fields.get(fieldName);
String backendName = metaData.getBackendName() != null ? metaData.getBackendName() : fieldName;
- switch(metaData.getType())
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ // if the field backend name has dots in it, interpret that to mean traversal down sub-objects //
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ JSONObject jsonObjectToUse = jsonObject;
+ if(backendName.contains("."))
{
- case INTEGER -> record.setValue(fieldName, jsonObject.optInt(backendName));
- case DECIMAL -> record.setValue(fieldName, jsonObject.optBigDecimal(backendName, null));
- case BOOLEAN -> record.setValue(fieldName, jsonObject.optBoolean(backendName));
- case DATE_TIME ->
+ ArrayList levels = new ArrayList<>(List.of(backendName.split("\\.")));
+ backendName = levels.remove(levels.size() - 1);
+
+ for(String level : levels)
{
- String dateTimeString = jsonObject.optString(backendName);
- if(StringUtils.hasContent(dateTimeString))
+ try
{
- try
+ jsonObjectToUse = jsonObjectToUse.optJSONObject(level);
+ if(jsonObjectToUse == null)
{
- record.setValue(fieldName, LocalDateTime.parse(dateTimeString, DateTimeFormatter.ISO_ZONED_DATE_TIME));
- }
- catch(DateTimeParseException dtpe1)
- {
- record.setValue(fieldName, LocalDateTime.parse(dateTimeString, DateTimeFormatter.ISO_DATE_TIME));
+ continue FIELDS_LOOP;
}
}
+ catch(Exception e)
+ {
+ continue FIELDS_LOOP;
+ }
}
- default -> record.setValue(fieldName, jsonObject.optString(backendName));
+ }
+
+ switch(metaData.getType())
+ {
+ case INTEGER -> record.setValue(fieldName, jsonObjectToUse.optInt(backendName));
+ case DECIMAL -> record.setValue(fieldName, jsonObjectToUse.optBigDecimal(backendName, null));
+ case BOOLEAN -> record.setValue(fieldName, jsonObjectToUse.optBoolean(backendName));
+ case DATE_TIME ->
+ {
+ String dateTimeString = jsonObjectToUse.optString(backendName);
+ if(StringUtils.hasContent(dateTimeString))
+ {
+ Instant instant = ValueUtils.getValueAsInstant(dateTimeString);
+ record.setValue(fieldName, instant);
+ }
+ }
+ default -> record.setValue(fieldName, jsonObjectToUse.optString(backendName));
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java
index 12a6fbff..022c6541 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java
@@ -30,6 +30,7 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
+import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Calendar;
@@ -498,6 +499,22 @@ public class ValueUtils
}
else
{
+ try
+ {
+ return LocalDateTime.parse(s, DateTimeFormatter.ISO_ZONED_DATE_TIME).toInstant(ZoneOffset.UTC);
+ }
+ catch(DateTimeParseException e2)
+ {
+ try
+ {
+ return LocalDateTime.parse(s, DateTimeFormatter.ISO_DATE_TIME).toInstant(ZoneOffset.UTC);
+ }
+ catch(Exception e3)
+ {
+ // just throw the original
+ }
+ }
+
throw (e);
}
}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java
new file mode 100644
index 00000000..dd576194
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.backend.core.actions.tables;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+
+/*******************************************************************************
+ ** Unit test for GetAction
+ **
+ *******************************************************************************/
+class GetActionTest
+{
+
+ /*******************************************************************************
+ ** At the core level, there isn't much that can be asserted, as it uses the
+ ** mock implementation - just confirming that all of the "wiring" works.
+ **
+ *******************************************************************************/
+ @Test
+ public void test() throws QException
+ {
+ GetInput request = new GetInput(TestUtils.defineInstance());
+ request.setSession(TestUtils.getMockSession());
+ request.setTableName("person");
+ request.setPrimaryKey(1);
+ request.setShouldGenerateDisplayValues(true);
+ request.setShouldTranslatePossibleValues(true);
+ GetOutput result = new GetAction().execute(request);
+ assertNotNull(result);
+ assertNotNull(result.getRecord());
+ }
+}
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java
index f922fcc9..a2ee45e2 100644
--- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java
@@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.module.api;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
+import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
@@ -31,6 +32,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.module.api.actions.APICountAction;
+import com.kingsrook.qqq.backend.module.api.actions.APIGetAction;
import com.kingsrook.qqq.backend.module.api.actions.APIInsertAction;
import com.kingsrook.qqq.backend.module.api.actions.APIQueryAction;
@@ -94,6 +96,17 @@ public class APIBackendModule implements QBackendModuleInterface
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public GetInterface getGetInterface()
+ {
+ return (new APIGetAction());
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APICountAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APICountAction.java
index 81240b42..2d816844 100644
--- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APICountAction.java
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APICountAction.java
@@ -58,7 +58,7 @@ public class APICountAction extends AbstractAPIAction implements CountInterface
try
{
QQueryFilter filter = countInput.getFilter();
- String paramString = apiActionUtil.buildQueryString(filter, null, null, table.getFields());
+ String paramString = apiActionUtil.buildQueryStringForGet(filter, null, null, table.getFields());
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
HttpClient client = httpClientBuilder.build();
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIGetAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIGetAction.java
new file mode 100644
index 00000000..a00c1b9d
--- /dev/null
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIGetAction.java
@@ -0,0 +1,83 @@
+/*
+ * 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.backend.module.api.actions;
+
+
+import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class APIGetAction extends AbstractAPIAction implements GetInterface
+{
+ private static final Logger LOG = LogManager.getLogger(APIGetAction.class);
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public GetOutput execute(GetInput getInput) throws QException
+ {
+ QTableMetaData table = getInput.getTable();
+ preAction(getInput);
+
+ try
+ {
+ String urlSuffix = apiActionUtil.buildUrlSuffixForSingleRecordGet(getInput.getPrimaryKey());
+
+ HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
+ HttpClient client = httpClientBuilder.build();
+
+ String url = apiActionUtil.buildTableUrl(table);
+ HttpGet request = new HttpGet(url + urlSuffix);
+
+ apiActionUtil.setupAuthorizationInRequest(request);
+ apiActionUtil.setupContentTypeInRequest(request);
+ apiActionUtil.setupAdditionalHeaders(request);
+
+ HttpResponse response = client.execute(request);
+ QRecord record = apiActionUtil.processSingleRecordGetResponse(table, response);
+
+ GetOutput rs = new GetOutput();
+ rs.setRecord(record);
+ return rs;
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error in API get", e);
+ throw new QException("Error executing get: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIQueryAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIQueryAction.java
index d7a9aaa2..99b27436 100644
--- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIQueryAction.java
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIQueryAction.java
@@ -58,7 +58,7 @@ public class APIQueryAction extends AbstractAPIAction implements QueryInterface
try
{
QQueryFilter filter = queryInput.getFilter();
- String paramString = apiActionUtil.buildQueryString(filter, queryInput.getLimit(), queryInput.getSkip(), table.getFields());
+ String paramString = apiActionUtil.buildQueryStringForGet(filter, queryInput.getLimit(), queryInput.getSkip(), table.getFields());
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
HttpClient client = httpClientBuilder.build();
diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java
index ce277999..cdbae1ab 100644
--- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java
+++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java
@@ -32,14 +32,16 @@ import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
-import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetails;
import org.apache.http.HttpEntity;
@@ -101,7 +103,7 @@ public class BaseAPIActionUtil
** method to build up a query string based on a given QFilter object
**
*******************************************************************************/
- protected String buildQueryString(QQueryFilter filter, Integer limit, Integer skip, Map fields) throws QException
+ protected String buildQueryStringForGet(QQueryFilter filter, Integer limit, Integer skip, Map fields) throws QException
{
// todo: reasonable default action
return (null);
@@ -109,6 +111,19 @@ public class BaseAPIActionUtil
+ /*******************************************************************************
+ ** Do a default query string for a single-record GET - e.g., a query for just 1 record.
+ *******************************************************************************/
+ public String buildUrlSuffixForSingleRecordGet(Serializable primaryKey) throws QException
+ {
+ QTableMetaData table = actionInput.getTable();
+ QQueryFilter filter = new QQueryFilter()
+ .withCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.EQUALS, List.of(primaryKey)));
+ return (buildQueryStringForGet(filter, 1, 0, table.getFields()));
+ }
+
+
+
/*******************************************************************************
** As part of making a request - set up its authorization header (not just
** strictly "Authorization", but whatever is needed for auth).
@@ -308,14 +323,7 @@ public class BaseAPIActionUtil
*******************************************************************************/
protected QRecord processPostResponse(QTableMetaData table, QRecord record, HttpResponse response) throws IOException
{
- int statusCode = response.getStatusLine().getStatusCode();
- LOG.debug(statusCode);
-
- HttpEntity entity = response.getEntity();
- String resultString = EntityUtils.toString(entity);
- LOG.debug(resultString);
-
- JSONObject jsonObject = JsonUtils.toJSONObject(resultString);
+ JSONObject jsonObject = getJsonObject(response);
String primaryKeyFieldName = table.getPrimaryKeyField();
String primaryKeyBackendName = getFieldBackendName(table.getField(primaryKeyFieldName));
@@ -346,6 +354,24 @@ public class BaseAPIActionUtil
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private JSONObject getJsonObject(HttpResponse response) throws IOException
+ {
+ int statusCode = response.getStatusLine().getStatusCode();
+ LOG.debug(statusCode);
+
+ HttpEntity entity = response.getEntity();
+ String resultString = EntityUtils.toString(entity);
+ LOG.debug(resultString);
+
+ JSONObject jsonObject = JsonUtils.toJSONObject(resultString);
+ return jsonObject;
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -418,8 +444,18 @@ public class BaseAPIActionUtil
/*******************************************************************************
**
*******************************************************************************/
- protected String urlEncode(String s)
+ protected String urlEncode(Serializable s)
{
- return (URLEncoder.encode(s, StandardCharsets.UTF_8));
+ return (URLEncoder.encode(ValueUtils.getValueAsString(s), StandardCharsets.UTF_8));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QRecord processSingleRecordGetResponse(QTableMetaData table, HttpResponse response) throws IOException
+ {
+ return (jsonObjectToRecord(getJsonObject(response), table.getFields()));
}
}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
index 1f1cbe7a..d7ca6f3a 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
@@ -44,6 +44,7 @@ import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction;
import com.kingsrook.qqq.backend.core.actions.reporting.ExportAction;
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.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
@@ -69,10 +70,10 @@ 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;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
-import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
-import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
@@ -501,38 +502,31 @@ public class QJavalinImplementation
String tableName = context.pathParam("table");
QTableMetaData table = qInstance.getTable(tableName);
String primaryKey = context.pathParam("primaryKey");
- QueryInput queryInput = new QueryInput(qInstance);
+ GetInput getInput = new GetInput(qInstance);
- setupSession(context, queryInput);
- queryInput.setTableName(tableName);
- queryInput.setShouldGenerateDisplayValues(true);
- queryInput.setShouldTranslatePossibleValues(true);
+ setupSession(context, getInput);
+ getInput.setTableName(tableName);
+ getInput.setShouldGenerateDisplayValues(true);
+ getInput.setShouldTranslatePossibleValues(true);
// todo - validate that the primary key is of the proper type (e.g,. not a string for an id field)
// and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error)
- ///////////////////////////////////////////////////////
- // setup a filter for the primaryKey = the path-pram //
- ///////////////////////////////////////////////////////
- queryInput.setFilter(new QQueryFilter()
- .withCriteria(new QFilterCriteria()
- .withFieldName(table.getPrimaryKeyField())
- .withOperator(QCriteriaOperator.EQUALS)
- .withValues(List.of(primaryKey))));
+ getInput.setPrimaryKey(primaryKey);
- QueryAction queryAction = new QueryAction();
- QueryOutput queryOutput = queryAction.execute(queryInput);
+ GetAction getAction = new GetAction();
+ GetOutput getOutput = getAction.execute(getInput);
///////////////////////////////////////////////////////
// throw a not found error if the record isn't found //
///////////////////////////////////////////////////////
- if(queryOutput.getRecords().isEmpty())
+ if(getOutput.getRecord() == null)
{
throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
+ table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
}
- context.result(JsonUtils.toJson(queryOutput.getRecords().get(0)));
+ context.result(JsonUtils.toJson(getOutput.getRecord()));
}
catch(Exception e)
{