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 84137b7d..84105d4e 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 @@ -380,11 +380,7 @@ public class ApiImplementation // 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, apiName, version)); - } + ArrayList> records = QRecordApiAdapter.qRecordsToApiMapList(queryOutput.getRecords(), tableName, apiName, version); ///////////////////////////// // optionally do the count // @@ -615,7 +611,7 @@ public class ApiImplementation + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); } - Map outputRecord = QRecordApiAdapter.qRecordToApiMap(record, tableName, apiInstanceMetaData.getName(), version); + Map outputRecord = QRecordApiAdapter.qRecordsToApiMapList(List.of(record), tableName, apiInstanceMetaData.getName(), version).get(0); return (outputRecord); } @@ -1144,8 +1140,8 @@ public class ApiImplementation ApiProcessOutputInterface output = apiProcessMetaData.getOutput(); if(output != null) { - Serializable outputForProcess = output.getOutputForProcess(runProcessInput, runProcessOutput); - HttpApiResponse httpApiResponse = new HttpApiResponse(output.getSuccessStatusCode(runProcessInput, runProcessOutput), outputForProcess); + Serializable outputForProcess = output.getOutputForProcess(runProcessInput, runProcessOutput); + HttpApiResponse httpApiResponse = new HttpApiResponse(output.getSuccessStatusCode(runProcessInput, runProcessOutput), outputForProcess); output.customizeHttpApiResponse(httpApiResponse, runProcessInput, runProcessOutput); return httpApiResponse; } 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 da2e211a..07cee3a7 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 @@ -33,6 +33,7 @@ import com.kingsrook.qqq.api.javalin.QBadRequestException; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.APIVersionRange; import com.kingsrook.qqq.api.model.actions.ApiFieldCustomValueMapper; +import com.kingsrook.qqq.api.model.actions.ApiFieldCustomValueMapperBulkSupportInterface; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; @@ -67,9 +68,61 @@ public class QRecordApiAdapter /******************************************************************************* - ** Convert a QRecord to a map for the API + ** Simple/short form of convert a QRecord to a map for the API - e.g., meant for + ** public consumption. *******************************************************************************/ public static Map qRecordToApiMap(QRecord record, String tableName, String apiName, String apiVersion) throws QException + { + return qRecordsToApiMapList(List.of(record), tableName, apiName, apiVersion).get(0); + } + + + + /******************************************************************************* + ** bulk-version of the qRecordToApiMap - will use + ** ApiFieldCustomValueMapperBulkSupportInterface's in the bulky way. + *******************************************************************************/ + public static ArrayList> qRecordsToApiMapList(List records, String tableName, String apiName, String apiVersion) throws QException + { + Map fieldValueMappers = new HashMap<>(); + + List tableApiFields = GetTableApiFieldsAction.getTableApiFieldList(new GetTableApiFieldsAction.ApiNameVersionAndTableName(apiName, apiVersion, tableName)); + for(QFieldMetaData field : tableApiFields) + { + ApiFieldMetaData apiFieldMetaData = ObjectUtils.tryAndRequireNonNullElse(() -> ApiFieldMetaDataContainer.of(field).getApiFieldMetaData(apiName), new ApiFieldMetaData()); + String apiFieldName = ApiFieldMetaData.getEffectiveApiFieldName(apiName, field); + + if(apiFieldMetaData.getCustomValueMapper() != null) + { + if(!fieldValueMappers.containsKey(apiFieldMetaData.getCustomValueMapper().getName())) + { + ApiFieldCustomValueMapper customValueMapper = QCodeLoader.getAdHoc(ApiFieldCustomValueMapper.class, apiFieldMetaData.getCustomValueMapper()); + fieldValueMappers.put(apiFieldMetaData.getCustomValueMapper().getName(), customValueMapper); + + if(customValueMapper instanceof ApiFieldCustomValueMapperBulkSupportInterface bulkMapper) + { + bulkMapper.prepareToProduceApiValues(records); + } + } + } + } + + ArrayList> rs = new ArrayList<>(); + for(QRecord record : records) + { + rs.add(QRecordApiAdapter.qRecordToApiMap(record, tableName, apiName, apiVersion, fieldValueMappers)); + } + + return (rs); + } + + + + /******************************************************************************* + ** private version of convert a QRecord to a map for the API - takes params to + ** support working in bulk w/ customizers much better. + *******************************************************************************/ + private static Map qRecordToApiMap(QRecord record, String tableName, String apiName, String apiVersion, Map fieldValueMappers) throws QException { if(record == null) { @@ -87,14 +140,25 @@ public class QRecordApiAdapter ApiFieldMetaData apiFieldMetaData = ObjectUtils.tryAndRequireNonNullElse(() -> ApiFieldMetaDataContainer.of(field).getApiFieldMetaData(apiName), new ApiFieldMetaData()); String apiFieldName = ApiFieldMetaData.getEffectiveApiFieldName(apiName, field); - Serializable value = null; + Serializable value; if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName())) { value = record.getValue(apiFieldMetaData.getReplacedByFieldName()); } else if(apiFieldMetaData.getCustomValueMapper() != null) { - ApiFieldCustomValueMapper customValueMapper = QCodeLoader.getAdHoc(ApiFieldCustomValueMapper.class, apiFieldMetaData.getCustomValueMapper()); + if(fieldValueMappers == null) + { + fieldValueMappers = new HashMap<>(); + } + + String customValueMapperName = apiFieldMetaData.getCustomValueMapper().getName(); + if(!fieldValueMappers.containsKey(customValueMapperName)) + { + fieldValueMappers.put(customValueMapperName, QCodeLoader.getAdHoc(ApiFieldCustomValueMapper.class, apiFieldMetaData.getCustomValueMapper())); + } + + ApiFieldCustomValueMapper customValueMapper = fieldValueMappers.get(customValueMapperName); value = customValueMapper.produceApiValue(record, apiFieldName); } else @@ -331,5 +395,4 @@ public class QRecordApiAdapter return (null); } - } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/ApiFieldCustomValueMapperBulkSupportInterface.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/ApiFieldCustomValueMapperBulkSupportInterface.java new file mode 100644 index 00000000..703b2a7e --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/ApiFieldCustomValueMapperBulkSupportInterface.java @@ -0,0 +1,45 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. 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.model.actions; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.model.data.QRecord; + + +/******************************************************************************* + ** optional interface that can be added to an ApiFieldCustomValueMapper to + ** signal that the customizer knows how to work in bulk. + ** + ** e.g., given a list of records, do a bulk query for data; memoize that data; + ** then use it in multiple calls to produceApiValue, without, e.g., going back to + ** a backend to fetch data. + *******************************************************************************/ +public interface ApiFieldCustomValueMapperBulkSupportInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void prepareToProduceApiValues(List records); + +} diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/ApiImplementationTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/ApiImplementationTest.java index a55b2027..376562c2 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/ApiImplementationTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/ApiImplementationTest.java @@ -26,12 +26,14 @@ import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDate; import java.time.Month; +import java.util.Collections; 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.api.model.actions.ApiFieldCustomValueMapper; +import com.kingsrook.qqq.api.model.actions.ApiFieldCustomValueMapperBulkSupportInterface; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; @@ -197,6 +199,43 @@ class ApiImplementationTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBulkValueCustomizer() throws QException + { + QInstance qInstance = QContext.getQInstance(); + ApiInstanceMetaData apiInstanceMetaData = ApiInstanceMetaDataContainer.of(qInstance).getApiInstanceMetaData(TestUtils.API_NAME); + TestUtils.insertSimpsons(); + + //////////////////////////////////////////////////////////////////// + // set up a custom value mapper on lastName field of person table // + //////////////////////////////////////////////////////////////////// + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON); + QFieldMetaData field = table.getField("lastName"); + field.withSupplementalMetaData(new ApiFieldMetaDataContainer() + .withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData() + .withInitialVersion(TestUtils.V2022_Q4) + .withCustomValueMapper(new QCodeReference(PersonLastNameBulkApiValueCustomizer.class)))); + + //////////////////////////////////////////////// + // get a person - make sure custom method ran // + //////////////////////////////////////////////// + Map person = ApiImplementation.get(apiInstanceMetaData, TestUtils.CURRENT_API_VERSION, "person", "1"); + assertEquals("value from prepareToProduceApiValues", person.get("lastName")); + assertEquals(1, PersonLastNameBulkApiValueCustomizer.prepareWasCalledWithThisNoOfRecords); + + ///////////////////////////////////////////////////// + // query for persons - make sure custom method ran // + ///////////////////////////////////////////////////// + Map queryResult = ApiImplementation.query(apiInstanceMetaData, TestUtils.CURRENT_API_VERSION, "person", Collections.emptyMap()); + assertEquals("value from prepareToProduceApiValues", ((List>) queryResult.get("records")).get(0).get("lastName")); + assertEquals(queryResult.get("count"), PersonLastNameBulkApiValueCustomizer.prepareWasCalledWithThisNoOfRecords); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -264,4 +303,39 @@ class ApiImplementationTest extends BaseTest } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class PersonLastNameBulkApiValueCustomizer extends ApiFieldCustomValueMapper implements ApiFieldCustomValueMapperBulkSupportInterface + { + static Integer prepareWasCalledWithThisNoOfRecords = null; + + private String valueToPutInRecords = null; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Serializable produceApiValue(QRecord record, String apiFieldName) + { + return (valueToPutInRecords); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void prepareToProduceApiValues(List records) + { + prepareWasCalledWithThisNoOfRecords = records.size(); + valueToPutInRecords = "value from prepareToProduceApiValues"; + } + } + } \ No newline at end of file