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 537548d2..1332e018 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 @@ -39,6 +39,7 @@ 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.ApiOperation; 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; @@ -334,16 +335,26 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction operationProviders = List.of(apiInstanceMetaData, apiTableMetaData); + + boolean getEnabled = ApiOperation.GET.isOperationEnabled(operationProviders) && getCapability; + boolean queryByQueryStringEnabled = ApiOperation.QUERY_BY_QUERY_STRING.isOperationEnabled(operationProviders) && queryCapability; + boolean insertEnabled = ApiOperation.UPDATE.isOperationEnabled(operationProviders) && insertCapability; + boolean insertBulkEnabled = ApiOperation.BULK_INSERT.isOperationEnabled(operationProviders) && insertCapability; + boolean updateEnabled = ApiOperation.INSERT.isOperationEnabled(operationProviders) && updateCapability; + boolean updateBulkEnabled = ApiOperation.BULK_UPDATE.isOperationEnabled(operationProviders) && updateCapability; + boolean deleteEnabled = ApiOperation.DELETE.isOperationEnabled(operationProviders) && deleteCapability; + boolean deleteBulkEnabled = ApiOperation.BULK_DELETE.isOperationEnabled(operationProviders) && deleteCapability; + + if(!getEnabled && !queryByQueryStringEnabled && !insertEnabled && !insertBulkEnabled && !updateEnabled && !updateBulkEnabled && !deleteEnabled && !deleteBulkEnabled) { - LOG.debug("Omitting table [" + tableName + "] because it does not have any supported capabilities"); + LOG.debug("Omitting table [" + tableName + "] because it does not have any supported capabilities / enabled operations"); continue; } String tableApiName = StringUtils.hasContent(apiTableMetaData.getApiTableName()) ? apiTableMetaData.getApiTableName() : tableName; String tableApiNameUcFirst = StringUtils.ucFirst(tableApiName); String tableLabel = table.getLabel(); - String primaryKeyName = table.getPrimaryKeyField(); QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField()); String primaryKeyLabel = primaryKeyField.getLabel(); String primaryKeyApiName = ApiFieldMetaData.getEffectiveApiFieldName(apiName, primaryKeyField); @@ -396,15 +407,18 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction badRequestMessages = new ArrayList<>(); - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName); + QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.QUERY_BY_QUERY_STRING); String tableName = table.getName(); QueryInput queryInput = new QueryInput(); - setupSession(context, queryInput, version); + setupSession(context, queryInput, version, apiInstanceMetaData); QJavalinAccessLogger.logStart("apiQuery", logPair("table", tableName)); queryInput.setTableName(tableName); @@ -1123,36 +1130,54 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private static QTableMetaData validateTableAndVersion(Context context, ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName) throws QNotFoundException + private static QTableMetaData validateTableAndVersion(Context context, ApiInstanceMetaData apiInstanceMetaData, String version, String tableApiName, ApiOperation operation) throws QNotFoundException { QNotFoundException qNotFoundException = new QNotFoundException("Could not find any resources at path " + context.path()); - QTableMetaData table = getTableByApiName(apiInstanceMetaData.getName(), version, tableApiName); + QTableMetaData table = getTableByApiName(apiInstanceMetaData.getName(), version, tableApiName); + LogPair[] logPairs = new LogPair[] { logPair("apiName", apiInstanceMetaData.getName()), logPair("version", version), logPair("tableApiName", tableApiName), logPair("operation", operation) }; if(table == null) { + LOG.info("404 because table is null", logPairs); throw (qNotFoundException); } if(BooleanUtils.isTrue(table.getIsHidden())) { + LOG.info("404 because table isHidden", logPairs); throw (qNotFoundException); } ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); if(apiTableMetaDataContainer == null) { + LOG.info("404 because table apiMetaDataContainer is null", logPairs); throw (qNotFoundException); } ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiInstanceMetaData.getName()); if(apiTableMetaData == null) { + LOG.info("404 because table apiMetaData is null", logPairs); throw (qNotFoundException); } if(BooleanUtils.isTrue(apiTableMetaData.getIsExcluded())) { + LOG.info("404 because table is excluded", logPairs); + throw (qNotFoundException); + } + + if(!operation.isOperationEnabled(List.of(apiInstanceMetaData, apiTableMetaData))) + { + LOG.info("404 because api operation is not enabled", logPairs); + throw (qNotFoundException); + } + + if(!table.isCapabilityEnabled(qInstance.getBackendForTable(table.getName()), operation.getCapability())) + { + LOG.info("404 because table capability is not enabled", logPairs); throw (qNotFoundException); } @@ -1160,11 +1185,13 @@ public class QJavalinApiHandler List supportedVersions = apiInstanceMetaData.getSupportedVersions(); if(CollectionUtils.nullSafeIsEmpty(supportedVersions) || !supportedVersions.contains(requestApiVersion)) { + LOG.info("404 because requested version is not supported", logPairs); throw (qNotFoundException); } if(!apiTableMetaData.getApiVersionRange().includes(requestApiVersion)) { + LOG.info("404 because table version range does not include requested version", logPairs); throw (qNotFoundException); } @@ -1351,12 +1378,12 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName); + QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.INSERT); String tableName = table.getName(); InsertInput insertInput = new InsertInput(); - setupSession(context, insertInput, version); + setupSession(context, insertInput, version, apiInstanceMetaData); QJavalinAccessLogger.logStart("apiInsert", logPair("table", tableName)); insertInput.setTableName(tableName); @@ -1392,6 +1419,22 @@ public class QJavalinApiHandler InsertAction insertAction = new InsertAction(); InsertOutput insertOutput = insertAction.execute(insertInput); + List errors = insertOutput.getRecords().get(0).getErrors(); + if(CollectionUtils.nullSafeHasContents(errors)) + { + boolean isBadRequest = areAnyErrorsBadRequest(errors); + + String message = "Error inserting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors); + if(isBadRequest) + { + throw (new QBadRequestException(message)); + } + else + { + throw (new QException(message)); + } + } + LinkedHashMap outputRecord = new LinkedHashMap<>(); outputRecord.put(table.getPrimaryKeyField(), insertOutput.getRecords().get(0).getValue(table.getPrimaryKeyField())); @@ -1410,6 +1453,20 @@ public class QJavalinApiHandler + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean areAnyErrorsBadRequest(List errors) + { + boolean isBadRequest = errors.stream().anyMatch(e -> + e.contains("Missing value in required field") + || e.contains("You do not have permission") + ); + return isBadRequest; + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -1421,12 +1478,12 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName); + QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_INSERT); String tableName = table.getName(); InsertInput insertInput = new InsertInput(); - setupSession(context, insertInput, version); + setupSession(context, insertInput, version, apiInstanceMetaData); QJavalinAccessLogger.logStart("apiBulkInsert", logPair("table", tableName)); insertInput.setTableName(tableName); @@ -1530,12 +1587,12 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName); + QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_UPDATE); String tableName = table.getName(); UpdateInput updateInput = new UpdateInput(); - setupSession(context, updateInput, version); + setupSession(context, updateInput, version, apiInstanceMetaData); QJavalinAccessLogger.logStart("apiBulkUpdate", logPair("table", tableName)); updateInput.setTableName(tableName); @@ -1672,12 +1729,12 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName); + QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.BULK_DELETE); String tableName = table.getName(); DeleteInput deleteInput = new DeleteInput(); - setupSession(context, deleteInput, version); + setupSession(context, deleteInput, version, apiInstanceMetaData); QJavalinAccessLogger.logStart("apiBulkDelete", logPair("table", tableName)); deleteInput.setTableName(tableName); @@ -1804,12 +1861,12 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName); + QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.UPDATE); String tableName = table.getName(); UpdateInput updateInput = new UpdateInput(); - setupSession(context, updateInput, version); + setupSession(context, updateInput, version, apiInstanceMetaData); QJavalinAccessLogger.logStart("apiUpdate", logPair("table", tableName)); updateInput.setTableName(tableName); @@ -1856,10 +1913,17 @@ public class QJavalinApiHandler } else { - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // todo - could be smarter here, about some of these errors being 400, not 500... e.g., a missing required field // - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - throw (new QException("Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors))); + boolean isBadRequest = areAnyErrorsBadRequest(errors); + + String message = "Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors); + if(isBadRequest) + { + throw (new QBadRequestException(message)); + } + else + { + throw (new QException(message)); + } } } @@ -1888,12 +1952,12 @@ public class QJavalinApiHandler try { - QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName); + QTableMetaData table = validateTableAndVersion(context, apiInstanceMetaData, version, tableApiName, ApiOperation.DELETE); String tableName = table.getName(); DeleteInput deleteInput = new DeleteInput(); - setupSession(context, deleteInput, version); + setupSession(context, deleteInput, version, apiInstanceMetaData); QJavalinAccessLogger.logStart("apiDelete", logPair("table", tableName)); deleteInput.setTableName(tableName); 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 50a0581b..ec38dac0 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 @@ -22,6 +22,7 @@ package com.kingsrook.qqq.api.model.metadata; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -41,7 +42,7 @@ import org.apache.commons.lang.BooleanUtils; /******************************************************************************* ** *******************************************************************************/ -public class ApiInstanceMetaData +public class ApiInstanceMetaData implements ApiOperation.EnabledOperationsProvider { private String name; private String label; @@ -56,6 +57,9 @@ public class ApiInstanceMetaData private List servers; + private Set enabledOperations = new HashSet<>(); + private Set disabledOperations = new HashSet<>(); + private boolean includeErrorTooManyRequests = true; @@ -466,4 +470,124 @@ public class ApiInstanceMetaData return (this); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Set getEnabledOperations() + { + return (enabledOperations); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Set getDisabledOperations() + { + return (disabledOperations); + } + + + + /******************************************************************************* + ** Setter for enabledOperations + *******************************************************************************/ + public void setEnabledOperations(Set enabledOperations) + { + this.enabledOperations = enabledOperations; + } + + + + /******************************************************************************* + ** Fluent setter for enabledOperations + *******************************************************************************/ + public ApiInstanceMetaData withEnabledOperations(Set enabledOperations) + { + this.enabledOperations = enabledOperations; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for enabledOperations + *******************************************************************************/ + public ApiInstanceMetaData withEnabledOperation(ApiOperation operation) + { + return withEnabledOperations(operation); + } + + + + /******************************************************************************* + ** Fluent setter for enabledOperations + *******************************************************************************/ + public ApiInstanceMetaData withEnabledOperations(ApiOperation... operations) + { + if(this.enabledOperations == null) + { + this.enabledOperations = new HashSet<>(); + } + if(operations != null) + { + Collections.addAll(this.enabledOperations, operations); + } + return (this); + } + + + + /******************************************************************************* + ** Setter for disabledOperations + *******************************************************************************/ + public void setDisabledOperations(Set disabledOperations) + { + this.disabledOperations = disabledOperations; + } + + + + /******************************************************************************* + ** Fluent setter for disabledOperations + *******************************************************************************/ + public ApiInstanceMetaData withDisabledOperations(Set disabledOperations) + { + this.disabledOperations = disabledOperations; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for disabledOperations + *******************************************************************************/ + public ApiInstanceMetaData withDisabledOperation(ApiOperation operation) + { + return withDisabledOperations(operation); + } + + + + /******************************************************************************* + ** Fluent setter for disabledOperations + *******************************************************************************/ + public ApiInstanceMetaData withDisabledOperations(ApiOperation... operations) + { + if(this.disabledOperations == null) + { + this.disabledOperations = new HashSet<>(); + } + if(operations != null) + { + Collections.addAll(this.disabledOperations, operations); + } + 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 2ceb4254..5b3a1aa0 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 @@ -23,9 +23,13 @@ package com.kingsrook.qqq.api.model.metadata.tables; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import com.kingsrook.qqq.api.ApiMiddlewareType; import com.kingsrook.qqq.api.model.APIVersionRange; +import com.kingsrook.qqq.api.model.metadata.ApiOperation; 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; @@ -36,7 +40,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* ** *******************************************************************************/ -public class ApiTableMetaData +public class ApiTableMetaData implements ApiOperation.EnabledOperationsProvider { private String initialVersion; private String finalVersion; @@ -46,6 +50,9 @@ public class ApiTableMetaData private List removedApiFields; + private Set enabledOperations = new HashSet<>(); + private Set disabledOperations = new HashSet<>(); + /******************************************************************************* @@ -283,4 +290,124 @@ public class ApiTableMetaData return (this); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Set getEnabledOperations() + { + return (enabledOperations); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Set getDisabledOperations() + { + return (disabledOperations); + } + + + + /******************************************************************************* + ** Setter for enabledOperations + *******************************************************************************/ + public void setEnabledOperations(Set enabledOperations) + { + this.enabledOperations = enabledOperations; + } + + + + /******************************************************************************* + ** Fluent setter for enabledOperations + *******************************************************************************/ + public ApiTableMetaData withEnabledOperations(Set enabledOperations) + { + this.enabledOperations = enabledOperations; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for enabledOperations + *******************************************************************************/ + public ApiTableMetaData withEnabledOperation(ApiOperation operation) + { + return withEnabledOperations(operation); + } + + + + /******************************************************************************* + ** Fluent setter for enabledOperations + *******************************************************************************/ + public ApiTableMetaData withEnabledOperations(ApiOperation... operations) + { + if(this.enabledOperations == null) + { + this.enabledOperations = new HashSet<>(); + } + if(operations != null) + { + Collections.addAll(this.enabledOperations, operations); + } + return (this); + } + + + + /******************************************************************************* + ** Setter for disabledOperations + *******************************************************************************/ + public void setDisabledOperations(Set disabledOperations) + { + this.disabledOperations = disabledOperations; + } + + + + /******************************************************************************* + ** Fluent setter for disabledOperations + *******************************************************************************/ + public ApiTableMetaData withDisabledOperations(Set disabledOperations) + { + this.disabledOperations = disabledOperations; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for disabledOperations + *******************************************************************************/ + public ApiTableMetaData withDisabledOperation(ApiOperation operation) + { + return withDisabledOperations(operation); + } + + + + /******************************************************************************* + ** Fluent setter for disabledOperations + *******************************************************************************/ + public ApiTableMetaData withDisabledOperations(ApiOperation... operations) + { + if(this.disabledOperations == null) + { + this.disabledOperations = new HashSet<>(); + } + if(operations != null) + { + Collections.addAll(this.disabledOperations, operations); + } + return (this); + } + }