CE-1113 Add ApiFieldCustomValueMapperBulkSupportInterface

This commit is contained in:
2024-06-14 08:56:48 -05:00
parent e1fd6d51c4
commit 00a3f6632c
4 changed files with 190 additions and 12 deletions

View File

@ -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<Map<String, Serializable>> records = new ArrayList<>();
for(QRecord record : queryOutput.getRecords())
{
records.add(QRecordApiAdapter.qRecordToApiMap(record, tableName, apiName, version));
}
ArrayList<Map<String, Serializable>> 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<String, Serializable> outputRecord = QRecordApiAdapter.qRecordToApiMap(record, tableName, apiInstanceMetaData.getName(), version);
Map<String, Serializable> 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;
}

View File

@ -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<String, Serializable> 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<Map<String, Serializable>> qRecordsToApiMapList(List<QRecord> records, String tableName, String apiName, String apiVersion) throws QException
{
Map<String, ApiFieldCustomValueMapper> fieldValueMappers = new HashMap<>();
List<QFieldMetaData> 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<Map<String, Serializable>> 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<String, Serializable> qRecordToApiMap(QRecord record, String tableName, String apiName, String apiVersion, Map<String, ApiFieldCustomValueMapper> 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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QRecord> records);
}

View File

@ -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<String, Serializable> 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<String, Serializable> queryResult = ApiImplementation.query(apiInstanceMetaData, TestUtils.CURRENT_API_VERSION, "person", Collections.emptyMap());
assertEquals("value from prepareToProduceApiValues", ((List<Map<String, Object>>) 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<QRecord> records)
{
prepareWasCalledWithThisNoOfRecords = records.size();
valueToPutInRecords = "value from prepareToProduceApiValues";
}
}
}