Support multiple api's within a q instance. For science!

This commit is contained in:
2023-04-04 13:40:32 -05:00
parent e35761e0a6
commit e779c392bb
21 changed files with 1171 additions and 341 deletions

View File

@ -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> T tryElse(UnsafeSupplier<T, ?> 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> T tryAndRequireNonNullElse(UnsafeSupplier<T, ?> supplier, T defaultIfThrew)
{
try
{
T t = supplier.get();
if(t != null)
{
return (t);
}
}
catch(Exception e)
{
//////////
// noop //
//////////
}
return (defaultIfThrew);
}
}

View File

@ -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"));
}
}

View File

@ -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<GenerateO
*******************************************************************************/
public GenerateOpenApiSpecOutput execute(GenerateOpenApiSpecInput input) throws QException
{
String version = input.getVersion();
String basePath = "/api/" + version + "/";
QInstance qInstance = QContext.getQInstance();
String version = input.getVersion();
ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(qInstance);
if(apiInstanceMetaDataContainer == null)
{
throw new QException("No ApiInstanceMetaDataContainer exists in this instance");
}
if(!StringUtils.hasContent(input.getApiName()))
{
throw new QException("Missing required input: apiName");
}
ApiInstanceMetaData apiInstanceMetaData = apiInstanceMetaDataContainer.getApiInstanceMetaData(input.getApiName());
if(apiInstanceMetaData == null)
{
throw new QException("Could not find apiInstanceMetaData named [" + input.getApiName() + "] in this instance");
}
if(!StringUtils.hasContent(input.getVersion()))
{
throw new QException("Missing required input: version");
}
ApiInstanceMetaData apiInstanceMetaData = ApiInstanceMetaData.of(qInstance);
if(!apiInstanceMetaData.getSupportedVersions().contains(new APIVersion(version)))
{
throw (new QException("[" + version + "] is not a supported API Version."));
}
String basePath = apiInstanceMetaData.getPath() + version + "/";
String apiName = apiInstanceMetaData.getName();
OpenAPI openAPI = new OpenAPI()
.withVersion("3.0.3")
.withInfo(new Info()
.withTitle(apiInstanceMetaData.getName())
.withTitle(apiInstanceMetaData.getLabel())
.withDescription(apiInstanceMetaData.getDescription())
.withContact(new Contact()
.withEmail(apiInstanceMetaData.getContactEmail()))
@ -272,7 +295,14 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
continue;
}
ApiTableMetaData apiTableMetaData = ApiTableMetaData.of(table);
ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table);
if(apiTableMetaDataContainer == null)
{
LOG.debug("Omitting table [" + tableName + "] because it does not have an apiTableMetaDataContainer");
continue;
}
ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiName);
if(apiTableMetaData == null)
{
LOG.debug("Omitting table [" + tableName + "] because it does not have any apiTableMetaData");
@ -312,8 +342,8 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
String primaryKeyName = table.getPrimaryKeyField();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
String primaryKeyLabel = primaryKeyField.getLabel();
String primaryKeyApiName = ApiFieldMetaData.getEffectiveApiFieldName(primaryKeyField);
List<QFieldMetaData> tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version)).getFields();
String primaryKeyApiName = ApiFieldMetaData.getEffectiveApiFieldName(apiName, primaryKeyField);
List<QFieldMetaData> 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<GenerateO
for(QFieldMetaData field : tableApiFields)
{
Schema fieldSchema = getFieldSchema(table, field);
tableFields.put(ApiFieldMetaData.getEffectiveApiFieldName(field), fieldSchema);
tableFields.put(ApiFieldMetaData.getEffectiveApiFieldName(apiInstanceMetaData.getName(), field), fieldSchema);
}
//////////////////////////////////
// recursively add associations //
//////////////////////////////////
addAssociations(table, tableSchema);
addAssociations(apiName, table, tableSchema);
//////////////////////////////////////////////////////////////////////////////
// table as a search result (the base search result, plus the table itself) //
@ -770,13 +800,13 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
/*******************************************************************************
**
*******************************************************************************/
private static void addAssociations(QTableMetaData table, Schema tableSchema)
private static void addAssociations(String apiName, QTableMetaData table, Schema tableSchema)
{
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
String associatedTableName = association.getAssociatedTableName();
QTableMetaData associatedTable = QContext.getQInstance().getTable(associatedTableName);
ApiTableMetaData associatedApiTableMetaData = Objects.requireNonNullElse(ApiTableMetaData.of(associatedTable), new ApiTableMetaData());
ApiTableMetaData associatedApiTableMetaData = ObjectUtils.tryElse(() -> ApiTableMetaDataContainer.of(associatedTable).getApiTableMetaData(apiName), new ApiTableMetaData());
String associatedTableApiName = StringUtils.hasContent(associatedApiTableMetaData.getApiTableName()) ? associatedApiTableMetaData.getApiTableName() : associatedTableName;
tableSchema.getProperties().put(association.getName(), new Schema()

View File

@ -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<GetTableApi
fieldList.sort(Comparator.comparing(QFieldMetaData::getLabel));
for(QFieldMetaData field : fieldList)
{
if(!isExcluded(field) && getApiVersionRange(field).includes(version))
if(!isExcluded(input.getApiName(), field) && getApiVersionRange(input.getApiName(), field).includes(version))
{
fields.add(field);
}
@ -82,9 +85,9 @@ public class GetTableApiFieldsAction extends AbstractQActionFunction<GetTableApi
//////////////////////////////////////////////////////////////////////////////////////////////////
// look for removed fields (e.g., not currently in the table anymore), that are in this version //
//////////////////////////////////////////////////////////////////////////////////////////////////
for(QFieldMetaData field : CollectionUtils.nonNullList(getRemovedApiFields(table)))
for(QFieldMetaData field : CollectionUtils.nonNullList(getRemovedApiFields(input.getApiName(), table)))
{
if(!isExcluded(field) && getApiVersionRangeForRemovedField(field).includes(version))
if(!isExcluded(input.getApiName(), field) && getApiVersionRangeForRemovedField(input.getApiName(), field).includes(version))
{
fields.add(field);
}
@ -98,9 +101,9 @@ public class GetTableApiFieldsAction extends AbstractQActionFunction<GetTableApi
/*******************************************************************************
**
*******************************************************************************/
private boolean isExcluded(QFieldMetaData field)
private boolean isExcluded(String apiName, QFieldMetaData field)
{
ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(field);
ApiFieldMetaData apiFieldMetaData = getApiFieldMetaData(apiName, field);
if(apiFieldMetaData != null && BooleanUtils.isTrue(apiFieldMetaData.getIsExcluded()))
{
return (true);
@ -114,14 +117,14 @@ public class GetTableApiFieldsAction extends AbstractQActionFunction<GetTableApi
/*******************************************************************************
**
*******************************************************************************/
private APIVersionRange getApiVersionRangeForRemovedField(QFieldMetaData field)
private APIVersionRange getApiVersionRangeForRemovedField(String apiName, QFieldMetaData field)
{
ApiFieldMetaData middlewareMetaData = ApiFieldMetaData.of(field);
if(middlewareMetaData != null && middlewareMetaData.getInitialVersion() != null)
ApiFieldMetaData apiFieldMetaData = getApiFieldMetaData(apiName, field);
if(apiFieldMetaData != null && apiFieldMetaData.getInitialVersion() != null)
{
if(StringUtils.hasContent(middlewareMetaData.getFinalVersion()))
if(StringUtils.hasContent(apiFieldMetaData.getFinalVersion()))
{
return (APIVersionRange.betweenAndIncluding(middlewareMetaData.getInitialVersion(), middlewareMetaData.getFinalVersion()));
return (APIVersionRange.betweenAndIncluding(apiFieldMetaData.getInitialVersion(), apiFieldMetaData.getFinalVersion()));
}
else
{
@ -139,12 +142,12 @@ public class GetTableApiFieldsAction extends AbstractQActionFunction<GetTableApi
/*******************************************************************************
**
*******************************************************************************/
private APIVersionRange getApiVersionRange(QFieldMetaData field)
private APIVersionRange getApiVersionRange(String apiName, QFieldMetaData field)
{
ApiFieldMetaData middlewareMetaData = ApiFieldMetaData.of(field);
if(middlewareMetaData != null && middlewareMetaData.getInitialVersion() != null)
ApiFieldMetaData apiFieldMetaData = getApiFieldMetaData(apiName, field);
if(apiFieldMetaData != null && apiFieldMetaData.getInitialVersion() != null)
{
return (APIVersionRange.afterAndIncluding(middlewareMetaData.getInitialVersion()));
return (APIVersionRange.afterAndIncluding(apiFieldMetaData.getInitialVersion()));
}
return (APIVersionRange.none());
@ -155,9 +158,19 @@ public class GetTableApiFieldsAction extends AbstractQActionFunction<GetTableApi
/*******************************************************************************
**
*******************************************************************************/
private List<QFieldMetaData> 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<QFieldMetaData> getRemovedApiFields(String apiName, QTableMetaData table)
{
ApiTableMetaData apiTableMetaData = ObjectUtils.tryAndRequireNonNullElse(() -> ApiTableMetaDataContainer.of(table).getApiTableMetaData(apiName), new ApiTableMetaData());
if(apiTableMetaData != null)
{
return (apiTableMetaData.getRemovedApiFields());

View File

@ -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<Pair<String, String>, List<QFieldMetaData>> fieldListCache = new HashMap<>();
private static Map<Pair<String, String>, Map<String, QFieldMetaData>> fieldMapCache = new HashMap<>();
private static Map<ApiNameVersionAndTableName, List<QFieldMetaData>> fieldListCache = new HashMap<>();
private static Map<ApiNameVersionAndTableName, Map<String, QFieldMetaData>> fieldMapCache = new HashMap<>();
/*******************************************************************************
** Convert a QRecord to a map for the API
*******************************************************************************/
public static Map<String, Serializable> qRecordToApiMap(QRecord record, String tableName, String apiVersion) throws QException
public static Map<String, Serializable> qRecordToApiMap(QRecord record, String tableName, String apiName, String apiVersion) throws QException
{
List<QFieldMetaData> tableApiFields = getTableApiFieldList(tableName, apiVersion);
List<QFieldMetaData> tableApiFields = getTableApiFieldList(new ApiNameVersionAndTableName(apiName, apiVersion, tableName));
LinkedHashMap<String, Serializable> 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<String, QFieldMetaData> apiFieldsMap = getTableApiFieldMap(tableName, apiVersion);
Map<String, QFieldMetaData> apiFieldsMap = getTableApiFieldMap(new ApiNameVersionAndTableName(apiName, apiVersion, tableName));
List<String> 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<String, QFieldMetaData> getTableApiFieldMap(String tableName, String apiVersion) throws QException
private static Map<String, QFieldMetaData> getTableApiFieldMap(ApiNameVersionAndTableName apiNameVersionAndTableName) throws QException
{
Pair<String, String> key = new Pair<>(tableName, apiVersion);
if(!fieldMapCache.containsKey(key))
if(!fieldMapCache.containsKey(apiNameVersionAndTableName))
{
Map<String, QFieldMetaData> map = getTableApiFieldList(tableName, apiVersion).stream().collect(Collectors.toMap(f -> (ApiFieldMetaData.getEffectiveApiFieldName(f)), f -> f));
fieldMapCache.put(key, map);
Map<String, QFieldMetaData> 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<QFieldMetaData> getTableApiFieldList(String tableName, String apiVersion) throws QException
private static List<QFieldMetaData> getTableApiFieldList(ApiNameVersionAndTableName apiNameVersionAndTableName) throws QException
{
Pair<String, String> key = new Pair<>(tableName, apiVersion);
if(!fieldListCache.containsKey(key))
if(!fieldListCache.containsKey(apiNameVersionAndTableName))
{
List<QFieldMetaData> value = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(apiVersion)).getFields();
fieldListCache.put(key, value);
List<QFieldMetaData> 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)
{
}
}

View File

@ -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<String, Map<String, QTableMetaData>> tableApiNameMap = new HashMap<>();
/////////////////////////////////////
// key: Pair<apiName, apiVersion> //
/////////////////////////////////////
private static Map<Pair<String, String>, Map<String, QTableMetaData>> tableApiNameMap = new HashMap<>();
private static Map<String, Integer> 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)));
ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(qInstance);
for(Map.Entry<String, ApiInstanceMetaData> entry : apiInstanceMetaDataContainer.getApis().entrySet())
{
ApiInstanceMetaData apiInstanceMetaData = entry.getValue();
String rootPath = apiInstanceMetaData.getPath();
//////////////////////////////////////////////
// default page is the current version spec //
//////////////////////////////////////////////
ApiBuilder.get("/api/", QJavalinApiHandler::doSpecHtml);
ApiBuilder.get(rootPath, context -> doSpecHtml(context, apiInstanceMetaData));
ApiBuilder.path("/api/{version}", () -> // todo - configurable, that /api/ bit?
ApiBuilder.path(rootPath + "{version}", () ->
{
////////////////////////////////////////////
// default page for a version is its spec //
////////////////////////////////////////////
ApiBuilder.get("/", QJavalinApiHandler::doSpecHtml);
ApiBuilder.get("/", context -> doSpecHtml(context, apiInstanceMetaData));
ApiBuilder.get("/openapi.yaml", QJavalinApiHandler::doSpecYaml);
ApiBuilder.get("/openapi.json", QJavalinApiHandler::doSpecJson);
ApiBuilder.get("/openapi.html", QJavalinApiHandler::doSpecHtml);
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.path("/{tableName}", () ->
{
ApiBuilder.get("/openapi.yaml", QJavalinApiHandler::doSpecYaml);
ApiBuilder.get("/openapi.json", QJavalinApiHandler::doSpecJson);
ApiBuilder.get("/openapi.yaml", context -> doSpecYaml(context, apiInstanceMetaData));
ApiBuilder.get("/openapi.json", context -> doSpecJson(context, apiInstanceMetaData));
ApiBuilder.post("/", QJavalinApiHandler::doInsert);
ApiBuilder.post("/", context -> doInsert(context, apiInstanceMetaData));
ApiBuilder.get("/query", QJavalinApiHandler::doQuery);
// ApiBuilder.post("/query", QJavalinApiHandler::doQuery);
ApiBuilder.get("/query", context -> doQuery(context, apiInstanceMetaData));
// ApiBuilder.post("/query", context -> doQuery(context, apiInstanceMetaData));
ApiBuilder.post("/bulk", QJavalinApiHandler::bulkInsert);
ApiBuilder.patch("/bulk", QJavalinApiHandler::bulkUpdate);
ApiBuilder.delete("/bulk", QJavalinApiHandler::bulkDelete);
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}", QJavalinApiHandler::doGet);
ApiBuilder.patch("/{primaryKey}", QJavalinApiHandler::doUpdate);
ApiBuilder.delete("/{primaryKey}", QJavalinApiHandler::doDelete);
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<String, Object> 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);
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("<option value=\"/api/").append(supportedVersion).append("/openapi.html\">").append(supportedVersion).append("</option>");
otherVersionOptions.append("<option value=\"").append(apiInstanceMetaData.getPath()).append(supportedVersion).append("/openapi.html\">").append(supportedVersion).append("</option>");
}
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<String, Serializable> outputRecord = QRecordApiAdapter.qRecordToApiMap(record, tableName, version);
Map<String, Serializable> 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<String> 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<Map<String, Serializable>> 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<APIVersion> supportedVersions = ApiInstanceMetaData.of(qInstance).getSupportedVersions();
List<APIVersion> 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<String, QTableMetaData>. //
// 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<String, String> key = new Pair<>(apiName, version);
if(tableApiNameMap.get(key) == null)
{
Map<String, QTableMetaData> map = new HashMap<>();
for(QTableMetaData table : qInstance.getTables().values())
{
ApiTableMetaData apiTableMetaData = ApiTableMetaData.of(table);
ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table);
if(apiTableMetaDataContainer != null)
{
ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiName);
if(apiTableMetaData != null)
{
String name = table.getName();
if(apiTableMetaData != null && StringUtils.hasContent(apiTableMetaData.getApiTableName()))
if(StringUtils.hasContent(apiTableMetaData.getApiTableName()))
{
name = apiTableMetaData.getApiTableName();
}
map.put(name, table);
}
tableApiNameMap.put(version, map);
}
}
return (tableApiNameMap.get(version).get(tableApiName));
tableApiNameMap.put(key, map);
}
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();

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
@ -105,11 +106,19 @@ public class APILogMetaDataProvider
)));
List<QPossibleValue<?>> apiVersionPossibleValues = new ArrayList<>();
ApiInstanceMetaData apiInstanceMetaData = ApiInstanceMetaData.of(instance);
LinkedHashSet<APIVersion> allVersions = new LinkedHashSet<>();
////////////////////////////////////////////////////////////////////////////////////////////////////
// todo... this, this whole thing, should probably have "which api" as another field too... ugh. //
////////////////////////////////////////////////////////////////////////////////////////////////////
TreeSet<APIVersion> allVersions = new TreeSet<>();
ApiInstanceMetaDataContainer apiInstanceMetaDataContainer = ApiInstanceMetaDataContainer.of(instance);
for(Map.Entry<String, ApiInstanceMetaData> entry : apiInstanceMetaDataContainer.getApis().entrySet())
{
ApiInstanceMetaData apiInstanceMetaData = entry.getValue();
allVersions.addAll(apiInstanceMetaData.getPastVersions());
allVersions.addAll(apiInstanceMetaData.getSupportedVersions());
allVersions.addAll(apiInstanceMetaData.getFutureVersions());
}
for(APIVersion version : allVersions)
{

View File

@ -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<APIVersion> 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);
ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table);
if(apiTableMetaDataContainer != null)
{
ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiName);
if(apiTableMetaData != null)
{
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.");
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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, ApiInstanceMetaData> 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<String, ApiInstanceMetaData> entry : CollectionUtils.nonNullMap(apis).entrySet())
{
entry.getValue().validate(entry.getKey(), qInstance, validator);
}
}
/*******************************************************************************
** Getter for apis
*******************************************************************************/
public Map<String, ApiInstanceMetaData> 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<String, ApiInstanceMetaData> apis)
{
this.apis = apis;
}
/*******************************************************************************
** Fluent setter for apis
*******************************************************************************/
public ApiInstanceMetaDataContainer withApis(Map<String, ApiInstanceMetaData> 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);
}
}

View File

@ -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,36 +42,19 @@ public class ApiFieldMetaData extends QMiddlewareFieldMetaData
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ApiFieldMetaData()
public static String getEffectiveApiFieldName(String apiName, QFieldMetaData field)
{
setType("api");
}
/*******************************************************************************
**
*******************************************************************************/
public static ApiFieldMetaData of(QFieldMetaData field)
ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.of(field);
if(apiFieldMetaDataContainer != null)
{
return ((ApiFieldMetaData) field.getMiddlewareMetaData(ApiMiddlewareType.NAME));
}
/*******************************************************************************
**
*******************************************************************************/
public static String getEffectiveApiFieldName(QFieldMetaData field)
{
ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(field);
ApiFieldMetaData apiFieldMetaData = apiFieldMetaDataContainer.getApiFieldMetaData(apiName);
if(apiFieldMetaData != null && StringUtils.hasContent(apiFieldMetaData.apiFieldName))
{
return (apiFieldMetaData.apiFieldName);
}
}
return (field.getName());
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, ApiFieldMetaData> apis;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ApiFieldMetaDataContainer()
{
setType("api");
}
/*******************************************************************************
**
*******************************************************************************/
public static ApiFieldMetaDataContainer of(QFieldMetaData field)
{
return ((ApiFieldMetaDataContainer) field.getMiddlewareMetaData(ApiMiddlewareType.NAME));
}
/*******************************************************************************
** Getter for apis
*******************************************************************************/
public Map<String, ApiFieldMetaData> 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<String, ApiFieldMetaData> apis)
{
this.apis = apis;
}
/*******************************************************************************
** Fluent setter for apis
*******************************************************************************/
public ApiFieldMetaDataContainer withApis(Map<String, ApiFieldMetaData> 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);
}
}

View File

@ -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));
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ApiTableMetaData()
ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.of(field);
if(apiFieldMetaDataContainer.getApiFieldMetaData(apiName) == null)
{
setType("api");
apiFieldMetaDataContainer.withApiFieldMetaData(apiName, new ApiFieldMetaData());
}
return (apiFieldMetaDataContainer.getApiFieldMetaData(apiName));
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, ApiTableMetaData> 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<String, ApiTableMetaData> entry : CollectionUtils.nonNullMap(apis).entrySet())
{
entry.getValue().enrich(entry.getKey(), table);
}
}
/*******************************************************************************
** Getter for apis
*******************************************************************************/
public Map<String, ApiTableMetaData> 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<String, ApiTableMetaData> apis)
{
this.apis = apis;
}
/*******************************************************************************
** Fluent setter for apis
*******************************************************************************/
public ApiTableMetaDataContainer withApis(Map<String, ApiTableMetaData> 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);
}
}

View File

@ -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")
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)))
.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))

View File

@ -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<String> 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<String> 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");
}
}

View File

@ -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<? extends QFieldMetaData> 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();

View File

@ -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<String, Serializable> pastApiRecord = QRecordApiAdapter.qRecordToApiMap(person, TestUtils.TABLE_NAME_PERSON, TestUtils.V2022_Q4);
Map<String, Serializable> 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<String, Serializable> currentApiRecord = QRecordApiAdapter.qRecordToApiMap(person, TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q1);
Map<String, Serializable> 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<String, Serializable> futureApiRecord = QRecordApiAdapter.qRecordToApiMap(person, TestUtils.TABLE_NAME_PERSON, TestUtils.V2023_Q2);
Map<String, Serializable> 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<String, Serializable> 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"));

View File

@ -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<String> 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<String> 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<String> 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);
}

View File

@ -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<String> 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<String> errors = validator.getErrors();
assertEquals(expectedErrors.size(), errors.size(), "Expected # of validation errors (got: " + errors + ")");