Add association api-meta data (so they can be versioned or excluded); add api field custom value mapper

This commit is contained in:
2023-06-28 11:06:15 -05:00
parent b4507ba431
commit 360bf56481
13 changed files with 666 additions and 35 deletions

View File

@ -281,6 +281,10 @@ public class ApiImplementation
try
{
////////////////////////////////////////////////////////////////////////////////////////////////
// todo - deal with removed fields; fields w/ custom value mappers (need new method(s) there) //
////////////////////////////////////////////////////////////////////////////////////////////////
QFieldMetaData field = table.getField(name);
for(String value : values)
{

View File

@ -48,6 +48,7 @@ import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData;
import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaDataContainer;
import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessOutputInterface;
import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessUtils;
import com.kingsrook.qqq.api.model.metadata.tables.ApiAssociationMetaData;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer;
import com.kingsrook.qqq.api.model.openapi.Components;
@ -428,7 +429,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
//////////////////////////////////////
// build the schemas for this table //
//////////////////////////////////////
Schema tableSchema = buildTableSchema(apiInstanceMetaData, table, tableApiFields);
Schema tableSchema = buildTableSchema(apiInstanceMetaData, version, table, tableApiFields);
componentSchemas.put(tableApiName, tableSchema);
//////////////////////////////////////////////////////////////////////////////
@ -1188,7 +1189,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withVersion(version)
.withApiName(apiName)).getFields();
componentSchemas.put(tableApiName, buildTableSchema(apiInstanceMetaData, table, tableApiFields));
componentSchemas.put(tableApiName, buildTableSchema(apiInstanceMetaData, version, table, tableApiFields));
addedAny = true;
break;
}
@ -1202,7 +1203,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
/*******************************************************************************
**
*******************************************************************************/
private Schema buildTableSchema(ApiInstanceMetaData apiInstanceMetaData, QTableMetaData table, List<QFieldMetaData> tableApiFields)
private Schema buildTableSchema(ApiInstanceMetaData apiInstanceMetaData, String version, QTableMetaData table, List<QFieldMetaData> tableApiFields)
{
LinkedHashMap<String, Schema> tableFields = new LinkedHashMap<>();
Schema tableSchema = new Schema()
@ -1225,7 +1226,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
//////////////////////////////////
// recursively add associations //
//////////////////////////////////
addAssociations(apiInstanceMetaData.getName(), table, tableSchema);
addAssociations(apiInstanceMetaData.getName(), version, table, tableSchema);
return (tableSchema);
}
@ -1367,8 +1368,10 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
/*******************************************************************************
**
*******************************************************************************/
private void addAssociations(String apiName, QTableMetaData table, Schema tableSchema)
private void addAssociations(String apiName, String version, QTableMetaData table, Schema tableSchema)
{
ApiTableMetaData thisApiTableMetaData = ObjectUtils.tryElse(() -> ApiTableMetaDataContainer.of(table).getApiTableMetaData(apiName), new ApiTableMetaData());
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
String associatedTableName = association.getAssociatedTableName();
@ -1376,6 +1379,23 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
ApiTableMetaData associatedApiTableMetaData = ObjectUtils.tryElse(() -> ApiTableMetaDataContainer.of(associatedTable).getApiTableMetaData(apiName), new ApiTableMetaData());
String associatedTableApiName = StringUtils.hasContent(associatedApiTableMetaData.getApiTableName()) ? associatedApiTableMetaData.getApiTableName() : associatedTableName;
ApiAssociationMetaData apiAssociationMetaData = thisApiTableMetaData.getApiAssociationMetaData().get(association.getName());
if(apiAssociationMetaData != null)
{
if(BooleanUtils.isTrue(apiAssociationMetaData.getIsExcluded()))
{
LOG.debug("Omitting table [" + table.getName() + "] association [" + association.getName() + "] because it is marked as excluded.");
continue;
}
APIVersionRange apiVersionRange = apiAssociationMetaData.getApiVersionRange();
if(!apiVersionRange.includes(new APIVersion(version)))
{
LOG.debug("Omitting table [" + table.getName() + "] association [" + association.getName() + "] because its api version range [" + apiVersionRange + "] does not include this version [" + version + "]");
continue;
}
}
neededTableSchemas.add(associatedTable.getName());
tableSchema.getProperties().put(association.getName(), new Schema()

View File

@ -31,9 +31,16 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
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.GetTableApiFieldsInput;
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.ApiAssociationMetaData;
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.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
@ -45,6 +52,7 @@ 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.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.lang.BooleanUtils;
import org.json.JSONArray;
import org.json.JSONObject;
@ -87,6 +95,11 @@ public class QRecordApiAdapter
{
value = record.getValue(apiFieldMetaData.getReplacedByFieldName());
}
else if(apiFieldMetaData.getCustomValueMapper() != null)
{
ApiFieldCustomValueMapper customValueMapper = QCodeLoader.getAdHoc(ApiFieldCustomValueMapper.class, apiFieldMetaData.getCustomValueMapper());
value = customValueMapper.produceApiValue(record);
}
else
{
value = record.getValue(field.getName());
@ -107,6 +120,11 @@ public class QRecordApiAdapter
QTableMetaData table = QContext.getQInstance().getTable(tableName);
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
if(isAssociationOmitted(apiName, apiVersion, table, association))
{
continue;
}
ArrayList<Map<String, Serializable>> associationList = new ArrayList<>();
outputRecord.put(association.getName(), associationList);
@ -121,6 +139,31 @@ public class QRecordApiAdapter
/*******************************************************************************
**
*******************************************************************************/
private static boolean isAssociationOmitted(String apiName, String apiVersion, QTableMetaData table, Association association)
{
ApiTableMetaData thisApiTableMetaData = ObjectUtils.tryElse(() -> ApiTableMetaDataContainer.of(table).getApiTableMetaData(apiName), new ApiTableMetaData());
ApiAssociationMetaData apiAssociationMetaData = thisApiTableMetaData.getApiAssociationMetaData().get(association.getName());
if(apiAssociationMetaData != null)
{
if(BooleanUtils.isTrue(apiAssociationMetaData.getIsExcluded()))
{
return (true);
}
APIVersionRange apiVersionRange = apiAssociationMetaData.getApiVersionRange();
if(!apiVersionRange.includes(new APIVersion(apiVersion)))
{
return true;
}
}
return false;
}
/*******************************************************************************
**
*******************************************************************************/
@ -136,9 +179,12 @@ public class QRecordApiAdapter
Map<String, Association> associationMap = new HashMap<>();
QTableMetaData table = QContext.getQInstance().getTable(tableName);
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
if(!isAssociationOmitted(apiName, apiVersion, table, association))
{
associationMap.put(association.getName(), association);
}
}
//////////////////////////////////////////
// iterate over keys in the json object //
@ -179,6 +225,11 @@ public class QRecordApiAdapter
{
qRecord.setValue(apiFieldMetaData.getReplacedByFieldName(), value);
}
else if(apiFieldMetaData.getCustomValueMapper() != null)
{
ApiFieldCustomValueMapper customValueMapper = QCodeLoader.getAdHoc(ApiFieldCustomValueMapper.class, apiFieldMetaData.getCustomValueMapper());
customValueMapper.consumeApiValue(qRecord, value, jsonObject);
}
else
{
qRecord.setValue(field.getName(), value);

View File

@ -0,0 +1,59 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.model.actions;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import org.json.JSONObject;
/*******************************************************************************
**
*******************************************************************************/
public abstract class ApiFieldCustomValueMapper
{
/*******************************************************************************
**
*******************************************************************************/
public Serializable produceApiValue(QRecord record)
{
/////////////////////
// null by default //
/////////////////////
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
public void consumeApiValue(QRecord record, Object value, JSONObject fullApiJsonObject)
{
/////////////////////
// noop by default //
/////////////////////
}
}

View File

@ -123,6 +123,11 @@ public class ApiInstanceMetaDataProvider
ApiInstanceMetaData apiInstanceMetaData = entry.getValue();
allVersions.addAll(apiInstanceMetaData.getPastVersions());
allVersions.addAll(apiInstanceMetaData.getSupportedVersions());
///////////////////////////////////////////////////////////////////////////////////////////////////////
// I think we don't want future-versions in this dropdown, I think... //
// grr, actually todo maybe we want this to be a table-backed enum, with past/present/future columns //
///////////////////////////////////////////////////////////////////////////////////////////////////////
allVersions.addAll(apiInstanceMetaData.getFutureVersions());
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.api.model.metadata.fields;
import java.util.Map;
import com.kingsrook.qqq.api.model.openapi.Example;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -41,6 +42,7 @@ public class ApiFieldMetaData
private Boolean isExcluded;
private String replacedByFieldName;
private QCodeReference customValueMapper;
private Example example;
private Map<String, Example> examples;
@ -313,4 +315,35 @@ public class ApiFieldMetaData
return (this);
}
/*******************************************************************************
** Getter for customValueMapper
*******************************************************************************/
public QCodeReference getCustomValueMapper()
{
return (this.customValueMapper);
}
/*******************************************************************************
** Setter for customValueMapper
*******************************************************************************/
public void setCustomValueMapper(QCodeReference customValueMapper)
{
this.customValueMapper = customValueMapper;
}
/*******************************************************************************
** Fluent setter for customValueMapper
*******************************************************************************/
public ApiFieldMetaData withCustomValueMapper(QCodeReference customValueMapper)
{
this.customValueMapper = customValueMapper;
return (this);
}
}

View File

@ -40,6 +40,7 @@ public class ApiFieldMetaDataContainer extends QSupplementalFieldMetaData
private ApiFieldMetaData defaultApiFieldMetaData;
/*******************************************************************************
** Constructor
**
@ -61,6 +62,23 @@ public class ApiFieldMetaDataContainer extends QSupplementalFieldMetaData
/*******************************************************************************
** either get the container attached to a field - or create a new one and attach
** it to the field, and return that.
*******************************************************************************/
public static ApiFieldMetaDataContainer ofOrWithNew(QFieldMetaData field)
{
ApiFieldMetaDataContainer apiFieldMetaDataContainer = (ApiFieldMetaDataContainer) field.getSupplementalMetaData(ApiSupplementType.NAME);
if(apiFieldMetaDataContainer == null)
{
apiFieldMetaDataContainer = new ApiFieldMetaDataContainer();
field.withSupplementalMetaData(apiFieldMetaDataContainer);
}
return (apiFieldMetaDataContainer);
}
/*******************************************************************************
** either get the container attached to a field - or a new one - note - the new
** one will NOT be attached to the field!!

View File

@ -0,0 +1,147 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.model.metadata.tables;
import com.kingsrook.qqq.api.model.APIVersionRange;
/*******************************************************************************
**
*******************************************************************************/
public class ApiAssociationMetaData
{
private String initialVersion;
private String finalVersion;
private Boolean isExcluded;
/*******************************************************************************
**
*******************************************************************************/
public APIVersionRange getApiVersionRange()
{
if(getInitialVersion() == null)
{
return APIVersionRange.none();
}
return (getFinalVersion() != null
? APIVersionRange.betweenAndIncluding(getInitialVersion(), getFinalVersion())
: APIVersionRange.afterAndIncluding(getInitialVersion()));
}
/*******************************************************************************
** Getter for initialVersion
*******************************************************************************/
public String getInitialVersion()
{
return (this.initialVersion);
}
/*******************************************************************************
** Setter for initialVersion
*******************************************************************************/
public void setInitialVersion(String initialVersion)
{
this.initialVersion = initialVersion;
}
/*******************************************************************************
** Fluent setter for initialVersion
*******************************************************************************/
public ApiAssociationMetaData withInitialVersion(String initialVersion)
{
this.initialVersion = initialVersion;
return (this);
}
/*******************************************************************************
** Getter for finalVersion
*******************************************************************************/
public String getFinalVersion()
{
return (this.finalVersion);
}
/*******************************************************************************
** Setter for finalVersion
*******************************************************************************/
public void setFinalVersion(String finalVersion)
{
this.finalVersion = finalVersion;
}
/*******************************************************************************
** Fluent setter for finalVersion
*******************************************************************************/
public ApiAssociationMetaData withFinalVersion(String finalVersion)
{
this.finalVersion = finalVersion;
return (this);
}
/*******************************************************************************
** Getter for isExcluded
*******************************************************************************/
public Boolean getIsExcluded()
{
return (this.isExcluded);
}
/*******************************************************************************
** Setter for isExcluded
*******************************************************************************/
public void setIsExcluded(Boolean isExcluded)
{
this.isExcluded = isExcluded;
}
/*******************************************************************************
** Fluent setter for isExcluded
*******************************************************************************/
public ApiAssociationMetaData withIsExcluded(Boolean isExcluded)
{
this.isExcluded = isExcluded;
return (this);
}
}

View File

@ -24,8 +24,10 @@ package com.kingsrook.qqq.api.model.metadata.tables;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.kingsrook.qqq.api.ApiSupplementType;
import com.kingsrook.qqq.api.model.APIVersionRange;
@ -53,6 +55,8 @@ public class ApiTableMetaData implements ApiOperation.EnabledOperationsProvider
private Set<ApiOperation> enabledOperations = new HashSet<>();
private Set<ApiOperation> disabledOperations = new HashSet<>();
private Map<String, ApiAssociationMetaData> apiAssociationMetaData = new HashMap<>();
/*******************************************************************************
@ -410,4 +414,50 @@ public class ApiTableMetaData implements ApiOperation.EnabledOperationsProvider
return (this);
}
/*******************************************************************************
** Getter for apiAssociationMetaData
*******************************************************************************/
public Map<String, ApiAssociationMetaData> getApiAssociationMetaData()
{
return (this.apiAssociationMetaData);
}
/*******************************************************************************
** Setter for apiAssociationMetaData
*******************************************************************************/
public void setApiAssociationMetaData(Map<String, ApiAssociationMetaData> apiAssociationMetaData)
{
this.apiAssociationMetaData = apiAssociationMetaData;
}
/*******************************************************************************
** Fluent setter for apiAssociationMetaData
*******************************************************************************/
public ApiTableMetaData withApiAssociationMetaData(Map<String, ApiAssociationMetaData> apiAssociationMetaData)
{
this.apiAssociationMetaData = apiAssociationMetaData;
return (this);
}
/*******************************************************************************
** Fluent setter for apiAssociationMetaData
*******************************************************************************/
public ApiTableMetaData withApiAssociationMetaData(String associationName, ApiAssociationMetaData apiAssociationMetaData)
{
if(this.apiAssociationMetaData == null)
{
this.apiAssociationMetaData = new HashMap<>();
}
this.apiAssociationMetaData.put(associationName, apiAssociationMetaData);
return (this);
}
}

View File

@ -60,6 +60,23 @@ public class ApiTableMetaDataContainer extends QSupplementalTableMetaData
/*******************************************************************************
** either get the container attached to a table - or create a new one and attach
** it to the table, and return that.
*******************************************************************************/
public static ApiTableMetaDataContainer ofOrWithNew(QTableMetaData table)
{
ApiTableMetaDataContainer apiTableMetaDataContainer = (ApiTableMetaDataContainer) table.getSupplementalMetaData(ApiSupplementType.NAME);
if(apiTableMetaDataContainer == null)
{
apiTableMetaDataContainer = new ApiTableMetaDataContainer();
table.withSupplementalMetaData(apiTableMetaDataContainer);
}
return (apiTableMetaDataContainer);
}
/*******************************************************************************
**
*******************************************************************************/
@ -101,6 +118,26 @@ public class ApiTableMetaDataContainer extends QSupplementalTableMetaData
/*******************************************************************************
** Getter for api
*******************************************************************************/
public ApiTableMetaData getOrWithNewApiTableMetaData(String apiName)
{
if(this.apis == null)
{
this.apis = new LinkedHashMap<>();
}
if(!this.apis.containsKey(apiName))
{
this.apis.put(apiName, new ApiTableMetaData());
}
return (this.apis.get(apiName));
}
/*******************************************************************************
** Setter for apis
*******************************************************************************/

View File

@ -546,6 +546,28 @@ public class TestUtils
/*******************************************************************************
**
*******************************************************************************/
public static void insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic() throws QException
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("orderNo", "ORD123").withValue("storeId", 47)
.withAssociatedRecord("orderLines", new QRecord().withValue("lineNumber", 1).withValue("sku", "BASIC1").withValue("quantity", 42)
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Size").withValue("value", "Medium"))
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Discount").withValue("value", "3.50"))
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Color").withValue("value", "Red")))
.withAssociatedRecord("orderLines", new QRecord().withValue("lineNumber", 2).withValue("sku", "BASIC2").withValue("quantity", 42)
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Size").withValue("value", "Medium")))
.withAssociatedRecord("orderLines", new QRecord().withValue("lineNumber", 3).withValue("sku", "BASIC3").withValue("quantity", 42))
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "shopifyOrderNo").withValue("value", "#1032"))
));
new InsertAction().execute(insertInput);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -0,0 +1,207 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.actions;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.api.BaseTest;
import com.kingsrook.qqq.api.TestUtils;
import com.kingsrook.qqq.api.model.actions.ApiFieldCustomValueMapper;
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;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer;
import com.kingsrook.qqq.api.model.metadata.tables.ApiAssociationMetaData;
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.GetAction;
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.get.GetInput;
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.code.QCodeReference;
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.ValueUtils;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for ApiImplementation
*******************************************************************************/
class ApiImplementationTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testExcludedAssociation() throws QException
{
QInstance qInstance = QContext.getQInstance();
ApiInstanceMetaData apiInstanceMetaData = ApiInstanceMetaDataContainer.of(qInstance).getApiInstanceMetaData(TestUtils.API_NAME);
TestUtils.insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic();
/////////////////////////////////////////////////
// get the order - make sure it has extrinsics //
/////////////////////////////////////////////////
Map<String, Serializable> order = ApiImplementation.get(apiInstanceMetaData, TestUtils.CURRENT_API_VERSION, "order", "1");
assertTrue(order.containsKey("extrinsics"));
/////////////////////////////////////////////////////
// turn off the extrinsics association for the api //
/////////////////////////////////////////////////////
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_ORDER);
ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table);
ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(TestUtils.API_NAME);
apiTableMetaData.withApiAssociationMetaData("extrinsics", new ApiAssociationMetaData().withIsExcluded(true));
/////////////////////////////////////////////////
// re-fetch - should no longer have extrinsics //
/////////////////////////////////////////////////
order = ApiImplementation.get(apiInstanceMetaData, TestUtils.CURRENT_API_VERSION, "order", "1");
assertFalse(order.containsKey("extrinsics"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAssociationVersions() throws QException
{
QInstance qInstance = QContext.getQInstance();
ApiInstanceMetaData apiInstanceMetaData = ApiInstanceMetaDataContainer.of(qInstance).getApiInstanceMetaData(TestUtils.API_NAME);
TestUtils.insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic();
/////////////////////////////////////////////////
// get the order - make sure it has extrinsics //
/////////////////////////////////////////////////
Map<String, Serializable> order = ApiImplementation.get(apiInstanceMetaData, TestUtils.CURRENT_API_VERSION, "order", "1");
assertTrue(order.containsKey("extrinsics"));
/////////////////////////////////////////////////
// set the initial version for the association //
/////////////////////////////////////////////////
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_ORDER);
ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table);
ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(TestUtils.API_NAME);
apiTableMetaData.withApiAssociationMetaData("extrinsics", new ApiAssociationMetaData().withInitialVersion(TestUtils.V2023_Q1));
////////////////////////////////////////////////////
// re-fetch - should have or not based on version //
////////////////////////////////////////////////////
assertFalse(ApiImplementation.get(apiInstanceMetaData, TestUtils.V2022_Q4, "order", "1").containsKey("extrinsics"));
assertTrue(ApiImplementation.get(apiInstanceMetaData, TestUtils.V2023_Q1, "order", "1").containsKey("extrinsics"));
/////////////////////////////////////////////////
// set the final version for the association //
/////////////////////////////////////////////////
apiTableMetaData.withApiAssociationMetaData("extrinsics", new ApiAssociationMetaData().withInitialVersion(TestUtils.V2022_Q4).withFinalVersion(TestUtils.V2022_Q4));
////////////////////////////////////////////////////
// re-fetch - should have or not based on version //
////////////////////////////////////////////////////
assertTrue(ApiImplementation.get(apiInstanceMetaData, TestUtils.V2022_Q4, "order", "1").containsKey("extrinsics"));
assertFalse(ApiImplementation.get(apiInstanceMetaData, TestUtils.V2023_Q1, "order", "1").containsKey("extrinsics"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testValueCustomizer() 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(PersonLastNameApiValueCustomizer.class))));
////////////////////////////////////////////////
// get a person - make sure custom method ran //
////////////////////////////////////////////////
Map<String, Serializable> person = ApiImplementation.get(apiInstanceMetaData, TestUtils.CURRENT_API_VERSION, "person", "1");
assertEquals("customValue-Simpson", person.get("lastName"));
////////////////////////////////////////////////////
// insert a person - make sure custom method runs //
////////////////////////////////////////////////////
ApiImplementation.insert(apiInstanceMetaData, TestUtils.CURRENT_API_VERSION, "person", """
{"firstName": "Ned", "lastName": "stripThisAway-Flanders"}
""");
QRecord insertedPerson = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_PERSON).withPrimaryKey(6));
assertEquals("Flanders", insertedPerson.getValueString("lastName"));
}
/*******************************************************************************
**
*******************************************************************************/
public static class PersonLastNameApiValueCustomizer extends ApiFieldCustomValueMapper
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public Serializable produceApiValue(QRecord record)
{
return ("customValue-" + record.getValueString("lastName"));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void consumeApiValue(QRecord record, Object value, JSONObject fullApiJsonObject)
{
String valueString = ValueUtils.getValueAsString(value);
valueString = valueString.replaceFirst("^stripThisAway-", "");
record.setValue("lastName", valueString);
}
}
}

