From f5f6446069d4848a383838af2d66a39f60eb9e18 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 21 Oct 2022 14:36:23 -0500 Subject: [PATCH] Add GET action, and usage in API --- .../core/actions/interfaces/GetInterface.java | 40 ++++ .../core/actions/tables/GetAction.java | 161 +++++++++++++++ .../model/actions/tables/get/GetInput.java | 186 ++++++++++++++++++ .../model/actions/tables/get/GetOutput.java | 72 +++++++ .../backend/QBackendModuleInterface.java | 10 + .../qqq/backend/core/utils/JsonUtils.java | 57 ++++-- .../qqq/backend/core/utils/ValueUtils.java | 17 ++ .../core/actions/tables/GetActionTest.java | 58 ++++++ .../backend/module/api/APIBackendModule.java | 13 ++ .../module/api/actions/APICountAction.java | 2 +- .../module/api/actions/APIGetAction.java | 83 ++++++++ .../module/api/actions/APIQueryAction.java | 2 +- .../module/api/actions/BaseAPIActionUtil.java | 60 ++++-- .../javalin/QJavalinImplementation.java | 32 ++- 14 files changed, 743 insertions(+), 50 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/GetInterface.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/GetAction.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetOutput.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/GetActionTest.java create mode 100644 qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIGetAction.java 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) {