From 360bf56481aaba4f86c2a67b198e1c38eb276c67 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 28 Jun 2023 11:06:15 -0500 Subject: [PATCH] Add association api-meta data (so they can be versioned or excluded); add api field custom value mapper --- .../qqq/api/actions/ApiImplementation.java | 4 + .../actions/GenerateOpenApiSpecAction.java | 30 ++- .../qqq/api/actions/QRecordApiAdapter.java | 53 ++++- .../actions/ApiFieldCustomValueMapper.java | 59 +++++ .../metadata/ApiInstanceMetaDataProvider.java | 5 + .../metadata/fields/ApiFieldMetaData.java | 37 +++- .../fields/ApiFieldMetaDataContainer.java | 18 ++ .../tables/ApiAssociationMetaData.java | 147 +++++++++++++ .../metadata/tables/ApiTableMetaData.java | 50 +++++ .../tables/ApiTableMetaDataContainer.java | 37 ++++ .../java/com/kingsrook/qqq/api/TestUtils.java | 22 ++ .../api/actions/ApiImplementationTest.java | 207 ++++++++++++++++++ .../api/javalin/QJavalinApiHandlerTest.java | 32 +-- 13 files changed, 666 insertions(+), 35 deletions(-) create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/ApiFieldCustomValueMapper.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiAssociationMetaData.java create mode 100644 qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/ApiImplementationTest.java 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 315d9ed4..1a2bc025 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 @@ -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) { diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java index acb85f13..8783965e 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java @@ -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 tableApiFields) + private Schema buildTableSchema(ApiInstanceMetaData apiInstanceMetaData, String version, QTableMetaData table, List tableApiFields) { LinkedHashMap tableFields = new LinkedHashMap<>(); Schema tableSchema = new Schema() @@ -1225,7 +1226,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction 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 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() 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 ab9e982c..c8e5635c 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 @@ -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> 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; + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -137,7 +180,10 @@ public class QRecordApiAdapter QTableMetaData table = QContext.getQInstance().getTable(tableName); for(Association association : CollectionUtils.nonNullList(table.getAssociations())) { - associationMap.put(association.getName(), association); + if(!isAssociationOmitted(apiName, apiVersion, table, association)) + { + associationMap.put(association.getName(), association); + } } ////////////////////////////////////////// @@ -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); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/ApiFieldCustomValueMapper.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/ApiFieldCustomValueMapper.java new file mode 100644 index 00000000..26f0b3cd --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/ApiFieldCustomValueMapper.java @@ -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 . + */ + +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 // + ///////////////////// + } + +} 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 e4308096..a46c33dc 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 @@ -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()); } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java index d03c7fa4..3d479445 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java @@ -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; @@ -39,8 +40,9 @@ public class ApiFieldMetaData private String apiFieldName; private String description; - private Boolean isExcluded; - private String replacedByFieldName; + private Boolean isExcluded; + private String replacedByFieldName; + private QCodeReference customValueMapper; private Example example; private Map 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); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java index fa9fe94a..9d37ac00 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java @@ -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!! diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiAssociationMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiAssociationMetaData.java new file mode 100644 index 00000000..42dddf5c --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiAssociationMetaData.java @@ -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 . + */ + +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); + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaData.java index ec21098b..0abc4547 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaData.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaData.java @@ -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 enabledOperations = new HashSet<>(); private Set disabledOperations = new HashSet<>(); + private Map apiAssociationMetaData = new HashMap<>(); + /******************************************************************************* @@ -410,4 +414,50 @@ public class ApiTableMetaData implements ApiOperation.EnabledOperationsProvider return (this); } + + + /******************************************************************************* + ** Getter for apiAssociationMetaData + *******************************************************************************/ + public Map getApiAssociationMetaData() + { + return (this.apiAssociationMetaData); + } + + + + /******************************************************************************* + ** Setter for apiAssociationMetaData + *******************************************************************************/ + public void setApiAssociationMetaData(Map apiAssociationMetaData) + { + this.apiAssociationMetaData = apiAssociationMetaData; + } + + + + /******************************************************************************* + ** Fluent setter for apiAssociationMetaData + *******************************************************************************/ + public ApiTableMetaData withApiAssociationMetaData(Map 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); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java index 8dd779fc..1735a656 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java @@ -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 *******************************************************************************/ 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 fea81122..1cc32785 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 @@ -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); + } + + + /******************************************************************************* ** *******************************************************************************/ 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 new file mode 100644 index 00000000..31fd442b --- /dev/null +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/ApiImplementationTest.java @@ -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 . + */ + +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 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 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 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); + } + + } + +} \ No newline at end of file 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 99ba063e..fb564198 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 @@ -219,7 +219,7 @@ class QJavalinApiHandlerTest extends BaseTest @Test void testGetAssociations() throws QException { - insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic(); + TestUtils.insert1Order3Lines4LineExtrinsicsAnd1OrderExtrinsic(); HttpResponse 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 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 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 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); - } - - - /******************************************************************************* ** *******************************************************************************/