View File

@ -219,7 +219,7 @@ class QJavalinApiHandlerTest extends BaseTest
@Test
void testGetAssociations() throws QException
{
insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic();
TestUtils.insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic();
HttpResponse<String> response = Unirest.get(BASE_URL + "/api/" + VERSION + "/order/1").asString();
assertEquals(HttpStatus.OK_200, response.getStatus());
@ -529,7 +529,7 @@ class QJavalinApiHandlerTest extends BaseTest
@Test
void testQueryAssociations() throws QException
{
insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic();
TestUtils.insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic();
HttpResponse<String> response = Unirest.get(BASE_URL + "/api/" + VERSION + "/order/query?id=1").asString();
assertEquals(HttpStatus.OK_200, response.getStatus());
@ -959,7 +959,7 @@ class QJavalinApiHandlerTest extends BaseTest
@Test
void testUpdateErrorsFromCustomizer() throws QException
{
insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic();
TestUtils.insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic();
HttpResponse<String> response = Unirest.patch(BASE_URL + "/api/" + VERSION + "/order/1")
.body("""
@ -1010,7 +1010,7 @@ class QJavalinApiHandlerTest extends BaseTest
@Test
void testUpdateAssociations() throws QException
{
insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic();
TestUtils.insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic();
HttpResponse<String> response = Unirest.patch(BASE_URL + "/api/" + VERSION + "/order/1")
.body("""
@ -1331,7 +1331,7 @@ class QJavalinApiHandlerTest extends BaseTest
@Test
void testDeleteAssociations() throws QException
{
insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic();
TestUtils.insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic();
assertEquals(1, queryTable(TestUtils.TABLE_NAME_ORDER).size());
assertEquals(4, queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).size());
@ -1350,28 +1350,6 @@ class QJavalinApiHandlerTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
private static void insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic() throws QException
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("orderNo", "ORD123").withValue("storeId", 47)
.withAssociatedRecord("orderLines", new QRecord().withValue("lineNumber", 1).withValue("sku", "BASIC1").withValue("quantity", 42)
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Size").withValue("value", "Medium"))
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Discount").withValue("value", "3.50"))
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Color").withValue("value", "Red")))
.withAssociatedRecord("orderLines", new QRecord().withValue("lineNumber", 2).withValue("sku", "BASIC2").withValue("quantity", 42)
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "Size").withValue("value", "Medium")))
.withAssociatedRecord("orderLines", new QRecord().withValue("lineNumber", 3).withValue("sku", "BASIC3").withValue("quantity", 42))
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "shopifyOrderNo").withValue("value", "#1032"))
));
new InsertAction().execute(insertInput);
}
/*******************************************************************************
**
*******************************************************************************/