From d12bf3decc58532dae66037976469f918a74f01d Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 27 Apr 2023 12:48:01 -0500 Subject: [PATCH 01/15] Make it an option in the module's interface, whether or not to query for all records being updated or deleted first (makes more sense for an api backend to NOT do this). --- .../core/actions/interfaces/DeleteInterface.java | 9 +++++++++ .../core/actions/interfaces/UpdateInterface.java | 10 ++++++++++ .../backend/core/actions/tables/DeleteAction.java | 6 +++--- .../backend/core/actions/tables/UpdateAction.java | 15 +++++++++++---- .../module/api/actions/APIUpdateAction.java | 12 ++++++++++++ 5 files changed, 45 insertions(+), 7 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/DeleteInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/DeleteInterface.java index 669967d2..43781917 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/DeleteInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/DeleteInterface.java @@ -50,4 +50,13 @@ public interface DeleteInterface return (false); } + /******************************************************************************* + ** Specify whether this particular module's delete action can & should fetch + ** records before deleting them, e.g., for audits or "not-found-checks" + *******************************************************************************/ + default boolean supportsPreFetchQuery() + { + return (true); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/UpdateInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/UpdateInterface.java index fa87f5e8..e89baeed 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/UpdateInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/UpdateInterface.java @@ -37,4 +37,14 @@ public interface UpdateInterface ** *******************************************************************************/ UpdateOutput execute(UpdateInput updateInput) throws QException; + + /******************************************************************************* + ** Specify whether this particular module's update action can & should fetch + ** records before updating them, e.g., for audits or "not-found-checks" + *******************************************************************************/ + default boolean supportsPreFetchQuery() + { + return (true); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java index 195200e7..ec240c2c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java @@ -76,13 +76,13 @@ public class DeleteAction QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend()); + DeleteInterface deleteInterface = qModule.getDeleteInterface(); if(CollectionUtils.nullSafeHasContents(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null) { throw (new QException("A delete request may not contain both a list of primary keys and a query filter.")); } - DeleteInterface deleteInterface = qModule.getDeleteInterface(); if(deleteInput.getQueryFilter() != null && !deleteInterface.supportsQueryFilterInput()) { LOG.info("Querying for primary keys, for backend module " + qModule.getBackendType() + " which does not support queryFilter input for deletes"); @@ -99,8 +99,8 @@ public class DeleteAction } } - List recordListForAudit = getRecordListForAuditIfNeeded(deleteInput); - List recordsWithValidationErrors = validateRecordsExistAndCanBeAccessed(deleteInput, recordListForAudit); + List recordListForAudit = deleteInterface.supportsPreFetchQuery() ? getRecordListForAuditIfNeeded(deleteInput) : new ArrayList<>(); + List recordsWithValidationErrors = deleteInterface.supportsPreFetchQuery() ? validateRecordsExistAndCanBeAccessed(deleteInput, recordListForAudit) : new ArrayList<>(); DeleteOutput deleteOutput = deleteInterface.execute(deleteInput); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java index 0ada6d9e..17663aa7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java @@ -33,6 +33,7 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction; import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater; +import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface; import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper; import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; import com.kingsrook.qqq.backend.core.context.QContext; @@ -87,18 +88,24 @@ public class UpdateAction ValueBehaviorApplier.applyFieldBehaviors(updateInput.getInstance(), updateInput.getTable(), updateInput.getRecords()); // todo - need to handle records with errors coming out of here... - List oldRecordList = getOldRecordListForAuditIfNeeded(updateInput); - QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(updateInput.getBackend()); + UpdateInterface updateInterface = qModule.getUpdateInterface(); + + List oldRecordList = updateInterface.supportsPreFetchQuery() ? getOldRecordListForAuditIfNeeded(updateInput) : new ArrayList<>(); validatePrimaryKeysAreGiven(updateInput); - validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList); + + if(updateInterface.supportsPreFetchQuery()) + { + validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList); + } + validateRequiredFields(updateInput); ValidateRecordSecurityLockHelper.validateSecurityFields(updateInput.getTable(), updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE); // todo pre-customization - just get to modify the request? - UpdateOutput updateOutput = qModule.getUpdateInterface().execute(updateInput); + UpdateOutput updateOutput = updateInterface.execute(updateInput); // todo post-customization - can do whatever w/ the result if you want List errors = updateOutput.getRecords().stream().flatMap(r -> r.getErrors().stream()).toList(); diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java index 89cafaf5..e0af323d 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java @@ -45,4 +45,16 @@ public class APIUpdateAction extends AbstractAPIAction implements UpdateInterfac return (apiActionUtil.doUpdate(table, updateInput)); } + + + /******************************************************************************* + ** Specify whether this particular module's update action can & should fetch + ** records before updating them, e.g., for audits or "not-found-checks" + *******************************************************************************/ + @Override + public boolean supportsPreFetchQuery() + { + return (false); + } + } From 0c5e3a8002753cc444514e4b1a5cc84908f62c33 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 27 Apr 2023 12:49:15 -0500 Subject: [PATCH 02/15] Many tests. small refactors to support. --- qqq-backend-module-api/pom.xml | 4 - .../module/api/actions/BaseAPIActionUtil.java | 26 +- .../qqq/backend/module/api/TestUtils.java | 66 +- .../api/actions/BaseAPIActionUtilTest.java | 672 ++++++++++++++++++ .../module/api/mocks/MockApiActionUtils.java | 108 +++ .../module/api/mocks/MockApiUtilsHelper.java | 226 ++++++ .../module/api/mocks/MockHttpResponse.java | 302 ++++++++ 7 files changed, 1394 insertions(+), 10 deletions(-) create mode 100644 qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtilTest.java create mode 100644 qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiActionUtils.java create mode 100644 qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiUtilsHelper.java create mode 100644 qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockHttpResponse.java diff --git a/qqq-backend-module-api/pom.xml b/qqq-backend-module-api/pom.xml index f86fb9d8..e390f623 100644 --- a/qqq-backend-module-api/pom.xml +++ b/qqq-backend-module-api/pom.xml @@ -34,10 +34,6 @@ - - - 0.00 - 0.00 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 d8ecf17a..f3cd1107 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 @@ -634,7 +634,7 @@ public class BaseAPIActionUtil request.setEntity(new StringEntity(postBody)); request.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); - HttpResponse response = client.execute(request); + HttpResponse response = executeOAuthTokenRequest(client, request); int statusCode = response.getStatusLine().getStatusCode(); HttpEntity entity = response.getEntity(); String resultString = EntityUtils.toString(entity); @@ -669,6 +669,16 @@ public class BaseAPIActionUtil + /******************************************************************************* + ** one-line method, factored out so mock/tests can override + *******************************************************************************/ + protected CloseableHttpResponse executeOAuthTokenRequest(CloseableHttpClient client, HttpPost request) throws IOException + { + return client.execute(request); + } + + + /******************************************************************************* ** As part of making a request - set up its content-type header. *******************************************************************************/ @@ -880,7 +890,7 @@ public class BaseAPIActionUtil LOG.info("POST contents [" + ((HttpPost) request).getEntity().toString() + "]"); } - try(CloseableHttpResponse response = httpClient.execute(request)) + try(CloseableHttpResponse response = executeHttpRequest(request, httpClient)) { QHttpResponse qResponse = new QHttpResponse(response); @@ -924,7 +934,7 @@ public class BaseAPIActionUtil rateLimitsCaught++; if(rateLimitsCaught > getMaxAllowedRateLimitErrors()) { - LOG.error("Giving up POST to [" + table.getName() + "] after too many rate-limit errors (" + getMaxAllowedRateLimitErrors() + ")"); + LOG.error("Giving up " + request.getMethod() + " to [" + table.getName() + "] after too many rate-limit errors (" + getMaxAllowedRateLimitErrors() + ")"); throw (new QException(rle)); } @@ -950,6 +960,16 @@ public class BaseAPIActionUtil + /******************************************************************************* + ** one-line method, factored out so mock/tests can override + *******************************************************************************/ + protected CloseableHttpResponse executeHttpRequest(HttpRequestBase request, CloseableHttpClient httpClient) throws IOException + { + return httpClient.execute(request); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java index 1217445b..27a12a55 100644 --- a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java @@ -32,6 +32,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; +import com.kingsrook.qqq.backend.module.api.mocks.MockApiActionUtils; import com.kingsrook.qqq.backend.module.api.model.AuthorizationType; import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData; import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetails; @@ -42,7 +44,10 @@ import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetail *******************************************************************************/ public class TestUtils { + public static final String MEMORY_BACKEND_NAME = "memory"; public static final String EASYPOST_BACKEND_NAME = "easypost"; + public static final String MOCK_BACKEND_NAME = "mock"; + public static final String MOCK_TABLE_NAME = "mock"; @@ -52,14 +57,69 @@ public class TestUtils public static QInstance defineInstance() { QInstance qInstance = new QInstance(); - qInstance.addBackend(defineBackend()); - qInstance.addTable(defineTableEasypostTracker()); qInstance.setAuthentication(defineAuthentication()); + + qInstance.addBackend(defineMemoryBackend()); + + qInstance.addBackend(defineMockBackend()); + qInstance.addTable(defineMockTable()); + + qInstance.addBackend(defineEasypostBackend()); + qInstance.addTable(defineTableEasypostTracker()); + return (qInstance); } + /******************************************************************************* + ** Define the in-memory backend used in standard tests + *******************************************************************************/ + public static QBackendMetaData defineMemoryBackend() + { + return new QBackendMetaData() + .withName(MEMORY_BACKEND_NAME) + .withBackendType(MemoryBackendModule.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QBackendMetaData defineMockBackend() + { + return (new APIBackendMetaData() + .withName(MOCK_BACKEND_NAME) + .withAuthorizationType(AuthorizationType.API_KEY_HEADER) + .withBaseUrl("http://localhost:9999/mock") + .withContentType("application/json") + .withActionUtil(new QCodeReference(MockApiActionUtils.class, QCodeUsage.CUSTOMIZER)) + ); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QTableMetaData defineMockTable() + { + return (new QTableMetaData() + .withName(MOCK_TABLE_NAME) + .withBackendName(MOCK_BACKEND_NAME) + .withField(new QFieldMetaData("id", QFieldType.STRING)) + .withField(new QFieldMetaData("name", QFieldType.STRING)) + .withPrimaryKeyField("id") + .withBackendDetails(new APITableBackendDetails() + .withTablePath("mock") + .withTableWrapperObjectName("mocks") + ) + ); + } + + + /******************************************************************************* ** Define the authentication used in standard tests - using 'mock' type. ** @@ -76,7 +136,7 @@ public class TestUtils /******************************************************************************* ** *******************************************************************************/ - public static QBackendMetaData defineBackend() + public static QBackendMetaData defineEasypostBackend() { String apiKey = new QMetaDataVariableInterpreter().interpret("${env.EASYPOST_API_KEY}"); diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtilTest.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtilTest.java new file mode 100644 index 00000000..04275a80 --- /dev/null +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtilTest.java @@ -0,0 +1,672 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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 java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.tables.CountAction; +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; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.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.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; +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.tables.UniqueKey; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.module.api.BaseTest; +import com.kingsrook.qqq.backend.module.api.TestUtils; +import com.kingsrook.qqq.backend.module.api.exceptions.RateLimitException; +import com.kingsrook.qqq.backend.module.api.mocks.MockApiActionUtils; +import com.kingsrook.qqq.backend.module.api.mocks.MockApiUtilsHelper; +import com.kingsrook.qqq.backend.module.api.model.AuthorizationType; +import com.kingsrook.qqq.backend.module.api.model.OutboundAPILog; +import com.kingsrook.qqq.backend.module.api.model.OutboundAPILogMetaDataProvider; +import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData; +import org.apache.http.Header; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for com.kingsrook.qqq.backend.module.api.actions.BaseAPIActionUtil + *******************************************************************************/ +class BaseAPIActionUtilTest extends BaseTest +{ + private static MockApiUtilsHelper mockApiUtilsHelper = new MockApiUtilsHelper(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() + { + mockApiUtilsHelper = new MockApiUtilsHelper(); + mockApiUtilsHelper.setUseMock(true); + MockApiActionUtils.mockApiUtilsHelper = mockApiUtilsHelper; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCount() throws QException + { + mockApiUtilsHelper.enqueueMockResponse(""" + [ + {"id": 1, "name": "Homer"}, + {"id": 2, "name": "Marge"}, + {"id": 3, "name": "Bart"}, + {"id": 4, "name": "Lisa"}, + {"id": 5, "name": "Maggie"} + ] + """); + + CountInput countInput = new CountInput(); + countInput.setTableName(TestUtils.MOCK_TABLE_NAME); + CountOutput countOutput = new CountAction().execute(countInput); + assertEquals(5, countOutput.getCount()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCountError() throws QException + { + //////////////////////////////////////// + // avoid the fully mocked makeRequest // + //////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent(""" + {"error": "Server error"} + """)); + + CountInput countInput = new CountInput(); + countInput.setTableName(TestUtils.MOCK_TABLE_NAME); + assertThatThrownBy(() -> new CountAction().execute(countInput)).hasRootCauseInstanceOf(Exception.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGet() throws QException + { + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 3, "name": "Bart"}, + """); + + GetInput getInput = new GetInput(); + getInput.setTableName(TestUtils.MOCK_TABLE_NAME); + getInput.setPrimaryKey(3); + GetOutput getOutput = new GetAction().execute(getInput); + assertEquals(3, getOutput.getRecord().getValueInteger("id")); + assertEquals("Bart", getOutput.getRecord().getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetByKey() throws QException + { + QContext.getQInstance().getTable(TestUtils.MOCK_TABLE_NAME).withUniqueKey(new UniqueKey("id")); + + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 3, "name": "Bart"}, + """); + + GetInput getInput = new GetInput(); + getInput.setTableName(TestUtils.MOCK_TABLE_NAME); + getInput.setUniqueKey(Map.of("id", 3)); + GetOutput getOutput = new GetAction().execute(getInput); + assertEquals(3, getOutput.getRecord().getValueInteger("id")); + assertEquals("Bart", getOutput.getRecord().getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQuery() throws QException + { + mockApiUtilsHelper.enqueueMockResponse(""" + [ + {"id": 1, "name": "Homer"}, + {"id": 2, "name": "Marge"}, + {"id": 3, "name": "Bart"}, + {"id": 4, "name": "Lisa"}, + {"id": 5, "name": "Maggie"} + ] + """); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.MOCK_TABLE_NAME); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(5, queryOutput.getRecords().size()); + assertEquals(1, queryOutput.getRecords().get(0).getValueInteger("id")); + assertEquals("Homer", queryOutput.getRecords().get(0).getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryObjectWrappingList() throws QException + { + mockApiUtilsHelper.enqueueMockResponse(""" + {"mocks": [ + {"id": 1, "name": "Homer"}, + {"id": 2, "name": "Marge"}, + {"id": 3, "name": "Bart"}, + {"id": 4, "name": "Lisa"}, + {"id": 5, "name": "Maggie"} + ]} + """); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.MOCK_TABLE_NAME); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(5, queryOutput.getRecords().size()); + assertEquals(1, queryOutput.getRecords().get(0).getValueInteger("id")); + assertEquals("Homer", queryOutput.getRecords().get(0).getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryObjectWrappingSingleObject() throws QException + { + mockApiUtilsHelper.enqueueMockResponse(""" + {"mocks": + {"id": 1, "name": "Homer"} + } + """); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.MOCK_TABLE_NAME); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size()); + assertEquals(1, queryOutput.getRecords().get(0).getValueInteger("id")); + assertEquals("Homer", queryOutput.getRecords().get(0).getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryPaginate() throws QException + { + String oneObject = """ + {"id": 1, "name": "Homer"} + """; + StringBuilder response = new StringBuilder("["); + for(int i = 0; i < 19; i++) + { + response.append(oneObject).append(","); + } + response.append(oneObject); + response.append("]"); + mockApiUtilsHelper.enqueueMockResponse(response.toString()); + mockApiUtilsHelper.enqueueMockResponse(response.toString()); + mockApiUtilsHelper.enqueueMockResponse(response.toString()); + mockApiUtilsHelper.enqueueMockResponse("[]"); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.MOCK_TABLE_NAME); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(60, queryOutput.getRecords().size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryError() throws QException + { + //////////////////////////////////////// + // avoid the fully mocked makeRequest // + //////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent(""" + {"error": "Server error"} + """)); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.MOCK_TABLE_NAME); + assertThatThrownBy(() -> new QueryAction().execute(queryInput)).hasRootCauseInstanceOf(Exception.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInsert() throws QException + { + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 6} + """); + + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.MOCK_TABLE_NAME); + insertInput.setRecords(List.of(new QRecord().withValue("name", "Milhouse"))); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + assertEquals(6, insertOutput.getRecords().get(0).getValueInteger("id")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInsertEmptyInputList() throws QException + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.MOCK_TABLE_NAME); + insertInput.setRecords(List.of()); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInsertError() throws QException + { + //////////////////////////////////////// + // avoid the fully mocked makeRequest // + //////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent(""" + {"error": "Server error"} + """)); + + InsertInput insertInput = new InsertInput(); + insertInput.setRecords(List.of(new QRecord().withValue("name", "Milhouse"))); + insertInput.setTableName(TestUtils.MOCK_TABLE_NAME); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + assertTrue(CollectionUtils.nullSafeHasContents(insertOutput.getRecords().get(0).getErrors())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testUpdate() throws QException + { + mockApiUtilsHelper.enqueueMockResponse(""); + + mockApiUtilsHelper.setMockRequestAsserter(httpRequestBase -> + { + String requestBody = MockApiUtilsHelper.readRequestBody(httpRequestBase); + JSONObject requestObject = new JSONObject(requestBody); + + JSONArray mocks = requestObject.getJSONArray("mocks"); + JSONObject record = mocks.getJSONObject(0); + + assertEquals("Bartholomew", record.getString("name")); + assertEquals(3, record.getInt("id")); + }); + + UpdateInput updateInput = new UpdateInput(); + updateInput.setTableName(TestUtils.MOCK_TABLE_NAME); + updateInput.setRecords(List.of(new QRecord().withValue("id", "3").withValue("name", "Bartholomew"))); + UpdateOutput updateOutput = new UpdateAction().execute(updateInput); + + // not sure what to assert in here... + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testUpdateEmptyInputList() throws QException + { + UpdateInput updateInput = new UpdateInput(); + updateInput.setTableName(TestUtils.MOCK_TABLE_NAME); + updateInput.setRecords(List.of()); + UpdateOutput updateOutput = new UpdateAction().execute(updateInput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testUpdateError() throws QException + { + //////////////////////////////////////// + // avoid the fully mocked makeRequest // + //////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent(""" + {"error": "Server error"} + """)); + + UpdateInput updateInput = new UpdateInput(); + updateInput.setRecords(List.of(new QRecord().withValue("name", "Milhouse"))); + updateInput.setTableName(TestUtils.MOCK_TABLE_NAME); + + ///////////////////////////////////////////////////////////////////////////////// + // note - right now this is inconsistent with insertAction (and rdbms update), // + // where errors are placed in the records, rather than thrown... // + ///////////////////////////////////////////////////////////////////////////////// + assertThatThrownBy(() -> new UpdateAction().execute(updateInput)).hasRootCauseInstanceOf(Exception.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMakeRequest() throws QException + { + //////////////////////////////////////////////////////////////////////////////////////////// + // this will make it not use the mock makeRequest method, // + // but instead the mock executeHttpRequest, so we can test code from the base makeRequest // + //////////////////////////////////////////////////////////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 3, "name": "Bart"}, + """); + + GetInput getInput = new GetInput(); + getInput.setTableName(TestUtils.MOCK_TABLE_NAME); + getInput.setPrimaryKey(3); + GetOutput getOutput = new GetAction().execute(getInput); + assertEquals(3, getOutput.getRecord().getValueInteger("id")); + assertEquals("Bart", getOutput.getRecord().getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test429Then200() throws QException + { + //////////////////////////////////////////////////////////////////////////////////////////// + // this will make it not use the mock makeRequest method, // + // but instead the mock executeHttpRequest, so we can test code from the base makeRequest // + // specifically, that we can get one 429, and then eventually a 200 // + //////////////////////////////////////////////////////////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(429).withContent("Try again")); + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 3, "name": "Bart"}, + """); + + GetInput getInput = new GetInput(); + getInput.setTableName(TestUtils.MOCK_TABLE_NAME); + getInput.setPrimaryKey(3); + GetOutput getOutput = new GetAction().execute(getInput); + assertEquals(3, getOutput.getRecord().getValueInteger("id")); + assertEquals("Bart", getOutput.getRecord().getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTooMany429() throws QException + { + //////////////////////////////////////////////////////////////////////////////////////////// + // this will make it not use the mock makeRequest method, // + // but instead the mock executeHttpRequest, so we can test code from the base makeRequest // + // specifically, that after too many 429's we get an error // + //////////////////////////////////////////////////////////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(429).withContent("Try again")); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(429).withContent("Try again")); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(429).withContent("Try again")); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(429).withContent("Try again")); + + GetInput getInput = new GetInput(); + getInput.setTableName(TestUtils.MOCK_TABLE_NAME); + getInput.setPrimaryKey(3); + + assertThatThrownBy(() -> new GetAction().execute(getInput)).hasRootCauseInstanceOf(RateLimitException.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testApiLogs() throws QException + { + QInstance qInstance = QContext.getQInstance(); + OutboundAPILogMetaDataProvider.defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null); + + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 6} + """); + + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.MOCK_TABLE_NAME); + insertInput.setRecords(List.of(new QRecord().withValue("name", "Milhouse"))); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + assertEquals(6, insertOutput.getRecords().get(0).getValueInteger("id")); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(OutboundAPILog.TABLE_NAME); + QueryOutput apiLogRecords = new QueryAction().execute(queryInput); + assertEquals(1, apiLogRecords.getRecords().size()); + assertEquals("POST", apiLogRecords.getRecords().get(0).getValueString("method")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBasicAuthApiKey() throws QException + { + APIBackendMetaData backend = (APIBackendMetaData) QContext.getQInstance().getBackend(TestUtils.MOCK_BACKEND_NAME); + backend.setAuthorizationType(AuthorizationType.BASIC_AUTH_API_KEY); + backend.setApiKey("9876-WXYZ"); + + //////////////////////////////////////////////////////////////////////////////////////////// + // this will make it not use the mock makeRequest method, // + // but instead the mock executeHttpRequest, so we can test code from the base makeRequest // + //////////////////////////////////////////////////////////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 3, "name": "Bart"}, + """); + + mockApiUtilsHelper.setMockRequestAsserter(request -> + { + Header authHeader = request.getFirstHeader("Authorization"); + assertTrue(authHeader.getValue().startsWith("Basic ")); + String apiKey = new String(Base64.getDecoder().decode(authHeader.getValue().replace("Basic ", "")), StandardCharsets.UTF_8); + assertEquals("9876-WXYZ", apiKey); + }); + + runSimpleGetAction(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBasicAuthUsernamePassword() throws QException + { + APIBackendMetaData backend = (APIBackendMetaData) QContext.getQInstance().getBackend(TestUtils.MOCK_BACKEND_NAME); + backend.setAuthorizationType(AuthorizationType.BASIC_AUTH_USERNAME_PASSWORD); + backend.setUsername("god"); + backend.setPassword("5fingers"); + + //////////////////////////////////////////////////////////////////////////////////////////// + // this will make it not use the mock makeRequest method, // + // but instead the mock executeHttpRequest, so we can test code from the base makeRequest // + //////////////////////////////////////////////////////////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 3, "name": "Bart"}, + """); + + mockApiUtilsHelper.setMockRequestAsserter(request -> + { + Header authHeader = request.getFirstHeader("Authorization"); + assertTrue(authHeader.getValue().startsWith("Basic ")); + String usernamePassword = new String(Base64.getDecoder().decode(authHeader.getValue().replace("Basic ", "")), StandardCharsets.UTF_8); + assertEquals("god:5fingers", usernamePassword); + }); + + runSimpleGetAction(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOAuth2ValidToken() throws QException + { + APIBackendMetaData backend = (APIBackendMetaData) QContext.getQInstance().getBackend(TestUtils.MOCK_BACKEND_NAME); + backend.setAuthorizationType(AuthorizationType.OAUTH2); + backend.withCustomValue("accessToken", "validToken"); + + //////////////////////////////////////////////////////////////////////////////////////////// + // this will make it not use the mock makeRequest method, // + // but instead the mock executeHttpRequest, so we can test code from the base makeRequest // + //////////////////////////////////////////////////////////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 3, "name": "Bart"}, + """); + + mockApiUtilsHelper.setMockRequestAsserter(request -> + { + Header authHeader = request.getFirstHeader("Authorization"); + assertTrue(authHeader.getValue().startsWith("Bearer ")); + String token = authHeader.getValue().replace("Bearer ", ""); + assertEquals("validToken", token); + }); + + runSimpleGetAction(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOAuth2NullToken() throws QException + { + APIBackendMetaData backend = (APIBackendMetaData) QContext.getQInstance().getBackend(TestUtils.MOCK_BACKEND_NAME); + backend.setAuthorizationType(AuthorizationType.OAUTH2); + + //////////////////////////////////////////////////////////////////////////////////////////// + // this will make it not use the mock makeRequest method, // + // but instead the mock executeHttpRequest, so we can test code from the base makeRequest // + //////////////////////////////////////////////////////////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(""" + {"access_token": "myNewToken"} + """); + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 3, "name": "Bart"}, + """); + + GetOutput getOutput = runSimpleGetAction(); + assertEquals(3, getOutput.getRecord().getValueInteger("id")); + assertEquals("Bart", getOutput.getRecord().getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static GetOutput runSimpleGetAction() throws QException + { + GetInput getInput = new GetInput(); + getInput.setTableName(TestUtils.MOCK_TABLE_NAME); + getInput.setPrimaryKey(3); + return (new GetAction().execute(getInput)); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiActionUtils.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiActionUtils.java new file mode 100644 index 00000000..b54f37d7 --- /dev/null +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiActionUtils.java @@ -0,0 +1,108 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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.mocks; + + +import java.io.IOException; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.module.api.actions.BaseAPIActionUtil; +import com.kingsrook.qqq.backend.module.api.actions.QHttpResponse; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.impl.client.CloseableHttpClient; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class MockApiActionUtils extends BaseAPIActionUtil +{ + public static MockApiUtilsHelper mockApiUtilsHelper; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QHttpResponse makeRequest(QTableMetaData table, HttpRequestBase request) throws QException + { + return (mockApiUtilsHelper.defaultMockMakeRequest(mockApiUtilsHelper, table, request, () -> super.makeRequest(table, request))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + protected CloseableHttpResponse executeHttpRequest(HttpRequestBase request, CloseableHttpClient httpClient) throws IOException + { + runMockAsserter(request); + return new MockHttpResponse(mockApiUtilsHelper); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void runMockAsserter(HttpRequestBase request) + { + if(mockApiUtilsHelper.getMockRequestAsserter() != null) + { + try + { + mockApiUtilsHelper.getMockRequestAsserter().run(request); + } + catch(Exception e) + { + throw (new RuntimeException("Error running mock request asserter", e)); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + protected CloseableHttpResponse executeOAuthTokenRequest(CloseableHttpClient client, HttpPost request) throws IOException + { + runMockAsserter(request); + return new MockHttpResponse(mockApiUtilsHelper); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected int getInitialRateLimitBackoffMillis() + { + return (1); + } + +} diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiUtilsHelper.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiUtilsHelper.java new file mode 100644 index 00000000..65b973df --- /dev/null +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiUtilsHelper.java @@ -0,0 +1,226 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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.mocks; + + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeConsumer; +import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier; +import com.kingsrook.qqq.backend.module.api.actions.QHttpResponse; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import static org.junit.jupiter.api.Assertions.fail; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class MockApiUtilsHelper +{ + private static final QLogger LOG = QLogger.getLogger(MockApiUtilsHelper.class); + + private boolean useMock = true; + private Deque mockResponseQueue = new ArrayDeque<>(); + private UnsafeConsumer mockRequestAsserter = null; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void enqueueMockResponse(String json) + { + mockResponseQueue.addLast(new QHttpResponse() + .withStatusCode(200) + .withContent(json) + ); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void enqueueMockResponse(QHttpResponse qHttpResponse) + { + mockResponseQueue.addLast(qHttpResponse); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QHttpResponse returnMockResponseFromQueue(HttpRequestBase request) throws QException + { + if(getMockRequestAsserter() != null) + { + try + { + getMockRequestAsserter().run(request); + } + catch(Exception e) + { + throw (new QException("Error running mock request asserter", e)); + } + } + + if(mockResponseQueue.isEmpty()) + { + fail("No mock response is in the queue for " + request.getMethod() + " " + request.getURI()); + } + + LOG.info("Returning mock http response for " + request.getMethod() + " " + request.getURI()); + return (mockResponseQueue.removeFirst()); + } + + + + /******************************************************************************* + ** Getter for useMock + *******************************************************************************/ + public boolean getUseMock() + { + return (this.useMock); + } + + + + /******************************************************************************* + ** Setter for useMock + *******************************************************************************/ + public void setUseMock(boolean useMock) + { + this.useMock = useMock; + } + + + + /******************************************************************************* + ** Fluent setter for useMock + *******************************************************************************/ + public MockApiUtilsHelper withUseMock(boolean useMock) + { + this.useMock = useMock; + return (this); + } + + + + /******************************************************************************* + ** Getter for mockResponseQueue + *******************************************************************************/ + public Deque getMockResponseQueue() + { + return (this.mockResponseQueue); + } + + + + /******************************************************************************* + ** Setter for mockResponseQueue + *******************************************************************************/ + public void setMockResponseQueue(Deque mockResponseQueue) + { + this.mockResponseQueue = mockResponseQueue; + } + + + + /******************************************************************************* + ** Fluent setter for mockResponseQueue + *******************************************************************************/ + public MockApiUtilsHelper withMockResponseQueue(Deque mockResponseQueue) + { + this.mockResponseQueue = mockResponseQueue; + return (this); + } + + + + /******************************************************************************* + ** Getter for mockRequestAsserter + *******************************************************************************/ + public UnsafeConsumer getMockRequestAsserter() + { + return (this.mockRequestAsserter); + } + + + + /******************************************************************************* + ** Setter for mockRequestAsserter + *******************************************************************************/ + public void setMockRequestAsserter(UnsafeConsumer mockRequestAsserter) + { + this.mockRequestAsserter = mockRequestAsserter; + } + + + + /******************************************************************************* + ** Fluent setter for mockRequestAsserter + *******************************************************************************/ + public MockApiUtilsHelper withMockRequestAsserter(UnsafeConsumer mockRequestAsserter) + { + this.mockRequestAsserter = mockRequestAsserter; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QHttpResponse defaultMockMakeRequest(MockApiUtilsHelper mockApiUtilsHelper, QTableMetaData table, HttpRequestBase request, UnsafeSupplier superMethod) throws QException + { + if(!mockApiUtilsHelper.getUseMock()) + { + QHttpResponse superResponse = superMethod.get(); + System.out.println("== non-mock response content: =="); + System.out.println("Code: " + superResponse.getStatusCode()); + System.out.println(superResponse.getContent()); + System.out.println("== =="); + return (superResponse); + } + + return mockApiUtilsHelper.returnMockResponseFromQueue(request); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + public static String readRequestBody(HttpRequestBase request) throws IOException + { + return (StringUtils.join("\n", IOUtils.readLines(((HttpPost) request).getEntity().getContent()))); + } +} diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockHttpResponse.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockHttpResponse.java new file mode 100644 index 00000000..dbc61f69 --- /dev/null +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockHttpResponse.java @@ -0,0 +1,302 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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.mocks; + + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Locale; +import com.kingsrook.qqq.backend.module.api.actions.QHttpResponse; +import org.apache.http.Header; +import org.apache.http.HeaderIterator; +import org.apache.http.HttpEntity; +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.message.BasicStatusLine; +import org.apache.http.params.HttpParams; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class MockHttpResponse implements CloseableHttpResponse +{ + private final MockApiUtilsHelper mockApiUtilsHelper; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public MockHttpResponse(MockApiUtilsHelper mockApiUtilsHelper) + { + this.mockApiUtilsHelper = mockApiUtilsHelper; + } + + + + @Override + public void close() throws IOException + { + + } + + + + @Override + public StatusLine getStatusLine() + { + ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1); + + if(!mockApiUtilsHelper.getMockResponseQueue().isEmpty()) + { + QHttpResponse qHttpResponse = mockApiUtilsHelper.getMockResponseQueue().peekFirst(); + return (new BasicStatusLine(protocolVersion, qHttpResponse.getStatusCode(), qHttpResponse.getStatusReasonPhrase())); + } + else + { + return (new BasicStatusLine(protocolVersion, 200, "OK")); + } + } + + + + @Override + public void setStatusLine(StatusLine statusLine) + { + + } + + + + @Override + public void setStatusLine(ProtocolVersion protocolVersion, int i) + { + + } + + + + @Override + public void setStatusLine(ProtocolVersion protocolVersion, int i, String s) + { + + } + + + + @Override + public void setStatusCode(int i) throws IllegalStateException + { + + } + + + + @Override + public void setReasonPhrase(String s) throws IllegalStateException + { + + } + + + + @Override + public HttpEntity getEntity() + { + BasicHttpEntity basicHttpEntity = new BasicHttpEntity(); + + if(!mockApiUtilsHelper.getMockResponseQueue().isEmpty()) + { + QHttpResponse qHttpResponse = mockApiUtilsHelper.getMockResponseQueue().removeFirst(); + basicHttpEntity.setContent(new ByteArrayInputStream(qHttpResponse.getContent().getBytes())); + } + else + { + basicHttpEntity.setContent(new ByteArrayInputStream("".getBytes())); + } + return (basicHttpEntity); + } + + + + @Override + public void setEntity(HttpEntity httpEntity) + { + + } + + + + @Override + public Locale getLocale() + { + return null; + } + + + + @Override + public void setLocale(Locale locale) + { + + } + + + + @Override + public ProtocolVersion getProtocolVersion() + { + return null; + } + + + + @Override + public boolean containsHeader(String s) + { + return false; + } + + + + @Override + public Header[] getHeaders(String s) + { + return new Header[0]; + } + + + + @Override + public Header getFirstHeader(String s) + { + return null; + } + + + + @Override + public Header getLastHeader(String s) + { + return null; + } + + + + @Override + public Header[] getAllHeaders() + { + return new Header[0]; + } + + + + @Override + public void addHeader(Header header) + { + + } + + + + @Override + public void addHeader(String s, String s1) + { + + } + + + + @Override + public void setHeader(Header header) + { + + } + + + + @Override + public void setHeader(String s, String s1) + { + + } + + + + @Override + public void setHeaders(Header[] headers) + { + + } + + + + @Override + public void removeHeader(Header header) + { + + } + + + + @Override + public void removeHeaders(String s) + { + + } + + + + @Override + public HeaderIterator headerIterator() + { + return null; + } + + + + @Override + public HeaderIterator headerIterator(String s) + { + return null; + } + + + + @Override + public HttpParams getParams() + { + return null; + } + + + + @Override + public void setParams(HttpParams httpParams) + { + + } +} From 4003323b88e199254c3405f5bb3e481434209ee4 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 27 Apr 2023 18:55:34 -0500 Subject: [PATCH 03/15] Working version of ApiScriptUtils. Moved actual api imlpementation out of javalin class, into implemnetation class. --- .../scripts/RunAdHocRecordScriptAction.java | 32 +- .../qqq/api/actions/ApiImplementation.java | 1118 +++++++++++++++++ .../qqq/api/javalin/QJavalinApiHandler.java | 1031 +-------------- .../com/kingsrook/qqq/api/model/APILog.java | 4 +- ....java => ApiInstanceMetaDataProvider.java} | 19 +- .../qqq/api/utils/ApiScriptUtils.java | 249 ++++ 6 files changed, 1453 insertions(+), 1000 deletions(-) create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java rename qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/{APILogMetaDataProvider.java => ApiInstanceMetaDataProvider.java} (94%) create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java index d38e4d63..8313d8b0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.actions.scripts; +import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -55,6 +56,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.scripts.Script; import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* @@ -126,7 +128,7 @@ public class RunAdHocRecordScriptAction executeCodeInput.getContext().put("scriptUtils", input.getScriptUtils()); } - executeCodeInput.getContext().put("api", new ScriptApi()); + addApiUtilityToContext(executeCodeInput.getContext(), scriptRevision); executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!! @@ -158,6 +160,34 @@ public class RunAdHocRecordScriptAction + /******************************************************************************* + ** Try to (dynamically) load the ApiScriptUtils object from the api middleware + ** module -- in case the runtime doesn't have that module deployed (e.g, not in + ** the project pom). + *******************************************************************************/ + private void addApiUtilityToContext(Map context, ScriptRevision scriptRevision) + { + try + { + Class apiScriptUtilsClass = Class.forName("com.kingsrook.qqq.api.utils.ApiScriptUtils"); + Object apiScriptUtilsObject = apiScriptUtilsClass.getConstructor().newInstance(); + context.put("api", (Serializable) apiScriptUtilsObject); + } + catch(ClassNotFoundException e) + { + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // this is the only exception we're kinda expecting here - so catch for it specifically, and just log.trace - others, warn // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + LOG.trace("Couldn't load ApiScriptUtils class - qqq-middleware-api not on the classpath?"); + } + catch(Exception e) + { + LOG.warn("Error adding api utility to script context", e, logPair("scriptRevisionId", scriptRevision.getId())); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java new file mode 100644 index 00000000..e58533cd --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java @@ -0,0 +1,1118 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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.api.actions; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import com.kingsrook.qqq.api.javalin.QBadRequestException; +import com.kingsrook.qqq.api.model.APIVersion; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; +import com.kingsrook.qqq.api.model.metadata.ApiOperation; +import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; +import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; +import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; +import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; +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; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; +import com.kingsrook.qqq.backend.core.logging.LogPair; +import com.kingsrook.qqq.backend.core.logging.QLogger; +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.QFilterOrderBy; +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.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +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.Pair; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; +import org.apache.commons.lang.BooleanUtils; +import org.eclipse.jetty.http.HttpStatus; +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONTokener; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiImplementation +{ + private static final QLogger LOG = QLogger.getLogger(ApiImplementation.class); + + ///////////////////////////////////// + // key: Pair // + ///////////////////////////////////// + private static Map, Map> tableApiNameMap = new HashMap<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Map query(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, Map> paramMap) throws QException + { + List badRequestMessages = new ArrayList<>(); + + QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.QUERY_BY_QUERY_STRING); + String tableName = table.getName(); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(tableName); + queryInput.setIncludeAssociations(true); + + PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ); + + String pageSizeParam = getSingleParam(paramMap, "pageSize"); + String pageNoParam = getSingleParam(paramMap, "pageNo"); + String booleanOperator = getSingleParam(paramMap, "booleanOperator"); + String includeCountParam = getSingleParam(paramMap, "includeCount"); + String orderBy = getSingleParam(paramMap, "orderBy"); + + Integer pageSize = 50; + if(StringUtils.hasContent(pageSizeParam)) + { + try + { + pageSize = ValueUtils.getValueAsInteger(pageSizeParam); + } + catch(Exception e) + { + badRequestMessages.add("Could not parse pageSize as an integer"); + } + } + if(pageSize < 1 || pageSize > 1000) + { + badRequestMessages.add("pageSize must be between 1 and 1000."); + } + + Integer pageNo = 1; + if(StringUtils.hasContent(pageNoParam)) + { + try + { + pageNo = ValueUtils.getValueAsInteger(pageNoParam); + } + catch(Exception e) + { + badRequestMessages.add("Could not parse pageNo as an integer"); + } + } + if(pageNo < 1) + { + badRequestMessages.add("pageNo must be greater than 0."); + } + + queryInput.setLimit(pageSize); + queryInput.setSkip((pageNo - 1) * pageSize); + + // queryInput.setQueryJoins(processQueryJoinsParam(context)); + + QQueryFilter filter = new QQueryFilter(); + if("and".equalsIgnoreCase(booleanOperator)) + { + filter.setBooleanOperator(QQueryFilter.BooleanOperator.AND); + } + else if("or".equalsIgnoreCase(booleanOperator)) + { + filter.setBooleanOperator(QQueryFilter.BooleanOperator.OR); + } + else if(StringUtils.hasContent(booleanOperator)) + { + badRequestMessages.add("booleanOperator must be either AND or OR."); + } + + boolean includeCount = true; + if("true".equalsIgnoreCase(includeCountParam)) + { + includeCount = true; + } + else if("false".equalsIgnoreCase(includeCountParam)) + { + includeCount = false; + } + else if(StringUtils.hasContent(includeCountParam)) + { + badRequestMessages.add("includeCount must be either true or false"); + } + + if(StringUtils.hasContent(orderBy)) + { + for(String orderByPart : orderBy.split(",")) + { + orderByPart = orderByPart.trim(); + String[] orderByNameDirection = orderByPart.split(" +"); + boolean asc = true; + if(orderByNameDirection.length == 2) + { + if("asc".equalsIgnoreCase(orderByNameDirection[1])) + { + asc = true; + } + else if("desc".equalsIgnoreCase(orderByNameDirection[1])) + { + asc = false; + } + else + { + badRequestMessages.add("orderBy direction for field " + orderByNameDirection[0] + " must be either ASC or DESC."); + } + } + else if(orderByNameDirection.length > 2) + { + badRequestMessages.add("Unrecognized format for orderBy clause: " + orderByPart + ". Expected: fieldName [ASC|DESC]."); + } + + try + { + QFieldMetaData field = table.getField(orderByNameDirection[0]); + filter.withOrderBy(new QFilterOrderBy(field.getName(), asc)); + } + catch(Exception e) + { + badRequestMessages.add("Unrecognized orderBy field name: " + orderByNameDirection[0] + "."); + } + } + } + else + { + filter.withOrderBy(new QFilterOrderBy(table.getPrimaryKeyField(), false)); + } + + Set nonFilterParams = Set.of("pageSize", "pageNo", "orderBy", "booleanOperator", "includeCount"); + + //////////////////////////// + // look for filter params // + //////////////////////////// + for(Map.Entry> entry : paramMap.entrySet()) + { + String name = entry.getKey(); + List values = entry.getValue(); + + if(nonFilterParams.contains(name)) + { + continue; + } + + try + { + QFieldMetaData field = table.getField(name); + for(String value : values) + { + if(StringUtils.hasContent(value)) + { + try + { + filter.addCriteria(parseQueryParamToCriteria(name, value)); + } + catch(Exception e) + { + badRequestMessages.add(e.getMessage()); + } + } + } + } + catch(Exception e) + { + badRequestMessages.add("Unrecognized filter criteria field: " + name); + } + } + + ////////////////////////////////////////// + // no more badRequest checks below here // + ////////////////////////////////////////// + if(!badRequestMessages.isEmpty()) + { + if(badRequestMessages.size() == 1) + { + throw (new QBadRequestException(badRequestMessages.get(0))); + } + else + { + throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join(" \n", badRequestMessages))); + } + } + + ////////////////// + // do the query // + ////////////////// + QueryAction queryAction = new QueryAction(); + queryInput.setFilter(filter); + QueryOutput queryOutput = queryAction.execute(queryInput); + + Map output = new LinkedHashMap<>(); + output.put("pageNo", pageNo); + output.put("pageSize", pageSize); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // map record fields for api // + // note - don't put them in the output until after the count, just because that looks a little nicer, i think // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ArrayList> records = new ArrayList<>(); + for(QRecord record : queryOutput.getRecords()) + { + records.add(QRecordApiAdapter.qRecordToApiMap(record, tableName, apiInstanceMetaData.getName(), version)); + } + + ///////////////////////////// + // optionally do the count // + ///////////////////////////// + if(includeCount) + { + CountInput countInput = new CountInput(); + countInput.setTableName(tableName); + countInput.setFilter(filter); + CountOutput countOutput = new CountAction().execute(countInput); + output.put("count", countOutput.getCount()); + } + + output.put("records", records); + + return (output); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Map insert(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String body) throws QException + { + QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.INSERT); + String tableName = table.getName(); + + InsertInput insertInput = new InsertInput(); + + insertInput.setTableName(tableName); + + PermissionsHelper.checkTablePermissionThrowing(insertInput, TablePermissionSubType.INSERT); + + try + { + if(!StringUtils.hasContent(body)) + { + throw (new QBadRequestException("Missing required POST body")); + } + + JSONTokener jsonTokener = new JSONTokener(body.trim()); + JSONObject jsonObject = new JSONObject(jsonTokener); + + insertInput.setRecords(List.of(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, false))); + + if(jsonTokener.more()) + { + throw (new QBadRequestException("Body contained more than a single JSON object.")); + } + } + catch(QBadRequestException qbre) + { + throw (qbre); + } + catch(Exception e) + { + throw (new QBadRequestException("Body could not be parsed as a JSON object: " + e.getMessage(), e)); + } + + InsertAction insertAction = new InsertAction(); + InsertOutput insertOutput = insertAction.execute(insertInput); + + List errors = insertOutput.getRecords().get(0).getErrors(); + if(CollectionUtils.nullSafeHasContents(errors)) + { + boolean isBadRequest = areAnyErrorsBadRequest(errors); + + String message = "Error inserting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors); + if(isBadRequest) + { + throw (new QBadRequestException(message)); + } + else + { + throw (new QException(message)); + } + } + + LinkedHashMap outputRecord = new LinkedHashMap<>(); + outputRecord.put(table.getPrimaryKeyField(), insertOutput.getRecords().get(0).getValue(table.getPrimaryKeyField())); + return (outputRecord); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static List> bulkInsert(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String body) throws QException + { + QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_INSERT); + String tableName = table.getName(); + + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(tableName); + + PermissionsHelper.checkTablePermissionThrowing(insertInput, TablePermissionSubType.INSERT); + + ///////////////// + // build input // + ///////////////// + try + { + if(!StringUtils.hasContent(body)) + { + throw (new QBadRequestException("Missing required POST body")); + } + + ArrayList recordList = new ArrayList<>(); + insertInput.setRecords(recordList); + + JSONTokener jsonTokener = new JSONTokener(body.trim()); + JSONArray jsonArray = new JSONArray(jsonTokener); + + for(int i = 0; i < jsonArray.length(); i++) + { + JSONObject jsonObject = jsonArray.getJSONObject(i); + recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, false)); + } + + if(jsonTokener.more()) + { + throw (new QBadRequestException("Body contained more than a single JSON array.")); + } + + if(recordList.isEmpty()) + { + throw (new QBadRequestException("No records were found in the POST body")); + } + } + catch(QBadRequestException qbre) + { + throw (qbre); + } + catch(Exception e) + { + throw (new QBadRequestException("Body could not be parsed as a JSON array: " + e.getMessage(), e)); + } + + ////////////// + // execute! // + ////////////// + InsertAction insertAction = new InsertAction(); + InsertOutput insertOutput = insertAction.execute(insertInput); + + /////////////////////////////////////// + // process records to build response // + /////////////////////////////////////// + List> response = new ArrayList<>(); + for(QRecord record : insertOutput.getRecords()) + { + LinkedHashMap outputRecord = new LinkedHashMap<>(); + response.add(outputRecord); + + List errors = record.getErrors(); + if(CollectionUtils.nullSafeHasContents(errors)) + { + outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode()); + outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage()); + outputRecord.put("error", "Error inserting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)); + } + else + { + outputRecord.put("statusCode", HttpStatus.Code.CREATED.getCode()); + outputRecord.put("statusText", HttpStatus.Code.CREATED.getMessage()); + outputRecord.put(table.getPrimaryKeyField(), record.getValue(table.getPrimaryKeyField())); + } + } + + return (response); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Map get(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String primaryKey) throws QException + { + QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.GET); + String tableName = table.getName(); + + GetInput getInput = new GetInput(); + getInput.setTableName(tableName); + + PermissionsHelper.checkTablePermissionThrowing(getInput, TablePermissionSubType.READ); + + getInput.setPrimaryKey(primaryKey); + getInput.setIncludeAssociations(true); + + GetAction getAction = new GetAction(); + GetOutput getOutput = getAction.execute(getInput); + + /////////////////////////////////////////////////////// + // throw a not found error if the record isn't found // + /////////////////////////////////////////////////////// + QRecord record = getOutput.getRecord(); + if(record == null) + { + throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); + } + + Map outputRecord = QRecordApiAdapter.qRecordToApiMap(record, tableName, apiInstanceMetaData.getName(), version); + return (outputRecord); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void update(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String primaryKey, String body) throws QException + { + QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.UPDATE); + String tableName = table.getName(); + + UpdateInput updateInput = new UpdateInput(); + updateInput.setTableName(tableName); + + PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT); + + try + { + if(!StringUtils.hasContent(body)) + { + throw (new QBadRequestException("Missing required PATCH body")); + } + + JSONTokener jsonTokener = new JSONTokener(body.trim()); + JSONObject jsonObject = new JSONObject(jsonTokener); + + QRecord qRecord = QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, false); + qRecord.setValue(table.getPrimaryKeyField(), primaryKey); + updateInput.setRecords(List.of(qRecord)); + + if(jsonTokener.more()) + { + throw (new QBadRequestException("Body contained more than a single JSON object.")); + } + } + catch(QBadRequestException qbre) + { + throw (qbre); + } + catch(Exception e) + { + throw (new QBadRequestException("Body could not be parsed as a JSON object: " + e.getMessage(), e)); + } + + UpdateAction updateAction = new UpdateAction(); + UpdateOutput updateOutput = updateAction.execute(updateInput); + + List errors = updateOutput.getRecords().get(0).getErrors(); + if(CollectionUtils.nullSafeHasContents(errors)) + { + if(areAnyErrorsNotFound(errors)) + { + throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); + } + else + { + boolean isBadRequest = areAnyErrorsBadRequest(errors); + + String message = "Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors); + if(isBadRequest) + { + throw (new QBadRequestException(message)); + } + else + { + throw (new QException(message)); + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static List> bulkUpdate(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String body) throws QException + { + QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_UPDATE); + String tableName = table.getName(); + + UpdateInput updateInput = new UpdateInput(); + updateInput.setTableName(tableName); + + PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT); + + ///////////////// + // build input // + ///////////////// + try + { + if(!StringUtils.hasContent(body)) + { + throw (new QBadRequestException("Missing required PATCH body")); + } + + ArrayList recordList = new ArrayList<>(); + updateInput.setRecords(recordList); + + JSONTokener jsonTokener = new JSONTokener(body.trim()); + JSONArray jsonArray = new JSONArray(jsonTokener); + + for(int i = 0; i < jsonArray.length(); i++) + { + JSONObject jsonObject = jsonArray.getJSONObject(i); + recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, true)); + } + + if(jsonTokener.more()) + { + throw (new QBadRequestException("Body contained more than a single JSON array.")); + } + + if(recordList.isEmpty()) + { + throw (new QBadRequestException("No records were found in the PATCH body")); + } + } + catch(QBadRequestException qbre) + { + throw (qbre); + } + catch(Exception e) + { + throw (new QBadRequestException("Body could not be parsed as a JSON array: " + e.getMessage(), e)); + } + + ////////////// + // execute! // + ////////////// + UpdateAction updateAction = new UpdateAction(); + UpdateOutput updateOutput = updateAction.execute(updateInput); + + /////////////////////////////////////// + // process records to build response // + /////////////////////////////////////// + List> response = new ArrayList<>(); + int i = 0; + for(QRecord record : updateOutput.getRecords()) + { + LinkedHashMap outputRecord = new LinkedHashMap<>(); + response.add(outputRecord); + + try + { + QRecord inputRecord = updateInput.getRecords().get(i); + Serializable primaryKey = inputRecord.getValue(table.getPrimaryKeyField()); + outputRecord.put(table.getPrimaryKeyField(), primaryKey); + } + catch(Exception e) + { + ////////// + // omit // + ////////// + } + + List errors = record.getErrors(); + if(CollectionUtils.nullSafeHasContents(errors)) + { + outputRecord.put("error", "Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)); + if(areAnyErrorsNotFound(errors)) + { + outputRecord.put("statusCode", HttpStatus.Code.NOT_FOUND.getCode()); + outputRecord.put("statusText", HttpStatus.Code.NOT_FOUND.getMessage()); + } + else + { + outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode()); + outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage()); + } + } + else + { + outputRecord.put("statusCode", HttpStatus.Code.NO_CONTENT.getCode()); + outputRecord.put("statusText", HttpStatus.Code.NO_CONTENT.getMessage()); + } + + i++; + } + + return (response); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void delete(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String primaryKey) throws QException + { + QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.DELETE); + String tableName = table.getName(); + + DeleteInput deleteInput = new DeleteInput(); + deleteInput.setTableName(tableName); + deleteInput.setPrimaryKeys(List.of(primaryKey)); + + PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE); + + /////////////////// + // do the delete // + /////////////////// + DeleteAction deleteAction = new DeleteAction(); + DeleteOutput deleteOutput = deleteAction.execute(deleteInput); + if(CollectionUtils.nullSafeHasContents(deleteOutput.getRecordsWithErrors())) + { + if(areAnyErrorsNotFound(deleteOutput.getRecordsWithErrors().get(0).getErrors())) + { + throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); + } + else + { + throw (new QException("Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(deleteOutput.getRecordsWithErrors().get(0).getErrors()))); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static List> bulkDelete(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String body) throws QException + { + QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_DELETE); + String tableName = table.getName(); + + DeleteInput deleteInput = new DeleteInput(); + deleteInput.setTableName(tableName); + + PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE); + + ///////////////// + // build input // + ///////////////// + try + { + if(!StringUtils.hasContent(body)) + { + throw (new QBadRequestException("Missing required DELETE body")); + } + + ArrayList primaryKeyList = new ArrayList<>(); + deleteInput.setPrimaryKeys(primaryKeyList); + + JSONTokener jsonTokener = new JSONTokener(body.trim()); + JSONArray jsonArray = new JSONArray(jsonTokener); + + for(int i = 0; i < jsonArray.length(); i++) + { + Object object = jsonArray.get(i); + if(object instanceof JSONArray || object instanceof JSONObject) + { + throw (new QBadRequestException("One or more elements inside the DELETE body JSONArray was not a primitive value")); + } + primaryKeyList.add(String.valueOf(object)); + } + + if(jsonTokener.more()) + { + throw (new QBadRequestException("Body contained more than a single JSON array.")); + } + + if(primaryKeyList.isEmpty()) + { + throw (new QBadRequestException("No primary keys were found in the DELETE body")); + } + } + catch(QBadRequestException qbre) + { + throw (qbre); + } + catch(Exception e) + { + throw (new QBadRequestException("Body could not be parsed as a JSON array: " + e.getMessage(), e)); + } + + ////////////// + // execute! // + ////////////// + DeleteAction deleteAction = new DeleteAction(); + DeleteOutput deleteOutput = deleteAction.execute(deleteInput); + + /////////////////////////////////////// + // process records to build response // + /////////////////////////////////////// + List> response = new ArrayList<>(); + + List recordsWithErrors = deleteOutput.getRecordsWithErrors(); + Map> primaryKeyToErrorsMap = new HashMap<>(); + for(QRecord recordWithError : CollectionUtils.nonNullList(recordsWithErrors)) + { + String primaryKey = recordWithError.getValueString(table.getPrimaryKeyField()); + primaryKeyToErrorsMap.put(primaryKey, recordWithError.getErrors()); + } + + for(Serializable primaryKey : deleteInput.getPrimaryKeys()) + { + LinkedHashMap outputRecord = new LinkedHashMap<>(); + response.add(outputRecord); + outputRecord.put(table.getPrimaryKeyField(), primaryKey); + + String primaryKeyString = ValueUtils.getValueAsString(primaryKey); + List errors = primaryKeyToErrorsMap.get(primaryKeyString); + if(CollectionUtils.nullSafeHasContents(errors)) + { + outputRecord.put("error", "Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)); + if(areAnyErrorsNotFound(errors)) + { + outputRecord.put("statusCode", HttpStatus.Code.NOT_FOUND.getCode()); + outputRecord.put("statusText", HttpStatus.Code.NOT_FOUND.getMessage()); + } + else + { + outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode()); + outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage()); + } + } + else + { + outputRecord.put("statusCode", HttpStatus.Code.NO_CONTENT.getCode()); + outputRecord.put("statusText", HttpStatus.Code.NO_CONTENT.getMessage()); + } + } + + return (response); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static String getSingleParam(Map> paramMap, String name) + { + if(CollectionUtils.nullSafeHasContents(paramMap.get(name))) + { + return (paramMap.get(name).get(0)); + } + + return (null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private enum Operator + { + /////////////////////////////////////////////////////////////////////////////////// + // order of these is important (e.g., because some are a sub-string of others!!) // + /////////////////////////////////////////////////////////////////////////////////// + EQ("=", QCriteriaOperator.EQUALS, QCriteriaOperator.NOT_EQUALS, 1), + LTE("<=", QCriteriaOperator.LESS_THAN_OR_EQUALS, null, 1), + GTE(">=", QCriteriaOperator.GREATER_THAN_OR_EQUALS, null, 1), + LT("<", QCriteriaOperator.LESS_THAN, null, 1), + GT(">", QCriteriaOperator.GREATER_THAN, null, 1), + EMPTY("EMPTY", QCriteriaOperator.IS_BLANK, QCriteriaOperator.IS_NOT_BLANK, 0), + BETWEEN("BETWEEN ", QCriteriaOperator.BETWEEN, QCriteriaOperator.NOT_BETWEEN, 2), + IN("IN ", QCriteriaOperator.IN, QCriteriaOperator.NOT_IN, null), + LIKE("LIKE ", QCriteriaOperator.LIKE, QCriteriaOperator.NOT_LIKE, 1); + + + private final String prefix; + private final QCriteriaOperator positiveOperator; + private final QCriteriaOperator negativeOperator; + private final Integer noOfValues; // null means many (IN) + + + + /******************************************************************************* + ** + *******************************************************************************/ + Operator(String prefix, QCriteriaOperator positiveOperator, QCriteriaOperator negativeOperator, Integer noOfValues) + { + this.prefix = prefix; + this.positiveOperator = positiveOperator; + this.negativeOperator = negativeOperator; + this.noOfValues = noOfValues; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QFilterCriteria parseQueryParamToCriteria(String name, String value) throws QException + { + /////////////////////////////////// + // process & discard a leading ! // + /////////////////////////////////// + boolean isNot = false; + if(value.startsWith("!") && value.length() > 1) + { + isNot = true; + value = value.substring(1); + } + + ////////////////////////// + // look for an operator // + ////////////////////////// + Operator selectedOperator = null; + for(Operator op : Operator.values()) + { + if(value.startsWith(op.prefix)) + { + selectedOperator = op; + if(selectedOperator.negativeOperator == null && isNot) + { + throw (new QBadRequestException("Unsupported operator: !" + selectedOperator.prefix)); + } + break; + } + } + + ///////////////////////////////////////////////////////////////////////////////////////////// + // if an operator was found, strip it away from the value for figuring out the values part // + ///////////////////////////////////////////////////////////////////////////////////////////// + if(selectedOperator != null) + { + value = value.substring(selectedOperator.prefix.length()); + } + else + { + //////////////////////////////////////////////////////////////// + // else - assume the default operator, and use the full value // + //////////////////////////////////////////////////////////////// + selectedOperator = Operator.EQ; + } + + //////////////////////////////////// + // figure out the criteria values // + // todo - quotes? // + //////////////////////////////////// + List criteriaValues; + if(selectedOperator.noOfValues == null) + { + criteriaValues = Arrays.asList(value.split(",")); + } + else if(selectedOperator.noOfValues == 1) + { + criteriaValues = ListBuilder.of(value); + } + else if(selectedOperator.noOfValues == 0) + { + if(StringUtils.hasContent(value)) + { + throw (new QBadRequestException("Unexpected value after operator " + selectedOperator.prefix + " for field " + name)); + } + criteriaValues = null; + } + else if(selectedOperator.noOfValues == 2) + { + criteriaValues = Arrays.asList(value.split(",")); + if(criteriaValues.size() != 2) + { + throw (new QBadRequestException("Operator " + selectedOperator.prefix + " for field " + name + " requires 2 values (received " + criteriaValues.size() + ")")); + } + } + else + { + throw (new QException("Unexpected noOfValues [" + selectedOperator.noOfValues + "] in operator [" + selectedOperator + "]")); + } + + return (new QFilterCriteria(name, isNot ? selectedOperator.negativeOperator : selectedOperator.positiveOperator, criteriaValues)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QTableMetaData validateTableAndVersion(String path, ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, ApiOperation operation) throws QNotFoundException + { + QNotFoundException qNotFoundException = new QNotFoundException("Could not find any resources at path " + path); + + QTableMetaData table = getTableByApiName(apiInstanceMetaData.getName(), version, tableApiName); + LogPair[] logPairs = new LogPair[] { logPair("apiName", apiInstanceMetaData.getName()), logPair("version", version), logPair("tableApiName", tableApiName), logPair("operation", operation) }; + + if(table == null) + { + LOG.info("404 because table is null (tableApiName=" + tableApiName + ")", logPairs); + throw (qNotFoundException); + } + + if(BooleanUtils.isTrue(table.getIsHidden())) + { + LOG.info("404 because table isHidden", logPairs); + throw (qNotFoundException); + } + + ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); + if(apiTableMetaDataContainer == null) + { + LOG.info("404 because table apiMetaDataContainer is null", logPairs); + throw (qNotFoundException); + } + + ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiInstanceMetaData.getName()); + if(apiTableMetaData == null) + { + LOG.info("404 because table apiMetaData is null", logPairs); + throw (qNotFoundException); + } + + if(BooleanUtils.isTrue(apiTableMetaData.getIsExcluded())) + { + LOG.info("404 because table is excluded", logPairs); + throw (qNotFoundException); + } + + if(!operation.isOperationEnabled(List.of(apiInstanceMetaData, apiTableMetaData))) + { + LOG.info("404 because api operation is not enabled", logPairs); + throw (qNotFoundException); + } + + if(!table.isCapabilityEnabled(QContext.getQInstance().getBackendForTable(table.getName()), operation.getCapability())) + { + LOG.info("404 because table capability is not enabled", logPairs); + throw (qNotFoundException); + } + + APIVersion requestApiVersion = new APIVersion(version); + List supportedVersions = apiInstanceMetaData.getSupportedVersions(); + if(CollectionUtils.nullSafeIsEmpty(supportedVersions) || !supportedVersions.contains(requestApiVersion)) + { + LOG.info("404 because requested version is not supported", logPairs); + throw (qNotFoundException); + } + + if(!apiTableMetaData.getApiVersionRange().includes(requestApiVersion)) + { + LOG.info("404 because table version range does not include requested version", logPairs); + throw (qNotFoundException); + } + + return (table); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QTableMetaData getTableByApiName(String apiName, String version, String tableApiName) + { + ///////////////////////////////////////////////////////////////////////////////////////////// + // tableApiNameMap is a map of (apiName,apiVersion) => Map. // + // that is to say, a 2-level map. The first level is keyed by (apiName,apiVersion) pairs. // + // the second level is keyed by tableApiNames. // + ///////////////////////////////////////////////////////////////////////////////////////////// + Pair key = new Pair<>(apiName, version); + if(tableApiNameMap.get(key) == null) + { + Map map = new HashMap<>(); + + for(QTableMetaData table : QContext.getQInstance().getTables().values()) + { + ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); + if(apiTableMetaDataContainer != null) + { + ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiName); + if(apiTableMetaData != null) + { + String name = table.getName(); + if(StringUtils.hasContent(apiTableMetaData.getApiTableName())) + { + name = apiTableMetaData.getApiTableName(); + } + map.put(name, table); + } + } + } + + tableApiNameMap.put(key, map); + } + + return (tableApiNameMap.get(key).get(tableApiName)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean areAnyErrorsBadRequest(List errors) + { + boolean isBadRequest = errors.stream().anyMatch(e -> + e.contains("Missing value in required field") + || e.contains("You do not have permission") + ); + return isBadRequest; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean areAnyErrorsNotFound(List errors) + { + return errors.stream().anyMatch(e -> e.startsWith(UpdateAction.NOT_FOUND_ERROR_PREFIX) || e.startsWith(DeleteAction.NOT_FOUND_ERROR_PREFIX)); + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java index 0cb92760..a4375bd4 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java @@ -27,35 +27,25 @@ import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; -import java.util.Arrays; import java.util.Base64; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.stream.Collectors; +import com.kingsrook.qqq.api.actions.ApiImplementation; import com.kingsrook.qqq.api.actions.GenerateOpenApiSpecAction; -import com.kingsrook.qqq.api.actions.QRecordApiAdapter; import com.kingsrook.qqq.api.model.APILog; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput; -import com.kingsrook.qqq.api.model.metadata.APILogMetaDataProvider; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; -import com.kingsrook.qqq.api.model.metadata.ApiOperation; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataProvider; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; -import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; -import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; -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; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.AccessTokenException; import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; @@ -64,30 +54,17 @@ import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; -import com.kingsrook.qqq.backend.core.logging.LogPair; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; -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.QFilterOrderBy; 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.actions.tables.update.UpdateInput; -import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; 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.authentication.Auth0AuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData; -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.model.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QUser; @@ -96,10 +73,7 @@ import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModu import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; -import com.kingsrook.qqq.backend.core.utils.Pair; import com.kingsrook.qqq.backend.core.utils.StringUtils; -import com.kingsrook.qqq.backend.core.utils.ValueUtils; -import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; import com.kingsrook.qqq.backend.javalin.QJavalinAccessLogger; import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; @@ -110,9 +84,6 @@ import io.javalin.http.Context; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.BooleanUtils; import org.eclipse.jetty.http.HttpStatus; -import org.json.JSONArray; -import org.json.JSONObject; -import org.json.JSONTokener; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; import static com.kingsrook.qqq.backend.javalin.QJavalinImplementation.SLOW_LOG_THRESHOLD_MS; @@ -126,11 +97,6 @@ public class QJavalinApiHandler private static QInstance qInstance; - ///////////////////////////////////// - // key: Pair // - ///////////////////////////////////// - private static Map, Map> tableApiNameMap = new HashMap<>(); - private static Map apiLogUserIdCache = new HashMap<>(); @@ -432,7 +398,6 @@ public class QJavalinApiHandler context.status(HttpStatus.Code.OK.getCode()); context.result(accessToken); QJavalinAccessLogger.logEndSuccess(); - return; } catch(AccessTokenException aae) { @@ -446,7 +411,6 @@ public class QJavalinApiHandler context.status(aae.getStatusCode()); context.result(aae.getMessage()); QJavalinAccessLogger.logEndSuccess(); - return; } //////////////////////////////////////////////////////// @@ -647,38 +611,10 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.GET); - String tableName = table.getName(); + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiGet", logPair("table", tableApiName), logPair("primaryKey", primaryKey)); - GetInput getInput = new GetInput(); - - setupSession(context, getInput, version, apiInstanceMetaData); - QJavalinAccessLogger.logStart("apiGet", logPair("table", tableName), logPair("primaryKey", primaryKey)); - - getInput.setTableName(tableName); - - PermissionsHelper.checkTablePermissionThrowing(getInput, TablePermissionSubType.READ); - - // 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) - - getInput.setPrimaryKey(primaryKey); - getInput.setIncludeAssociations(true); - - GetAction getAction = new GetAction(); - GetOutput getOutput = getAction.execute(getInput); - - /////////////////////////////////////////////////////// - // throw a not found error if the record isn't found // - /////////////////////////////////////////////////////// - QRecord record = getOutput.getRecord(); - if(record == null) - { - throw (new QNotFoundException("Could not find " + table.getLabel() + " with " - + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); - } - - Map outputRecord = QRecordApiAdapter.qRecordToApiMap(record, tableName, apiInstanceMetaData.getName(), version); + Map outputRecord = ApiImplementation.get(apiInstanceMetaData, version, tableApiName, primaryKey); QJavalinAccessLogger.logEndSuccess(); String resultString = JsonUtils.toJson(outputRecord); @@ -769,7 +705,7 @@ public class QJavalinApiHandler *******************************************************************************/ private static Integer getApiLogUserId(QSession qSession) throws QException { - String tableName = APILogMetaDataProvider.TABLE_NAME_API_LOG_USER; + String tableName = ApiInstanceMetaDataProvider.TABLE_NAME_API_LOG_USER; if(qSession == null) { @@ -874,7 +810,7 @@ public class QJavalinApiHandler private static Integer fetchApiLogUserIdFromName(String name) throws QException { GetInput getInput = new GetInput(); - getInput.setTableName(APILogMetaDataProvider.TABLE_NAME_API_LOG_USER); + getInput.setTableName(ApiInstanceMetaDataProvider.TABLE_NAME_API_LOG_USER); getInput.setUniqueKey(Map.of("name", name)); GetOutput getOutput = new GetAction().execute(getInput); if(getOutput.getRecord() != null) @@ -892,228 +828,20 @@ public class QJavalinApiHandler *******************************************************************************/ private static void doQuery(Context context, ApiInstanceMetaData apiInstanceMetaData) { - String version = context.pathParam("version"); - String tableApiName = context.pathParam("tableName"); - QQueryFilter filter = null; - APILog apiLog = newAPILog(context); + String version = context.pathParam("version"); + String tableApiName = context.pathParam("tableName"); + + QQueryFilter filter = null; + APILog apiLog = newAPILog(context); try { - List badRequestMessages = new ArrayList<>(); + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiQuery", logPair("table", tableApiName)); - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.QUERY_BY_QUERY_STRING); - String tableName = table.getName(); + Map output = ApiImplementation.query(apiInstanceMetaData, version, tableApiName, context.queryParamMap()); - QueryInput queryInput = new QueryInput(); - setupSession(context, queryInput, version, apiInstanceMetaData); - QJavalinAccessLogger.logStart("apiQuery", logPair("table", tableName)); - - queryInput.setTableName(tableName); - queryInput.setIncludeAssociations(true); - - PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ); - - Integer pageSize = 50; - if(StringUtils.hasContent(context.queryParam("pageSize"))) - { - try - { - pageSize = ValueUtils.getValueAsInteger(context.queryParam("pageSize")); - } - catch(Exception e) - { - badRequestMessages.add("Could not parse pageSize as an integer"); - } - } - if(pageSize < 1 || pageSize > 1000) - { - badRequestMessages.add("pageSize must be between 1 and 1000."); - } - - Integer pageNo = 1; - if(StringUtils.hasContent(context.queryParam("pageNo"))) - { - try - { - pageNo = ValueUtils.getValueAsInteger(context.queryParam("pageNo")); - } - catch(Exception e) - { - badRequestMessages.add("Could not parse pageNo as an integer"); - } - } - if(pageNo < 1) - { - badRequestMessages.add("pageNo must be greater than 0."); - } - - queryInput.setLimit(pageSize); - queryInput.setSkip((pageNo - 1) * pageSize); - - // queryInput.setQueryJoins(processQueryJoinsParam(context)); - - filter = new QQueryFilter(); - if("and".equalsIgnoreCase(context.queryParam("booleanOperator"))) - { - filter.setBooleanOperator(QQueryFilter.BooleanOperator.AND); - } - else if("or".equalsIgnoreCase(context.queryParam("booleanOperator"))) - { - filter.setBooleanOperator(QQueryFilter.BooleanOperator.OR); - } - else if(StringUtils.hasContent(context.queryParam("booleanOperator"))) - { - badRequestMessages.add("booleanOperator must be either AND or OR."); - } - - boolean includeCount = true; - if("true".equalsIgnoreCase(context.queryParam("includeCount"))) - { - includeCount = true; - } - else if("false".equalsIgnoreCase(context.queryParam("includeCount"))) - { - includeCount = false; - } - else if(StringUtils.hasContent(context.queryParam("includeCount"))) - { - badRequestMessages.add("includeCount must be either true or false"); - } - - String orderBy = context.queryParam("orderBy"); - if(StringUtils.hasContent(orderBy)) - { - for(String orderByPart : orderBy.split(",")) - { - orderByPart = orderByPart.trim(); - String[] orderByNameDirection = orderByPart.split(" +"); - boolean asc = true; - if(orderByNameDirection.length == 2) - { - if("asc".equalsIgnoreCase(orderByNameDirection[1])) - { - asc = true; - } - else if("desc".equalsIgnoreCase(orderByNameDirection[1])) - { - asc = false; - } - else - { - badRequestMessages.add("orderBy direction for field " + orderByNameDirection[0] + " must be either ASC or DESC."); - } - } - else if(orderByNameDirection.length > 2) - { - badRequestMessages.add("Unrecognized format for orderBy clause: " + orderByPart + ". Expected: fieldName [ASC|DESC]."); - } - - try - { - QFieldMetaData field = table.getField(orderByNameDirection[0]); - filter.withOrderBy(new QFilterOrderBy(field.getName(), asc)); - } - catch(Exception e) - { - badRequestMessages.add("Unrecognized orderBy field name: " + orderByNameDirection[0] + "."); - } - } - } - else - { - filter.withOrderBy(new QFilterOrderBy(table.getPrimaryKeyField(), false)); - } - - Set nonFilterParams = Set.of("pageSize", "pageNo", "orderBy", "booleanOperator", "includeCount"); - - //////////////////////////// - // look for filter params // - //////////////////////////// - for(Map.Entry> entry : context.queryParamMap().entrySet()) - { - String name = entry.getKey(); - List values = entry.getValue(); - - if(nonFilterParams.contains(name)) - { - continue; - } - - try - { - QFieldMetaData field = table.getField(name); - for(String value : values) - { - if(StringUtils.hasContent(value)) - { - try - { - filter.addCriteria(parseQueryParamToCriteria(name, value)); - } - catch(Exception e) - { - badRequestMessages.add(e.getMessage()); - } - } - } - } - catch(Exception e) - { - badRequestMessages.add("Unrecognized filter criteria field: " + name); - } - } - - ////////////////////////////////////////// - // no more badRequest checks below here // - ////////////////////////////////////////// - if(!badRequestMessages.isEmpty()) - { - if(badRequestMessages.size() == 1) - { - throw (new QBadRequestException(badRequestMessages.get(0))); - } - else - { - throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join(" \n", badRequestMessages))); - } - } - - ////////////////// - // do the query // - ////////////////// - QueryAction queryAction = new QueryAction(); - queryInput.setFilter(filter); - QueryOutput queryOutput = queryAction.execute(queryInput); - - Map output = new LinkedHashMap<>(); - output.put("pageNo", pageNo); - output.put("pageSize", pageSize); - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // map record fields for api // - // note - don't put them in the output until after the count, just because that looks a little nicer, i think // - //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - ArrayList> records = new ArrayList<>(); - for(QRecord record : queryOutput.getRecords()) - { - records.add(QRecordApiAdapter.qRecordToApiMap(record, tableName, apiInstanceMetaData.getName(), version)); - } - - ///////////////////////////// - // optionally do the count // - ///////////////////////////// - if(includeCount) - { - CountInput countInput = new CountInput(); - countInput.setTableName(tableName); - countInput.setFilter(filter); - CountOutput countOutput = new CountAction().execute(countInput); - output.put("count", countOutput.getCount()); - } - - output.put("records", records); - - QJavalinAccessLogger.logEndSuccess(logPair("recordCount", queryOutput.getRecords().size()), QJavalinAccessLogger.logPairIfSlow("filter", filter, SLOW_LOG_THRESHOLD_MS)); + QJavalinAccessLogger.logEndSuccess(logPair("recordCount", () -> ((List) output.get("records")).size()), QJavalinAccessLogger.logPairIfSlow("filter", filter, SLOW_LOG_THRESHOLD_MS)); String resultString = JsonUtils.toJson(output); context.result(resultString); storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); @@ -1127,246 +855,6 @@ public class QJavalinApiHandler - /******************************************************************************* - ** - *******************************************************************************/ - private static QTableMetaData validateTableAndVersion(Context context, ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, ApiOperation operation) throws QNotFoundException - { - QNotFoundException qNotFoundException = new QNotFoundException("Could not find any resources at path " + context.path()); - - QTableMetaData table = getTableByApiName(apiInstanceMetaData.getName(), version, tableApiName); - LogPair[] logPairs = new LogPair[] { logPair("apiName", apiInstanceMetaData.getName()), logPair("version", version), logPair("tableApiName", tableApiName), logPair("operation", operation) }; - - if(table == null) - { - LOG.info("404 because table is null", logPairs); - throw (qNotFoundException); - } - - if(BooleanUtils.isTrue(table.getIsHidden())) - { - LOG.info("404 because table isHidden", logPairs); - throw (qNotFoundException); - } - - ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); - if(apiTableMetaDataContainer == null) - { - LOG.info("404 because table apiMetaDataContainer is null", logPairs); - throw (qNotFoundException); - } - - ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiInstanceMetaData.getName()); - if(apiTableMetaData == null) - { - LOG.info("404 because table apiMetaData is null", logPairs); - throw (qNotFoundException); - } - - if(BooleanUtils.isTrue(apiTableMetaData.getIsExcluded())) - { - LOG.info("404 because table is excluded", logPairs); - throw (qNotFoundException); - } - - if(!operation.isOperationEnabled(List.of(apiInstanceMetaData, apiTableMetaData))) - { - LOG.info("404 because api operation is not enabled", logPairs); - throw (qNotFoundException); - } - - if(!table.isCapabilityEnabled(qInstance.getBackendForTable(table.getName()), operation.getCapability())) - { - LOG.info("404 because table capability is not enabled", logPairs); - throw (qNotFoundException); - } - - APIVersion requestApiVersion = new APIVersion(version); - List supportedVersions = apiInstanceMetaData.getSupportedVersions(); - if(CollectionUtils.nullSafeIsEmpty(supportedVersions) || !supportedVersions.contains(requestApiVersion)) - { - LOG.info("404 because requested version is not supported", logPairs); - throw (qNotFoundException); - } - - if(!apiTableMetaData.getApiVersionRange().includes(requestApiVersion)) - { - LOG.info("404 because table version range does not include requested version", logPairs); - throw (qNotFoundException); - } - - return (table); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private static QTableMetaData getTableByApiName(String apiName, String version, String tableApiName) - { - ///////////////////////////////////////////////////////////////////////////////////////////// - // tableApiNameMap is a map of (apiName,apiVersion) => Map. // - // that is to say, a 2-level map. The first level is keyed by (apiName,apiVersion) pairs. // - // the second level is keyed by tableApiNames. // - ///////////////////////////////////////////////////////////////////////////////////////////// - Pair key = new Pair<>(apiName, version); - if(tableApiNameMap.get(key) == null) - { - Map map = new HashMap<>(); - - for(QTableMetaData table : qInstance.getTables().values()) - { - ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); - if(apiTableMetaDataContainer != null) - { - ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiName); - if(apiTableMetaData != null) - { - String name = table.getName(); - if(StringUtils.hasContent(apiTableMetaData.getApiTableName())) - { - name = apiTableMetaData.getApiTableName(); - } - map.put(name, table); - } - } - } - - tableApiNameMap.put(key, map); - } - - return (tableApiNameMap.get(key).get(tableApiName)); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private enum Operator - { - /////////////////////////////////////////////////////////////////////////////////// - // order of these is important (e.g., because some are a sub-string of others!!) // - /////////////////////////////////////////////////////////////////////////////////// - EQ("=", QCriteriaOperator.EQUALS, QCriteriaOperator.NOT_EQUALS, 1), - LTE("<=", QCriteriaOperator.LESS_THAN_OR_EQUALS, null, 1), - GTE(">=", QCriteriaOperator.GREATER_THAN_OR_EQUALS, null, 1), - LT("<", QCriteriaOperator.LESS_THAN, null, 1), - GT(">", QCriteriaOperator.GREATER_THAN, null, 1), - EMPTY("EMPTY", QCriteriaOperator.IS_BLANK, QCriteriaOperator.IS_NOT_BLANK, 0), - BETWEEN("BETWEEN ", QCriteriaOperator.BETWEEN, QCriteriaOperator.NOT_BETWEEN, 2), - IN("IN ", QCriteriaOperator.IN, QCriteriaOperator.NOT_IN, null), - LIKE("LIKE ", QCriteriaOperator.LIKE, QCriteriaOperator.NOT_LIKE, 1); - - - private final String prefix; - private final QCriteriaOperator positiveOperator; - private final QCriteriaOperator negativeOperator; - private final Integer noOfValues; // null means many (IN) - - - - /******************************************************************************* - ** - *******************************************************************************/ - Operator(String prefix, QCriteriaOperator positiveOperator, QCriteriaOperator negativeOperator, Integer noOfValues) - { - this.prefix = prefix; - this.positiveOperator = positiveOperator; - this.negativeOperator = negativeOperator; - this.noOfValues = noOfValues; - } - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private static QFilterCriteria parseQueryParamToCriteria(String name, String value) throws QException - { - /////////////////////////////////// - // process & discard a leading ! // - /////////////////////////////////// - boolean isNot = false; - if(value.startsWith("!") && value.length() > 1) - { - isNot = true; - value = value.substring(1); - } - - ////////////////////////// - // look for an operator // - ////////////////////////// - Operator selectedOperator = null; - for(Operator op : Operator.values()) - { - if(value.startsWith(op.prefix)) - { - selectedOperator = op; - if(selectedOperator.negativeOperator == null && isNot) - { - throw (new QBadRequestException("Unsupported operator: !" + selectedOperator.prefix)); - } - break; - } - } - - ///////////////////////////////////////////////////////////////////////////////////////////// - // if an operator was found, strip it away from the value for figuring out the values part // - ///////////////////////////////////////////////////////////////////////////////////////////// - if(selectedOperator != null) - { - value = value.substring(selectedOperator.prefix.length()); - } - else - { - //////////////////////////////////////////////////////////////// - // else - assume the default operator, and use the full value // - //////////////////////////////////////////////////////////////// - selectedOperator = Operator.EQ; - } - - //////////////////////////////////// - // figure out the criteria values // - // todo - quotes? // - //////////////////////////////////// - List criteriaValues; - if(selectedOperator.noOfValues == null) - { - criteriaValues = Arrays.asList(value.split(",")); - } - else if(selectedOperator.noOfValues == 1) - { - criteriaValues = ListBuilder.of(value); - } - else if(selectedOperator.noOfValues == 0) - { - if(StringUtils.hasContent(value)) - { - throw (new QBadRequestException("Unexpected value after operator " + selectedOperator.prefix + " for field " + name)); - } - criteriaValues = null; - } - else if(selectedOperator.noOfValues == 2) - { - criteriaValues = Arrays.asList(value.split(",")); - if(criteriaValues.size() != 2) - { - throw (new QBadRequestException("Operator " + selectedOperator.prefix + " for field " + name + " requires 2 values (received " + criteriaValues.size() + ")")); - } - } - else - { - throw (new QException("Unexpected noOfValues [" + selectedOperator.noOfValues + "] in operator [" + selectedOperator + "]")); - } - - return (new QFilterCriteria(name, isNot ? selectedOperator.negativeOperator : selectedOperator.positiveOperator, criteriaValues)); - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -1378,65 +866,10 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.INSERT); - String tableName = table.getName(); + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiInsert", logPair("table", tableApiName)); - InsertInput insertInput = new InsertInput(); - - setupSession(context, insertInput, version, apiInstanceMetaData); - QJavalinAccessLogger.logStart("apiInsert", logPair("table", tableName)); - - insertInput.setTableName(tableName); - - PermissionsHelper.checkTablePermissionThrowing(insertInput, TablePermissionSubType.INSERT); - - try - { - if(!StringUtils.hasContent(context.body())) - { - throw (new QBadRequestException("Missing required POST body")); - } - - JSONTokener jsonTokener = new JSONTokener(context.body().trim()); - JSONObject jsonObject = new JSONObject(jsonTokener); - - insertInput.setRecords(List.of(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, false))); - - if(jsonTokener.more()) - { - throw (new QBadRequestException("Body contained more than a single JSON object.")); - } - } - catch(QBadRequestException qbre) - { - throw (qbre); - } - catch(Exception e) - { - throw (new QBadRequestException("Body could not be parsed as a JSON object: " + e.getMessage(), e)); - } - - InsertAction insertAction = new InsertAction(); - InsertOutput insertOutput = insertAction.execute(insertInput); - - List errors = insertOutput.getRecords().get(0).getErrors(); - if(CollectionUtils.nullSafeHasContents(errors)) - { - boolean isBadRequest = areAnyErrorsBadRequest(errors); - - String message = "Error inserting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors); - if(isBadRequest) - { - throw (new QBadRequestException(message)); - } - else - { - throw (new QException(message)); - } - } - - LinkedHashMap outputRecord = new LinkedHashMap<>(); - outputRecord.put(table.getPrimaryKeyField(), insertOutput.getRecords().get(0).getValue(table.getPrimaryKeyField())); + Map outputRecord = ApiImplementation.insert(apiInstanceMetaData, version, tableApiName, context.body()); QJavalinAccessLogger.logEndSuccess(); context.status(HttpStatus.Code.CREATED.getCode()); @@ -1453,20 +886,6 @@ public class QJavalinApiHandler - /******************************************************************************* - ** - *******************************************************************************/ - private static boolean areAnyErrorsBadRequest(List errors) - { - boolean isBadRequest = errors.stream().anyMatch(e -> - e.contains("Missing value in required field") - || e.contains("You do not have permission") - ); - return isBadRequest; - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -1478,90 +897,12 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_INSERT); - String tableName = table.getName(); + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiBulkInsert", logPair("table", tableApiName)); - InsertInput insertInput = new InsertInput(); + List> response = ApiImplementation.bulkInsert(apiInstanceMetaData, version, tableApiName, context.body()); - setupSession(context, insertInput, version, apiInstanceMetaData); - QJavalinAccessLogger.logStart("apiBulkInsert", logPair("table", tableName)); - - insertInput.setTableName(tableName); - - PermissionsHelper.checkTablePermissionThrowing(insertInput, TablePermissionSubType.INSERT); - - ///////////////// - // build input // - ///////////////// - try - { - if(!StringUtils.hasContent(context.body())) - { - throw (new QBadRequestException("Missing required POST body")); - } - - ArrayList recordList = new ArrayList<>(); - insertInput.setRecords(recordList); - - JSONTokener jsonTokener = new JSONTokener(context.body().trim()); - JSONArray jsonArray = new JSONArray(jsonTokener); - - for(int i = 0; i < jsonArray.length(); i++) - { - JSONObject jsonObject = jsonArray.getJSONObject(i); - recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, false)); - } - - if(jsonTokener.more()) - { - throw (new QBadRequestException("Body contained more than a single JSON array.")); - } - - if(recordList.isEmpty()) - { - throw (new QBadRequestException("No records were found in the POST body")); - } - } - catch(QBadRequestException qbre) - { - throw (qbre); - } - catch(Exception e) - { - throw (new QBadRequestException("Body could not be parsed as a JSON array: " + e.getMessage(), e)); - } - - ////////////// - // execute! // - ////////////// - InsertAction insertAction = new InsertAction(); - InsertOutput insertOutput = insertAction.execute(insertInput); - - /////////////////////////////////////// - // process records to build response // - /////////////////////////////////////// - List> response = new ArrayList<>(); - for(QRecord record : insertOutput.getRecords()) - { - LinkedHashMap outputRecord = new LinkedHashMap<>(); - response.add(outputRecord); - - List errors = record.getErrors(); - if(CollectionUtils.nullSafeHasContents(errors)) - { - outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode()); - outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage()); - outputRecord.put("error", "Error inserting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)); - } - else - { - outputRecord.put("statusCode", HttpStatus.Code.CREATED.getCode()); - outputRecord.put("statusText", HttpStatus.Code.CREATED.getMessage()); - outputRecord.put(table.getPrimaryKeyField(), record.getValue(table.getPrimaryKeyField())); - } - } - - QJavalinAccessLogger.logEndSuccess(logPair("recordCount", insertInput.getRecords().size())); + QJavalinAccessLogger.logEndSuccess(logPair("recordCount", response.size())); context.status(HttpStatus.Code.MULTI_STATUS.getCode()); String resultString = JsonUtils.toJson(response); context.result(resultString); @@ -1587,113 +928,12 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_UPDATE); - String tableName = table.getName(); + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiBulkUpdate", logPair("table", tableApiName)); - UpdateInput updateInput = new UpdateInput(); + List> response = ApiImplementation.bulkUpdate(apiInstanceMetaData, version, tableApiName, context.body()); - setupSession(context, updateInput, version, apiInstanceMetaData); - QJavalinAccessLogger.logStart("apiBulkUpdate", logPair("table", tableName)); - - updateInput.setTableName(tableName); - - PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT); - - ///////////////// - // build input // - ///////////////// - try - { - if(!StringUtils.hasContent(context.body())) - { - throw (new QBadRequestException("Missing required PATCH body")); - } - - ArrayList recordList = new ArrayList<>(); - updateInput.setRecords(recordList); - - JSONTokener jsonTokener = new JSONTokener(context.body().trim()); - JSONArray jsonArray = new JSONArray(jsonTokener); - - for(int i = 0; i < jsonArray.length(); i++) - { - JSONObject jsonObject = jsonArray.getJSONObject(i); - recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, true)); - } - - if(jsonTokener.more()) - { - throw (new QBadRequestException("Body contained more than a single JSON array.")); - } - - if(recordList.isEmpty()) - { - throw (new QBadRequestException("No records were found in the PATCH body")); - } - } - catch(QBadRequestException qbre) - { - throw (qbre); - } - catch(Exception e) - { - throw (new QBadRequestException("Body could not be parsed as a JSON array: " + e.getMessage(), e)); - } - - ////////////// - // execute! // - ////////////// - UpdateAction updateAction = new UpdateAction(); - UpdateOutput updateOutput = updateAction.execute(updateInput); - - /////////////////////////////////////// - // process records to build response // - /////////////////////////////////////// - List> response = new ArrayList<>(); - int i = 0; - for(QRecord record : updateOutput.getRecords()) - { - LinkedHashMap outputRecord = new LinkedHashMap<>(); - response.add(outputRecord); - - try - { - QRecord inputRecord = updateInput.getRecords().get(i); - Serializable primaryKey = inputRecord.getValue(table.getPrimaryKeyField()); - outputRecord.put(table.getPrimaryKeyField(), primaryKey); - } - catch(Exception e) - { - ////////// - // omit // - ////////// - } - - List errors = record.getErrors(); - if(CollectionUtils.nullSafeHasContents(errors)) - { - outputRecord.put("error", "Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)); - if(areAnyErrorsNotFound(errors)) - { - outputRecord.put("statusCode", HttpStatus.Code.NOT_FOUND.getCode()); - outputRecord.put("statusText", HttpStatus.Code.NOT_FOUND.getMessage()); - } - else - { - outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode()); - outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage()); - } - } - else - { - outputRecord.put("statusCode", HttpStatus.Code.NO_CONTENT.getCode()); - outputRecord.put("statusText", HttpStatus.Code.NO_CONTENT.getMessage()); - } - - i++; - } - - QJavalinAccessLogger.logEndSuccess(logPair("recordCount", updateInput.getRecords().size())); + QJavalinAccessLogger.logEndSuccess(logPair("recordCount", response.size())); context.status(HttpStatus.Code.MULTI_STATUS.getCode()); String resultString = JsonUtils.toJson(response); context.result(resultString); @@ -1708,16 +948,6 @@ public class QJavalinApiHandler - /******************************************************************************* - ** - *******************************************************************************/ - private static boolean areAnyErrorsNotFound(List errors) - { - return errors.stream().anyMatch(e -> e.startsWith(UpdateAction.NOT_FOUND_ERROR_PREFIX) || e.startsWith(DeleteAction.NOT_FOUND_ERROR_PREFIX)); - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -1729,112 +959,12 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_DELETE); - String tableName = table.getName(); + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiBulkDelete", logPair("table", tableApiName)); - DeleteInput deleteInput = new DeleteInput(); + List> response = ApiImplementation.bulkDelete(apiInstanceMetaData, version, tableApiName, context.body()); - setupSession(context, deleteInput, version, apiInstanceMetaData); - QJavalinAccessLogger.logStart("apiBulkDelete", logPair("table", tableName)); - - deleteInput.setTableName(tableName); - - PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE); - - ///////////////// - // build input // - ///////////////// - try - { - if(!StringUtils.hasContent(context.body())) - { - throw (new QBadRequestException("Missing required DELETE body")); - } - - ArrayList primaryKeyList = new ArrayList<>(); - deleteInput.setPrimaryKeys(primaryKeyList); - - JSONTokener jsonTokener = new JSONTokener(context.body().trim()); - JSONArray jsonArray = new JSONArray(jsonTokener); - - for(int i = 0; i < jsonArray.length(); i++) - { - Object object = jsonArray.get(i); - if(object instanceof JSONArray || object instanceof JSONObject) - { - throw (new QBadRequestException("One or more elements inside the DELETE body JSONArray was not a primitive value")); - } - primaryKeyList.add(String.valueOf(object)); - } - - if(jsonTokener.more()) - { - throw (new QBadRequestException("Body contained more than a single JSON array.")); - } - - if(primaryKeyList.isEmpty()) - { - throw (new QBadRequestException("No primary keys were found in the DELETE body")); - } - } - catch(QBadRequestException qbre) - { - throw (qbre); - } - catch(Exception e) - { - throw (new QBadRequestException("Body could not be parsed as a JSON array: " + e.getMessage(), e)); - } - - ////////////// - // execute! // - ////////////// - DeleteAction deleteAction = new DeleteAction(); - DeleteOutput deleteOutput = deleteAction.execute(deleteInput); - - /////////////////////////////////////// - // process records to build response // - /////////////////////////////////////// - List> response = new ArrayList<>(); - - List recordsWithErrors = deleteOutput.getRecordsWithErrors(); - Map> primaryKeyToErrorsMap = new HashMap<>(); - for(QRecord recordWithError : CollectionUtils.nonNullList(recordsWithErrors)) - { - String primaryKey = recordWithError.getValueString(table.getPrimaryKeyField()); - primaryKeyToErrorsMap.put(primaryKey, recordWithError.getErrors()); - } - - for(Serializable primaryKey : deleteInput.getPrimaryKeys()) - { - LinkedHashMap outputRecord = new LinkedHashMap<>(); - response.add(outputRecord); - outputRecord.put(table.getPrimaryKeyField(), primaryKey); - - String primaryKeyString = ValueUtils.getValueAsString(primaryKey); - List errors = primaryKeyToErrorsMap.get(primaryKeyString); - if(CollectionUtils.nullSafeHasContents(errors)) - { - outputRecord.put("error", "Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)); - if(areAnyErrorsNotFound(errors)) - { - outputRecord.put("statusCode", HttpStatus.Code.NOT_FOUND.getCode()); - outputRecord.put("statusText", HttpStatus.Code.NOT_FOUND.getMessage()); - } - else - { - outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode()); - outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage()); - } - } - else - { - outputRecord.put("statusCode", HttpStatus.Code.NO_CONTENT.getCode()); - outputRecord.put("statusText", HttpStatus.Code.NO_CONTENT.getMessage()); - } - } - - QJavalinAccessLogger.logEndSuccess(logPair("recordCount", deleteInput.getPrimaryKeys().size())); + QJavalinAccessLogger.logEndSuccess(logPair("recordCount", response.size())); context.status(HttpStatus.Code.MULTI_STATUS.getCode()); String resultString = JsonUtils.toJson(response); context.result(resultString); @@ -1861,71 +991,10 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.UPDATE); - String tableName = table.getName(); + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiUpdate", logPair("table", tableApiName)); - UpdateInput updateInput = new UpdateInput(); - - setupSession(context, updateInput, version, apiInstanceMetaData); - QJavalinAccessLogger.logStart("apiUpdate", logPair("table", tableName)); - - updateInput.setTableName(tableName); - - PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT); - - try - { - if(!StringUtils.hasContent(context.body())) - { - throw (new QBadRequestException("Missing required PATCH body")); - } - - JSONTokener jsonTokener = new JSONTokener(context.body().trim()); - JSONObject jsonObject = new JSONObject(jsonTokener); - - QRecord qRecord = QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, false); - qRecord.setValue(table.getPrimaryKeyField(), primaryKey); - updateInput.setRecords(List.of(qRecord)); - - if(jsonTokener.more()) - { - throw (new QBadRequestException("Body contained more than a single JSON object.")); - } - } - catch(QBadRequestException qbre) - { - throw (qbre); - } - catch(Exception e) - { - throw (new QBadRequestException("Body could not be parsed as a JSON object: " + e.getMessage(), e)); - } - - UpdateAction updateAction = new UpdateAction(); - UpdateOutput updateOutput = updateAction.execute(updateInput); - - List errors = updateOutput.getRecords().get(0).getErrors(); - if(CollectionUtils.nullSafeHasContents(errors)) - { - if(areAnyErrorsNotFound(errors)) - { - throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); - } - else - { - boolean isBadRequest = areAnyErrorsBadRequest(errors); - - String message = "Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors); - if(isBadRequest) - { - throw (new QBadRequestException(message)); - } - else - { - throw (new QException(message)); - } - } - } + ApiImplementation.update(apiInstanceMetaData, version, tableApiName, primaryKey, context.body()); QJavalinAccessLogger.logEndSuccess(); context.status(HttpStatus.Code.NO_CONTENT.getCode()); @@ -1952,35 +1021,10 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.DELETE); - String tableName = table.getName(); + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiDelete", logPair("table", tableApiName)); - DeleteInput deleteInput = new DeleteInput(); - - setupSession(context, deleteInput, version, apiInstanceMetaData); - QJavalinAccessLogger.logStart("apiDelete", logPair("table", tableName)); - - deleteInput.setTableName(tableName); - deleteInput.setPrimaryKeys(List.of(primaryKey)); - - PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE); - - /////////////////// - // do the delete // - /////////////////// - DeleteAction deleteAction = new DeleteAction(); - DeleteOutput deleteOutput = deleteAction.execute(deleteInput); - if(CollectionUtils.nullSafeHasContents(deleteOutput.getRecordsWithErrors())) - { - if(areAnyErrorsNotFound(deleteOutput.getRecordsWithErrors().get(0).getErrors())) - { - throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); - } - else - { - throw (new QException("Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(deleteOutput.getRecordsWithErrors().get(0).getErrors()))); - } - } + ApiImplementation.delete(apiInstanceMetaData, version, tableApiName, primaryKey); QJavalinAccessLogger.logEndSuccess(); context.status(HttpStatus.Code.NO_CONTENT.getCode()); @@ -2018,6 +1062,7 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ + @SuppressWarnings("UnnecessaryReturnStatement") public static void handleException(HttpStatus.Code statusCode, Context context, Exception e, APILog apiLog) { QBadRequestException badRequestException = ExceptionUtils.findClassInRootChain(e, QBadRequestException.class); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java index 3086e0d7..8f6cbcc3 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java @@ -26,7 +26,7 @@ import java.io.Serializable; import java.time.Instant; import java.util.HashMap; import java.util.Map; -import com.kingsrook.qqq.api.model.metadata.APILogMetaDataProvider; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataProvider; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException; import com.kingsrook.qqq.backend.core.model.data.QField; @@ -49,7 +49,7 @@ public class APILog extends QRecordEntity @QField(isEditable = false) private Instant timestamp; - @QField(possibleValueSourceName = APILogMetaDataProvider.TABLE_NAME_API_LOG_USER, label = "User") + @QField(possibleValueSourceName = ApiInstanceMetaDataProvider.TABLE_NAME_API_LOG_USER, label = "User") private Integer apiLogUserId; @QField(possibleValueSourceName = "apiMethod") diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataProvider.java similarity index 94% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java rename to qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataProvider.java index cf0d4fd7..0d7779bc 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataProvider.java @@ -50,7 +50,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; /******************************************************************************* ** *******************************************************************************/ -public class APILogMetaDataProvider +public class ApiInstanceMetaDataProvider { public static final String TABLE_NAME_API_LOG = "apiLog"; public static final String TABLE_NAME_API_LOG_USER = "apiLogUser"; @@ -105,21 +105,32 @@ public class APILogMetaDataProvider new QPossibleValue<>(500, "500 (Internal Server Error)") ))); + //////////////////////////////////////////////////////////////////////////// + // loop over api names and versions, building out possible values sources // + //////////////////////////////////////////////////////////////////////////// + List> apiNamePossibleValues = new ArrayList<>(); List> apiVersionPossibleValues = new ArrayList<>(); - //////////////////////////////////////////////////////////////////////////////////////////////////// - // todo... this, this whole thing, should probably have "which api" as another field too... ugh. // - //////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////// + // todo... apiName should maybe be a field on apiLog table, eh? // + ////////////////////////////////////////////////////////////////// TreeSet allVersions = new TreeSet<>(); ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(instance); for(Map.Entry entry : apiInstanceMetaDataContainer.getApis().entrySet()) { + apiNamePossibleValues.add(new QPossibleValue<>(entry.getKey())); + ApiInstanceMetaData apiInstanceMetaData = entry.getValue(); allVersions.addAll(apiInstanceMetaData.getPastVersions()); allVersions.addAll(apiInstanceMetaData.getSupportedVersions()); allVersions.addAll(apiInstanceMetaData.getFutureVersions()); } + instance.addPossibleValueSource(new QPossibleValueSource() + .withName("apiName") + .withType(QPossibleValueSourceType.ENUM) + .withEnumValues(apiNamePossibleValues)); + for(APIVersion version : allVersions) { apiVersionPossibleValues.add(new QPossibleValue<>(version.toString())); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java new file mode 100644 index 00000000..fefd6c56 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java @@ -0,0 +1,249 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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.api.utils; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.api.actions.ApiImplementation; +import com.kingsrook.qqq.api.model.APIVersion; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; + + +/******************************************************************************* + ** Object injected into script context, for interfacing with a QQQ API. + *******************************************************************************/ +public class ApiScriptUtils implements Serializable +{ + private String apiName; + private String apiVersion; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ApiScriptUtils() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ApiScriptUtils(String apiName, String apiVersion) + { + setApiName(apiName); + this.apiVersion = apiVersion; + } + + + + /******************************************************************************* + ** Setter for apiName + ** + *******************************************************************************/ + public void setApiName(String apiName) + { + ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(QContext.getQInstance()); + if(apiInstanceMetaDataContainer.getApis().containsKey(apiName)) + { + this.apiName = apiName; + } + else + { + throw (new IllegalArgumentException("[" + apiName + "] is not a valid API name. Valid values are: " + apiInstanceMetaDataContainer.getApis().keySet())); + } + } + + + + /******************************************************************************* + ** Setter for apiVersion + ** + *******************************************************************************/ + public void setApiVersion(String apiVersion) + { + if(apiName == null) + { + throw (new IllegalArgumentException("You must set apiName before setting apiVersion.")); + } + + ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(QContext.getQInstance()); + ApiInstanceMetaData apiInstanceMetaData = apiInstanceMetaDataContainer.getApis().get(apiName); + if(apiInstanceMetaData.getSupportedVersions().contains(new APIVersion(apiVersion))) + { + this.apiVersion = apiVersion; + } + else + { + throw (new IllegalArgumentException("[" + apiVersion + "] is not a supported version for this API. Supported versions are: " + apiInstanceMetaData.getSupportedVersions())); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void validateApiNameAndVersion(String description) + { + if(apiName == null || apiVersion == null) + { + throw (new IllegalStateException("Both apiName and apiVersion must be set before calling this method (" + description + ").")); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Map get(String tableApiName, Object primaryKey) throws QException + { + validateApiNameAndVersion("get(" + tableApiName + "," + primaryKey + ")"); + return (ApiImplementation.get(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(primaryKey))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Map query(String urlPart) throws QException + { + validateApiNameAndVersion("query(" + urlPart + ")"); + String[] urlParts = urlPart.split("\\?", 2); + Map> paramMap = parseQueryString(urlParts.length > 1 ? urlParts[1] : null); + return (ApiImplementation.query(getApiInstanceMetaData(), apiVersion, urlParts[0], paramMap)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Map insert(String tableApiName, Object body) throws QException + { + validateApiNameAndVersion("insert(" + tableApiName + ")"); + return (ApiImplementation.insert(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public List> bulkInsert(String tableApiName, Object body) throws QException + { + validateApiNameAndVersion("bulkInsert(" + tableApiName + ")"); + return (ApiImplementation.bulkInsert(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void update(String tableApiName, Object primaryKey, Object body) throws QException + { + validateApiNameAndVersion("update(" + tableApiName + "," + primaryKey + ")"); + ApiImplementation.update(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(primaryKey), String.valueOf(body)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public List> bulkUpdate(String tableApiName, Object body) throws QException + { + validateApiNameAndVersion("bulkUpdate(" + tableApiName + ")"); + return (ApiImplementation.bulkUpdate(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void delete(String tableApiName, Object primaryKey) throws QException + { + validateApiNameAndVersion("delete(" + tableApiName + "," + primaryKey + ")"); + ApiImplementation.delete(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(primaryKey)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public List> bulkDelete(String tableApiName, Object body) throws QException + { + validateApiNameAndVersion("bulkDelete(" + tableApiName + ")"); + return (ApiImplementation.bulkDelete(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private ApiInstanceMetaData getApiInstanceMetaData() + { + ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(QContext.getQInstance()); + ApiInstanceMetaData apiInstanceMetaData = apiInstanceMetaDataContainer.getApiInstanceMetaData(apiName); + return apiInstanceMetaData; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Map> parseQueryString(String queryString) + { + Map> paramMap = new LinkedHashMap<>(); + if(queryString != null) + { + for(String nameValuePair : queryString.split("&")) + { + String[] nameValue = nameValuePair.split("=", 2); + if(nameValue.length == 2) + { + paramMap.computeIfAbsent(nameValue[0], (k) -> new ArrayList<>()); + paramMap.get(nameValue[0]).add(nameValue[1]); + } + } + } + return paramMap; + } +} From 37fa78417fbad7a887836668162e38e7bcc942eb Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 27 Apr 2023 20:19:31 -0500 Subject: [PATCH 04/15] Initial checkin --- .../qqq/api/utils/ApiScriptUtilsTest.java | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java new file mode 100644 index 00000000..cd66985e --- /dev/null +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java @@ -0,0 +1,276 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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.api.utils; + + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.api.BaseTest; +import com.kingsrook.qqq.api.TestUtils; +import com.kingsrook.qqq.api.javalin.QBadRequestException; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; +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.ValueUtils; +import org.junit.jupiter.api.Test; +import static com.kingsrook.qqq.api.TestUtils.insertSimpsons; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/******************************************************************************* + ** Unit test for com.kingsrook.qqq.api.utils.ApiScriptUtils + *******************************************************************************/ +class ApiScriptUtilsTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSetApiNameAndApiVersion() + { + ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils(); + + assertThatThrownBy(() -> apiScriptUtils.setApiName("not an api")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("not a valid API name"); + + assertThatThrownBy(() -> apiScriptUtils.setApiVersion("not a version")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("not a supported version"); + + assertThatThrownBy(() -> new ApiScriptUtils("not an api", TestUtils.CURRENT_API_VERSION)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("not a valid API name"); + + assertThatThrownBy(() -> new ApiScriptUtils(TestUtils.ALTERNATIVE_API_NAME, "not a version")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("not a supported version"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGet() throws QException + { + ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils(); + + assertThatThrownBy(() -> apiScriptUtils.get(TestUtils.TABLE_NAME_PERSON, 1)) + .isInstanceOf(QNotFoundException.class); + + insertSimpsons(); + + Map result = apiScriptUtils.get(TestUtils.TABLE_NAME_PERSON, 1); + assertEquals("Homer", result.get("firstName")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQuery() throws QException + { + ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils(); + + assertThatThrownBy(() -> apiScriptUtils.query(TestUtils.TABLE_NAME_PERSON + "?foo=bar")) + .isInstanceOf(QBadRequestException.class) + .hasMessageContaining("Unrecognized filter criteria field: foo"); + + insertSimpsons(); + + Map result = apiScriptUtils.query(TestUtils.TABLE_NAME_PERSON + "?id=2"); + assertEquals(1, result.get("count")); + assertEquals(1, ((List) result.get("records")).size()); + assertEquals("Marge", ((Map) ((List) result.get("records")).get(0)).get("firstName")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInsert() throws QException + { + ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils(); + Map result = apiScriptUtils.insert(TestUtils.TABLE_NAME_PERSON, """ + { "firstName": "Mr.", "lastName": "Burns" } + """); + assertEquals(1, result.get("id")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBulkInsert() throws QException + { + ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils(); + List> result = apiScriptUtils.bulkInsert(TestUtils.TABLE_NAME_PERSON, """ + [ + { "firstName": "Mr.", "lastName": "Burns" }, + { "firstName": "Waylon", "lastName": "Smithers" } + ] + """); + assertEquals(2, result.size()); + assertEquals(1, result.get(0).get("id")); + assertEquals(2, result.get(1).get("id")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testUpdate() throws QException + { + ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils(); + String updateJSON = """ + { "firstName": "Homer J." } + """; + + assertThatThrownBy(() -> apiScriptUtils.update(TestUtils.TABLE_NAME_PERSON, 1, updateJSON)) + .isInstanceOf(QNotFoundException.class); + + insertSimpsons(); + + apiScriptUtils.update(TestUtils.TABLE_NAME_PERSON, 1, updateJSON); + + GetInput getInput = new GetInput(); + getInput.setTableName(TestUtils.TABLE_NAME_PERSON); + getInput.setPrimaryKey(1); + GetOutput getOutput = new GetAction().execute(getInput); + assertEquals("Homer J.", getOutput.getRecord().getValueString("firstName")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBulkUpdate() throws QException + { + ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils(); + + insertSimpsons(); + + String updateJSON = """ + [ + { "id": 1, "firstName": "Homer J." }, + { "id": 6, "firstName": "C.M." } + ] + """; + + List> result = apiScriptUtils.bulkUpdate(TestUtils.TABLE_NAME_PERSON, updateJSON); + + assertEquals(2, result.size()); + assertEquals(1, result.get(0).get("id")); + assertEquals(6, result.get(1).get("id")); + assertEquals(404, result.get(1).get("statusCode")); + assertNotNull(result.get(1).get("error")); + + GetInput getInput = new GetInput(); + getInput.setTableName(TestUtils.TABLE_NAME_PERSON); + getInput.setPrimaryKey(1); + GetOutput getOutput = new GetAction().execute(getInput); + assertEquals("Homer J.", getOutput.getRecord().getValueString("firstName")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDelete() throws QException + { + ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils(); + assertThatThrownBy(() -> apiScriptUtils.delete(TestUtils.TABLE_NAME_PERSON, 1)) + .isInstanceOf(QNotFoundException.class); + + insertSimpsons(); + + apiScriptUtils.delete(TestUtils.TABLE_NAME_PERSON, 1); + + GetInput getInput = new GetInput(); + getInput.setTableName(TestUtils.TABLE_NAME_PERSON); + getInput.setPrimaryKey(1); + GetOutput getOutput = new GetAction().execute(getInput); + assertNull(getOutput.getRecord()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBulkDelete() throws QException + { + ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils(); + + insertSimpsons(); + + List> result = apiScriptUtils.bulkDelete(TestUtils.TABLE_NAME_PERSON, "[1,6]"); + + assertEquals(2, result.size()); + assertEquals(1, ValueUtils.getValueAsInteger(result.get(0).get("id"))); + assertEquals(6, ValueUtils.getValueAsInteger(result.get(1).get("id"))); + assertEquals(404, result.get(1).get("statusCode")); + assertNotNull(result.get(1).get("error")); + + GetInput getInput = new GetInput(); + getInput.setTableName(TestUtils.TABLE_NAME_PERSON); + getInput.setPrimaryKey(1); + GetOutput getOutput = new GetAction().execute(getInput); + assertNull(getOutput.getRecord()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static ApiScriptUtils newDefaultApiScriptUtils() + { + ApiScriptUtils apiScriptUtils = new ApiScriptUtils(); + apiScriptUtils.setApiName(TestUtils.API_NAME); + apiScriptUtils.setApiVersion(TestUtils.CURRENT_API_VERSION); + return apiScriptUtils; + } + +} \ No newline at end of file From 6b6dd546fd0143e1f22304a9089e3f71da38d99d Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 27 Apr 2023 20:19:44 -0500 Subject: [PATCH 05/15] Initial checkin --- .../main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java index fefd6c56..5f01dd26 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java @@ -62,7 +62,7 @@ public class ApiScriptUtils implements Serializable public ApiScriptUtils(String apiName, String apiVersion) { setApiName(apiName); - this.apiVersion = apiVersion; + setApiVersion(apiVersion); } From 1314ee98b800ca80efa3ffd5bacc720a40c002db Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 27 Apr 2023 20:19:59 -0500 Subject: [PATCH 06/15] add more store-jacoco calls --- .circleci/config.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3e2ff6f9..da983b05 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -51,10 +51,18 @@ commands: module: qqq-backend-module-filesystem - store_jacoco_site: module: qqq-backend-module-rdbms + - store_jacoco_site: + module: qqq-backend-module-api + - store_jacoco_site: + module: qqq-middleware-api - store_jacoco_site: module: qqq-middleware-javalin - store_jacoco_site: module: qqq-middleware-picocli + - store_jacoco_site: + module: qqq-middleware-slack + - store_jacoco_site: + module: qqq-language-support-javascript - store_jacoco_site: module: qqq-sample-project - run: From a40f9afd38694a6a7d6ae9a147b77e2bdfc279ee Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 27 Apr 2023 20:20:17 -0500 Subject: [PATCH 07/15] Move insert person methods to TestUtils --- .../java/com/kingsrook/qqq/api/TestUtils.java | 41 +++++++++++++++++++ .../api/javalin/QJavalinApiHandlerTest.java | 39 +----------------- 2 files changed, 43 insertions(+), 37 deletions(-) diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java index f775fa94..4ddeb37f 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.api; +import java.time.LocalDate; import java.util.List; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; @@ -30,7 +31,11 @@ import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; +import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -324,4 +329,40 @@ public class TestUtils .withOrderBy(new QFilterOrderBy("key")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void insertPersonRecord(Integer id, String firstName, String lastName) throws QException + { + insertPersonRecord(id, firstName, lastName, null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void insertPersonRecord(Integer id, String firstName, String lastName, LocalDate birthDate) throws QException + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_PERSON); + insertInput.setRecords(List.of(new QRecord().withValue("id", id).withValue("firstName", firstName).withValue("lastName", lastName).withValue("birthDate", birthDate))); + new InsertAction().execute(insertInput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void insertSimpsons() throws QException + { + insertPersonRecord(1, "Homer", "Simpson"); + insertPersonRecord(2, "Marge", "Simpson"); + insertPersonRecord(3, "Bart", "Simpson"); + insertPersonRecord(4, "Lisa", "Simpson"); + insertPersonRecord(5, "Maggie", "Simpson"); + } } diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java index ca5e3d64..58908fa6 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java @@ -61,6 +61,8 @@ import org.json.JSONObject; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static com.kingsrook.qqq.api.TestUtils.insertPersonRecord; +import static com.kingsrook.qqq.api.TestUtils.insertSimpsons; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -1240,43 +1242,6 @@ class QJavalinApiHandlerTest extends BaseTest - /******************************************************************************* - ** - *******************************************************************************/ - private static void insertPersonRecord(Integer id, String firstName, String lastName) throws QException - { - insertPersonRecord(id, firstName, lastName, null); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private static void insertPersonRecord(Integer id, String firstName, String lastName, LocalDate birthDate) throws QException - { - InsertInput insertInput = new InsertInput(); - insertInput.setTableName(TestUtils.TABLE_NAME_PERSON); - insertInput.setRecords(List.of(new QRecord().withValue("id", id).withValue("firstName", firstName).withValue("lastName", lastName).withValue("birthDate", birthDate))); - new InsertAction().execute(insertInput); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private static void insertSimpsons() throws QException - { - insertPersonRecord(1, "Homer", "Simpson"); - insertPersonRecord(2, "Marge", "Simpson"); - insertPersonRecord(3, "Bart", "Simpson"); - insertPersonRecord(4, "Lisa", "Simpson"); - insertPersonRecord(5, "Maggie", "Simpson"); - } - - - /******************************************************************************* ** *******************************************************************************/ From 4135607a4cd6b2ed214848c708c2e13edd6c1b63 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 28 Apr 2023 12:07:45 -0500 Subject: [PATCH 08/15] Base class for run-script inputs --- .../scripts/AbstractRunScriptInput.java | 198 +++++++++++++++++ .../scripts/RunAdHocRecordScriptInput.java | 200 +----------------- .../scripts/RunAssociatedScriptInput.java | 176 +-------------- 3 files changed, 202 insertions(+), 372 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/AbstractRunScriptInput.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/AbstractRunScriptInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/AbstractRunScriptInput.java new file mode 100644 index 00000000..f6d76a1f --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/AbstractRunScriptInput.java @@ -0,0 +1,198 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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.scripts; + + +import java.io.Serializable; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; +import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; + + +/******************************************************************************* + ** Base class for input wrappers that end up running scripts (ExecuteCodeAction) + *******************************************************************************/ +public class AbstractRunScriptInput extends AbstractTableActionInput +{ + private C codeReference; + private Map inputValues; + private QCodeExecutionLoggerInterface logger; + private Serializable outputObject; + private Serializable scriptUtils; + + + + /******************************************************************************* + ** Getter for codeReference + *******************************************************************************/ + public C getCodeReference() + { + return (this.codeReference); + } + + + + /******************************************************************************* + ** Setter for codeReference + *******************************************************************************/ + public void setCodeReference(C codeReference) + { + this.codeReference = codeReference; + } + + + + /******************************************************************************* + ** Fluent setter for codeReference + *******************************************************************************/ + public AbstractRunScriptInput withCodeReference(C codeReference) + { + this.codeReference = codeReference; + return (this); + } + + + + /******************************************************************************* + ** Getter for inputValues + *******************************************************************************/ + public Map getInputValues() + { + return (this.inputValues); + } + + + + /******************************************************************************* + ** Setter for inputValues + *******************************************************************************/ + public void setInputValues(Map inputValues) + { + this.inputValues = inputValues; + } + + + + /******************************************************************************* + ** Fluent setter for inputValues + *******************************************************************************/ + public AbstractRunScriptInput withInputValues(Map inputValues) + { + this.inputValues = inputValues; + return (this); + } + + + + /******************************************************************************* + ** Getter for logger + *******************************************************************************/ + public QCodeExecutionLoggerInterface getLogger() + { + return (this.logger); + } + + + + /******************************************************************************* + ** Setter for logger + *******************************************************************************/ + public void setLogger(QCodeExecutionLoggerInterface logger) + { + this.logger = logger; + } + + + + /******************************************************************************* + ** Fluent setter for logger + *******************************************************************************/ + public AbstractRunScriptInput withLogger(QCodeExecutionLoggerInterface logger) + { + this.logger = logger; + return (this); + } + + + + /******************************************************************************* + ** Getter for outputObject + *******************************************************************************/ + public Serializable getOutputObject() + { + return (this.outputObject); + } + + + + /******************************************************************************* + ** Setter for outputObject + *******************************************************************************/ + public void setOutputObject(Serializable outputObject) + { + this.outputObject = outputObject; + } + + + + /******************************************************************************* + ** Fluent setter for outputObject + *******************************************************************************/ + public AbstractRunScriptInput withOutputObject(Serializable outputObject) + { + this.outputObject = outputObject; + return (this); + } + + + + /******************************************************************************* + ** Getter for scriptUtils + *******************************************************************************/ + public Serializable getScriptUtils() + { + return (this.scriptUtils); + } + + + + /******************************************************************************* + ** Setter for scriptUtils + *******************************************************************************/ + public void setScriptUtils(Serializable scriptUtils) + { + this.scriptUtils = scriptUtils; + } + + + + /******************************************************************************* + ** Fluent setter for scriptUtils + *******************************************************************************/ + public AbstractRunScriptInput withScriptUtils(Serializable scriptUtils) + { + this.scriptUtils = scriptUtils; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAdHocRecordScriptInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAdHocRecordScriptInput.java index cf097f4f..f8191b57 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAdHocRecordScriptInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAdHocRecordScriptInput.java @@ -24,9 +24,6 @@ package com.kingsrook.qqq.backend.core.model.actions.scripts; import java.io.Serializable; import java.util.List; -import java.util.Map; -import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; -import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReference; @@ -34,18 +31,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReferen /******************************************************************************* ** *******************************************************************************/ -public class RunAdHocRecordScriptInput extends AbstractTableActionInput +public class RunAdHocRecordScriptInput extends AbstractRunScriptInput { - private AdHocScriptCodeReference codeReference; - private Map inputValues; - private List recordPrimaryKeyList; // can either supply recordList, or recordPrimaryKeyList - private List recordList; - private String tableName; - private QCodeExecutionLoggerInterface logger; - - private Serializable outputObject; - - private Serializable scriptUtils; + private List recordPrimaryKeyList; // can either supply recordList, or recordPrimaryKeyList + private List recordList; @@ -58,189 +47,6 @@ public class RunAdHocRecordScriptInput extends AbstractTableActionInput - /******************************************************************************* - ** Getter for inputValues - ** - *******************************************************************************/ - public Map getInputValues() - { - return inputValues; - } - - - - /******************************************************************************* - ** Setter for inputValues - ** - *******************************************************************************/ - public void setInputValues(Map inputValues) - { - this.inputValues = inputValues; - } - - - - /******************************************************************************* - ** Fluent setter for inputValues - ** - *******************************************************************************/ - public RunAdHocRecordScriptInput withInputValues(Map inputValues) - { - this.inputValues = inputValues; - return (this); - } - - - - /******************************************************************************* - ** Getter for outputObject - ** - *******************************************************************************/ - public Serializable getOutputObject() - { - return outputObject; - } - - - - /******************************************************************************* - ** Setter for outputObject - ** - *******************************************************************************/ - public void setOutputObject(Serializable outputObject) - { - this.outputObject = outputObject; - } - - - - /******************************************************************************* - ** Fluent setter for outputObject - ** - *******************************************************************************/ - public RunAdHocRecordScriptInput withOutputObject(Serializable outputObject) - { - this.outputObject = outputObject; - return (this); - } - - - - /******************************************************************************* - ** Getter for logger - *******************************************************************************/ - public QCodeExecutionLoggerInterface getLogger() - { - return (this.logger); - } - - - - /******************************************************************************* - ** Setter for logger - *******************************************************************************/ - public void setLogger(QCodeExecutionLoggerInterface logger) - { - this.logger = logger; - } - - - - /******************************************************************************* - ** Fluent setter for logger - *******************************************************************************/ - public RunAdHocRecordScriptInput withLogger(QCodeExecutionLoggerInterface logger) - { - this.logger = logger; - return (this); - } - - - - /******************************************************************************* - ** Getter for scriptUtils - ** - *******************************************************************************/ - public Serializable getScriptUtils() - { - return scriptUtils; - } - - - - /******************************************************************************* - ** Setter for scriptUtils - ** - *******************************************************************************/ - public void setScriptUtils(Serializable scriptUtils) - { - this.scriptUtils = scriptUtils; - } - - - - /******************************************************************************* - ** Getter for codeReference - *******************************************************************************/ - public AdHocScriptCodeReference getCodeReference() - { - return (this.codeReference); - } - - - - /******************************************************************************* - ** Setter for codeReference - *******************************************************************************/ - public void setCodeReference(AdHocScriptCodeReference codeReference) - { - this.codeReference = codeReference; - } - - - - /******************************************************************************* - ** Fluent setter for codeReference - *******************************************************************************/ - public RunAdHocRecordScriptInput withCodeReference(AdHocScriptCodeReference codeReference) - { - this.codeReference = codeReference; - return (this); - } - - - - /******************************************************************************* - ** Getter for tableName - *******************************************************************************/ - public String getTableName() - { - return (this.tableName); - } - - - - /******************************************************************************* - ** Setter for tableName - *******************************************************************************/ - public void setTableName(String tableName) - { - this.tableName = tableName; - } - - - - /******************************************************************************* - ** Fluent setter for tableName - *******************************************************************************/ - public RunAdHocRecordScriptInput withTableName(String tableName) - { - this.tableName = tableName; - return (this); - } - - - /******************************************************************************* ** Getter for recordList *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptInput.java index 4da5c558..e1a51b21 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptInput.java @@ -22,187 +22,13 @@ package com.kingsrook.qqq.backend.core.model.actions.scripts; -import java.io.Serializable; -import java.util.Map; -import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; -import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; import com.kingsrook.qqq.backend.core.model.metadata.code.AssociatedScriptCodeReference; /******************************************************************************* ** *******************************************************************************/ -public class RunAssociatedScriptInput extends AbstractTableActionInput +public class RunAssociatedScriptInput extends AbstractRunScriptInput { - private AssociatedScriptCodeReference codeReference; - private Map inputValues; - private QCodeExecutionLoggerInterface logger; - private Serializable outputObject; - - private Serializable scriptUtils; - - - - /******************************************************************************* - ** - *******************************************************************************/ - public RunAssociatedScriptInput() - { - } - - - - /******************************************************************************* - ** Getter for codeReference - ** - *******************************************************************************/ - public AssociatedScriptCodeReference getCodeReference() - { - return codeReference; - } - - - - /******************************************************************************* - ** Setter for codeReference - ** - *******************************************************************************/ - public void setCodeReference(AssociatedScriptCodeReference codeReference) - { - this.codeReference = codeReference; - } - - - - /******************************************************************************* - ** Fluent setter for codeReference - ** - *******************************************************************************/ - public RunAssociatedScriptInput withCodeReference(AssociatedScriptCodeReference codeReference) - { - this.codeReference = codeReference; - return (this); - } - - - - /******************************************************************************* - ** Getter for inputValues - ** - *******************************************************************************/ - public Map getInputValues() - { - return inputValues; - } - - - - /******************************************************************************* - ** Setter for inputValues - ** - *******************************************************************************/ - public void setInputValues(Map inputValues) - { - this.inputValues = inputValues; - } - - - - /******************************************************************************* - ** Fluent setter for inputValues - ** - *******************************************************************************/ - public RunAssociatedScriptInput withInputValues(Map inputValues) - { - this.inputValues = inputValues; - return (this); - } - - - - /******************************************************************************* - ** Getter for outputObject - ** - *******************************************************************************/ - public Serializable getOutputObject() - { - return outputObject; - } - - - - /******************************************************************************* - ** Setter for outputObject - ** - *******************************************************************************/ - public void setOutputObject(Serializable outputObject) - { - this.outputObject = outputObject; - } - - - - /******************************************************************************* - ** Fluent setter for outputObject - ** - *******************************************************************************/ - public RunAssociatedScriptInput withOutputObject(Serializable outputObject) - { - this.outputObject = outputObject; - return (this); - } - - - - /******************************************************************************* - ** Getter for logger - *******************************************************************************/ - public QCodeExecutionLoggerInterface getLogger() - { - return (this.logger); - } - - - - /******************************************************************************* - ** Setter for logger - *******************************************************************************/ - public void setLogger(QCodeExecutionLoggerInterface logger) - { - this.logger = logger; - } - - - - /******************************************************************************* - ** Fluent setter for logger - *******************************************************************************/ - public RunAssociatedScriptInput withLogger(QCodeExecutionLoggerInterface logger) - { - this.logger = logger; - return (this); - } - - - - /******************************************************************************* - ** Getter for scriptUtils - ** - *******************************************************************************/ - public Serializable getScriptUtils() - { - return scriptUtils; - } - - - - /******************************************************************************* - ** Setter for scriptUtils - ** - *******************************************************************************/ - public void setScriptUtils(Serializable scriptUtils) - { - this.scriptUtils = scriptUtils; - } } From 6f99111c52af409f14ae11bc5a048c9fe8e84d7a Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 28 Apr 2023 12:11:56 -0500 Subject: [PATCH 09/15] Move api name & version into ScriptRevision; make the records that go into record-scripts be api versions of records. --- .../PollingAutomationPerTableRunner.java | 3 + .../actions/scripts/ExecuteCodeAction.java | 102 +++++++++++++++++- .../scripts/RunAdHocRecordScriptAction.java | 64 +++-------- .../scripts/RunAssociatedScriptAction.java | 39 +------ .../core/actions/scripts/ScriptApi.java | 5 + .../automation/TableAutomationAction.java | 37 +++++++ .../core/model/scripts/ScriptRevision.java | 73 ++++++++++++- .../scripts/ScriptsMetaDataProvider.java | 1 + .../ExtractViaQueryStep.java | 13 +++ .../scripts/RunRecordScriptExtractStep.java | 13 +++ .../StoreScriptRevisionProcessStep.java | 2 + .../scripts/TestScriptProcessStep.java | 1 + .../metadata/ApiInstanceMetaDataProvider.java | 2 +- .../qqq/api/utils/ApiScriptUtils.java | 22 +++- .../qqq/api/utils/ApiScriptUtilsTest.java | 5 +- 15 files changed, 283 insertions(+), 99 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java index 89eab8f2..0322a823 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java @@ -278,6 +278,7 @@ public class PollingAutomationPerTableRunner implements Runnable .withPriority(record.getValueInteger("priority")) .withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class)) .withValues(MapBuilder.of("scriptId", record.getValue("scriptId"))) + .withIncludeRecordAssociations(true) ); } } @@ -392,6 +393,8 @@ public class PollingAutomationPerTableRunner implements Runnable queryInput.setFilter(filter); + queryInput.setIncludeAssociations(action.getIncludeRecordAssociations()); + return (new QueryAction().execute(queryInput).getRecords()); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java index 44db3df9..b04737df 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java @@ -25,13 +25,21 @@ package com.kingsrook.qqq.backend.core.actions.scripts; import java.io.Serializable; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import com.kingsrook.qqq.backend.core.actions.scripts.logging.Log4jCodeExecutionLogger; import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; +import com.kingsrook.qqq.backend.core.actions.scripts.logging.ScriptExecutionLoggerInterface; +import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger; import com.kingsrook.qqq.backend.core.exceptions.QCodeException; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.scripts.AbstractRunScriptInput; import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput; import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; +import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision; +import com.kingsrook.qqq.backend.core.utils.StringUtils; /******************************************************************************* @@ -49,6 +57,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; *******************************************************************************/ public class ExecuteCodeAction { + private static final QLogger LOG = QLogger.getLogger(ExecuteCodeAction.class); + + /******************************************************************************* ** @@ -68,10 +79,10 @@ public class ExecuteCodeAction try { String languageExecutor = switch(codeReference.getCodeType()) - { - case JAVA -> "com.kingsrook.qqq.backend.core.actions.scripts.QJavaExecutor"; - case JAVA_SCRIPT -> "com.kingsrook.qqq.languages.javascript.QJavaScriptExecutor"; - }; + { + case JAVA -> "com.kingsrook.qqq.backend.core.actions.scripts.QJavaExecutor"; + case JAVA_SCRIPT -> "com.kingsrook.qqq.languages.javascript.QJavaScriptExecutor"; + }; @SuppressWarnings("unchecked") Class executorClass = (Class) Class.forName(languageExecutor); @@ -108,6 +119,89 @@ public class ExecuteCodeAction + /******************************************************************************* + ** + *******************************************************************************/ + public static ExecuteCodeInput setupExecuteCodeInput(AbstractRunScriptInput input, ScriptRevision scriptRevision) + { + ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(); + executeCodeInput.setInput(new HashMap<>(Objects.requireNonNullElseGet(input.getInputValues(), HashMap::new))); + executeCodeInput.setContext(new HashMap<>()); + if(input.getOutputObject() != null) + { + executeCodeInput.getContext().put("output", input.getOutputObject()); + } + + if(input.getScriptUtils() != null) + { + executeCodeInput.getContext().put("scriptUtils", input.getScriptUtils()); + } + + executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!! + + ExecuteCodeAction.addApiUtilityToContext(executeCodeInput.getContext(), scriptRevision); + ExecuteCodeAction.setExecutionLoggerInExecuteCodeInput(input, scriptRevision, executeCodeInput); + + return (executeCodeInput); + } + + + + /******************************************************************************* + ** Try to (dynamically) load the ApiScriptUtils object from the api middleware + ** module -- in case the runtime doesn't have that module deployed (e.g, not in + ** the project pom). + *******************************************************************************/ + private static void addApiUtilityToContext(Map context, ScriptRevision scriptRevision) + { + if(!StringUtils.hasContent(scriptRevision.getApiName()) || !StringUtils.hasContent(scriptRevision.getApiVersion())) + { + return; + } + + try + { + Class apiScriptUtilsClass = Class.forName("com.kingsrook.qqq.api.utils.ApiScriptUtils"); + Object apiScriptUtilsObject = apiScriptUtilsClass.getConstructor(String.class, String.class).newInstance(scriptRevision.getApiName(), scriptRevision.getApiVersion()); + context.put("api", (Serializable) apiScriptUtilsObject); + } + catch(ClassNotFoundException e) + { + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // this is the only exception we're kinda expecting here - so catch for it specifically, and just log.trace - others, warn // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + LOG.trace("Couldn't load ApiScriptUtils class - qqq-middleware-api not on the classpath?"); + } + catch(Exception e) + { + LOG.warn("Error adding api utility to script context", e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void setExecutionLoggerInExecuteCodeInput(AbstractRunScriptInput input, ScriptRevision scriptRevision, ExecuteCodeInput executeCodeInput) + { + ///////////////////////////////////////////////////////////////////////////////////////////////// + // let caller supply a logger, or by default use StoreScriptLogAndScriptLogLineExecutionLogger // + ///////////////////////////////////////////////////////////////////////////////////////////////// + QCodeExecutionLoggerInterface executionLogger = Objects.requireNonNullElseGet(input.getLogger(), () -> new StoreScriptLogAndScriptLogLineExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId())); + executeCodeInput.setExecutionLogger(executionLogger); + if(executionLogger instanceof ScriptExecutionLoggerInterface scriptExecutionLoggerInterface) + { + //////////////////////////////////////////////////////////////////////////////////////////////////// + // if logger is aware of scripts (as opposed to a generic CodeExecution logger), give it the ids. // + //////////////////////////////////////////////////////////////////////////////////////////////////// + scriptExecutionLoggerInterface.setScriptId(scriptRevision.getScriptId()); + scriptExecutionLoggerInterface.setScriptRevisionId(scriptRevision.getId()); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java index 8313d8b0..f440e16f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java @@ -23,15 +23,13 @@ package com.kingsrook.qqq.backend.core.actions.scripts; import java.io.Serializable; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import com.kingsrook.qqq.backend.core.actions.ActionHelper; -import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; -import com.kingsrook.qqq.backend.core.actions.scripts.logging.ScriptExecutionLoggerInterface; -import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger; import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.context.QContext; @@ -50,13 +48,10 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReference; -import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; -import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.scripts.Script; import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; -import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* @@ -98,6 +93,7 @@ public class RunAdHocRecordScriptAction QueryInput queryInput = new QueryInput(); queryInput.setTableName(input.getTableName()); queryInput.setFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, input.getRecordPrimaryKeyList()))); + queryInput.setIncludeAssociations(true); QueryOutput queryOutput = new QueryAction().execute(queryInput); input.setRecordList(queryOutput.getRecords()); } @@ -114,43 +110,14 @@ public class RunAdHocRecordScriptAction ///////////// // run it! // ///////////// - ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(); - executeCodeInput.setInput(new HashMap<>(Objects.requireNonNullElseGet(input.getInputValues(), HashMap::new))); - executeCodeInput.getInput().put("records", new ArrayList<>(input.getRecordList())); - executeCodeInput.setContext(new HashMap<>()); - if(input.getOutputObject() != null) - { - executeCodeInput.getContext().put("output", input.getOutputObject()); - } - - if(input.getScriptUtils() != null) - { - executeCodeInput.getContext().put("scriptUtils", input.getScriptUtils()); - } - - addApiUtilityToContext(executeCodeInput.getContext(), scriptRevision); - - executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!! - - ///////////////////////////////////////////////////////////////////////////////////////////////// - // let caller supply a logger, or by default use StoreScriptLogAndScriptLogLineExecutionLogger // - ///////////////////////////////////////////////////////////////////////////////////////////////// - QCodeExecutionLoggerInterface executionLogger = Objects.requireNonNullElseGet(input.getLogger(), () -> new StoreScriptLogAndScriptLogLineExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId())); - executeCodeInput.setExecutionLogger(executionLogger); - if(executionLogger instanceof ScriptExecutionLoggerInterface scriptExecutionLoggerInterface) - { - //////////////////////////////////////////////////////////////////////////////////////////////////// - // if logger is aware of scripts (as opposed to a generic CodeExecution logger), give it the ids. // - //////////////////////////////////////////////////////////////////////////////////////////////////// - scriptExecutionLoggerInterface.setScriptId(scriptRevision.getScriptId()); - scriptExecutionLoggerInterface.setScriptRevisionId(scriptRevision.getId()); - } + ExecuteCodeInput executeCodeInput = ExecuteCodeAction.setupExecuteCodeInput(input, scriptRevision); + executeCodeInput.getInput().put("records", getRecordsForScript(input, scriptRevision)); ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput(); new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput); output.setOutput(executeCodeOutput.getOutput()); - output.setLogger(executionLogger); + output.setLogger(executeCodeInput.getExecutionLogger()); } catch(Exception e) { @@ -161,17 +128,18 @@ public class RunAdHocRecordScriptAction /******************************************************************************* - ** Try to (dynamically) load the ApiScriptUtils object from the api middleware - ** module -- in case the runtime doesn't have that module deployed (e.g, not in - ** the project pom). + ** *******************************************************************************/ - private void addApiUtilityToContext(Map context, ScriptRevision scriptRevision) + private static ArrayList getRecordsForScript(RunAdHocRecordScriptInput input, ScriptRevision scriptRevision) { try { - Class apiScriptUtilsClass = Class.forName("com.kingsrook.qqq.api.utils.ApiScriptUtils"); - Object apiScriptUtilsObject = apiScriptUtilsClass.getConstructor().newInstance(); - context.put("api", (Serializable) apiScriptUtilsObject); + Class apiScriptUtilsClass = Class.forName("com.kingsrook.qqq.api.utils.ApiScriptUtils"); + Method qRecordListToApiRecordList = apiScriptUtilsClass.getMethod("qRecordListToApiRecordList", List.class, String.class, String.class, String.class); + Object apiRecordList = qRecordListToApiRecordList.invoke(null, input.getRecordList(), input.getTableName(), scriptRevision.getApiName(), scriptRevision.getApiVersion()); + + // noinspection unchecked + return (ArrayList) apiRecordList; } catch(ClassNotFoundException e) { @@ -182,8 +150,10 @@ public class RunAdHocRecordScriptAction } catch(Exception e) { - LOG.warn("Error adding api utility to script context", e, logPair("scriptRevisionId", scriptRevision.getId())); + LOG.warn("Error converting QRecord list to api record list", e); } + + return (new ArrayList<>(input.getRecordList())); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptAction.java index 3f1d2c8a..f8d9d511 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptAction.java @@ -25,11 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.scripts; import java.io.Serializable; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import com.kingsrook.qqq.backend.core.actions.ActionHelper; -import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; -import com.kingsrook.qqq.backend.core.actions.scripts.logging.ScriptExecutionLoggerInterface; -import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger; import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; @@ -40,8 +36,6 @@ import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptO 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.metadata.code.AssociatedScriptCodeReference; -import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; -import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.scripts.Script; import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision; @@ -54,6 +48,7 @@ public class RunAssociatedScriptAction private Map scriptRevisionCache = new HashMap<>(); + /******************************************************************************* ** *******************************************************************************/ @@ -61,36 +56,8 @@ public class RunAssociatedScriptAction { ActionHelper.validateSession(input); - ScriptRevision scriptRevision = getScriptRevision(input); - - ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(); - executeCodeInput.setInput(new HashMap<>(input.getInputValues())); - executeCodeInput.setContext(new HashMap<>()); - if(input.getOutputObject() != null) - { - executeCodeInput.getContext().put("output", input.getOutputObject()); - } - - if(input.getScriptUtils() != null) - { - executeCodeInput.getContext().put("scriptUtils", input.getScriptUtils()); - } - - executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!! - - ///////////////////////////////////////////////////////////////////////////////////////////////// - // let caller supply a logger, or by default use StoreScriptLogAndScriptLogLineExecutionLogger // - ///////////////////////////////////////////////////////////////////////////////////////////////// - QCodeExecutionLoggerInterface executionLogger = Objects.requireNonNullElseGet(input.getLogger(), () -> new StoreScriptLogAndScriptLogLineExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId())); - executeCodeInput.setExecutionLogger(executionLogger); - if(executionLogger instanceof ScriptExecutionLoggerInterface scriptExecutionLoggerInterface) - { - //////////////////////////////////////////////////////////////////////////////////////////////////// - // if logger is aware of scripts (as opposed to a generic CodeExecution logger), give it the ids. // - //////////////////////////////////////////////////////////////////////////////////////////////////// - scriptExecutionLoggerInterface.setScriptId(scriptRevision.getScriptId()); - scriptExecutionLoggerInterface.setScriptRevisionId(scriptRevision.getId()); - } + ScriptRevision scriptRevision = getScriptRevision(input); + ExecuteCodeInput executeCodeInput = ExecuteCodeAction.setupExecuteCodeInput(input, scriptRevision); ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput(); new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ScriptApi.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ScriptApi.java index c8fcd9dd..fa2f7154 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ScriptApi.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ScriptApi.java @@ -47,6 +47,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; /******************************************************************************* ** Object made available to scripts for access to qqq api (e.g., query, insert, ** etc, plus object constructors). + ** + ** Before scripts knew about the API, this class made sense and was used. + ** But, now that scripts do know about the API, it feels like this class could + ** be deleted... but, what about, a QQQ deployment without the API module... + ** In that case, we might still want this class... think about it. *******************************************************************************/ public class ScriptApi implements Serializable { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TableAutomationAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TableAutomationAction.java index d44fa9c1..aa043a8b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TableAutomationAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TableAutomationAction.java @@ -38,6 +38,12 @@ public class TableAutomationAction private Integer priority = 500; private QQueryFilter filter; + //////////////////////////////////////////////////////////////////////// + // flag that will cause the records to cause their associations to be // + // fetched, when they are looked up for passing into the action // + //////////////////////////////////////////////////////////////////////// + private boolean includeRecordAssociations = false; + private Map values; //////////////////////////////// @@ -292,4 +298,35 @@ public class TableAutomationAction return (this); } + + + /******************************************************************************* + ** Getter for includeRecordAssociations + *******************************************************************************/ + public boolean getIncludeRecordAssociations() + { + return (this.includeRecordAssociations); + } + + + + /******************************************************************************* + ** Setter for includeRecordAssociations + *******************************************************************************/ + public void setIncludeRecordAssociations(boolean includeRecordAssociations) + { + this.includeRecordAssociations = includeRecordAssociations; + } + + + + /******************************************************************************* + ** Fluent setter for includeRecordAssociations + *******************************************************************************/ + public TableAutomationAction withIncludeRecordAssociations(boolean includeRecordAssociations) + { + this.includeRecordAssociations = includeRecordAssociations; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptRevision.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptRevision.java index 0169c561..917ebfb2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptRevision.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptRevision.java @@ -27,6 +27,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.data.QField; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; +import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; /******************************************************************************* @@ -48,16 +49,22 @@ public class ScriptRevision extends QRecordEntity @QField(possibleValueSourceName = "script") private Integer scriptId; + @QField(possibleValueSourceName = "apiVersion", label = "API Version") + private String apiVersion; + + @QField(possibleValueSourceName = "apiName", label = "API Name") + private String apiName; + @QField() private String contents; @QField() private Integer sequenceNo; - @QField() + @QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS) private String commitMessage; - @QField() + @QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS) private String author; @@ -353,4 +360,66 @@ public class ScriptRevision extends QRecordEntity return (this); } + + + /******************************************************************************* + ** Getter for apiVersion + *******************************************************************************/ + public String getApiVersion() + { + return (this.apiVersion); + } + + + + /******************************************************************************* + ** Setter for apiVersion + *******************************************************************************/ + public void setApiVersion(String apiVersion) + { + this.apiVersion = apiVersion; + } + + + + /******************************************************************************* + ** Fluent setter for apiVersion + *******************************************************************************/ + public ScriptRevision withApiVersion(String apiVersion) + { + this.apiVersion = apiVersion; + return (this); + } + + + + /******************************************************************************* + ** Getter for apiName + *******************************************************************************/ + public String getApiName() + { + return (this.apiName); + } + + + + /******************************************************************************* + ** Setter for apiName + *******************************************************************************/ + public void setApiName(String apiName) + { + this.apiName = apiName; + } + + + + /******************************************************************************* + ** Fluent setter for apiName + *******************************************************************************/ + public ScriptRevision withApiName(String apiName) + { + this.apiName = apiName; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java index d8235a2b..2e64aa29 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java @@ -382,6 +382,7 @@ public class ScriptsMetaDataProvider .withRecordLabelFormat("%s v%s") .withRecordLabelFields(List.of("scriptId", "sequenceNo")) .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "scriptId", "sequenceNo"))) + .withSection(new QFieldSection("api", "API", new QIcon().withName("code"), Tier.T2, List.of("apiName", "apiVersion"))) .withSection(new QFieldSection("code", new QIcon().withName("data_object"), Tier.T2, List.of("contents"))) .withSection(new QFieldSection("changeManagement", new QIcon().withName("history"), Tier.T2, List.of("commitMessage", "author"))) .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java index 414b0c54..ee23b94f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java @@ -83,6 +83,9 @@ public class ExtractViaQueryStep extends AbstractExtractStep queryInput.setRecordPipe(getRecordPipe()); queryInput.setLimit(getLimit()); queryInput.setAsyncJobCallback(runBackendStepInput.getAsyncJobCallback()); + + customizeInputPreQuery(queryInput); + new QueryAction().execute(queryInput); /////////////////////////////////////////////////////////////////// @@ -92,6 +95,16 @@ public class ExtractViaQueryStep extends AbstractExtractStep + /******************************************************************************* + ** chance for sub-classes to change things about the query input, if they want. + *******************************************************************************/ + protected void customizeInputPreQuery(QueryInput queryInput) + { + + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/RunRecordScriptExtractStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/RunRecordScriptExtractStep.java index 4599b0c8..6dda828d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/RunRecordScriptExtractStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/RunRecordScriptExtractStep.java @@ -28,6 +28,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInpu import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; 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.QueryInput; import com.kingsrook.qqq.backend.core.model.scripts.Script; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -70,4 +71,16 @@ public class RunRecordScriptExtractStep extends ExtractViaQueryStep super.preRun(runBackendStepInput, runBackendStepOutput); } + + + /******************************************************************************* + ** Make sure associations are fetched (so api records have children!) + *******************************************************************************/ + @Override + protected void customizeInputPreQuery(QueryInput queryInput) + { + super.customizeInputPreQuery(queryInput); + queryInput.setIncludeAssociations(true); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/StoreScriptRevisionProcessStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/StoreScriptRevisionProcessStep.java index d1a0fb82..db523e40 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/StoreScriptRevisionProcessStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/StoreScriptRevisionProcessStep.java @@ -112,6 +112,8 @@ public class StoreScriptRevisionProcessStep implements BackendStep QRecord scriptRevision = new QRecord() .withValue("scriptId", script.getValue("id")) .withValue("contents", input.getValueString("contents")) + .withValue("apiName", input.getValueString("apiName")) + .withValue("apiVersion", input.getValueString("apiVersion")) .withValue("commitMessage", commitMessage) .withValue("sequenceNo", nextSequenceNo); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/TestScriptProcessStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/TestScriptProcessStep.java index 0abab72c..cf9df9df 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/TestScriptProcessStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/TestScriptProcessStep.java @@ -106,6 +106,7 @@ public class TestScriptProcessStep implements BackendStep QueryInput queryInput = new QueryInput(); queryInput.setTableName(tableName); queryInput.setFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, recordPrimaryKeyList.split(",")))); + queryInput.setIncludeAssociations(true); QueryOutput queryOutput = new QueryAction().execute(queryInput); if(CollectionUtils.nullSafeIsEmpty(queryOutput.getRecords())) { diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataProvider.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataProvider.java index 0d7779bc..e8ff2d9d 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataProvider.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataProvider.java @@ -118,7 +118,7 @@ public class ApiInstanceMetaDataProvider ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(instance); for(Map.Entry entry : apiInstanceMetaDataContainer.getApis().entrySet()) { - apiNamePossibleValues.add(new QPossibleValue<>(entry.getKey())); + apiNamePossibleValues.add(new QPossibleValue<>(entry.getKey(), entry.getValue().getLabel())); ApiInstanceMetaData apiInstanceMetaData = entry.getValue(); allVersions.addAll(apiInstanceMetaData.getPastVersions()); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java index 5f01dd26..08586a41 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java @@ -28,11 +28,13 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.kingsrook.qqq.api.actions.ApiImplementation; +import com.kingsrook.qqq.api.actions.QRecordApiAdapter; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.data.QRecord; /******************************************************************************* @@ -49,20 +51,30 @@ public class ApiScriptUtils implements Serializable ** Constructor ** *******************************************************************************/ - public ApiScriptUtils() + public ApiScriptUtils(String apiName, String apiVersion) { + setApiName(apiName); + setApiVersion(apiVersion); } /******************************************************************************* - ** Constructor ** *******************************************************************************/ - public ApiScriptUtils(String apiName, String apiVersion) + public static ArrayList> qRecordListToApiRecordList(List qRecordList, String tableName, String apiName, String apiVersion) throws QException { - setApiName(apiName); - setApiVersion(apiVersion); + if(qRecordList == null) + { + return (null); + } + + ArrayList> rs = new ArrayList<>(); + for(QRecord qRecord : qRecordList) + { + rs.add(QRecordApiAdapter.qRecordToApiMap(qRecord, tableName, apiName, apiVersion)); + } + return (rs); } diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java index cd66985e..3d450744 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java @@ -267,10 +267,7 @@ class ApiScriptUtilsTest extends BaseTest *******************************************************************************/ private static ApiScriptUtils newDefaultApiScriptUtils() { - ApiScriptUtils apiScriptUtils = new ApiScriptUtils(); - apiScriptUtils.setApiName(TestUtils.API_NAME); - apiScriptUtils.setApiVersion(TestUtils.CURRENT_API_VERSION); - return apiScriptUtils; + return (new ApiScriptUtils(TestUtils.API_NAME, TestUtils.CURRENT_API_VERSION)); } } \ No newline at end of file From b7e39d69538505f03e6d284c5a9ca19cb89ce791 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 28 Apr 2023 12:14:12 -0500 Subject: [PATCH 10/15] Update to allow includeAssociations when querying into a record pipe. This meant propagating a lot of exceptions... --- .../actions/reporting/BufferedRecordPipe.java | 5 +-- .../core/actions/reporting/RecordPipe.java | 15 ++++---- .../core/actions/tables/QueryAction.java | 26 +++++--------- .../core/adapters/CsvToQRecordAdapter.java | 9 ++--- .../actions/tables/query/QueryOutput.java | 5 +-- .../tables/query/QueryOutputRecordPipe.java | 5 +-- .../query/QueryOutputStorageInterface.java | 5 +-- .../core/actions/tables/QueryActionTest.java | 36 +++++++++++++++++++ .../adapters/CsvToQRecordAdapterTest.java | 29 +++++++-------- .../tablesync/TableSyncProcessTest.java | 5 ++- .../module/rdbms/jdbc/QueryManager.java | 7 ++-- 11 files changed, 93 insertions(+), 54 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/BufferedRecordPipe.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/BufferedRecordPipe.java index 811827c8..0dc1ec09 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/BufferedRecordPipe.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/BufferedRecordPipe.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.reporting; import java.util.ArrayList; import java.util.List; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.data.QRecord; @@ -63,7 +64,7 @@ public class BufferedRecordPipe extends RecordPipe ** *******************************************************************************/ @Override - public void addRecord(QRecord record) + public void addRecord(QRecord record) throws QException { buffer.add(record); if(buffer.size() >= bufferSize) @@ -78,7 +79,7 @@ public class BufferedRecordPipe extends RecordPipe /******************************************************************************* ** *******************************************************************************/ - public void finalFlush() + public void finalFlush() throws QException { if(!buffer.isEmpty()) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java index b95e754e..dff2c4de 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java @@ -26,10 +26,11 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.utils.SleepUtils; +import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeConsumer; /******************************************************************************* @@ -47,7 +48,7 @@ public class RecordPipe private boolean isTerminated = false; - private Consumer> postRecordActions = null; + private UnsafeConsumer, QException> postRecordActions = null; ///////////////////////////////////// // See usage below for explanation // @@ -93,7 +94,7 @@ public class RecordPipe /******************************************************************************* ** Add a record to the pipe. Will block if the pipe is full. Will noop if pipe is terminated. *******************************************************************************/ - public void addRecord(QRecord record) + public void addRecord(QRecord record) throws QException { if(isTerminated) { @@ -109,7 +110,7 @@ public class RecordPipe // (which we'll create as a field in this class, to avoid always re-constructing) // //////////////////////////////////////////////////////////////////////////////////// singleRecordListForPostRecordActions.add(record); - postRecordActions.accept(singleRecordListForPostRecordActions); + postRecordActions.run(singleRecordListForPostRecordActions); record = singleRecordListForPostRecordActions.remove(0); } @@ -152,11 +153,11 @@ public class RecordPipe /******************************************************************************* ** Add a list of records to the pipe. Will block if the pipe is full. Will noop if pipe is terminated. *******************************************************************************/ - public void addRecords(List records) + public void addRecords(List records) throws QException { if(postRecordActions != null) { - postRecordActions.accept(records); + postRecordActions.run(records); } ////////////////////////////////////////////////////////////////////////////////////////////////// @@ -207,7 +208,7 @@ public class RecordPipe /******************************************************************************* ** *******************************************************************************/ - public void setPostRecordActions(Consumer> postRecordActions) + public void setPostRecordActions(UnsafeConsumer, QException> postRecordActions) { this.postRecordActions = postRecordActions; } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index 11763ab8..a90535f9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -85,14 +85,6 @@ public class QueryAction queryInput.getRecordPipe().setPostRecordActions(this::postRecordActions); } - if(queryInput.getIncludeAssociations() && queryInput.getRecordPipe() != null) - { - ////////////////////////////////////////////// - // todo - support this in the future maybe? // - ////////////////////////////////////////////// - throw (new QException("Associations may not be fetched into a RecordPipe.")); - } - QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend()); // todo pre-customization - just get to modify the request? @@ -109,11 +101,6 @@ public class QueryAction postRecordActions(queryOutput.getRecords()); } - if(queryInput.getIncludeAssociations()) - { - manageAssociations(queryInput, queryOutput); - } - return queryOutput; } @@ -122,7 +109,7 @@ public class QueryAction /******************************************************************************* ** *******************************************************************************/ - private void manageAssociations(QueryInput queryInput, QueryOutput queryOutput) throws QException + private void manageAssociations(QueryInput queryInput, List queryOutputRecords) throws QException { QTableMetaData table = queryInput.getTable(); for(Association association : CollectionUtils.nonNullList(table.getAssociations())) @@ -147,7 +134,7 @@ public class QueryAction { JoinOn joinOn = join.getJoinOns().get(0); Set values = new HashSet<>(); - for(QRecord record : queryOutput.getRecords()) + for(QRecord record : queryOutputRecords) { Serializable value = record.getValue(joinOn.getLeftField()); values.add(value); @@ -159,7 +146,7 @@ public class QueryAction { filter.setBooleanOperator(QQueryFilter.BooleanOperator.OR); - for(QRecord record : queryOutput.getRecords()) + for(QRecord record : queryOutputRecords) { QQueryFilter subFilter = new QQueryFilter(); filter.addSubFilter(subFilter); @@ -227,7 +214,7 @@ public class QueryAction ** not one created via List.of()). This may include setting display values, ** translating possible values, and running post-record customizations. *******************************************************************************/ - public void postRecordActions(List records) + public void postRecordActions(List records) throws QException { if(this.postQueryRecordCustomizer.isPresent()) { @@ -247,5 +234,10 @@ public class QueryAction { QValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), records); } + + if(queryInput.getIncludeAssociations()) + { + manageAssociations(queryInput, records); + } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java index 48dffd16..46f51d58 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; +import com.kingsrook.qqq.backend.core.exceptions.QException; 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.fields.QFieldMetaData; @@ -61,7 +62,7 @@ public class CsvToQRecordAdapter ** using a given mapping. ** *******************************************************************************/ - public void buildRecordsFromCsv(RecordPipe recordPipe, String csv, QTableMetaData table, AbstractQFieldMapping mapping, Consumer recordCustomizer) + public void buildRecordsFromCsv(RecordPipe recordPipe, String csv, QTableMetaData table, AbstractQFieldMapping mapping, Consumer recordCustomizer) throws QException { buildRecordsFromCsv(new InputWrapper().withRecordPipe(recordPipe).withCsv(csv).withTable(table).withMapping(mapping).withRecordCustomizer(recordCustomizer)); } @@ -73,7 +74,7 @@ public class CsvToQRecordAdapter ** using a given mapping. ** *******************************************************************************/ - public List buildRecordsFromCsv(String csv, QTableMetaData table, AbstractQFieldMapping mapping) + public List buildRecordsFromCsv(String csv, QTableMetaData table, AbstractQFieldMapping mapping) throws QException { buildRecordsFromCsv(new InputWrapper().withCsv(csv).withTable(table).withMapping(mapping)); return (recordList); @@ -87,7 +88,7 @@ public class CsvToQRecordAdapter ** ** todo - meta-data validation, type handling *******************************************************************************/ - public void buildRecordsFromCsv(InputWrapper inputWrapper) + public void buildRecordsFromCsv(InputWrapper inputWrapper) throws QException { String csv = inputWrapper.getCsv(); AbstractQFieldMapping mapping = inputWrapper.getMapping(); @@ -297,7 +298,7 @@ public class CsvToQRecordAdapter /******************************************************************************* ** Add a record - either to the pipe, or list, whichever we're building. *******************************************************************************/ - private void addRecord(QRecord record) + private void addRecord(QRecord record) throws QException { if(recordPipe != null) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java index a9e19342..da9dad45 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query; import java.io.Serializable; import java.util.List; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; @@ -63,7 +64,7 @@ public class QueryOutput extends AbstractActionOutput implements Serializable ** that could be read asynchronously, at any time, by another thread - SO - only ** completely populated records should be passed into this method. *******************************************************************************/ - public void addRecord(QRecord record) + public void addRecord(QRecord record) throws QException { storage.addRecord(record); } @@ -73,7 +74,7 @@ public class QueryOutput extends AbstractActionOutput implements Serializable /******************************************************************************* ** add a list of records to this output *******************************************************************************/ - public void addRecords(List records) + public void addRecords(List records) throws QException { storage.addRecords(records); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutputRecordPipe.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutputRecordPipe.java index b4984a2e..6ca24f02 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutputRecordPipe.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutputRecordPipe.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query; import java.util.List; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.data.QRecord; @@ -53,7 +54,7 @@ class QueryOutputRecordPipe implements QueryOutputStorageInterface ** add a record to this output *******************************************************************************/ @Override - public void addRecord(QRecord record) + public void addRecord(QRecord record) throws QException { recordPipe.addRecord(record); } @@ -64,7 +65,7 @@ class QueryOutputRecordPipe implements QueryOutputStorageInterface ** add a list of records to this output *******************************************************************************/ @Override - public void addRecords(List records) + public void addRecords(List records) throws QException { recordPipe.addRecords(records); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutputStorageInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutputStorageInterface.java index 95046765..c010b21e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutputStorageInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutputStorageInterface.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query; import java.util.List; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.data.QRecord; @@ -36,13 +37,13 @@ interface QueryOutputStorageInterface /******************************************************************************* ** add a records to this output *******************************************************************************/ - void addRecord(QRecord record); + void addRecord(QRecord record) throws QException; /******************************************************************************* ** add a list of records to this output *******************************************************************************/ - void addRecords(List records); + void addRecords(List records) throws QException; /******************************************************************************* ** Get all stored records diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java index b3be058f..9c70a049 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java @@ -164,6 +164,42 @@ class QueryActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryAssociationsWithPipe() throws QException + { + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); + insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); + + RecordPipe pipe = new RecordPipe(); + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + queryInput.setRecordPipe(pipe); + queryInput.setIncludeAssociations(true); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertNotNull(queryOutput); + + List records = pipe.consumeAvailableRecords(); + assertThat(records).isNotEmpty(); + + QRecord order0 = records.get(0); + assertEquals(2, order0.getAssociatedRecords().get("orderLine").size()); + assertEquals(3, order0.getAssociatedRecords().get("extrinsics").size()); + + QRecord orderLine00 = order0.getAssociatedRecords().get("orderLine").get(0); + assertEquals(1, orderLine00.getAssociatedRecords().get("extrinsics").size()); + QRecord orderLine01 = order0.getAssociatedRecords().get("orderLine").get(1); + assertEquals(2, orderLine01.getAssociatedRecords().get("extrinsics").size()); + + QRecord order1 = records.get(1); + assertEquals(1, order1.getAssociatedRecords().get("orderLine").size()); + assertEquals(1, order1.getAssociatedRecords().get("extrinsics").size()); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java index 345e71ca..e1fba082 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.adapters; import java.util.List; import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.QIndexBasedFieldMapping; import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.QKeyBasedFieldMapping; import com.kingsrook.qqq.backend.core.model.data.QRecord; @@ -48,7 +49,7 @@ class CsvToQRecordAdapterTest extends BaseTest ** *******************************************************************************/ @Test - public void test_buildRecordsFromCsv_nullInput() + public void test_buildRecordsFromCsv_nullInput() throws QException { testExpectedToThrow(null); } @@ -59,7 +60,7 @@ class CsvToQRecordAdapterTest extends BaseTest ** *******************************************************************************/ @Test - public void test_buildRecordsFromCsv_emptyStringInput() + public void test_buildRecordsFromCsv_emptyStringInput() throws QException { testExpectedToThrow(""); } @@ -69,7 +70,7 @@ class CsvToQRecordAdapterTest extends BaseTest /******************************************************************************* ** *******************************************************************************/ - private void testExpectedToThrow(String csv) + private void testExpectedToThrow(String csv) throws QException { try { @@ -92,7 +93,7 @@ class CsvToQRecordAdapterTest extends BaseTest ** *******************************************************************************/ @Test - public void test_buildRecordsFromCsv_emptyList() + public void test_buildRecordsFromCsv_emptyList() throws QException { CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); List qRecords = csvToQRecordAdapter.buildRecordsFromCsv(getPersonCsvHeader(), TestUtils.defineTablePerson(), null); @@ -142,7 +143,7 @@ class CsvToQRecordAdapterTest extends BaseTest ** *******************************************************************************/ @Test - public void test_buildRecordsFromCsv_oneRowStandardHeaderNoMapping() + public void test_buildRecordsFromCsv_oneRowStandardHeaderNoMapping() throws QException { CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); List qRecords = csvToQRecordAdapter.buildRecordsFromCsv(getPersonCsvHeader() + getPersonCsvRow1(), TestUtils.defineTablePerson(), null); @@ -159,7 +160,7 @@ class CsvToQRecordAdapterTest extends BaseTest ** *******************************************************************************/ @Test - public void test_buildRecordsFromCsv_twoRowsStandardHeaderNoMapping() + public void test_buildRecordsFromCsv_twoRowsStandardHeaderNoMapping() throws QException { CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); List qRecords = csvToQRecordAdapter.buildRecordsFromCsv(getPersonCsvHeader() + getPersonCsvRow1() + getPersonCsvRow2(), TestUtils.defineTablePerson(), null); @@ -179,7 +180,7 @@ class CsvToQRecordAdapterTest extends BaseTest ** *******************************************************************************/ @Test - public void test_buildRecordsFromCsv_oneRowCustomKeyBasedMapping() + public void test_buildRecordsFromCsv_oneRowCustomKeyBasedMapping() throws QException { String csvCustomHeader = """ "id","created","modified","first","last","birthday","email"\r @@ -209,7 +210,7 @@ class CsvToQRecordAdapterTest extends BaseTest ** *******************************************************************************/ @Test - public void test_buildRecordsFromCsv_twoRowsCustomIndexBasedMapping() + public void test_buildRecordsFromCsv_twoRowsCustomIndexBasedMapping() throws QException { int index = 1; QIndexBasedFieldMapping mapping = new QIndexBasedFieldMapping() @@ -241,7 +242,7 @@ class CsvToQRecordAdapterTest extends BaseTest ** header names on the RHS. *******************************************************************************/ @Test - public void test_duplicatedColumnHeaders() + public void test_duplicatedColumnHeaders() throws QException { QKeyBasedFieldMapping mapping = new QKeyBasedFieldMapping() .withMapping("id", "id") @@ -291,7 +292,7 @@ class CsvToQRecordAdapterTest extends BaseTest ** *******************************************************************************/ @Test - void testByteOrderMarker() + void testByteOrderMarker() throws QException { CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); @@ -313,7 +314,7 @@ class CsvToQRecordAdapterTest extends BaseTest ** Fix an IndexOutOfBounds that we used to throw. *******************************************************************************/ @Test - void testTooFewBodyColumns() + void testTooFewBodyColumns() throws QException { CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); List records = csvToQRecordAdapter.buildRecordsFromCsv(""" @@ -331,7 +332,7 @@ class CsvToQRecordAdapterTest extends BaseTest ** *******************************************************************************/ @Test - public void testTooFewColumnsIndexMapping() + public void testTooFewColumnsIndexMapping() throws QException { int index = 1; QIndexBasedFieldMapping mapping = new QIndexBasedFieldMapping() @@ -353,7 +354,7 @@ class CsvToQRecordAdapterTest extends BaseTest ** *******************************************************************************/ @Test - void testCaseSensitiveHeaders() + void testCaseSensitiveHeaders() throws QException { CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); csvToQRecordAdapter.buildRecordsFromCsv(new CsvToQRecordAdapter.InputWrapper() @@ -376,7 +377,7 @@ class CsvToQRecordAdapterTest extends BaseTest ** *******************************************************************************/ @Test - void testCaseInsensitiveHeaders() + void testCaseInsensitiveHeaders() throws QException { CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); csvToQRecordAdapter.buildRecordsFromCsv(new CsvToQRecordAdapter.InputWrapper() diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/tablesync/TableSyncProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/tablesync/TableSyncProcessTest.java index dbfba6d1..96a4b399 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/tablesync/TableSyncProcessTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/tablesync/TableSyncProcessTest.java @@ -222,7 +222,10 @@ class TableSyncProcessTest extends BaseTest public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException { List qRecords = TestUtils.queryTable(QContext.getQInstance(), TestUtils.TABLE_NAME_PERSON_MEMORY); - qRecords.forEach(r -> getRecordPipe().addRecord(r)); + for(QRecord qRecord : qRecords) + { + getRecordPipe().addRecord(qRecord); + } //////////////////////////////////////// // re-add records 1 and 5 to the pipe // diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java index 1fedb981..1f9dfbdd 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java @@ -50,6 +50,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -87,7 +88,7 @@ public class QueryManager /******************************************************************************* ** *******************************************************************************/ - void processResultSet(ResultSet rs) throws SQLException; + void processResultSet(ResultSet rs) throws SQLException, QException; } @@ -95,7 +96,7 @@ public class QueryManager /******************************************************************************* ** *******************************************************************************/ - public static void executeStatement(Connection connection, String sql, ResultSetProcessor processor, Object... params) throws SQLException + public static void executeStatement(Connection connection, String sql, ResultSetProcessor processor, Object... params) throws SQLException, QException { PreparedStatement statement = null; try @@ -118,7 +119,7 @@ public class QueryManager ** Let the caller provide their own prepared statement (e.g., possibly with some ** customized settings/optimizations). *******************************************************************************/ - public static void executeStatement(PreparedStatement statement, ResultSetProcessor processor, Object... params) throws SQLException + public static void executeStatement(PreparedStatement statement, ResultSetProcessor processor, Object... params) throws SQLException, QException { ResultSet resultSet = null; From 0b7c2db4529a087367f9f786c84a4b59b41cbb09 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 28 Apr 2023 14:22:29 -0500 Subject: [PATCH 11/15] Introduce RecordPipeBufferedWrapper, to be used in QueryAction when includingAssociations and writing to a pipe --- .../reporting/RecordPipeBufferedWrapper.java | 79 +++++++++++++++++++ .../core/actions/tables/QueryAction.java | 11 +++ .../ValidateRecordSecurityLockHelper.java | 2 +- .../memory/MemoryQueryAction.java | 12 ++- .../core/actions/tables/QueryActionTest.java | 57 +++++++++++++ 5 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipeBufferedWrapper.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipeBufferedWrapper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipeBufferedWrapper.java new file mode 100644 index 00000000..08117c06 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipeBufferedWrapper.java @@ -0,0 +1,79 @@ +/* + * 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.reporting; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.data.QRecord; + + +/******************************************************************************* + ** Subclass of BufferedRecordPipe, which ultimately sends records down to an + ** original RecordPipe. + ** + ** Meant to be used where: someone passed in a RecordPipe (so they have a reference + ** to it, and they are waiting to read from it), but the producer knows that + ** it will be better to buffer the records, so they want to use a buffered pipe + ** (but they still need the records to end up in the original pipe - thus - + ** it gets wrapped by an object of this class). + *******************************************************************************/ +public class RecordPipeBufferedWrapper extends BufferedRecordPipe +{ + private RecordPipe wrappedPipe; + + + + /******************************************************************************* + ** Constructor - uses default buffer size + ** + *******************************************************************************/ + public RecordPipeBufferedWrapper(RecordPipe wrappedPipe) + { + this.wrappedPipe = wrappedPipe; + } + + + + /******************************************************************************* + ** Constructor - customize buffer size. + ** + *******************************************************************************/ + public RecordPipeBufferedWrapper(Integer bufferSize, RecordPipe wrappedPipe) + { + super(bufferSize); + this.wrappedPipe = wrappedPipe; + } + + + + /******************************************************************************* + ** when it's time to actually add records into the pipe, actually add them + ** into the wrapped pipe! + *******************************************************************************/ + @Override + public void addRecords(List records) throws QException + { + wrappedPipe.addRecords(records); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index a90535f9..1a5e5c84 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -34,6 +34,7 @@ import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCusto 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.reporting.BufferedRecordPipe; +import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipeBufferedWrapper; import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.context.QContext; @@ -83,6 +84,15 @@ public class QueryAction if(queryInput.getRecordPipe() != null) { queryInput.getRecordPipe().setPostRecordActions(this::postRecordActions); + + if(queryInput.getIncludeAssociations()) + { + ////////////////////////////////////////////////////////////////////////////////////////// + // if the user requested to include associations, it's important that that is buffered, // + // (for performance reasons), so, wrap the user's pipe with a buffer // + ////////////////////////////////////////////////////////////////////////////////////////// + queryInput.setRecordPipe(new RecordPipeBufferedWrapper(queryInput.getRecordPipe())); + } } QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); @@ -111,6 +121,7 @@ public class QueryAction *******************************************************************************/ private void manageAssociations(QueryInput queryInput, List queryOutputRecords) throws QException { + LOG.info("In manageAssociations for " + queryInput.getTableName() + " with " + queryOutputRecords.size() + " records"); QTableMetaData table = queryInput.getTable(); for(Association association : CollectionUtils.nonNullList(table.getAssociations())) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ValidateRecordSecurityLockHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ValidateRecordSecurityLockHelper.java index c69a2fc0..ae44f8fe 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ValidateRecordSecurityLockHelper.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ValidateRecordSecurityLockHelper.java @@ -261,7 +261,7 @@ public class ValidateRecordSecurityLockHelper QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType()); if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN)) { - LOG.debug("Session has " + securityKeyType.getAllAccessKeyName() + " - not checking this lock."); + LOG.trace("Session has " + securityKeyType.getAllAccessKeyName() + " - not checking this lock."); } else { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryQueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryQueryAction.java index cd0e8bf7..962fe46a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryQueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryQueryAction.java @@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; import com.kingsrook.qqq.backend.core.exceptions.QException; 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; /******************************************************************************* @@ -43,7 +44,16 @@ public class MemoryQueryAction implements QueryInterface try { QueryOutput queryOutput = new QueryOutput(queryInput); - queryOutput.addRecords(MemoryRecordStore.getInstance().query(queryInput)); + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // add the records to the output one-by-one -- this more closely matches how "real" backends perform // + // and works better w/ pipes // + /////////////////////////////////////////////////////////////////////////////////////////////////////// + for(QRecord qRecord : MemoryRecordStore.getInstance().query(queryInput)) + { + queryOutput.addRecord(qRecord); + } + return (queryOutput); } catch(Exception e) diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java index 9c70a049..8051f33a 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.tables; import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -200,6 +201,41 @@ class QueryActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryManyRecordsAssociationsWithPipe() throws QException + { + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); + insertNOrdersWithAssociations(2500); + + RecordPipe pipe = new RecordPipe(1000); + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + queryInput.setRecordPipe(pipe); + queryInput.setIncludeAssociations(true); + + int recordsConsumed = new AsyncRecordPipeLoop().run("Test", null, pipe, (callback) -> + { + new QueryAction().execute(queryInput); + return (true); + }, () -> + { + List records = pipe.consumeAvailableRecords(); + for(QRecord record : records) + { + assertEquals(1, record.getAssociatedRecords().get("orderLine").size()); + assertEquals(1, record.getAssociatedRecords().get("extrinsics").size()); + } + return (records.size()); + }); + + assertEquals(2500, recordsConsumed); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -356,4 +392,25 @@ class QueryActionTest extends BaseTest )); new InsertAction().execute(insertInput); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void insertNOrdersWithAssociations(int n) throws QException + { + List recordList = new ArrayList<>(); + for(int i = 0; i < n; i++) + { + recordList.add(new QRecord().withValue("storeId", 1).withValue("orderNo", "ORD" + i) + .withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC1").withValue("quantity", 3)) + .withAssociatedRecord("extrinsics", new QRecord().withValue("key", "YOUR-FIELD").withValue("value", "YOUR-VALUE"))); + } + + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_ORDER); + insertInput.setRecords(recordList); + new InsertAction().execute(insertInput); + } } From d35f15020213494a2a2ca78423fddbc729cfa704 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 28 Apr 2023 15:46:36 -0500 Subject: [PATCH 12/15] Scripts and apis and all kinds of stuff --- .../scripts/TestScriptProcessStep.java | 5 ++- .../api/actions/BaseAPIActionUtilTest.java | 19 +++++++-- qqq-language-support-javascript/pom.xml | 4 +- .../qqq/api/actions/ApiImplementation.java | 38 +++++++++--------- .../api/javalin/QJavalinApiHandlerTest.java | 40 ++++++++++--------- 5 files changed, 60 insertions(+), 46 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/TestScriptProcessStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/TestScriptProcessStep.java index cf9df9df..2cd3544b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/TestScriptProcessStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/TestScriptProcessStep.java @@ -74,11 +74,12 @@ public class TestScriptProcessStep implements BackendStep // get inputs // //////////////// Integer scriptId = input.getValueInteger("scriptId"); - String code = input.getValueString("code"); ScriptRevision scriptRevision = new ScriptRevision(); scriptRevision.setScriptId(scriptId); - scriptRevision.setContents(code); + scriptRevision.setContents(input.getValueString("code")); + scriptRevision.setApiName(input.getValueString("apiName")); + scriptRevision.setApiVersion(input.getValueString("apiVersion")); BuildScriptLogAndScriptLogLineExecutionLogger executionLogger = new BuildScriptLogAndScriptLogLineExecutionLogger(null, null); diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtilTest.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtilTest.java index 04275a80..3930d5b6 100644 --- a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtilTest.java +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtilTest.java @@ -26,6 +26,7 @@ import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; @@ -47,6 +48,7 @@ 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.tables.UniqueKey; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; import com.kingsrook.qqq.backend.module.api.BaseTest; import com.kingsrook.qqq.backend.module.api.TestUtils; import com.kingsrook.qqq.backend.module.api.exceptions.RateLimitException; @@ -524,9 +526,20 @@ class BaseAPIActionUtilTest extends BaseTest InsertOutput insertOutput = new InsertAction().execute(insertInput); assertEquals(6, insertOutput.getRecords().get(0).getValueInteger("id")); - QueryInput queryInput = new QueryInput(); - queryInput.setTableName(OutboundAPILog.TABLE_NAME); - QueryOutput apiLogRecords = new QueryAction().execute(queryInput); + ////////////////////////////////////////////////////////////////////////////////////////// + // the outbound api log is inserted async, so... do or do not, and sleep some if needed // + ////////////////////////////////////////////////////////////////////////////////////////// + QueryOutput apiLogRecords = null; + int tries = 0; + do + { + SleepUtils.sleep(10, TimeUnit.MILLISECONDS); + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(OutboundAPILog.TABLE_NAME); + apiLogRecords = new QueryAction().execute(queryInput); + } + while(apiLogRecords.getRecords().isEmpty() && tries++ < 10); + assertEquals(1, apiLogRecords.getRecords().size()); assertEquals("POST", apiLogRecords.getRecords().get(0).getValueString("method")); } diff --git a/qqq-language-support-javascript/pom.xml b/qqq-language-support-javascript/pom.xml index ba2af5a4..0210a4a8 100644 --- a/qqq-language-support-javascript/pom.xml +++ b/qqq-language-support-javascript/pom.xml @@ -33,10 +33,8 @@ + - - 0.10 - 0.10 diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java index e58533cd..ea25ac48 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java @@ -102,7 +102,7 @@ public class ApiImplementation { List badRequestMessages = new ArrayList<>(); - QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.QUERY_BY_QUERY_STRING); + QTableMetaData table = validateTableAndVersion(apiInstanceMetaData, version, tableApiName, ApiOperation.QUERY_BY_QUERY_STRING); String tableName = table.getName(); QueryInput queryInput = new QueryInput(); @@ -326,7 +326,7 @@ public class ApiImplementation *******************************************************************************/ public static Map insert(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String body) throws QException { - QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.INSERT); + QTableMetaData table = validateTableAndVersion(apiInstanceMetaData, version, tableApiName, ApiOperation.INSERT); String tableName = table.getName(); InsertInput insertInput = new InsertInput(); @@ -392,7 +392,7 @@ public class ApiImplementation *******************************************************************************/ public static List> bulkInsert(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String body) throws QException { - QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_INSERT); + QTableMetaData table = validateTableAndVersion(apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_INSERT); String tableName = table.getName(); InsertInput insertInput = new InsertInput(); @@ -481,7 +481,7 @@ public class ApiImplementation *******************************************************************************/ public static Map get(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String primaryKey) throws QException { - QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.GET); + QTableMetaData table = validateTableAndVersion(apiInstanceMetaData, version, tableApiName, ApiOperation.GET); String tableName = table.getName(); GetInput getInput = new GetInput(); @@ -516,7 +516,7 @@ public class ApiImplementation *******************************************************************************/ public static void update(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String primaryKey, String body) throws QException { - QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.UPDATE); + QTableMetaData table = validateTableAndVersion(apiInstanceMetaData, version, tableApiName, ApiOperation.UPDATE); String tableName = table.getName(); UpdateInput updateInput = new UpdateInput(); @@ -586,7 +586,7 @@ public class ApiImplementation *******************************************************************************/ public static List> bulkUpdate(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String body) throws QException { - QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_UPDATE); + QTableMetaData table = validateTableAndVersion(apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_UPDATE); String tableName = table.getName(); UpdateInput updateInput = new UpdateInput(); @@ -698,7 +698,7 @@ public class ApiImplementation *******************************************************************************/ public static void delete(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String primaryKey) throws QException { - QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.DELETE); + QTableMetaData table = validateTableAndVersion(apiInstanceMetaData, version, tableApiName, ApiOperation.DELETE); String tableName = table.getName(); DeleteInput deleteInput = new DeleteInput(); @@ -732,7 +732,7 @@ public class ApiImplementation *******************************************************************************/ public static List> bulkDelete(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, String body) throws QException { - QTableMetaData table = validateTableAndVersion(tableApiName, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_DELETE); + QTableMetaData table = validateTableAndVersion(apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_DELETE); String tableName = table.getName(); DeleteInput deleteInput = new DeleteInput(); @@ -982,55 +982,53 @@ public class ApiImplementation /******************************************************************************* ** *******************************************************************************/ - public static QTableMetaData validateTableAndVersion(String path, ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, ApiOperation operation) throws QNotFoundException + public static QTableMetaData validateTableAndVersion(ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, ApiOperation operation) throws QNotFoundException { - QNotFoundException qNotFoundException = new QNotFoundException("Could not find any resources at path " + path); - QTableMetaData table = getTableByApiName(apiInstanceMetaData.getName(), version, tableApiName); LogPair[] logPairs = new LogPair[] { logPair("apiName", apiInstanceMetaData.getName()), logPair("version", version), logPair("tableApiName", tableApiName), logPair("operation", operation) }; if(table == null) { LOG.info("404 because table is null (tableApiName=" + tableApiName + ")", logPairs); - throw (qNotFoundException); + throw (new QNotFoundException("Could not find a table named " + tableApiName + " in this api.")); } if(BooleanUtils.isTrue(table.getIsHidden())) { LOG.info("404 because table isHidden", logPairs); - throw (qNotFoundException); + throw (new QNotFoundException("Could not find a table named " + tableApiName + " in this api.")); } ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); if(apiTableMetaDataContainer == null) { LOG.info("404 because table apiMetaDataContainer is null", logPairs); - throw (qNotFoundException); + throw (new QNotFoundException("Could not find a table named " + tableApiName + " in this api.")); } ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiInstanceMetaData.getName()); if(apiTableMetaData == null) { LOG.info("404 because table apiMetaData is null", logPairs); - throw (qNotFoundException); + throw (new QNotFoundException("Could not find a table named " + tableApiName + " in this api.")); } if(BooleanUtils.isTrue(apiTableMetaData.getIsExcluded())) { LOG.info("404 because table is excluded", logPairs); - throw (qNotFoundException); + throw (new QNotFoundException("Could not find a table named " + tableApiName + " in this api.")); } if(!operation.isOperationEnabled(List.of(apiInstanceMetaData, apiTableMetaData))) { LOG.info("404 because api operation is not enabled", logPairs); - throw (qNotFoundException); + throw (new QNotFoundException("Cannot perform operation [" + operation + "] on table named " + tableApiName + " in this api.")); } if(!table.isCapabilityEnabled(QContext.getQInstance().getBackendForTable(table.getName()), operation.getCapability())) { LOG.info("404 because table capability is not enabled", logPairs); - throw (qNotFoundException); + throw (new QNotFoundException("Cannot perform operation [" + operation + "] on table named " + tableApiName + " in this api.")); } APIVersion requestApiVersion = new APIVersion(version); @@ -1038,13 +1036,13 @@ public class ApiImplementation if(CollectionUtils.nullSafeIsEmpty(supportedVersions) || !supportedVersions.contains(requestApiVersion)) { LOG.info("404 because requested version is not supported", logPairs); - throw (qNotFoundException); + throw (new QNotFoundException(version + " is not a supported version in this api.")); } if(!apiTableMetaData.getApiVersionRange().includes(requestApiVersion)) { LOG.info("404 because table version range does not include requested version", logPairs); - throw (qNotFoundException); + throw (new QNotFoundException(version + " is not a supported version for table " + tableApiName + " in this api.")); } return (table); diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java index 58908fa6..a98d1a80 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java @@ -147,25 +147,29 @@ class QJavalinApiHandlerTest extends BaseTest @Test void testRandom404s() { - for(String method : new String[] { "get", "post", "patch", "delete" }) - { - assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request(method, BASE_URL + "/api/" + VERSION + "/notATable/").asString()); - assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request(method, BASE_URL + "/api/" + VERSION + "/notATable/notAnId").asString()); - assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request(method, BASE_URL + "/api/" + VERSION + "/person/1/2").asString()); - assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request(method, BASE_URL + "/api/foo").asString()); + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("get", BASE_URL + "/api/" + VERSION + "/notATable/").asString()); + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find a table named notATable in this api", Unirest.request("get", BASE_URL + "/api/" + VERSION + "/notATable/notAnId").asString()); + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("get", BASE_URL + "/api/" + VERSION + "/person/1/2").asString()); + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("get", BASE_URL + "/api/foo").asString()); + assertErrorResponse(HttpStatus.OK_200, null, Unirest.request("get", BASE_URL + "/api/").asString()); // this path returns the doc site for a GET - if(method.equals("get")) - { - ////////////////////////////////////////////// - // this path returns the doc site for a GET // - ////////////////////////////////////////////// - assertErrorResponse(HttpStatus.OK_200, null, Unirest.request(method, BASE_URL + "/api/").asString()); - } - else - { - assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request(method, BASE_URL + "/api/").asString()); - } - } + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find a table named notATable in this api", Unirest.request("post", BASE_URL + "/api/" + VERSION + "/notATable/").asString()); + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("post", BASE_URL + "/api/" + VERSION + "/notATable/notAnId").asString()); + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("post", BASE_URL + "/api/" + VERSION + "/person/1/2").asString()); + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("post", BASE_URL + "/api/foo").asString()); + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("post", BASE_URL + "/api/").asString()); + + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("patch", BASE_URL + "/api/" + VERSION + "/notATable/").asString()); + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find a table named notATable in this api", Unirest.request("patch", BASE_URL + "/api/" + VERSION + "/notATable/notAnId").asString()); + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("patch", BASE_URL + "/api/" + VERSION + "/person/1/2").asString()); + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("patch", BASE_URL + "/api/foo").asString()); + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("patch", BASE_URL + "/api/").asString()); + + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("delete", BASE_URL + "/api/" + VERSION + "/notATable/").asString()); + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find a table named notATable in this api", Unirest.request("delete", BASE_URL + "/api/" + VERSION + "/notATable/notAnId").asString()); + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("delete", BASE_URL + "/api/" + VERSION + "/person/1/2").asString()); + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("delete", BASE_URL + "/api/foo").asString()); + assertErrorResponse(HttpStatus.NOT_FOUND_404, "Could not find any resources at path", Unirest.request("delete", BASE_URL + "/api/").asString()); } From 9ce45934a81dce2db00de0efc040b7e976963e02 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 28 Apr 2023 16:00:06 -0500 Subject: [PATCH 13/15] Try to make happier when scripts & apis don't live together --- .../model/scripts/ScriptsMetaDataProvider.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java index 2e64aa29..703690cf 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java @@ -382,7 +382,6 @@ public class ScriptsMetaDataProvider .withRecordLabelFormat("%s v%s") .withRecordLabelFields(List.of("scriptId", "sequenceNo")) .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "scriptId", "sequenceNo"))) - .withSection(new QFieldSection("api", "API", new QIcon().withName("code"), Tier.T2, List.of("apiName", "apiVersion"))) .withSection(new QFieldSection("code", new QIcon().withName("data_object"), Tier.T2, List.of("contents"))) .withSection(new QFieldSection("changeManagement", new QIcon().withName("history"), Tier.T2, List.of("commitMessage", "author"))) .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))); @@ -390,6 +389,23 @@ public class ScriptsMetaDataProvider tableMetaData.getField("contents").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("javascript"))); tableMetaData.getField("scriptId").withFieldAdornment(AdornmentType.Size.LARGE.toAdornment()); + try + { + //////////////////////////////////////////////////////////////////////////////////////////////////// + // if the api module is loaded, then add a section to the table for the api name & version fields // + //////////////////////////////////////////////////////////////////////////////////////////////////// + Class.forName("com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataProvider"); + tableMetaData.getSections().add(1, new QFieldSection("api", "API", new QIcon().withName("code"), Tier.T2, List.of("apiName", "apiVersion"))); + } + catch(ClassNotFoundException e) + { + ///////////////////////////////////////////////////////////////////////////////////////////////////// + // if the api module is not loaded, then make sure we don't have these fields in our scripts table // + ///////////////////////////////////////////////////////////////////////////////////////////////////// + tableMetaData.getFields().remove("apiName"); + tableMetaData.getFields().remove("apiVersion"); + } + return (tableMetaData); } From 15acaec5238be3526537afbfb737977ff0b2c3c4 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Sun, 30 Apr 2023 19:56:41 -0500 Subject: [PATCH 14/15] More api name & version with scripts (eg, running test scripts) --- ...ssociatedScriptContextPrimerInterface.java | 41 ++++++++++++ .../actions/scripts/ExecuteCodeAction.java | 10 +-- .../scripts/RunAssociatedScriptAction.java | 5 ++ .../scripts/StoreAssociatedScriptAction.java | 2 + .../scripts/TestScriptActionInterface.java | 20 ++++-- .../scripts/RunAssociatedScriptInput.java | 33 ++++++++++ .../scripts/StoreAssociatedScriptInput.java | 64 ++++++++++++++++++ .../actions/scripts/TestScriptInput.java | 65 +++++++++++++++++++ .../qqq/api/utils/ApiScriptUtils.java | 9 ++- .../qqq/api/utils/ApiScriptUtilsTest.java | 4 +- .../javalin/QJavalinScriptsHandler.java | 12 ++-- 11 files changed, 243 insertions(+), 22 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/AssociatedScriptContextPrimerInterface.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/AssociatedScriptContextPrimerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/AssociatedScriptContextPrimerInterface.java new file mode 100644 index 00000000..77292400 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/AssociatedScriptContextPrimerInterface.java @@ -0,0 +1,41 @@ +/* + * 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.scripts; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput; +import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface AssociatedScriptContextPrimerInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void primeContext(ExecuteCodeInput executeCodeInput, ScriptRevision scriptRevision) throws QException; + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java index b04737df..09f6ebba 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java @@ -127,19 +127,21 @@ public class ExecuteCodeAction ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(); executeCodeInput.setInput(new HashMap<>(Objects.requireNonNullElseGet(input.getInputValues(), HashMap::new))); executeCodeInput.setContext(new HashMap<>()); + + Map context = executeCodeInput.getContext(); if(input.getOutputObject() != null) { - executeCodeInput.getContext().put("output", input.getOutputObject()); + context.put("output", input.getOutputObject()); } if(input.getScriptUtils() != null) { - executeCodeInput.getContext().put("scriptUtils", input.getScriptUtils()); + context.put("scriptUtils", input.getScriptUtils()); } executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!! - ExecuteCodeAction.addApiUtilityToContext(executeCodeInput.getContext(), scriptRevision); + ExecuteCodeAction.addApiUtilityToContext(context, scriptRevision); ExecuteCodeAction.setExecutionLoggerInExecuteCodeInput(input, scriptRevision, executeCodeInput); return (executeCodeInput); @@ -152,7 +154,7 @@ public class ExecuteCodeAction ** module -- in case the runtime doesn't have that module deployed (e.g, not in ** the project pom). *******************************************************************************/ - private static void addApiUtilityToContext(Map context, ScriptRevision scriptRevision) + public static void addApiUtilityToContext(Map context, ScriptRevision scriptRevision) { if(!StringUtils.hasContent(scriptRevision.getApiName()) || !StringUtils.hasContent(scriptRevision.getApiVersion())) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptAction.java index f8d9d511..53054839 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAssociatedScriptAction.java @@ -59,6 +59,11 @@ public class RunAssociatedScriptAction ScriptRevision scriptRevision = getScriptRevision(input); ExecuteCodeInput executeCodeInput = ExecuteCodeAction.setupExecuteCodeInput(input, scriptRevision); + if(input.getAssociatedScriptContextPrimerInterface() != null) + { + input.getAssociatedScriptContextPrimerInterface().primeContext(executeCodeInput, scriptRevision); + } + ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput(); new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptAction.java index 1d2d8848..04b1d51f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptAction.java @@ -184,6 +184,8 @@ public class StoreAssociatedScriptAction QRecord scriptRevision = new QRecord() .withValue("scriptId", script.getValue("id")) .withValue("contents", input.getCode()) + .withValue("apiName", input.getApiName()) + .withValue("apiVersion", input.getApiVersion()) .withValue("commitMessage", commitMessage) .withValue("sequenceNo", nextSequenceNo); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/TestScriptActionInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/TestScriptActionInterface.java index 47d741f8..92499b87 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/TestScriptActionInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/TestScriptActionInterface.java @@ -32,6 +32,7 @@ import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput; import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptInput; import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptOutput; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision; /******************************************************************************* @@ -47,7 +48,7 @@ public interface TestScriptActionInterface ** Note - such a method may want or need to put an "output" object into the ** executeCodeInput's context map. *******************************************************************************/ - void setupTestScriptInput(TestScriptInput testScriptInput, ExecuteCodeInput executeCodeInput); + void setupTestScriptInput(TestScriptInput testScriptInput, ExecuteCodeInput executeCodeInput) throws QException; /******************************************************************************* @@ -87,12 +88,21 @@ public interface TestScriptActionInterface BuildScriptLogAndScriptLogLineExecutionLogger executionLogger = new BuildScriptLogAndScriptLogLineExecutionLogger(null, null); executeCodeInput.setExecutionLogger(executionLogger); - setupTestScriptInput(input, executeCodeInput); - - ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput(); - try { + setupTestScriptInput(input, executeCodeInput); + + ScriptRevision scriptRevision = new ScriptRevision().withApiName(input.getApiName()).withApiVersion(input.getApiVersion()); + + if(this instanceof AssociatedScriptContextPrimerInterface associatedScriptContextPrimerInterface) + { + associatedScriptContextPrimerInterface.primeContext(executeCodeInput, scriptRevision); + } + + ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput(); + + ExecuteCodeAction.addApiUtilityToContext(executeCodeInput.getContext(), scriptRevision); + new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput); output.setOutputObject(processTestScriptOutput(executeCodeOutput)); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptInput.java index e1a51b21..7c6a5187 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAssociatedScriptInput.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.model.actions.scripts; +import com.kingsrook.qqq.backend.core.actions.scripts.AssociatedScriptContextPrimerInterface; import com.kingsrook.qqq.backend.core.model.metadata.code.AssociatedScriptCodeReference; @@ -30,5 +31,37 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.AssociatedScriptCodeRe *******************************************************************************/ public class RunAssociatedScriptInput extends AbstractRunScriptInput { + private AssociatedScriptContextPrimerInterface associatedScriptContextPrimerInterface; + + + + /******************************************************************************* + ** Getter for associatedScriptContextPrimerInterface + *******************************************************************************/ + public AssociatedScriptContextPrimerInterface getAssociatedScriptContextPrimerInterface() + { + return (this.associatedScriptContextPrimerInterface); + } + + + + /******************************************************************************* + ** Setter for associatedScriptContextPrimerInterface + *******************************************************************************/ + public void setAssociatedScriptContextPrimerInterface(AssociatedScriptContextPrimerInterface associatedScriptContextPrimerInterface) + { + this.associatedScriptContextPrimerInterface = associatedScriptContextPrimerInterface; + } + + + + /******************************************************************************* + ** Fluent setter for associatedScriptContextPrimerInterface + *******************************************************************************/ + public RunAssociatedScriptInput withAssociatedScriptContextPrimerInterface(AssociatedScriptContextPrimerInterface associatedScriptContextPrimerInterface) + { + this.associatedScriptContextPrimerInterface = associatedScriptContextPrimerInterface; + return (this); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/StoreAssociatedScriptInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/StoreAssociatedScriptInput.java index 1d522c38..e2e52510 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/StoreAssociatedScriptInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/StoreAssociatedScriptInput.java @@ -35,6 +35,8 @@ public class StoreAssociatedScriptInput extends AbstractTableActionInput private Serializable recordPrimaryKey; private String code; + private String apiName; + private String apiVersion; private String commitMessage; @@ -183,4 +185,66 @@ public class StoreAssociatedScriptInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Getter for apiName + *******************************************************************************/ + public String getApiName() + { + return (this.apiName); + } + + + + /******************************************************************************* + ** Setter for apiName + *******************************************************************************/ + public void setApiName(String apiName) + { + this.apiName = apiName; + } + + + + /******************************************************************************* + ** Fluent setter for apiName + *******************************************************************************/ + public StoreAssociatedScriptInput withApiName(String apiName) + { + this.apiName = apiName; + return (this); + } + + + + /******************************************************************************* + ** Getter for apiVersion + *******************************************************************************/ + public String getApiVersion() + { + return (this.apiVersion); + } + + + + /******************************************************************************* + ** Setter for apiVersion + *******************************************************************************/ + public void setApiVersion(String apiVersion) + { + this.apiVersion = apiVersion; + } + + + + /******************************************************************************* + ** Fluent setter for apiVersion + *******************************************************************************/ + public StoreAssociatedScriptInput withApiVersion(String apiVersion) + { + this.apiVersion = apiVersion; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/TestScriptInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/TestScriptInput.java index 8a116221..8b5c4939 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/TestScriptInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/TestScriptInput.java @@ -36,6 +36,9 @@ public class TestScriptInput extends AbstractTableActionInput private Map inputValues; private QCodeReference codeReference; + private String apiName; + private String apiVersion; + /******************************************************************************* @@ -113,4 +116,66 @@ public class TestScriptInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Getter for apiName + *******************************************************************************/ + public String getApiName() + { + return (this.apiName); + } + + + + /******************************************************************************* + ** Setter for apiName + *******************************************************************************/ + public void setApiName(String apiName) + { + this.apiName = apiName; + } + + + + /******************************************************************************* + ** Fluent setter for apiName + *******************************************************************************/ + public TestScriptInput withApiName(String apiName) + { + this.apiName = apiName; + return (this); + } + + + + /******************************************************************************* + ** Getter for apiVersion + *******************************************************************************/ + public String getApiVersion() + { + return (this.apiVersion); + } + + + + /******************************************************************************* + ** Setter for apiVersion + *******************************************************************************/ + public void setApiVersion(String apiVersion) + { + this.apiVersion = apiVersion; + } + + + + /******************************************************************************* + ** Fluent setter for apiVersion + *******************************************************************************/ + public TestScriptInput withApiVersion(String apiVersion) + { + this.apiVersion = apiVersion; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java index 08586a41..f06585d2 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java @@ -150,12 +150,11 @@ public class ApiScriptUtils implements Serializable /******************************************************************************* ** *******************************************************************************/ - public Map query(String urlPart) throws QException + public Map query(String tableName, String queryString) throws QException { - validateApiNameAndVersion("query(" + urlPart + ")"); - String[] urlParts = urlPart.split("\\?", 2); - Map> paramMap = parseQueryString(urlParts.length > 1 ? urlParts[1] : null); - return (ApiImplementation.query(getApiInstanceMetaData(), apiVersion, urlParts[0], paramMap)); + validateApiNameAndVersion("query(" + tableName + ")"); + Map> paramMap = parseQueryString(queryString); + return (ApiImplementation.query(getApiInstanceMetaData(), apiVersion, tableName, paramMap)); } diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java index 3d450744..ec4cfb2b 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java @@ -102,13 +102,13 @@ class ApiScriptUtilsTest extends BaseTest { ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils(); - assertThatThrownBy(() -> apiScriptUtils.query(TestUtils.TABLE_NAME_PERSON + "?foo=bar")) + assertThatThrownBy(() -> apiScriptUtils.query(TestUtils.TABLE_NAME_PERSON, "foo=bar")) .isInstanceOf(QBadRequestException.class) .hasMessageContaining("Unrecognized filter criteria field: foo"); insertSimpsons(); - Map result = apiScriptUtils.query(TestUtils.TABLE_NAME_PERSON + "?id=2"); + Map result = apiScriptUtils.query(TestUtils.TABLE_NAME_PERSON, "id=2"); assertEquals(1, result.get("count")); assertEquals(1, ((List) result.get("records")).size()); assertEquals("Marge", ((Map) ((List) result.get("records")).get(0)).get("firstName")); diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinScriptsHandler.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinScriptsHandler.java index 61f84bfa..eaaca7ae 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinScriptsHandler.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinScriptsHandler.java @@ -395,18 +395,18 @@ public class QJavalinScriptsHandler String key = entry.getKey(); String value = entry.getValue().get(0); - if(key.equals("code")) + switch(key) { - input.setCodeReference(new QCodeReference().withInlineCode(value).withCodeType(QCodeType.JAVA_SCRIPT)); - } - else - { - inputValues.put(key, value); + case "code" -> input.setCodeReference(new QCodeReference().withInlineCode(value).withCodeType(QCodeType.JAVA_SCRIPT)); + case "apiName" -> input.setApiName(value); + case "apiVersion" -> input.setApiVersion(value); + default -> inputValues.put(key, value); } } TestScriptActionInterface scriptTester = QCodeLoader.getAdHoc(TestScriptActionInterface.class, scriptTesterCodeRef); TestScriptOutput output = new TestScriptOutput(); + scriptTester.execute(input, output); QJavalinAccessLogger.logEndSuccess(); From 1e053d67ce6ced69cb6b92a4fb980bbec5774b9d Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 1 May 2023 08:32:02 -0500 Subject: [PATCH 15/15] Update to not NPE is record is null (rather, return null) --- .../com/kingsrook/qqq/api/actions/QRecordApiAdapter.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java index b7de314f..412a0ad5 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java @@ -64,6 +64,11 @@ public class QRecordApiAdapter *******************************************************************************/ public static Map qRecordToApiMap(QRecord record, String tableName, String apiName, String apiVersion) throws QException { + if(record == null) + { + return (null); + } + List tableApiFields = getTableApiFieldList(new ApiNameVersionAndTableName(apiName, apiVersion, tableName)); LinkedHashMap outputRecord = new LinkedHashMap<>();