diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ObjectUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ObjectUtils.java index 918aa497..3c16e64f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ObjectUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ObjectUtils.java @@ -22,6 +22,9 @@ package com.kingsrook.qqq.backend.core.utils; +import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier; + + /******************************************************************************* ** *******************************************************************************/ @@ -48,4 +51,49 @@ public class ObjectUtils throw (new NullPointerException("all null objects")); } + + + + /******************************************************************************* + ** Like Objects.requireNonNullElse, only use an (unsafe) supplier as the first + ** arg, and only if it throws, return the 2nd arg + *******************************************************************************/ + public static T tryElse(UnsafeSupplier supplier, T defaultIfThrew) + { + try + { + return (supplier.get()); + } + catch(Exception e) + { + return (defaultIfThrew); + } + } + + + + /******************************************************************************* + ** Like Objects.requireNonNullElse, only use an (unsafe) supplier as the first + ** arg, and if it throws or returns null, then return the 2nd arg + *******************************************************************************/ + public static T tryAndRequireNonNullElse(UnsafeSupplier supplier, T defaultIfThrew) + { + try + { + T t = supplier.get(); + if(t != null) + { + return (t); + } + } + catch(Exception e) + { + ////////// + // noop // + ////////// + } + + return (defaultIfThrew); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ObjectUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ObjectUtilsTest.java index 53e0a874..c08486b5 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ObjectUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ObjectUtilsTest.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.utils; 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.assertNull; /******************************************************************************* @@ -36,8 +37,9 @@ class ObjectUtilsTest /******************************************************************************* ** *******************************************************************************/ + @SuppressWarnings({ "DataFlowIssue", "ConfusingArgumentToVarargsMethod" }) @Test - void test() + void testRequireNonNullElse() { assertThatThrownBy(() -> ObjectUtils.requireNonNullElse(null)).isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> ObjectUtils.requireNonNullElse(null, null)).isInstanceOf(NullPointerException.class); @@ -47,4 +49,34 @@ class ObjectUtilsTest assertEquals("c", ObjectUtils.requireNonNullElse(null, null, "c")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings({ "StringOperationCanBeSimplified", "DataFlowIssue" }) + @Test + void testTryElse() + { + String nullString = null; + assertEquals("tried", ObjectUtils.tryElse(() -> "tried".toString(), "else")); + assertEquals("else", ObjectUtils.tryElse(() -> nullString.toString(), "else")); + assertNull(ObjectUtils.tryElse(() -> null, "else")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings({ "StringOperationCanBeSimplified", "DataFlowIssue" }) + @Test + void testTryAndRequireNonNullElse() + { + String nullString = null; + assertEquals("tried", ObjectUtils.tryAndRequireNonNullElse(() -> "tried".toString(), "else")); + assertEquals("else", ObjectUtils.tryAndRequireNonNullElse(() -> nullString.toString(), "else")); + assertEquals("else", ObjectUtils.tryAndRequireNonNullElse(() -> null, "else")); + } + } \ No newline at end of file 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 9efda835..aab67fe5 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 @@ -30,15 +30,16 @@ import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; 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.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; 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; import com.kingsrook.qqq.api.model.openapi.Contact; import com.kingsrook.qqq.api.model.openapi.Content; @@ -187,21 +188,43 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version)).getFields(); + String primaryKeyApiName = ApiFieldMetaData.getEffectiveApiFieldName(apiName, primaryKeyField); + List tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version).withApiName(apiName)).getFields(); /////////////////////////////// // permissions for the table // @@ -365,13 +395,13 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction ApiTableMetaDataContainer.of(associatedTable).getApiTableMetaData(apiName), new ApiTableMetaData()); String associatedTableApiName = StringUtils.hasContent(associatedApiTableMetaData.getApiTableName()) ? associatedApiTableMetaData.getApiTableName() : associatedTableName; tableSchema.getProperties().put(association.getName(), new Schema() 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 8f5809d9..d908ae37 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 @@ -30,13 +30,16 @@ import com.kingsrook.qqq.api.model.APIVersionRange; import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsInput; import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsOutput; 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.ApiTableMetaData; +import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; 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.ObjectUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.apache.commons.lang.BooleanUtils; @@ -73,7 +76,7 @@ public class GetTableApiFieldsAction extends AbstractQActionFunction getRemovedApiFields(QTableMetaData table) + private static ApiFieldMetaData getApiFieldMetaData(String apiName, QFieldMetaData field) { - ApiTableMetaData apiTableMetaData = ApiTableMetaData.of(table); + return ObjectUtils.tryAndRequireNonNullElse(() -> ApiFieldMetaDataContainer.of(field).getApiFieldMetaData(apiName), new ApiFieldMetaData()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private List getRemovedApiFields(String apiName, QTableMetaData table) + { + ApiTableMetaData apiTableMetaData = ObjectUtils.tryAndRequireNonNullElse(() -> ApiTableMetaDataContainer.of(table).getApiTableMetaData(apiName), new ApiTableMetaData()); 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 index d2547f26..b7de314f 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 @@ -32,6 +32,7 @@ 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.api.model.metadata.fields.ApiFieldMetaDataContainer; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; @@ -40,7 +41,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; 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.Pair; +import com.kingsrook.qqq.backend.core.utils.ObjectUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.json.JSONArray; import org.json.JSONObject; @@ -53,17 +54,17 @@ public class QRecordApiAdapter { private static final QLogger LOG = QLogger.getLogger(QRecordApiAdapter.class); - private static Map, List> fieldListCache = new HashMap<>(); - private static Map, Map> fieldMapCache = new HashMap<>(); + private static Map> fieldListCache = new HashMap<>(); + private static 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 + public static Map qRecordToApiMap(QRecord record, String tableName, String apiName, String apiVersion) throws QException { - List tableApiFields = getTableApiFieldList(tableName, apiVersion); + List tableApiFields = getTableApiFieldList(new ApiNameVersionAndTableName(apiName, apiVersion, tableName)); LinkedHashMap outputRecord = new LinkedHashMap<>(); ///////////////////////////////////////// @@ -71,9 +72,8 @@ public class QRecordApiAdapter ///////////////////////////////////////// for(QFieldMetaData field : tableApiFields) { - ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(field); - - String apiFieldName = ApiFieldMetaData.getEffectiveApiFieldName(field); + ApiFieldMetaData apiFieldMetaData = ObjectUtils.tryAndRequireNonNullElse(() -> ApiFieldMetaDataContainer.of(field).getApiFieldMetaData(apiName), new ApiFieldMetaData()); + String apiFieldName = ApiFieldMetaData.getEffectiveApiFieldName(apiName, field); if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName())) { outputRecord.put(apiFieldName, record.getValue(apiFieldMetaData.getReplacedByFieldName())); @@ -96,7 +96,7 @@ public class QRecordApiAdapter for(QRecord associatedRecord : CollectionUtils.nonNullList(CollectionUtils.nonNullMap(record.getAssociatedRecords()).get(association.getName()))) { - associationList.add(qRecordToApiMap(associatedRecord, association.getAssociatedTableName(), apiVersion)); + associationList.add(qRecordToApiMap(associatedRecord, association.getAssociatedTableName(), apiName, apiVersion)); } } @@ -108,12 +108,12 @@ public class QRecordApiAdapter /******************************************************************************* ** *******************************************************************************/ - public static QRecord apiJsonObjectToQRecord(JSONObject jsonObject, String tableName, String apiVersion, boolean includePrimaryKey) throws QException + public static QRecord apiJsonObjectToQRecord(JSONObject jsonObject, String tableName, String apiName, String apiVersion, boolean includePrimaryKey) throws QException { //////////////////////////////////////////////////////////////////////////////// // make map of apiFieldNames (e.g., names as api uses them) to QFieldMetaData // //////////////////////////////////////////////////////////////////////////////// - Map apiFieldsMap = getTableApiFieldMap(tableName, apiVersion); + Map apiFieldsMap = getTableApiFieldMap(new ApiNameVersionAndTableName(apiName, apiVersion, tableName)); List unrecognizedFieldNames = new ArrayList<>(); QRecord qRecord = new QRecord(); @@ -153,7 +153,7 @@ public class QRecordApiAdapter } } - ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(field); + ApiFieldMetaData apiFieldMetaData = ObjectUtils.tryAndRequireNonNullElse(() -> ApiFieldMetaDataContainer.of(field).getApiFieldMetaData(apiName), new ApiFieldMetaData()); if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName())) { qRecord.setValue(apiFieldMetaData.getReplacedByFieldName(), value); @@ -178,7 +178,7 @@ public class QRecordApiAdapter { if(subObject instanceof JSONObject subJsonObject) { - QRecord subRecord = apiJsonObjectToQRecord(subJsonObject, association.getAssociatedTableName(), apiVersion, includePrimaryKey); + QRecord subRecord = apiJsonObjectToQRecord(subJsonObject, association.getAssociatedTableName(), apiName, apiVersion, includePrimaryKey); qRecord.withAssociatedRecord(association.getName(), subRecord); } else @@ -214,16 +214,15 @@ public class QRecordApiAdapter /******************************************************************************* ** *******************************************************************************/ - private static Map getTableApiFieldMap(String tableName, String apiVersion) throws QException + private static Map getTableApiFieldMap(ApiNameVersionAndTableName apiNameVersionAndTableName) throws QException { - Pair key = new Pair<>(tableName, apiVersion); - if(!fieldMapCache.containsKey(key)) + if(!fieldMapCache.containsKey(apiNameVersionAndTableName)) { - Map map = getTableApiFieldList(tableName, apiVersion).stream().collect(Collectors.toMap(f -> (ApiFieldMetaData.getEffectiveApiFieldName(f)), f -> f)); - fieldMapCache.put(key, map); + Map map = getTableApiFieldList(apiNameVersionAndTableName).stream().collect(Collectors.toMap(f -> (ApiFieldMetaData.getEffectiveApiFieldName(apiNameVersionAndTableName.apiName(), f)), f -> f)); + fieldMapCache.put(apiNameVersionAndTableName, map); } - return (fieldMapCache.get(key)); + return (fieldMapCache.get(apiNameVersionAndTableName)); } @@ -231,14 +230,27 @@ public class QRecordApiAdapter /******************************************************************************* ** *******************************************************************************/ - private static List getTableApiFieldList(String tableName, String apiVersion) throws QException + private static List getTableApiFieldList(ApiNameVersionAndTableName apiNameVersionAndTableName) throws QException { - Pair key = new Pair<>(tableName, apiVersion); - if(!fieldListCache.containsKey(key)) + if(!fieldListCache.containsKey(apiNameVersionAndTableName)) { - List value = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(apiVersion)).getFields(); - fieldListCache.put(key, value); + List value = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput() + .withTableName(apiNameVersionAndTableName.tableName()) + .withVersion(apiNameVersionAndTableName.apiVersion()) + .withApiName(apiNameVersionAndTableName.apiName())).getFields(); + fieldListCache.put(apiNameVersionAndTableName, value); } - return (fieldListCache.get(key)); + return (fieldListCache.get(apiNameVersionAndTableName)); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private record ApiNameVersionAndTableName(String apiName, String apiVersion, String tableName) + { + + } + } 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 b6e723f1..f7357b3a 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 @@ -44,7 +44,9 @@ import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput; import com.kingsrook.qqq.api.model.metadata.APILogMetaDataProvider; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; 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.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; @@ -92,6 +94,7 @@ import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModu import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.Pair; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; @@ -121,7 +124,10 @@ public class QJavalinApiHandler private static QInstance qInstance; - private static Map> tableApiNameMap = new HashMap<>(); + ///////////////////////////////////// + // key: Pair // + ///////////////////////////////////// + private static Map, Map> tableApiNameMap = new HashMap<>(); private static Map apiLogUserIdCache = new HashMap<>(); @@ -156,56 +162,59 @@ public class QJavalinApiHandler ApiBuilder.get("/api/docs/js/rapidoc.min.js", (context) -> QJavalinApiHandler.serveResource(context, "rapidoc/rapidoc-9.3.4.min.js", MapBuilder.of("Content-Type", ContentType.JAVASCRIPT))); ApiBuilder.get("/api/docs/css/qqq-api-styles.css", (context) -> QJavalinApiHandler.serveResource(context, "rapidoc/rapidoc-overrides.css", MapBuilder.of("Content-Type", ContentType.CSS))); - ////////////////////////////////////////////// - // default page is the current version spec // - ////////////////////////////////////////////// - ApiBuilder.get("/api/", QJavalinApiHandler::doSpecHtml); - - ApiBuilder.path("/api/{version}", () -> // todo - configurable, that /api/ bit? + ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(qInstance); + for(Map.Entry entry : apiInstanceMetaDataContainer.getApis().entrySet()) { - //////////////////////////////////////////// - // default page for a version is its spec // - //////////////////////////////////////////// - ApiBuilder.get("/", QJavalinApiHandler::doSpecHtml); + ApiInstanceMetaData apiInstanceMetaData = entry.getValue(); + String rootPath = apiInstanceMetaData.getPath(); - ApiBuilder.get("/openapi.yaml", QJavalinApiHandler::doSpecYaml); - ApiBuilder.get("/openapi.json", QJavalinApiHandler::doSpecJson); - ApiBuilder.get("/openapi.html", QJavalinApiHandler::doSpecHtml); + ////////////////////////////////////////////// + // default page is the current version spec // + ////////////////////////////////////////////// + ApiBuilder.get(rootPath, context -> doSpecHtml(context, apiInstanceMetaData)); - ApiBuilder.path("/{tableName}", () -> + ApiBuilder.path(rootPath + "{version}", () -> { - ApiBuilder.get("/openapi.yaml", QJavalinApiHandler::doSpecYaml); - ApiBuilder.get("/openapi.json", QJavalinApiHandler::doSpecJson); + //////////////////////////////////////////// + // default page for a version is its spec // + //////////////////////////////////////////// + ApiBuilder.get("/", context -> doSpecHtml(context, apiInstanceMetaData)); - ApiBuilder.post("/", QJavalinApiHandler::doInsert); + ApiBuilder.get("/openapi.yaml", context -> doSpecYaml(context, apiInstanceMetaData)); + ApiBuilder.get("/openapi.json", context -> doSpecJson(context, apiInstanceMetaData)); + ApiBuilder.get("/openapi.html", context -> doSpecHtml(context, apiInstanceMetaData)); - ApiBuilder.get("/query", QJavalinApiHandler::doQuery); - // ApiBuilder.post("/query", QJavalinApiHandler::doQuery); + ApiBuilder.path("/{tableName}", () -> + { + ApiBuilder.get("/openapi.yaml", context -> doSpecYaml(context, apiInstanceMetaData)); + ApiBuilder.get("/openapi.json", context -> doSpecJson(context, apiInstanceMetaData)); - ApiBuilder.post("/bulk", QJavalinApiHandler::bulkInsert); - ApiBuilder.patch("/bulk", QJavalinApiHandler::bulkUpdate); - ApiBuilder.delete("/bulk", QJavalinApiHandler::bulkDelete); + ApiBuilder.post("/", context -> doInsert(context, apiInstanceMetaData)); - ////////////////////////////////////////////////////////////////// - // remember to keep the wildcard paths after the specific paths // - ////////////////////////////////////////////////////////////////// - ApiBuilder.get("/{primaryKey}", QJavalinApiHandler::doGet); - ApiBuilder.patch("/{primaryKey}", QJavalinApiHandler::doUpdate); - ApiBuilder.delete("/{primaryKey}", QJavalinApiHandler::doDelete); + ApiBuilder.get("/query", context -> doQuery(context, apiInstanceMetaData)); + // ApiBuilder.post("/query", context -> doQuery(context, apiInstanceMetaData)); + + ApiBuilder.post("/bulk", context -> bulkInsert(context, apiInstanceMetaData)); + ApiBuilder.patch("/bulk", context -> bulkUpdate(context, apiInstanceMetaData)); + ApiBuilder.delete("/bulk", context -> bulkDelete(context, apiInstanceMetaData)); + + ////////////////////////////////////////////////////////////////// + // remember to keep the wildcard paths after the specific paths // + ////////////////////////////////////////////////////////////////// + ApiBuilder.get("/{primaryKey}", context -> doGet(context, apiInstanceMetaData)); + ApiBuilder.patch("/{primaryKey}", context -> doUpdate(context, apiInstanceMetaData)); + ApiBuilder.delete("/{primaryKey}", context -> doDelete(context, apiInstanceMetaData)); + }); }); - }); - ApiBuilder.get("/api/versions.json", QJavalinApiHandler::doVersions); - - ApiBuilder.before("/*", QJavalinApiHandler::setupCORS); - - ////////////////////////////////////////////////////////////////////////////////////////////// - // default all other /api/ requests (for the methods we support) to a standard 404 response // - ////////////////////////////////////////////////////////////////////////////////////////////// - ApiBuilder.get("/api/*", QJavalinApiHandler::doPathNotFound); - ApiBuilder.delete("/api/*", QJavalinApiHandler::doPathNotFound); - ApiBuilder.patch("/api/*", QJavalinApiHandler::doPathNotFound); - ApiBuilder.post("/api/*", QJavalinApiHandler::doPathNotFound); + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // default all other requests under the root path (for the methods we support) to a standard 404 response // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + ApiBuilder.get(rootPath + "*", QJavalinApiHandler::doPathNotFound); + ApiBuilder.delete(rootPath + "*", QJavalinApiHandler::doPathNotFound); + ApiBuilder.patch(rootPath + "*", QJavalinApiHandler::doPathNotFound); + ApiBuilder.post(rootPath + "*", QJavalinApiHandler::doPathNotFound); + } /////////////////////////////////////////////////////////////////////////////////// // if the main implementation class has a hot-swapper installed, use it here too // @@ -241,10 +250,8 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private static void doVersions(Context context) + private static void doVersions(Context context, ApiInstanceMetaData apiInstanceMetaData) { - ApiInstanceMetaData apiInstanceMetaData = ApiInstanceMetaData.of(qInstance); - Map rs = new HashMap<>(); rs.put("supportedVersions", apiInstanceMetaData.getSupportedVersions().stream().map(String::valueOf).collect(Collectors.toList())); rs.put("currentVersion", apiInstanceMetaData.getCurrentVersion().toString()); @@ -401,7 +408,7 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private static void doSpecYaml(Context context) + private static void doSpecYaml(Context context, ApiInstanceMetaData apiInstanceMetaData) { try { @@ -409,6 +416,8 @@ public class QJavalinApiHandler String version = context.pathParam("version"); GenerateOpenApiSpecInput input = new GenerateOpenApiSpecInput().withVersion(version); + input.setApiName(apiInstanceMetaData.getName()); + try { if(StringUtils.hasContent(context.pathParam("tableName"))) @@ -438,13 +447,14 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private static void doSpecJson(Context context) + private static void doSpecJson(Context context, ApiInstanceMetaData apiInstanceMetaData) { try { QContext.init(qInstance, null); String version = context.pathParam("version"); GenerateOpenApiSpecInput input = new GenerateOpenApiSpecInput().withVersion(version); + input.setApiName(apiInstanceMetaData.getName()); try { @@ -475,7 +485,7 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private static void doSpecHtml(Context context) + private static void doSpecHtml(Context context, ApiInstanceMetaData apiInstanceMetaData) { String version; @@ -485,11 +495,10 @@ public class QJavalinApiHandler } catch(Exception e) { - ApiInstanceMetaData apiInstanceMetaData = ApiInstanceMetaData.of(qInstance); version = apiInstanceMetaData.getCurrentVersion().toString(); } - doSpecHtml(context, version); + doSpecHtml(context, version, apiInstanceMetaData); } @@ -497,12 +506,11 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private static void doSpecHtml(Context context, String version) + private static void doSpecHtml(Context context, String version, ApiInstanceMetaData apiInstanceMetaData) { try { - QBrandingMetaData branding = qInstance.getBranding(); - ApiInstanceMetaData apiInstanceMetaData = ApiInstanceMetaData.of(qInstance); + QBrandingMetaData branding = qInstance.getBranding(); if(!apiInstanceMetaData.getSupportedVersions().contains(new APIVersion(version))) { @@ -536,7 +544,7 @@ public class QJavalinApiHandler StringBuilder otherVersionOptions = new StringBuilder(); for(APIVersion supportedVersion : apiInstanceMetaData.getSupportedVersions()) { - otherVersionOptions.append(""); + otherVersionOptions.append(""); } html = html.replace("{otherVersionOptions}", otherVersionOptions.toString()); @@ -565,7 +573,7 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private static void doGet(Context context) + private static void doGet(Context context, ApiInstanceMetaData apiInstanceMetaData) { String version = context.pathParam("version"); String tableApiName = context.pathParam("tableName"); @@ -574,7 +582,7 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, version, tableApiName); + QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName); String tableName = table.getName(); GetInput getInput = new GetInput(); @@ -605,7 +613,7 @@ public class QJavalinApiHandler + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); } - Map outputRecord = QRecordApiAdapter.qRecordToApiMap(record, tableName, version); + Map outputRecord = QRecordApiAdapter.qRecordToApiMap(record, tableName, apiInstanceMetaData.getName(), version); QJavalinAccessLogger.logEndSuccess(); String resultString = JsonUtils.toJson(outputRecord); @@ -817,7 +825,7 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private static void doQuery(Context context) + private static void doQuery(Context context, ApiInstanceMetaData apiInstanceMetaData) { String version = context.pathParam("version"); String tableApiName = context.pathParam("tableName"); @@ -828,7 +836,7 @@ public class QJavalinApiHandler { List badRequestMessages = new ArrayList<>(); - QTableMetaData table = validateTableAndVersion(context, version, tableApiName); + QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName); String tableName = table.getName(); QueryInput queryInput = new QueryInput(); @@ -1023,7 +1031,7 @@ public class QJavalinApiHandler ArrayList> records = new ArrayList<>(); for(QRecord record : queryOutput.getRecords()) { - records.add(QRecordApiAdapter.qRecordToApiMap(record, tableName, version)); + records.add(QRecordApiAdapter.qRecordToApiMap(record, tableName, apiInstanceMetaData.getName(), version)); } ///////////////////////////// @@ -1057,11 +1065,11 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private static QTableMetaData validateTableAndVersion(Context context, String version, String tableApiName) throws QNotFoundException + private static QTableMetaData validateTableAndVersion(Context context, ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName) throws QNotFoundException { QNotFoundException qNotFoundException = new QNotFoundException("Could not find any resources at path " + context.path()); - QTableMetaData table = getTableByApiName(version, tableApiName); + QTableMetaData table = getTableByApiName(apiInstanceMetaData.getName(), version, tableApiName); if(table == null) { @@ -1073,7 +1081,13 @@ public class QJavalinApiHandler throw (qNotFoundException); } - ApiTableMetaData apiTableMetaData = ApiTableMetaData.of(table); + ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); + if(apiTableMetaDataContainer == null) + { + throw (qNotFoundException); + } + + ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiInstanceMetaData.getName()); if(apiTableMetaData == null) { throw (qNotFoundException); @@ -1085,7 +1099,7 @@ public class QJavalinApiHandler } APIVersion requestApiVersion = new APIVersion(version); - List supportedVersions = ApiInstanceMetaData.of(qInstance).getSupportedVersions(); + List supportedVersions = apiInstanceMetaData.getSupportedVersions(); if(CollectionUtils.nullSafeIsEmpty(supportedVersions) || !supportedVersions.contains(requestApiVersion)) { throw (qNotFoundException); @@ -1104,27 +1118,40 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private static QTableMetaData getTableByApiName(String version, String tableApiName) + private static QTableMetaData getTableByApiName(String apiName, String version, String tableApiName) { - if(tableApiNameMap.get(version) == null) + ///////////////////////////////////////////////////////////////////////////////////////////// + // tableApiNameMap is a map of (apiName,apiVersion) => Map. // + // that is to say, a 2-level map. The first level is keyed by (apiName,apiVersion) pairs. // + // the second level is keyed by tableApiNames. // + ///////////////////////////////////////////////////////////////////////////////////////////// + Pair key = new Pair<>(apiName, version); + if(tableApiNameMap.get(key) == null) { Map map = new HashMap<>(); for(QTableMetaData table : qInstance.getTables().values()) { - ApiTableMetaData apiTableMetaData = ApiTableMetaData.of(table); - String name = table.getName(); - if(apiTableMetaData != null && StringUtils.hasContent(apiTableMetaData.getApiTableName())) + ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); + if(apiTableMetaDataContainer != null) { - name = apiTableMetaData.getApiTableName(); + ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiName); + if(apiTableMetaData != null) + { + String name = table.getName(); + if(StringUtils.hasContent(apiTableMetaData.getApiTableName())) + { + name = apiTableMetaData.getApiTableName(); + } + map.put(name, table); + } } - map.put(name, table); } - tableApiNameMap.put(version, map); + tableApiNameMap.put(key, map); } - return (tableApiNameMap.get(version).get(tableApiName)); + return (tableApiNameMap.get(key).get(tableApiName)); } @@ -1258,7 +1285,7 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private static void doInsert(Context context) + private static void doInsert(Context context, ApiInstanceMetaData apiInstanceMetaData) { String version = context.pathParam("version"); String tableApiName = context.pathParam("tableName"); @@ -1266,7 +1293,7 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, version, tableApiName); + QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName); String tableName = table.getName(); InsertInput insertInput = new InsertInput(); @@ -1288,7 +1315,7 @@ public class QJavalinApiHandler JSONTokener jsonTokener = new JSONTokener(context.body().trim()); JSONObject jsonObject = new JSONObject(jsonTokener); - insertInput.setRecords(List.of(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, false))); + insertInput.setRecords(List.of(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, false))); if(jsonTokener.more()) { @@ -1328,7 +1355,7 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private static void bulkInsert(Context context) + private static void bulkInsert(Context context, ApiInstanceMetaData apiInstanceMetaData) { String version = context.pathParam("version"); String tableApiName = context.pathParam("tableName"); @@ -1336,7 +1363,7 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, version, tableApiName); + QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName); String tableName = table.getName(); InsertInput insertInput = new InsertInput(); @@ -1367,7 +1394,7 @@ public class QJavalinApiHandler for(int i = 0; i < jsonArray.length(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); - recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, false)); + recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, false)); } if(jsonTokener.more()) @@ -1437,7 +1464,7 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private static void bulkUpdate(Context context) + private static void bulkUpdate(Context context, ApiInstanceMetaData apiInstanceMetaData) { String version = context.pathParam("version"); String tableApiName = context.pathParam("tableName"); @@ -1445,7 +1472,7 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, version, tableApiName); + QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName); String tableName = table.getName(); UpdateInput updateInput = new UpdateInput(); @@ -1476,7 +1503,7 @@ public class QJavalinApiHandler for(int i = 0; i < jsonArray.length(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); - recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, true)); + recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, true)); } if(jsonTokener.more()) @@ -1579,7 +1606,7 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private static void bulkDelete(Context context) + private static void bulkDelete(Context context, ApiInstanceMetaData apiInstanceMetaData) { String version = context.pathParam("version"); String tableApiName = context.pathParam("tableName"); @@ -1587,7 +1614,7 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, version, tableApiName); + QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName); String tableName = table.getName(); DeleteInput deleteInput = new DeleteInput(); @@ -1710,7 +1737,7 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private static void doUpdate(Context context) + private static void doUpdate(Context context, ApiInstanceMetaData apiInstanceMetaData) { String version = context.pathParam("version"); String tableApiName = context.pathParam("tableName"); @@ -1719,7 +1746,7 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, version, tableApiName); + QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName); String tableName = table.getName(); UpdateInput updateInput = new UpdateInput(); @@ -1741,7 +1768,7 @@ public class QJavalinApiHandler JSONTokener jsonTokener = new JSONTokener(context.body().trim()); JSONObject jsonObject = new JSONObject(jsonTokener); - QRecord qRecord = QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, false); + QRecord qRecord = QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, apiInstanceMetaData.getName(), version, false); qRecord.setValue(table.getPrimaryKeyField(), primaryKey); updateInput.setRecords(List.of(qRecord)); @@ -1794,7 +1821,7 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private static void doDelete(Context context) + private static void doDelete(Context context, ApiInstanceMetaData apiInstanceMetaData) { String version = context.pathParam("version"); String tableApiName = context.pathParam("tableName"); @@ -1803,7 +1830,7 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, version, tableApiName); + QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName); String tableName = table.getName(); DeleteInput deleteInput = new DeleteInput(); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GenerateOpenApiSpecInput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GenerateOpenApiSpecInput.java index 97e7dd1a..41354615 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GenerateOpenApiSpecInput.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GenerateOpenApiSpecInput.java @@ -30,6 +30,7 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; *******************************************************************************/ public class GenerateOpenApiSpecInput extends AbstractActionInput { + private String apiName; private String version; private String tableName; @@ -95,4 +96,35 @@ public class GenerateOpenApiSpecInput extends AbstractActionInput return (this); } + + + /******************************************************************************* + ** Getter for apiName + *******************************************************************************/ + public String getApiName() + { + return (this.apiName); + } + + + + /******************************************************************************* + ** Setter for apiName + *******************************************************************************/ + public void setApiName(String apiName) + { + this.apiName = apiName; + } + + + + /******************************************************************************* + ** Fluent setter for apiName + *******************************************************************************/ + public GenerateOpenApiSpecInput withApiName(String apiName) + { + this.apiName = apiName; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GetTableApiFieldsInput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GetTableApiFieldsInput.java index 48927c52..2a2f991b 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GetTableApiFieldsInput.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GetTableApiFieldsInput.java @@ -30,6 +30,7 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; *******************************************************************************/ public class GetTableApiFieldsInput extends AbstractActionInput { + private String apiName; private String tableName; private String version; @@ -95,4 +96,35 @@ public class GetTableApiFieldsInput extends AbstractActionInput return (this); } + + + /******************************************************************************* + ** Getter for apiName + *******************************************************************************/ + public String getApiName() + { + return (this.apiName); + } + + + + /******************************************************************************* + ** Setter for apiName + *******************************************************************************/ + public void setApiName(String apiName) + { + this.apiName = apiName; + } + + + + /******************************************************************************* + ** Fluent setter for apiName + *******************************************************************************/ + public GetTableApiFieldsInput withApiName(String apiName) + { + this.apiName = apiName; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java index 2a4bf256..cf0d4fd7 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java @@ -23,8 +23,9 @@ package com.kingsrook.qqq.api.model.metadata; import java.util.ArrayList; -import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.TreeSet; import java.util.function.Consumer; import com.kingsrook.qqq.api.model.APILog; import com.kingsrook.qqq.api.model.APIVersion; @@ -104,12 +105,20 @@ public class APILogMetaDataProvider new QPossibleValue<>(500, "500 (Internal Server Error)") ))); - List> apiVersionPossibleValues = new ArrayList<>(); - ApiInstanceMetaData apiInstanceMetaData = ApiInstanceMetaData.of(instance); - LinkedHashSet allVersions = new LinkedHashSet<>(); - allVersions.addAll(apiInstanceMetaData.getPastVersions()); - allVersions.addAll(apiInstanceMetaData.getSupportedVersions()); - allVersions.addAll(apiInstanceMetaData.getFutureVersions()); + List> apiVersionPossibleValues = new ArrayList<>(); + + //////////////////////////////////////////////////////////////////////////////////////////////////// + // todo... this, this whole thing, should probably have "which api" as another field too... ugh. // + //////////////////////////////////////////////////////////////////////////////////////////////////// + TreeSet allVersions = new TreeSet<>(); + ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(instance); + for(Map.Entry entry : apiInstanceMetaDataContainer.getApis().entrySet()) + { + ApiInstanceMetaData apiInstanceMetaData = entry.getValue(); + allVersions.addAll(apiInstanceMetaData.getPastVersions()); + allVersions.addAll(apiInstanceMetaData.getSupportedVersions()); + allVersions.addAll(apiInstanceMetaData.getFutureVersions()); + } for(APIVersion version : allVersions) { 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 a4a123e6..50a0581b 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 @@ -24,14 +24,14 @@ package com.kingsrook.qqq.api.model.metadata; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; -import com.kingsrook.qqq.api.ApiMiddlewareType; import com.kingsrook.qqq.api.model.APIVersion; 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.Server; import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.QMiddlewareInstanceMetaData; 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; @@ -41,9 +41,11 @@ import org.apache.commons.lang.BooleanUtils; /******************************************************************************* ** *******************************************************************************/ -public class ApiInstanceMetaData extends QMiddlewareInstanceMetaData +public class ApiInstanceMetaData { private String name; + private String label; + private String path; private String description; private String contactEmail; @@ -59,58 +61,45 @@ public class ApiInstanceMetaData extends QMiddlewareInstanceMetaData /******************************************************************************* - ** Constructor ** *******************************************************************************/ - public ApiInstanceMetaData() + public void validate(String apiName, QInstance qInstance, QInstanceValidator validator) { - setType(ApiMiddlewareType.NAME); - } + validator.assertCondition(Objects.equals(apiName, name), "Name mismatch for instance api (" + apiName + " != " + name + ")"); + validator.assertCondition(StringUtils.hasContent(name), "Missing name for api " + apiName); + if(validator.assertCondition(StringUtils.hasContent(path), "Missing path for api " + apiName)) + { + validator.assertCondition(path.startsWith("/"), "Path for api " + apiName + " does not start with '/' (but it needs to)."); + validator.assertCondition(path.endsWith("/"), "Path for api " + apiName + " does not end with '/' (but it needs to)."); + } - - /******************************************************************************* - ** - *******************************************************************************/ - public static ApiInstanceMetaData of(QInstance qInstance) - { - return ((ApiInstanceMetaData) qInstance.getMiddlewareMetaData(ApiMiddlewareType.NAME)); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public void validate(QInstance qInstance, QInstanceValidator validator) - { - validator.assertCondition(StringUtils.hasContent(name), "Missing name for instance api"); - validator.assertCondition(StringUtils.hasContent(description), "Missing description for instance api"); - validator.assertCondition(StringUtils.hasContent(contactEmail), "Missing contactEmail for instance api"); + validator.assertCondition(StringUtils.hasContent(label), "Missing label for api " + apiName); + validator.assertCondition(StringUtils.hasContent(description), "Missing description for api " + apiName); + validator.assertCondition(StringUtils.hasContent(contactEmail), "Missing contactEmail for api " + apiName); Set allVersions = new HashSet<>(); - if(validator.assertCondition(currentVersion != null, "Missing currentVersion for instance api")) + if(validator.assertCondition(currentVersion != null, "Missing currentVersion for api " + apiName)) { allVersions.add(currentVersion); } - if(validator.assertCondition(supportedVersions != null, "Missing supportedVersions for instance api")) + if(validator.assertCondition(supportedVersions != null, "Missing supportedVersions for api " + apiName)) { - validator.assertCondition(supportedVersions.contains(currentVersion), "supportedVersions [" + supportedVersions + "] does not contain currentVersion [" + currentVersion + "] for instance api"); + validator.assertCondition(supportedVersions.contains(currentVersion), "supportedVersions [" + supportedVersions + "] does not contain currentVersion [" + currentVersion + "] for api " + apiName); allVersions.addAll(supportedVersions); } for(APIVersion pastVersion : CollectionUtils.nonNullList(pastVersions)) { - validator.assertCondition(pastVersion.compareTo(currentVersion) < 0, "pastVersion [" + pastVersion + "] is not lexicographically before currentVersion [" + currentVersion + "] for instance api"); + validator.assertCondition(pastVersion.compareTo(currentVersion) < 0, "pastVersion [" + pastVersion + "] is not lexicographically before currentVersion [" + currentVersion + "] for api " + apiName); allVersions.add(pastVersion); } for(APIVersion futureVersion : CollectionUtils.nonNullList(futureVersions)) { - validator.assertCondition(futureVersion.compareTo(currentVersion) > 0, "futureVersion [" + futureVersion + "] is not lexicographically after currentVersion [" + currentVersion + "] for instance api"); + validator.assertCondition(futureVersion.compareTo(currentVersion) > 0, "futureVersion [" + futureVersion + "] is not lexicographically after currentVersion [" + currentVersion + "] for api " + apiName); allVersions.add(futureVersion); } @@ -119,12 +108,16 @@ public class ApiInstanceMetaData extends QMiddlewareInstanceMetaData ///////////////////////////////// for(QTableMetaData table : qInstance.getTables().values()) { - ApiTableMetaData apiTableMetaData = ApiTableMetaData.of(table); - if(apiTableMetaData != null) + ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); + if(apiTableMetaDataContainer != null) { - if(BooleanUtils.isNotTrue(apiTableMetaData.getIsExcluded())) + ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiName); + if(apiTableMetaData != null) { - validator.assertCondition(allVersions.contains(new APIVersion(apiTableMetaData.getInitialVersion())), "Table " + table.getName() + "'s initial API version is not a recognized version."); + if(BooleanUtils.isNotTrue(apiTableMetaData.getIsExcluded())) + { + validator.assertCondition(allVersions.contains(new APIVersion(apiTableMetaData.getInitialVersion())), "Table " + table.getName() + "'s initial API version is not a recognized version for api " + apiName); + } } } } @@ -411,4 +404,66 @@ public class ApiInstanceMetaData extends QMiddlewareInstanceMetaData return (this); } + + + /******************************************************************************* + ** Getter for label + *******************************************************************************/ + public String getLabel() + { + return (this.label); + } + + + + /******************************************************************************* + ** Setter for label + *******************************************************************************/ + public void setLabel(String label) + { + this.label = label; + } + + + + /******************************************************************************* + ** Fluent setter for label + *******************************************************************************/ + public ApiInstanceMetaData withLabel(String label) + { + this.label = label; + return (this); + } + + + + /******************************************************************************* + ** Getter for path + *******************************************************************************/ + public String getPath() + { + return (this.path); + } + + + + /******************************************************************************* + ** Setter for path + *******************************************************************************/ + public void setPath(String path) + { + this.path = path; + } + + + + /******************************************************************************* + ** Fluent setter for path + *******************************************************************************/ + public ApiInstanceMetaData withPath(String path) + { + this.path = path; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java new file mode 100644 index 00000000..c26a9d39 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java @@ -0,0 +1,137 @@ +/* + * 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; + + +import java.util.LinkedHashMap; +import java.util.Map; +import com.kingsrook.qqq.api.ApiMiddlewareType; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QMiddlewareInstanceMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiInstanceMetaDataContainer extends QMiddlewareInstanceMetaData +{ + private Map apis; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ApiInstanceMetaDataContainer() + { + setType(ApiMiddlewareType.NAME); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static ApiInstanceMetaDataContainer of(QInstance qInstance) + { + return ((ApiInstanceMetaDataContainer) qInstance.getMiddlewareMetaData(ApiMiddlewareType.NAME)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void validate(QInstance qInstance, QInstanceValidator validator) + { + for(Map.Entry entry : CollectionUtils.nonNullMap(apis).entrySet()) + { + entry.getValue().validate(entry.getKey(), qInstance, validator); + } + } + + + + /******************************************************************************* + ** Getter for apis + *******************************************************************************/ + public Map getApis() + { + return (this.apis); + } + + + + /******************************************************************************* + ** Getter for apis + *******************************************************************************/ + public ApiInstanceMetaData getApiInstanceMetaData(String apiName) + { + if(this.apis == null) + { + return (null); + } + + return (this.apis.get(apiName)); + } + + + + /******************************************************************************* + ** Setter for apis + *******************************************************************************/ + public void setApis(Map apis) + { + this.apis = apis; + } + + + + /******************************************************************************* + ** Fluent setter for apis + *******************************************************************************/ + public ApiInstanceMetaDataContainer withApis(Map apis) + { + this.apis = apis; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for apis + *******************************************************************************/ + public ApiInstanceMetaDataContainer withApiInstanceMetaData(ApiInstanceMetaData apiInstanceMetaData) + { + if(this.apis == null) + { + this.apis = new LinkedHashMap<>(); + } + this.apis.put(apiInstanceMetaData.getName(), apiInstanceMetaData); + return (this); + } + +} \ No newline at end of file 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 c4d9c82c..fabc02f0 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,16 +22,14 @@ 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; import com.kingsrook.qqq.backend.core.utils.StringUtils; /******************************************************************************* ** *******************************************************************************/ -public class ApiFieldMetaData extends QMiddlewareFieldMetaData +public class ApiFieldMetaData { private String initialVersion; private String finalVersion; @@ -44,35 +42,18 @@ public class ApiFieldMetaData extends QMiddlewareFieldMetaData /******************************************************************************* - ** Constructor ** *******************************************************************************/ - public ApiFieldMetaData() + public static String getEffectiveApiFieldName(String apiName, QFieldMetaData field) { - setType("api"); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public static ApiFieldMetaData of(QFieldMetaData field) - { - return ((ApiFieldMetaData) field.getMiddlewareMetaData(ApiMiddlewareType.NAME)); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public static String getEffectiveApiFieldName(QFieldMetaData field) - { - ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(field); - if(apiFieldMetaData != null && StringUtils.hasContent(apiFieldMetaData.apiFieldName)) + ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.of(field); + if(apiFieldMetaDataContainer != null) { - return (apiFieldMetaData.apiFieldName); + ApiFieldMetaData apiFieldMetaData = apiFieldMetaDataContainer.getApiFieldMetaData(apiName); + if(apiFieldMetaData != null && StringUtils.hasContent(apiFieldMetaData.apiFieldName)) + { + return (apiFieldMetaData.apiFieldName); + } } return (field.getName()); 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 new file mode 100644 index 00000000..d370b20f --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java @@ -0,0 +1,121 @@ +/* + * 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.fields; + + +import java.util.LinkedHashMap; +import java.util.Map; +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; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiFieldMetaDataContainer extends QMiddlewareFieldMetaData +{ + private Map apis; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ApiFieldMetaDataContainer() + { + setType("api"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static ApiFieldMetaDataContainer of(QFieldMetaData field) + { + return ((ApiFieldMetaDataContainer) field.getMiddlewareMetaData(ApiMiddlewareType.NAME)); + } + + + + /******************************************************************************* + ** Getter for apis + *******************************************************************************/ + public Map getApis() + { + return (this.apis); + } + + + + /******************************************************************************* + ** Getter for apis + *******************************************************************************/ + public ApiFieldMetaData getApiFieldMetaData(String apiName) + { + if(this.apis == null) + { + return (null); + } + + return (this.apis.get(apiName)); + } + + + + /******************************************************************************* + ** Setter for apis + *******************************************************************************/ + public void setApis(Map apis) + { + this.apis = apis; + } + + + + /******************************************************************************* + ** Fluent setter for apis + *******************************************************************************/ + public ApiFieldMetaDataContainer withApis(Map apis) + { + this.apis = apis; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for apis + *******************************************************************************/ + public ApiFieldMetaDataContainer withApiFieldMetaData(String apiName, ApiFieldMetaData apiFieldMetaData) + { + if(this.apis == null) + { + this.apis = new LinkedHashMap<>(); + } + this.apis.put(apiName, apiFieldMetaData); + 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 5116c7ec..2ceb4254 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 @@ -27,8 +27,8 @@ import java.util.List; import com.kingsrook.qqq.api.ApiMiddlewareType; import com.kingsrook.qqq.api.model.APIVersionRange; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; +import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; 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; @@ -36,7 +36,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* ** *******************************************************************************/ -public class ApiTableMetaData extends QMiddlewareTableMetaData +public class ApiTableMetaData { private String initialVersion; private String finalVersion; @@ -48,16 +48,6 @@ public class ApiTableMetaData extends QMiddlewareTableMetaData - /******************************************************************************* - ** - *******************************************************************************/ - public static ApiTableMetaData of(QTableMetaData table) - { - return ((ApiTableMetaData) table.getMiddlewareMetaData(ApiMiddlewareType.NAME)); - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -78,16 +68,13 @@ public class ApiTableMetaData extends QMiddlewareTableMetaData /******************************************************************************* ** *******************************************************************************/ - @Override - public void enrich(QTableMetaData table) + public void enrich(String apiName, QTableMetaData table) { - super.enrich(table); - if(initialVersion != null) { for(QFieldMetaData field : table.getFields().values()) { - ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiMiddlewareMetaData(field); + ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiMiddlewareMetaData(apiName, field); if(apiFieldMetaData.getInitialVersion() == null) { apiFieldMetaData.setInitialVersion(initialVersion); @@ -96,7 +83,7 @@ public class ApiTableMetaData extends QMiddlewareTableMetaData for(QFieldMetaData field : CollectionUtils.nonNullList(removedApiFields)) { - ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiMiddlewareMetaData(field); + ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiMiddlewareMetaData(apiName, field); if(apiFieldMetaData.getInitialVersion() == null) { apiFieldMetaData.setInitialVersion(initialVersion); @@ -110,25 +97,20 @@ public class ApiTableMetaData extends QMiddlewareTableMetaData /******************************************************************************* ** *******************************************************************************/ - private static ApiFieldMetaData ensureFieldHasApiMiddlewareMetaData(QFieldMetaData field) + private static ApiFieldMetaData ensureFieldHasApiMiddlewareMetaData(String apiName, QFieldMetaData field) { if(field.getMiddlewareMetaData(ApiMiddlewareType.NAME) == null) { - field.withMiddlewareMetaData(new ApiFieldMetaData()); + field.withMiddlewareMetaData(new ApiFieldMetaDataContainer()); } - return (ApiFieldMetaData.of(field)); - } + ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.of(field); + if(apiFieldMetaDataContainer.getApiFieldMetaData(apiName) == null) + { + apiFieldMetaDataContainer.withApiFieldMetaData(apiName, new ApiFieldMetaData()); + } - - - /******************************************************************************* - ** Constructor - ** - *******************************************************************************/ - public ApiTableMetaData() - { - setType("api"); + return (apiFieldMetaDataContainer.getApiFieldMetaData(apiName)); } 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 new file mode 100644 index 00000000..0b38caed --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java @@ -0,0 +1,138 @@ +/* + * 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 java.util.LinkedHashMap; +import java.util.Map; +import com.kingsrook.qqq.api.ApiMiddlewareType; +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; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiTableMetaDataContainer extends QMiddlewareTableMetaData +{ + private Map apis; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ApiTableMetaDataContainer() + { + setType("api"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static ApiTableMetaDataContainer of(QTableMetaData table) + { + return ((ApiTableMetaDataContainer) table.getMiddlewareMetaData(ApiMiddlewareType.NAME)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void enrich(QTableMetaData table) + { + super.enrich(table); + + for(Map.Entry entry : CollectionUtils.nonNullMap(apis).entrySet()) + { + entry.getValue().enrich(entry.getKey(), table); + } + } + + + + /******************************************************************************* + ** Getter for apis + *******************************************************************************/ + public Map getApis() + { + return (this.apis); + } + + + + /******************************************************************************* + ** Getter for apis + *******************************************************************************/ + public ApiTableMetaData getApiTableMetaData(String apiName) + { + if(this.apis == null) + { + return (null); + } + + return (this.apis.get(apiName)); + } + + + + /******************************************************************************* + ** Setter for apis + *******************************************************************************/ + public void setApis(Map apis) + { + this.apis = apis; + } + + + + /******************************************************************************* + ** Fluent setter for apis + *******************************************************************************/ + public ApiTableMetaDataContainer withApis(Map apis) + { + this.apis = apis; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for apis + *******************************************************************************/ + public ApiTableMetaDataContainer withApiTableMetaData(String apiName, ApiTableMetaData apiTableMetaData) + { + if(this.apis == null) + { + this.apis = new LinkedHashMap<>(); + } + this.apis.put(apiName, apiTableMetaData); + return (this); + } + +} 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 f85df985..f775fa94 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,8 +25,11 @@ 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.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.ApiTableMetaData; +import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; @@ -57,6 +60,9 @@ public class TestUtils public static final String TABLE_NAME_LINE_ITEM_EXTRINSIC = "orderLineExtrinsic"; public static final String TABLE_NAME_ORDER_EXTRINSIC = "orderExtrinsic"; + public static final String API_NAME = "test-api"; + public static final String ALTERNATIVE_API_NAME = "person-api"; + public static final String V2022_Q4 = "2022.Q4"; public static final String V2023_Q1 = "2023.Q1"; public static final String V2023_Q2 = "2023.Q2"; @@ -85,14 +91,27 @@ public class TestUtils qInstance.setAuthentication(new Auth0AuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS).withName("anonymous")); - qInstance.withMiddlewareMetaData(new ApiInstanceMetaData() - .withName("TestAPI") - .withDescription("QQQ Test API") - .withContactEmail("contact@kingsrook.com") - .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))) + qInstance.withMiddlewareMetaData(new ApiInstanceMetaDataContainer() + .withApiInstanceMetaData(new ApiInstanceMetaData() + .withName(API_NAME) + .withPath("/api/") + .withLabel("Test API") + .withDescription("QQQ Test API") + .withContactEmail("contact@kingsrook.com") + .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)))) + .withApiInstanceMetaData(new ApiInstanceMetaData() + .withName(ALTERNATIVE_API_NAME) + .withPath("/person-api/") + .withLabel("Person-Only API") + .withDescription("QQQ Test API, that only has the Person table.") + .withContactEmail("contact@kingsrook.com") + .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); @@ -117,20 +136,10 @@ public class TestUtils *******************************************************************************/ public static QTableMetaData defineTablePerson() { - return new QTableMetaData() + QTableMetaData table = new QTableMetaData() .withName(TABLE_NAME_PERSON) .withLabel("Person") .withBackendName(MEMORY_BACKEND_NAME) - .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)) @@ -138,24 +147,48 @@ 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) - .withMiddlewareMetaData(new ApiFieldMetaData().withApiFieldName("birthDay")) - ) + .withField(new QFieldMetaData("birthDate", QFieldType.DATE)) .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) - .withMiddlewareMetaData(new ApiFieldMetaData().withInitialVersion(V2023_Q1))) + .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)); - ///////////////////////////////////////////////////////////////// - // 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))) - ; + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // make some changes to this table in the "main" api (but leave it like the backend in the ALTERNATIVE_API_NAME) // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + table.withMiddlewareMetaData(new ApiTableMetaDataContainer() + .withApiTableMetaData(API_NAME, 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 ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, + new ApiFieldMetaData().withFinalVersion(V2022_Q4).withReplacedByFieldName("noOfShoes")))) + ) + .withApiTableMetaData(ALTERNATIVE_API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))); + + ///////////////////////////////////////////////////// + // change the name for this field for the main api // + ///////////////////////////////////////////////////// + table.getField("birthDate").withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withApiFieldName("birthDay"))); + + //////////////////////////////////////////////////////////////////////////////// + // See above - we renamed this field (in the backend) for the 2023_Q1 version // + //////////////////////////////////////////////////////////////////////////////// + table.getField("noOfShoes").withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withInitialVersion(V2023_Q1))); + + ///////////////////////////////////////////////////////////////////////////////////////////////// + // 2 new fields - one will appear in a future version of the API, the other is always excluded // + ///////////////////////////////////////////////////////////////////////////////////////////////// + table.getField("cost").withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withInitialVersion(V2023_Q2))); + table.getField("price").withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withIsExcluded(true))); + + return (table); } @@ -168,7 +201,7 @@ public class TestUtils return new QTableMetaData() .withName(TABLE_NAME_ORDER) .withBackendName(MEMORY_BACKEND_NAME) - .withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(V2022_Q4)) + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) .withPrimaryKeyField("id") .withAssociation(new Association().withName("orderLines").withAssociatedTableName(TABLE_NAME_LINE_ITEM).withJoinName("orderLineItem")) .withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_ORDER_EXTRINSIC).withJoinName("orderOrderExtrinsic")) @@ -191,7 +224,7 @@ public class TestUtils return new QTableMetaData() .withName(TABLE_NAME_LINE_ITEM) .withBackendName(MEMORY_BACKEND_NAME) - .withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(V2022_Q4)) + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) .withPrimaryKeyField("id") .withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_LINE_ITEM_EXTRINSIC).withJoinName("lineItemLineItemExtrinsic")) .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) @@ -213,7 +246,7 @@ public class TestUtils return new QTableMetaData() .withName(TABLE_NAME_LINE_ITEM_EXTRINSIC) .withBackendName(MEMORY_BACKEND_NAME) - .withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(V2022_Q4)) + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) @@ -233,7 +266,7 @@ public class TestUtils return new QTableMetaData() .withName(TABLE_NAME_ORDER_EXTRINSIC) .withBackendName(MEMORY_BACKEND_NAME) - .withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(V2022_Q4)) + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) 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 51c8b949..afcc8ba6 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 @@ -28,6 +28,7 @@ import com.kingsrook.qqq.api.TestUtils; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; +import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -52,7 +53,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest @Test void test() throws QException { - GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion(TestUtils.CURRENT_API_VERSION)); + GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion(TestUtils.CURRENT_API_VERSION).withApiName(TestUtils.API_NAME)); System.out.println(output.getYaml()); } @@ -71,8 +72,8 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaData() - .withInitialVersion(TestUtils.V2022_Q4))); + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withInitialVersion(TestUtils.V2022_Q4)))); qInstance.addTable(new QTableMetaData() .withName("hiddenTable") @@ -80,16 +81,16 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) .withIsHidden(true) - .withMiddlewareMetaData(new ApiTableMetaData() - .withInitialVersion(TestUtils.V2022_Q4))); + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withInitialVersion(TestUtils.V2022_Q4)))); qInstance.addTable(new QTableMetaData() .withName("excludedTable") .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaData() - .withIsExcluded(true))); + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withIsExcluded(true)))); qInstance.addTable(new QTableMetaData() .withName("tableWithoutApiMetaData") @@ -102,17 +103,17 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaData() - .withInitialVersion(TestUtils.V2023_Q2))); + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withInitialVersion(TestUtils.V2023_Q2)))); qInstance.addTable(new QTableMetaData() .withName("tableWithOnlyPastVersions") .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaData() + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withInitialVersion(TestUtils.V2022_Q4) - .withFinalVersion(TestUtils.V2022_Q4))); + .withFinalVersion(TestUtils.V2022_Q4)))); qInstance.addTable(new QTableMetaData() .withName("tableWithNoSupportedCapabilities") @@ -120,10 +121,10 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) .withoutCapabilities(Capability.TABLE_QUERY, Capability.TABLE_GET, Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE) - .withMiddlewareMetaData(new ApiTableMetaData() - .withInitialVersion(TestUtils.V2022_Q4))); + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withInitialVersion(TestUtils.V2022_Q4)))); - GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion(TestUtils.CURRENT_API_VERSION)); + GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion(TestUtils.CURRENT_API_VERSION).withApiName(TestUtils.API_NAME)); Set apiPaths = output.getOpenAPI().getPaths().keySet(); assertTrue(apiPaths.stream().anyMatch(s -> s.contains("/supportedTable/"))); assertTrue(apiPaths.stream().noneMatch(s -> s.contains("/hiddenTable/"))); @@ -149,11 +150,11 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaData() + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withApiTableName("externalName") - .withInitialVersion(TestUtils.V2022_Q4))); + .withInitialVersion(TestUtils.V2022_Q4)))); - GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion(TestUtils.CURRENT_API_VERSION)); + GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion(TestUtils.CURRENT_API_VERSION).withApiName(TestUtils.API_NAME)); Set apiPaths = output.getOpenAPI().getPaths().keySet(); assertTrue(apiPaths.stream().anyMatch(s -> s.contains("/externalName/"))); assertTrue(apiPaths.stream().noneMatch(s -> s.contains("/internalName/"))); @@ -167,9 +168,31 @@ class GenerateOpenApiSpecActionTest extends BaseTest @Test void testBadVersion() { - assertThatThrownBy(() -> new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion("NotAVersion"))) + assertThatThrownBy(() -> new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withApiName(TestUtils.API_NAME))) + .isInstanceOf(QException.class) + .hasMessageContaining("Missing required input: version"); + + assertThatThrownBy(() -> new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion("NotAVersion").withApiName(TestUtils.API_NAME))) .isInstanceOf(QException.class) .hasMessageContaining("not a supported API Version"); + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBadApiName() + { + assertThatThrownBy(() -> new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withApiName("Not an api"))) + .isInstanceOf(QException.class) + .hasMessageContaining("Could not find apiInstanceMetaData named"); + + assertThatThrownBy(() -> new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion(TestUtils.CURRENT_API_VERSION))) + .isInstanceOf(QException.class) + .hasMessageContaining("Missing required input: apiName"); } } \ No newline at end of file diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsActionTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsActionTest.java index 1ab34d3e..0285b790 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsActionTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsActionTest.java @@ -27,9 +27,12 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import com.kingsrook.qqq.api.BaseTest; +import com.kingsrook.qqq.api.TestUtils; 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.ApiTableMetaData; +import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; @@ -57,7 +60,7 @@ class GetTableApiFieldsActionTest extends BaseTest *******************************************************************************/ private List getFields(String tableName, String version) throws QException { - return new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version)).getFields(); + return new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withApiName(TestUtils.API_NAME).withTableName(tableName).withVersion(version)).getFields(); } @@ -71,11 +74,11 @@ class GetTableApiFieldsActionTest extends BaseTest QInstance qInstance = QContext.getQInstance(); qInstance.addTable(new QTableMetaData() .withName(TABLE_NAME) - .withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion("1")) + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("1"))) .withField(new QFieldMetaData("a", STRING)) // inherit versionRange from the table - .withField(new QFieldMetaData("b", STRING).withMiddlewareMetaData(new ApiFieldMetaData().withInitialVersion("1"))) - .withField(new QFieldMetaData("c", STRING).withMiddlewareMetaData(new ApiFieldMetaData().withInitialVersion("2"))) - .withField(new QFieldMetaData("d", STRING).withMiddlewareMetaData(new ApiFieldMetaData().withInitialVersion("3"))) + .withField(new QFieldMetaData("b", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1")))) + .withField(new QFieldMetaData("c", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("2")))) + .withField(new QFieldMetaData("d", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("3")))) ); new QInstanceEnricher(qInstance).enrich(); @@ -95,13 +98,13 @@ class GetTableApiFieldsActionTest extends BaseTest QInstance qInstance = QContext.getQInstance(); qInstance.addTable(new QTableMetaData() .withName(TABLE_NAME) - .withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion("1") - .withRemovedApiField(new QFieldMetaData("c", STRING).withMiddlewareMetaData(new ApiFieldMetaData().withInitialVersion("1").withFinalVersion("2"))) - ) + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("1") + .withRemovedApiField(new QFieldMetaData("c", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1").withFinalVersion("2")))) + )) .withField(new QFieldMetaData("a", STRING)) // inherit versionRange from the table - .withField(new QFieldMetaData("b", STRING).withMiddlewareMetaData(new ApiFieldMetaData().withInitialVersion("1"))) + .withField(new QFieldMetaData("b", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1")))) // we used to have "c" here... now it's in the removed list above! - .withField(new QFieldMetaData("d", STRING).withMiddlewareMetaData(new ApiFieldMetaData().withInitialVersion("3"))) + .withField(new QFieldMetaData("d", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("3")))) ); new QInstanceEnricher(qInstance).enrich(); 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 index 452d9b5a..c53227ff 100644 --- 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 @@ -33,6 +33,7 @@ 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 com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.json.JSONObject; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -62,17 +63,17 @@ class QRecordApiAdapterTest extends BaseTest .withValue("cost", new BigDecimal("3.50")) .withValue("price", new BigDecimal("9.99")); - Map pastApiRecord = QRecordApiAdapter.qRecordToApiMap(person, TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4); + Map pastApiRecord = QRecordApiAdapter.qRecordToApiMap(person, TestUtils.TABLE_NAME_PERSON, TestUtils.API_NAME, 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); + Map currentApiRecord = QRecordApiAdapter.qRecordToApiMap(person, TestUtils.TABLE_NAME_PERSON, TestUtils.API_NAME, 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); + Map futureApiRecord = QRecordApiAdapter.qRecordToApiMap(person, TestUtils.TABLE_NAME_PERSON, TestUtils.API_NAME, 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. @@ -82,6 +83,18 @@ class QRecordApiAdapterTest extends BaseTest assertEquals(LocalDate.parse("1980-05-31"), apiRecord.get("birthDay")); // use the apiFieldName assertFalse(apiRecord.containsKey("price")); // excluded field never appears } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // confirm that for the alternative api, we get a record that looks just like the input record (per its api meta data) // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + for(String version : List.of(TestUtils.V2022_Q4, TestUtils.V2023_Q1, TestUtils.V2023_Q2)) + { + Map alternativeApiRecord = QRecordApiAdapter.qRecordToApiMap(person, TestUtils.TABLE_NAME_PERSON, TestUtils.ALTERNATIVE_API_NAME, version); + for(String key : person.getValues().keySet()) + { + assertEquals(person.getValueString(key), ValueUtils.getValueAsString(alternativeApiRecord.get(key))); + } + } } @@ -97,7 +110,7 @@ class QRecordApiAdapterTest extends BaseTest /////////////////////////////////////////////////////////////////////////////////////////////////////// QRecord recordFromOldApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" {"firstName": "Tim", "shoeCount": 2} - """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4, true); + """), TestUtils.TABLE_NAME_PERSON, TestUtils.API_NAME, TestUtils.V2022_Q4, true); assertEquals(2, recordFromOldApi.getValueInteger("noOfShoes")); /////////////////////////////////////////// @@ -105,7 +118,7 @@ class QRecordApiAdapterTest extends BaseTest /////////////////////////////////////////// QRecord recordFromCurrentApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" {"firstName": "Tim", "noOfShoes": 2} - """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1, true); + """), TestUtils.TABLE_NAME_PERSON, TestUtils.API_NAME, TestUtils.V2023_Q1, true); assertEquals(2, recordFromCurrentApi.getValueInteger("noOfShoes")); ///////////////////////////////////////////// @@ -113,7 +126,7 @@ class QRecordApiAdapterTest extends BaseTest ///////////////////////////////////////////// QRecord recordFromFutureApi = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" {"firstName": "Tim", "noOfShoes": 2, "cost": 3.50} - """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q2, true); + """), TestUtils.TABLE_NAME_PERSON, TestUtils.API_NAME, TestUtils.V2023_Q2, true); assertEquals(2, recordFromFutureApi.getValueInteger("noOfShoes")); assertEquals(new BigDecimal("3.50"), recordFromFutureApi.getValueBigDecimal("cost")); @@ -122,7 +135,7 @@ class QRecordApiAdapterTest extends BaseTest /////////////////////////////////////////////////////////////////// QRecord recordWithApiFieldName = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" {"firstName": "Tim", "birthDay": "1976-05-28"} - """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q2, true); + """), TestUtils.TABLE_NAME_PERSON, TestUtils.API_NAME, TestUtils.V2023_Q2, true); assertEquals("1976-05-28", recordWithApiFieldName.getValueString("birthDate")); //////////////////////////////////////////////////////////////////////////////////////////////// @@ -130,7 +143,7 @@ class QRecordApiAdapterTest extends BaseTest //////////////////////////////////////////////////////////////////////////////////////////////// assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" {"firstName": "Tim", "noOfShoes": 2} - """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4, true)) + """), TestUtils.TABLE_NAME_PERSON, TestUtils.API_NAME, TestUtils.V2022_Q4, true)) .isInstanceOf(QBadRequestException.class) .hasMessageContaining("unrecognized field name: noOfShoes"); @@ -139,7 +152,7 @@ class QRecordApiAdapterTest extends BaseTest ///////////////////////////////////////////////////////////////////////// assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" {"firstName": "Tim", "cost": 2} - """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1, true)) + """), TestUtils.TABLE_NAME_PERSON, TestUtils.API_NAME, TestUtils.V2023_Q1, true)) .isInstanceOf(QBadRequestException.class) .hasMessageContaining("unrecognized field name: cost"); @@ -150,7 +163,7 @@ class QRecordApiAdapterTest extends BaseTest { assertThatThrownBy(() -> QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" {"firstName": "Tim", "price": 2} - """), TestUtils.TABLE_NAME_PERSON, version, true)) + """), TestUtils.TABLE_NAME_PERSON, TestUtils.API_NAME, version, true)) .isInstanceOf(QBadRequestException.class) .hasMessageContaining("unrecognized field name: price"); } @@ -160,7 +173,7 @@ class QRecordApiAdapterTest extends BaseTest //////////////////////////////////////////// QRecord recordWithoutNonEditableFields = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" {"firstName": "Tim", "birthDay": "1976-05-28", "createDate": "2023-03-31T11:44:28Z", "id": 256} - """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1, false); + """), TestUtils.TABLE_NAME_PERSON, TestUtils.API_NAME, TestUtils.V2023_Q1, false); assertFalse(recordWithoutNonEditableFields.getValues().containsKey("createDate")); assertFalse(recordWithoutNonEditableFields.getValues().containsKey("id")); @@ -169,7 +182,7 @@ class QRecordApiAdapterTest extends BaseTest ///////////////////////////////////////////////////////////////////////// QRecord recordWithoutNonEditablePrimaryKeyFields = QRecordApiAdapter.apiJsonObjectToQRecord(new JSONObject(""" {"firstName": "Tim", "birthDay": "1976-05-28", "createDate": "2023-03-31T11:44:28Z", "id": 256} - """), TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1, true); + """), TestUtils.TABLE_NAME_PERSON, TestUtils.API_NAME, TestUtils.V2023_Q1, true); assertFalse(recordWithoutNonEditablePrimaryKeyFields.getValues().containsKey("createDate")); assertEquals(256, recordWithoutNonEditablePrimaryKeyFields.getValues().get("id")); 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 c7f891be..ca5e3d64 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 @@ -22,11 +22,14 @@ package com.kingsrook.qqq.api.javalin; +import java.time.LocalDate; +import java.time.Month; import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.api.BaseTest; import com.kingsrook.qqq.api.TestUtils; 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.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; @@ -49,6 +52,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.modules.authentication.implementations.FullyAnonymousAuthenticationModule; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; +import io.javalin.apibuilder.EndpointGroup; import kong.unirest.HttpResponse; import kong.unirest.Unirest; import org.eclipse.jetty.http.HttpStatus; @@ -91,13 +95,14 @@ class QJavalinApiHandlerTest extends BaseTest .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaData() + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withApiTableName("externalName") - .withInitialVersion(TestUtils.V2022_Q4))); + .withInitialVersion(TestUtils.V2022_Q4)))); qJavalinImplementation = new QJavalinImplementation(qInstance); qJavalinImplementation.startJavalinServer(PORT); - qJavalinImplementation.getJavalinService().routes(new QJavalinApiHandler(qInstance).getRoutes()); + EndpointGroup routes = new QJavalinApiHandler(qInstance).getRoutes(); + qJavalinImplementation.getJavalinService().routes(routes); } @@ -124,7 +129,7 @@ class QJavalinApiHandlerTest extends BaseTest System.out.println(response.getBody()); assertThat(response.getBody()) .contains(""" - title: "TestAPI" + title: "Test API" """) .contains(""" /person/query: @@ -274,6 +279,77 @@ class QJavalinApiHandlerTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQuery200AlternativeApi() + { + HttpResponse response = Unirest.get(BASE_URL + "/person-api/" + VERSION + "/person/query").asString(); + assertEquals(HttpStatus.OK_200, response.getStatus()); + JSONObject jsonObject = new JSONObject(response.getBody()); + assertEquals(0, jsonObject.getInt("count")); + assertEquals(1, jsonObject.getInt("pageNo")); + assertEquals(50, jsonObject.getInt("pageSize")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableOnlyInOneApi() + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // the person table (which most of our tests are against) is in both api's in this instance (/api/ and /person-api/). // + // do a request here for the order table, which is only in /api // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + HttpResponse response = Unirest.get(BASE_URL + "/api/" + VERSION + "/order/query").asString(); + assertEquals(HttpStatus.OK_200, response.getStatus()); + JSONObject jsonObject = new JSONObject(response.getBody()); + assertEquals(0, jsonObject.getInt("count")); + assertEquals(1, jsonObject.getInt("pageNo")); + assertEquals(50, jsonObject.getInt("pageSize")); + + /////////////////////////////////////////////////////////////////////// + // now make sure we get a 404 for it under the /person-api/ api path // + /////////////////////////////////////////////////////////////////////// + response = Unirest.get(BASE_URL + "/person-api/" + VERSION + "/order/query").asString(); + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldDifferencesBetweenApis() throws QException + { + insertPersonRecord(1, "Homer", "Simpson", LocalDate.of(1970, Month.JANUARY, 1)); + + ///////////////////////////////////////////////////////////// + // on the main api, birthDate has been renamed to birthDay // + ///////////////////////////////////////////////////////////// + HttpResponse response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/1").asString(); + assertEquals(HttpStatus.OK_200, response.getStatus()); + JSONObject jsonObject = new JSONObject(response.getBody()); + assertEquals("1970-01-01", jsonObject.getString("birthDay")); + assertFalse(jsonObject.has("birthDate")); + + /////////////////////////////////////////// + // but it's birthDate on the /person-api // + /////////////////////////////////////////// + response = Unirest.get(BASE_URL + "/person-api/" + VERSION + "/person/1").asString(); + assertEquals(HttpStatus.OK_200, response.getStatus()); + jsonObject = new JSONObject(response.getBody()); + assertEquals("1970-01-01", jsonObject.getString("birthDate")); + assertFalse(jsonObject.has("birthDay")); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -1168,10 +1244,20 @@ class QJavalinApiHandlerTest extends BaseTest ** *******************************************************************************/ private static void insertPersonRecord(Integer id, String firstName, String lastName) throws QException + { + insertPersonRecord(id, firstName, lastName, null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void insertPersonRecord(Integer id, String firstName, String lastName, LocalDate birthDate) throws QException { InsertInput insertInput = new InsertInput(); insertInput.setTableName(TestUtils.TABLE_NAME_PERSON); - insertInput.setRecords(List.of(new QRecord().withValue("id", id).withValue("firstName", firstName).withValue("lastName", lastName))); + insertInput.setRecords(List.of(new QRecord().withValue("id", id).withValue("firstName", firstName).withValue("lastName", lastName).withValue("birthDate", birthDate))); new InsertAction().execute(insertInput); } diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataTest.java index 2ab9d635..fcbb5ac7 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataTest.java @@ -23,8 +23,10 @@ package com.kingsrook.qqq.api.model.metadata; import java.util.List; +import com.kingsrook.qqq.api.TestUtils; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; +import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -46,15 +48,7 @@ class ApiInstanceMetaDataTest @Test void testValidationPasses() { - assertValidationErrors(new ApiInstanceMetaData() - .withName("QQQ API") - .withDescription("Test API for QQQ") - .withContactEmail("contact@kingsrook.com") - .withCurrentVersion(new APIVersion("2023.Q1")) - .withSupportedVersions(List.of(new APIVersion("2022.Q3"), new APIVersion("2022.Q4"), new APIVersion("2023.Q1"))) - .withPastVersions(List.of(new APIVersion("2022.Q2"), new APIVersion("2022.Q3"), new APIVersion("2022.Q4"))) - .withFutureVersions(List.of(new APIVersion("2023.Q2"))), - List.of()); + assertValidationErrors(makeBaselineValidApiInstanceMetaDataWithVersions(), List.of()); } @@ -67,6 +61,8 @@ class ApiInstanceMetaDataTest { assertValidationErrors(new ApiInstanceMetaData(), List.of( "Missing name", + "Missing label", + "Missing path", "Missing description", "Missing contactEmail", "Missing currentVersion", @@ -118,11 +114,11 @@ class ApiInstanceMetaDataTest qInstance.addTable(new QTableMetaData() .withName("myValidTable") - .withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion("2023.Q1"))); + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("2023.Q1")))); qInstance.addTable(new QTableMetaData() .withName("myInvalidTable") - .withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion("2022.Q1"))); + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("notAVersion")))); assertValidationErrors(qInstance, makeBaselineValidApiInstanceMetaData() .withCurrentVersion(new APIVersion("2023.Q1")) @@ -131,7 +127,7 @@ class ApiInstanceMetaDataTest qInstance.addTable(new QTableMetaData() .withName("myFutureValidTable") - .withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion("2024.Q1"))); + .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("2024.Q1")))); assertValidationErrors(qInstance, makeBaselineValidApiInstanceMetaData() .withCurrentVersion(new APIVersion("2023.Q1")) @@ -143,19 +139,46 @@ class ApiInstanceMetaDataTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPathSlashes() + { + assertValidationErrors(makeBaselineValidApiInstanceMetaDataWithVersions().withPath("myPath/"), List.of("does not start with '/'")); + assertValidationErrors(makeBaselineValidApiInstanceMetaDataWithVersions().withPath("/yourPath"), List.of("does not end with '/'")); + assertValidationErrors(makeBaselineValidApiInstanceMetaDataWithVersions().withPath("any/path"), List.of("does not start with '/'", "does not end with '/'")); + } + + + /******************************************************************************* ** *******************************************************************************/ private static ApiInstanceMetaData makeBaselineValidApiInstanceMetaData() { return (new ApiInstanceMetaData() - .withName("QQQ API") + .withName(TestUtils.API_NAME) + .withPath("/api/") + .withLabel("QQQ API") .withDescription("Test API for QQQ") .withContactEmail("contact@kingsrook.com")); } + /******************************************************************************* + ** + *******************************************************************************/ + private static ApiInstanceMetaData makeBaselineValidApiInstanceMetaDataWithVersions() + { + return (makeBaselineValidApiInstanceMetaData() + .withCurrentVersion(new APIVersion("1")) + .withSupportedVersions(List.of(new APIVersion("1")))); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -172,10 +195,10 @@ class ApiInstanceMetaDataTest *******************************************************************************/ private void assertValidationErrors(QInstance qInstance, ApiInstanceMetaData apiInstanceMetaData, List expectedErrors) { - qInstance.withMiddlewareMetaData(apiInstanceMetaData); + qInstance.withMiddlewareMetaData(new ApiInstanceMetaDataContainer().withApiInstanceMetaData(apiInstanceMetaData)); QInstanceValidator validator = new QInstanceValidator(); - apiInstanceMetaData.validate(qInstance, validator); + apiInstanceMetaData.validate(apiInstanceMetaData.getName(), qInstance, validator); List errors = validator.getErrors(); assertEquals(expectedErrors.size(), errors.size(), "Expected # of validation errors (got: " + errors + ")");