From c3694302615bdf6f92545775e07ccc89a6dce97e Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 22 Mar 2023 18:38:14 -0500 Subject: [PATCH] implemented replacedBy on removed field; apiFieldName; exclude --- .../kingsrook/qqq/api/ApiMiddlewareType.java | 34 ---- .../actions/GenerateOpenApiSpecAction.java | 5 +- .../api/actions/GetTableApiFieldsAction.java | 35 ++-- .../qqq/api/actions/QRecordApiAdapter.java | 183 ++++++++++++++++++ .../qqq/api/javalin/QJavalinApiHandler.java | 73 +------ .../actions/GetTableApiFieldsOutput.java | 8 +- .../model/metadata/ApiInstanceMetaData.java | 12 +- .../metadata/fields/ApiFieldMetaData.java | 110 +++++++++++ .../metadata/tables/ApiTableMetaData.java | 36 +++- .../java/com/kingsrook/qqq/api/TestUtils.java | 41 +++- .../GenerateOpenApiSpecActionTest.java | 2 +- .../api/actions/QRecordApiAdapterTest.java | 160 +++++++++++++++ 12 files changed, 570 insertions(+), 129 deletions(-) create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java create mode 100644 qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/QRecordApiAdapterTest.java diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/ApiMiddlewareType.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/ApiMiddlewareType.java index 05c89945..800ca283 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/ApiMiddlewareType.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/ApiMiddlewareType.java @@ -22,14 +22,6 @@ package com.kingsrook.qqq.api; -import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; -import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; -import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; - - /******************************************************************************* ** *******************************************************************************/ @@ -37,30 +29,4 @@ public interface ApiMiddlewareType { String NAME = "api"; - /******************************************************************************* - ** - *******************************************************************************/ - static ApiInstanceMetaData getApiInstanceMetaData(QInstance instance) - { - return ((ApiInstanceMetaData) instance.getMiddlewareMetaData(NAME)); - } - - - /******************************************************************************* - ** - *******************************************************************************/ - static ApiTableMetaData getApiTableMetaData(QTableMetaData table) - { - return ((ApiTableMetaData) table.getMiddlewareMetaData(NAME)); - } - - - /******************************************************************************* - ** - *******************************************************************************/ - static ApiFieldMetaData getApiFieldMetaData(QFieldMetaData field) - { - return ((ApiFieldMetaData) field.getMiddlewareMetaData(NAME)); - } - } 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 332a8ba4..9c2f4b26 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 @@ -26,7 +26,6 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import com.kingsrook.qqq.api.ApiMiddlewareType; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput; import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsInput; @@ -82,7 +81,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version)).getFields(); + List tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version)).getFields(); String tableReadPermissionName = PermissionsHelper.getTablePermissionName(tableName, TablePermissionSubType.READ); if(StringUtils.hasContent(tableReadPermissionName)) diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsAction.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsAction.java index 1c036f93..c705b6a0 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsAction.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsAction.java @@ -24,7 +24,6 @@ package com.kingsrook.qqq.api.actions; import java.util.ArrayList; import java.util.List; -import com.kingsrook.qqq.api.ApiMiddlewareType; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.APIVersionRange; import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsInput; @@ -38,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import org.apache.commons.lang.BooleanUtils; /******************************************************************************* @@ -70,7 +70,7 @@ public class GetTableApiFieldsAction extends AbstractQActionFunction getRemovedApiFields(QTableMetaData table) { - ApiTableMetaData apiTableMetaData = ApiMiddlewareType.getApiTableMetaData(table); + ApiTableMetaData apiTableMetaData = ApiTableMetaData.of(table); if(apiTableMetaData != null) { return (apiTableMetaData.getRemovedApiFields()); 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 new file mode 100644 index 00000000..16c6602a --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java @@ -0,0 +1,183 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api.actions; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +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.actions.GetTableApiFieldsInput; +import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.utils.Pair; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import org.json.JSONObject; + + +/******************************************************************************* + ** Methods for going back and forth from QRecords to API-versions of objects + *******************************************************************************/ +public class QRecordApiAdapter +{ + private static Map, List> fieldListCache = new HashMap<>(); + private static Map, Map> fieldMapCache = new HashMap<>(); + + + + /******************************************************************************* + ** Convert a QRecord to a map for the API + *******************************************************************************/ + public static Map qRecordToApiMap(QRecord record, String tableName, String apiVersion) throws QException + { + List tableApiFields = getTableApiFieldList(tableName, apiVersion); + LinkedHashMap outputRecord = new LinkedHashMap<>(); + + ///////////////////////////////////////// + // iterate over the table's api fields // + ///////////////////////////////////////// + for(QFieldMetaData field : tableApiFields) + { + ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(field); + + // todo - what about display values / possible values? + + String apiFieldName = apiFieldMetaData.getApiFieldName(); + if(!StringUtils.hasContent(apiFieldName)) + { + apiFieldName = field.getName(); + } + + if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName())) + { + outputRecord.put(apiFieldName, record.getValue(apiFieldMetaData.getReplacedByFieldName())); + } + else + { + outputRecord.put(apiFieldName, record.getValue(field.getName())); + } + } + return (outputRecord); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QRecord apiJsonObjectToQRecord(JSONObject jsonObject, String tableName, String apiVersion) throws QException + { + //////////////////////////////////////////////////////////////////////////////// + // make map of apiFieldNames (e.g., names as api uses them) to QFieldMetaData // + //////////////////////////////////////////////////////////////////////////////// + Map apiFieldsMap = getTableApiFieldMap(tableName, apiVersion); + List unrecognizedFieldNames = new ArrayList<>(); + QRecord qRecord = new QRecord(); + + ////////////////////////////////////////// + // iterate over keys in the json object // + ////////////////////////////////////////// + for(String jsonKey : jsonObject.keySet()) + { + //////////////////////////////////////////////// + // if it's a valid api field name, process it // + //////////////////////////////////////////////// + if(apiFieldsMap.containsKey(jsonKey)) + { + QFieldMetaData field = apiFieldsMap.get(jsonKey); + Object value = jsonObject.get(jsonKey); + + ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(field); + if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName())) + { + qRecord.setValue(apiFieldMetaData.getReplacedByFieldName(), value); + } + else + { + qRecord.setValue(field.getName(), value); + } + } + else + { + /////////////////////////////////////////////////// + // else add it to the list of unrecognized names // + /////////////////////////////////////////////////// + unrecognizedFieldNames.add(jsonKey); + } + } + + if(!unrecognizedFieldNames.isEmpty()) + { + throw (new QBadRequestException("Request body contained " + unrecognizedFieldNames.size() + " unrecognized field name" + StringUtils.plural(unrecognizedFieldNames) + ": " + StringUtils.joinWithCommasAndAnd(unrecognizedFieldNames))); + } + + return (qRecord); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Map getTableApiFieldMap(String tableName, String apiVersion) throws QException + { + Pair key = new Pair<>(tableName, apiVersion); + if(!fieldMapCache.containsKey(key)) + { + Map map = getTableApiFieldList(tableName, apiVersion).stream().collect(Collectors.toMap(f -> + { + ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(f); + String apiFieldName = apiFieldMetaData.getApiFieldName(); + if(!StringUtils.hasContent(apiFieldName)) + { + apiFieldName = f.getName(); + } + return (apiFieldName); + }, f -> f)); + fieldMapCache.put(key, map); + } + + return (fieldMapCache.get(key)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static List getTableApiFieldList(String tableName, String apiVersion) throws QException + { + Pair key = new Pair<>(tableName, apiVersion); + if(!fieldListCache.containsKey(key)) + { + List value = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(apiVersion)).getFields(); + fieldListCache.put(key, value); + } + return (fieldListCache.get(key)); + } +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java index be6607de..0cf24fe6 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java @@ -31,15 +31,13 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.stream.Collectors; -import com.kingsrook.qqq.api.ApiMiddlewareType; import com.kingsrook.qqq.api.actions.GenerateOpenApiSpecAction; -import com.kingsrook.qqq.api.actions.GetTableApiFieldsAction; +import com.kingsrook.qqq.api.actions.QRecordApiAdapter; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.APIVersionRange; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput; -import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsInput; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; @@ -205,7 +203,7 @@ public class QJavalinApiHandler *******************************************************************************/ private static APIVersionRange getApiVersionRange(QTableMetaData table) { - ApiTableMetaData middlewareMetaData = ApiMiddlewareType.getApiTableMetaData(table); + ApiTableMetaData middlewareMetaData = ApiTableMetaData.of(table); if(middlewareMetaData != null && middlewareMetaData.getInitialVersion() != null) { return (APIVersionRange.afterAndIncluding(middlewareMetaData.getInitialVersion())); @@ -259,7 +257,7 @@ public class QJavalinApiHandler + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); } - LinkedHashMap outputRecord = toApiRecord(record, tableName, version); + Map outputRecord = QRecordApiAdapter.qRecordToApiMap(record, tableName, version); QJavalinAccessLogger.logEndSuccess(); context.result(JsonUtils.toJson(outputRecord)); @@ -477,7 +475,7 @@ public class QJavalinApiHandler ArrayList> records = new ArrayList<>(); for(QRecord record : queryOutput.getRecords()) { - records.add(toApiRecord(record, tableName, version)); + records.add(QRecordApiAdapter.qRecordToApiMap(record, tableName, version)); } output.put("records", records); @@ -516,7 +514,7 @@ public class QJavalinApiHandler } APIVersion requestApiVersion = new APIVersion(version); - List supportedVersions = ApiMiddlewareType.getApiInstanceMetaData(qInstance).getSupportedVersions(); + List supportedVersions = ApiInstanceMetaData.of(qInstance).getSupportedVersions(); if(CollectionUtils.nullSafeIsEmpty(supportedVersions) || !supportedVersions.contains(requestApiVersion)) { throw (new QNotFoundException("This version of this API does not contain the resource path " + context.path())); @@ -689,7 +687,7 @@ public class QJavalinApiHandler } JSONObject jsonObject = new JSONObject(context.body()); - insertInput.setRecords(List.of(toQRecord(jsonObject, tableName, version))); + insertInput.setRecords(List.of(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version))); } catch(QBadRequestException qbre) { @@ -757,7 +755,7 @@ public class QJavalinApiHandler for(int i = 0; i < jsonArray.length(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); - recordList.add(toQRecord(jsonObject, tableName, version)); + recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version)); } if(recordList.isEmpty()) @@ -862,7 +860,7 @@ public class QJavalinApiHandler } JSONObject jsonObject = new JSONObject(context.body()); - QRecord qRecord = toQRecord(jsonObject, tableName, version); + QRecord qRecord = QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version); qRecord.setValue(table.getPrimaryKeyField(), primaryKey); updateInput.setRecords(List.of(qRecord)); } @@ -956,59 +954,6 @@ public class QJavalinApiHandler - /******************************************************************************* - ** - *******************************************************************************/ - private static LinkedHashMap toApiRecord(QRecord record, String tableName, String apiVersion) throws QException - { - List tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(apiVersion)).getFields(); - LinkedHashMap outputRecord = new LinkedHashMap<>(); - for(QFieldMetaData tableApiField : tableApiFields) - { - // todo - what about display values / possible values - // todo - handle removed-from-this-version fields!! - outputRecord.put(tableApiField.getName(), record.getValue(tableApiField.getName())); - } - return (outputRecord); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private static QRecord toQRecord(JSONObject jsonObject, String tableName, String apiVersion) throws QException - { - List unrecognizedFieldNames = new ArrayList<>(); - - List tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(apiVersion)).getFields(); - Map apiFieldsMap = tableApiFields.stream().collect(Collectors.toMap(f -> f.getName(), f -> f)); - - QRecord qRecord = new QRecord(); - - for(String jsonKey : jsonObject.keySet()) - { - if(apiFieldsMap.containsKey(jsonKey)) - { - QFieldMetaData field = apiFieldsMap.get(jsonKey); - qRecord.setValue(field.getName(), jsonObject.get(jsonKey)); - } - else - { - unrecognizedFieldNames.add(jsonKey); - } - } - - if(!unrecognizedFieldNames.isEmpty()) - { - throw (new QBadRequestException("Request body contained " + unrecognizedFieldNames.size() + " unrecognized field name" + StringUtils.plural(unrecognizedFieldNames) + ": " + StringUtils.joinWithCommasAndAnd(unrecognizedFieldNames))); - } - - return (qRecord); - } - - - /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GetTableApiFieldsOutput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GetTableApiFieldsOutput.java index 7054d037..1f66404c 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GetTableApiFieldsOutput.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GetTableApiFieldsOutput.java @@ -32,14 +32,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; *******************************************************************************/ public class GetTableApiFieldsOutput extends AbstractActionOutput { - private List fields; + private List fields; /******************************************************************************* ** Getter for fields *******************************************************************************/ - public List getFields() + public List getFields() { return (this.fields); } @@ -49,7 +49,7 @@ public class GetTableApiFieldsOutput extends AbstractActionOutput /******************************************************************************* ** Setter for fields *******************************************************************************/ - public void setFields(List fields) + public void setFields(List fields) { this.fields = fields; } @@ -59,7 +59,7 @@ public class GetTableApiFieldsOutput extends AbstractActionOutput /******************************************************************************* ** Fluent setter for fields *******************************************************************************/ - public GetTableApiFieldsOutput withFields(List fields) + public GetTableApiFieldsOutput withFields(List fields) { this.fields = fields; return (this); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java index b67daea2..c2e4707b 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java @@ -63,6 +63,16 @@ public class ApiInstanceMetaData extends QMiddlewareInstanceMetaData + /******************************************************************************* + ** + *******************************************************************************/ + public static ApiInstanceMetaData of(QInstance qInstance) + { + return ((ApiInstanceMetaData) qInstance.getMiddlewareMetaData(ApiMiddlewareType.NAME)); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -103,7 +113,7 @@ public class ApiInstanceMetaData extends QMiddlewareInstanceMetaData ///////////////////////////////// for(QTableMetaData table : qInstance.getTables().values()) { - ApiTableMetaData apiTableMetaData = ApiMiddlewareType.getApiTableMetaData(table); + ApiTableMetaData apiTableMetaData = ApiTableMetaData.of(table); if(apiTableMetaData != null) { validator.assertCondition(allVersions.contains(new APIVersion(apiTableMetaData.getInitialVersion())), "Table " + table.getName() + "'s initial API version is not a recognized version."); 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 5fa013f3..47bc1026 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 @@ -22,6 +22,8 @@ package com.kingsrook.qqq.api.model.metadata.fields; +import com.kingsrook.qqq.api.ApiMiddlewareType; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QMiddlewareFieldMetaData; @@ -33,6 +35,11 @@ public class ApiFieldMetaData extends QMiddlewareFieldMetaData private String initialVersion; private String finalVersion; + private String apiFieldName; + + private Boolean isExcluded; + private String replacedByFieldName; + /******************************************************************************* @@ -46,6 +53,16 @@ public class ApiFieldMetaData extends QMiddlewareFieldMetaData + /******************************************************************************* + ** + *******************************************************************************/ + public static ApiFieldMetaData of(QFieldMetaData field) + { + return ((ApiFieldMetaData) field.getMiddlewareMetaData(ApiMiddlewareType.NAME)); + } + + + /******************************************************************************* ** Getter for initialVersion *******************************************************************************/ @@ -106,4 +123,97 @@ public class ApiFieldMetaData extends QMiddlewareFieldMetaData return (this); } + + + /******************************************************************************* + ** Getter for replacedByFieldName + *******************************************************************************/ + public String getReplacedByFieldName() + { + return (this.replacedByFieldName); + } + + + + /******************************************************************************* + ** Setter for replacedByFieldName + *******************************************************************************/ + public void setReplacedByFieldName(String replacedByFieldName) + { + this.replacedByFieldName = replacedByFieldName; + } + + + + /******************************************************************************* + ** Fluent setter for replacedByFieldName + *******************************************************************************/ + public ApiFieldMetaData withReplacedByFieldName(String replacedByFieldName) + { + this.replacedByFieldName = replacedByFieldName; + 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 ApiFieldMetaData withIsExcluded(Boolean isExcluded) + { + this.isExcluded = isExcluded; + return (this); + } + + + + /******************************************************************************* + ** Getter for apiFieldName + *******************************************************************************/ + public String getApiFieldName() + { + return (this.apiFieldName); + } + + + + /******************************************************************************* + ** Setter for apiFieldName + *******************************************************************************/ + public void setApiFieldName(String apiFieldName) + { + this.apiFieldName = apiFieldName; + } + + + + /******************************************************************************* + ** Fluent setter for apiFieldName + *******************************************************************************/ + public ApiFieldMetaData withApiFieldName(String apiFieldName) + { + this.apiFieldName = apiFieldName; + 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 f48fd7f0..a3cd975b 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 @@ -29,6 +29,7 @@ import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QMiddlewareTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* @@ -43,6 +44,16 @@ public class ApiTableMetaData extends QMiddlewareTableMetaData + /******************************************************************************* + ** + *******************************************************************************/ + public static ApiTableMetaData of(QTableMetaData table) + { + return ((ApiTableMetaData) table.getMiddlewareMetaData(ApiMiddlewareType.NAME)); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -55,12 +66,16 @@ public class ApiTableMetaData extends QMiddlewareTableMetaData { for(QFieldMetaData field : table.getFields().values()) { - if(field.getMiddlewareMetaData(ApiMiddlewareType.NAME) == null) + ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiMiddlewareMetaData(field); + if(apiFieldMetaData.getInitialVersion() == null) { - field.withMiddlewareMetaData(new ApiFieldMetaData()); + apiFieldMetaData.setInitialVersion(initialVersion); } + } - ApiFieldMetaData apiFieldMetaData = (ApiFieldMetaData) field.getMiddlewareMetaData(ApiMiddlewareType.NAME); + for(QFieldMetaData field : CollectionUtils.nonNullList(removedApiFields)) + { + ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiMiddlewareMetaData(field); if(apiFieldMetaData.getInitialVersion() == null) { apiFieldMetaData.setInitialVersion(initialVersion); @@ -71,6 +86,21 @@ public class ApiTableMetaData extends QMiddlewareTableMetaData + /******************************************************************************* + ** + *******************************************************************************/ + private static ApiFieldMetaData ensureFieldHasApiMiddlewareMetaData(QFieldMetaData field) + { + if(field.getMiddlewareMetaData(ApiMiddlewareType.NAME) == null) + { + field.withMiddlewareMetaData(new ApiFieldMetaData()); + } + + return (ApiFieldMetaData.of(field)); + } + + + /******************************************************************************* ** Constructor ** 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 3e082426..6517d1c6 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 @@ -25,6 +25,7 @@ package com.kingsrook.qqq.api; import java.util.List; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; +import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; @@ -46,7 +47,11 @@ public class TestUtils public static final String MEMORY_BACKEND_NAME = "memory"; public static final String TABLE_NAME_PERSON = "person"; - public static final String API_VERSION = "2023.Q1"; + public static final String V2023_Q1 = "2023.Q1"; + public static final String V2022_Q4 = "2022.Q4"; + public static final String V2023_Q2 = "2023.Q2"; + + public static final String CURRENT_API_VERSION = V2023_Q1; @@ -65,8 +70,10 @@ public class TestUtils .withName("TestAPI") .withDescription("QQQ Test API") .withContactEmail("contact@kingsrook.com") - .withCurrentVersion(new APIVersion(API_VERSION)) - .withSupportedVersions(List.of(new APIVersion(API_VERSION))) + .withCurrentVersion(new APIVersion(CURRENT_API_VERSION)) + .withSupportedVersions(List.of(new APIVersion(V2022_Q4), new APIVersion(V2023_Q1))) + .withPastVersions(List.of(new APIVersion(V2022_Q4))) + .withFutureVersions(List.of(new APIVersion(V2023_Q2))) ); return (qInstance); @@ -95,7 +102,16 @@ public class TestUtils .withName(TABLE_NAME_PERSON) .withLabel("Person") .withBackendName(MEMORY_BACKEND_NAME) - .withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(API_VERSION)) + .withMiddlewareMetaData(new ApiTableMetaData() + .withInitialVersion(V2022_Q4) + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // in 2022.Q4, this table had a "shoeCount" field. but for the 2023.Q1 version, we renamed it to noOfShoes! // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + .withRemovedApiField(new QFieldMetaData("shoeCount", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS) + .withMiddlewareMetaData(new ApiFieldMetaData().withFinalVersion(V2022_Q4).withReplacedByFieldName("noOfShoes"))) + + ) .withPrimaryKeyField("id") .withUniqueKey(new UniqueKey("email")) .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) @@ -103,14 +119,23 @@ public class TestUtils .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) .withField(new QFieldMetaData("firstName", QFieldType.STRING)) .withField(new QFieldMetaData("lastName", QFieldType.STRING)) - .withField(new QFieldMetaData("birthDate", QFieldType.DATE)) + .withField(new QFieldMetaData("birthDate", QFieldType.DATE) + .withMiddlewareMetaData(new ApiFieldMetaData().withApiFieldName("birthDay")) + ) .withField(new QFieldMetaData("email", QFieldType.STRING)) // .withField(new QFieldMetaData("homeStateId", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_STATE)) // .withField(new QFieldMetaData("favoriteShapeId", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_SHAPE)) // .withField(new QFieldMetaData("customValue", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_CUSTOM)) - .withField(new QFieldMetaData("noOfShoes", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS)) - .withField(new QFieldMetaData("cost", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY)) - .withField(new QFieldMetaData("price", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY)) + .withField(new QFieldMetaData("noOfShoes", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS) + .withMiddlewareMetaData(new ApiFieldMetaData().withInitialVersion(V2023_Q1))) + + ///////////////////////////////////////////////////////////////// + // 2 new fields - they'll appear in future versions of the API // + ///////////////////////////////////////////////////////////////// + .withField(new QFieldMetaData("cost", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY) + .withMiddlewareMetaData(new ApiFieldMetaData().withInitialVersion(V2023_Q2))) + .withField(new QFieldMetaData("price", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY) + .withMiddlewareMetaData(new ApiFieldMetaData().withIsExcluded(true))) ; } diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecActionTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecActionTest.java index 4d28bcec..445ca4f0 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecActionTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecActionTest.java @@ -49,7 +49,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest @Test void test() throws QException { - String version = TestUtils.API_VERSION; + String version = TestUtils.V2023_Q1; QInstance qInstance = QContext.getQInstance(); qInstance.withMiddlewareMetaData(new ApiInstanceMetaData() diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/QRecordApiAdapterTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/QRecordApiAdapterTest.java new file mode 100644 index 00000000..ee872107 --- /dev/null +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/QRecordApiAdapterTest.java @@ -0,0 +1,160 @@ +/* + * 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.math.BigDecimal; +import java.time.LocalDate; +import java.time.Month; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.api.BaseTest; +import com.kingsrook.qqq.api.TestUtils; +import com.kingsrook.qqq.api.javalin.QBadRequestException; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + + +/******************************************************************************* + ** Unit test for QRecordApiAdapter + *******************************************************************************/ +class QRecordApiAdapterTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQRecordToApiMap() throws QException + { + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + // QRecord has values corresponding to what's defined in the QInstance (and the underlying backend system) // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + QRecord person = new QRecord() + .withValue("firstName", "Tim") + .withValue("noOfShoes", 2) + .withValue("birthDate", LocalDate.of(1980, Month.MAY, 31)) + .withValue("cost", new BigDecimal("3.50")) + .withValue("price", new BigDecimal("9.99")); + + Map pastApiRecord = QRecordApiAdapter.qRecordToApiMap(person, TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4); + assertEquals(2, pastApiRecord.get("shoeCount")); // old field name - not currently in the QTable, but we can still get its value! + assertFalse(pastApiRecord.containsKey("noOfShoes")); // current field name - doesn't appear in old api-version + assertFalse(pastApiRecord.containsKey("cost")); // a current field name, but also not in this old api version + + Map currentApiRecord = QRecordApiAdapter.qRecordToApiMap(person, TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1); + assertFalse(currentApiRecord.containsKey("shoeCount")); // old field name - not in this current api version + assertEquals(2, currentApiRecord.get("noOfShoes")); // current field name - value here as we expect + assertFalse(currentApiRecord.containsKey("cost")); // future field name - not in the current api (we added the field during new dev, and didn't change the api) + + Map futureApiRecord = QRecordApiAdapter.qRecordToApiMap(person, TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q2); + assertFalse(futureApiRecord.containsKey("shoeCount")); // old field name - also not in this future api version + assertEquals(2, futureApiRecord.get("noOfShoes")); // current field name - still here. + assertEquals(new BigDecimal("3.50"), futureApiRecord.get("cost")); // future field name appears now that we've requested this future api version. + + for(Map apiRecord : List.of(pastApiRecord, currentApiRecord, futureApiRecord)) + { + assertEquals(LocalDate.parse("1980-05-31"), apiRecord.get("birthDay")); // use the apiFieldName + assertFalse(apiRecord.containsKey("price")); // excluded field never appears + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testApiJsonObjectToQRecord() throws QException + { + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // past version took shoeCount - so we still take that, but now put it in noOfShoes field of qRecord // + /////////////////////////////////////////////////////////////////////////////////////////////////////// + QRecord recordFromOldApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" + {"firstName": "Tim", "shoeCount": 2} + """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4); + assertEquals(2, recordFromOldApi.getValueInteger("noOfShoes")); + + /////////////////////////////////////////// + // current version takes it as noOfShoes // + /////////////////////////////////////////// + QRecord recordFromCurrentApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" + {"firstName": "Tim", "noOfShoes": 2} + """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1); + assertEquals(2, recordFromCurrentApi.getValueInteger("noOfShoes")); + + ///////////////////////////////////////////// + // future version supports cost field too! // + ///////////////////////////////////////////// + QRecord recordFromFutureApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" + {"firstName": "Tim", "noOfShoes": 2, "cost": 3.50} + """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q2); + assertEquals(2, recordFromFutureApi.getValueInteger("noOfShoes")); + assertEquals(new BigDecimal("3.50"), recordFromFutureApi.getValueBigDecimal("cost")); + + /////////////////////////////////////////////////////////////////// + // make sure apiFieldName is used (instead of table's fieldName) // + /////////////////////////////////////////////////////////////////// + QRecord recordWithApiFieldName = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" + {"firstName": "Tim", "birthDay": "1976-05-28"} + """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q2); + assertEquals("1976-05-28", recordWithApiFieldName.getValueString("birthDate")); + + //////////////////////////////////////////////////////////////////////////////////////////////// + // past version didn't have noOfShoes field (they called it shoeCount) -- fail if it was sent // + //////////////////////////////////////////////////////////////////////////////////////////////// + assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" + {"firstName": "Tim", "noOfShoes": 2} + """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4)) + .isInstanceOf(QBadRequestException.class) + .hasMessageContaining("unrecognized field name: noOfShoes"); + + ///////////////////////////////////////////////////////////////////////// + // current version doesn't have cost field - fail if you send it to us // + ///////////////////////////////////////////////////////////////////////// + assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" + {"firstName": "Tim", "cost": 2} + """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1)) + .isInstanceOf(QBadRequestException.class) + .hasMessageContaining("unrecognized field name: cost"); + + ///////////////////////////////// + // excluded field always fails // + ///////////////////////////////// + for(String version : List.of(TestUtils.V2022_Q4, TestUtils.V2023_Q1, TestUtils.V2023_Q2)) + { + assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" + {"firstName": "Tim", "price": 2} + """), TestUtils.TABLE_NAME_PERSON, version)) + .isInstanceOf(QBadRequestException.class) + .hasMessageContaining("unrecognized field name: price"); + } + + } + +} \ No newline at end of file