mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
Checkpoint; bulkInsert working; some api instance validation
This commit is contained in:
@ -168,7 +168,7 @@ public class QInstanceValidator
|
|||||||
{
|
{
|
||||||
for(QMiddlewareInstanceMetaData middlewareInstanceMetaData : CollectionUtils.nonNullMap(qInstance.getMiddlewareMetaData()).values())
|
for(QMiddlewareInstanceMetaData middlewareInstanceMetaData : CollectionUtils.nonNullMap(qInstance.getMiddlewareMetaData()).values())
|
||||||
{
|
{
|
||||||
middlewareInstanceMetaData.validate(qInstance);
|
middlewareInstanceMetaData.validate(qInstance, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
package com.kingsrook.qqq.backend.core.model.metadata;
|
package com.kingsrook.qqq.backend.core.model.metadata;
|
||||||
|
|
||||||
|
|
||||||
|
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
|
|
||||||
|
|
||||||
@ -80,7 +81,7 @@ public abstract class QMiddlewareInstanceMetaData
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public void validate(QInstance qInstance)
|
public void validate(QInstance qInstance, QInstanceValidator validator)
|
||||||
{
|
{
|
||||||
////////////////////////
|
////////////////////////
|
||||||
// noop in base class //
|
// noop in base class //
|
||||||
|
@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.utils.collections;
|
|||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -31,8 +32,52 @@ import java.util.Map;
|
|||||||
** NPE's on nulls... So, replace it with this, which returns HashMaps, which
|
** NPE's on nulls... So, replace it with this, which returns HashMaps, which
|
||||||
** "don't suck"
|
** "don't suck"
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class MapBuilder
|
public class MapBuilder<K, V>
|
||||||
{
|
{
|
||||||
|
private Map<K, V> map;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private MapBuilder(Map<K, V> map)
|
||||||
|
{
|
||||||
|
this.map = map;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static <K, V> MapBuilder<K, V> of(Supplier<Map<K, V>> mapSupplier)
|
||||||
|
{
|
||||||
|
return (new MapBuilder<>(mapSupplier.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public MapBuilder<K, V> with(K key, V value)
|
||||||
|
{
|
||||||
|
map.put(key, value);
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public Map<K, V> build()
|
||||||
|
{
|
||||||
|
return (this.map);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
|
@ -26,9 +26,11 @@ import java.util.ArrayList;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import com.kingsrook.qqq.api.ApiMiddlewareType;
|
||||||
import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput;
|
import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput;
|
||||||
import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput;
|
import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput;
|
||||||
import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsInput;
|
import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsInput;
|
||||||
|
import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData;
|
||||||
import com.kingsrook.qqq.api.model.openapi.Components;
|
import com.kingsrook.qqq.api.model.openapi.Components;
|
||||||
import com.kingsrook.qqq.api.model.openapi.Contact;
|
import com.kingsrook.qqq.api.model.openapi.Contact;
|
||||||
import com.kingsrook.qqq.api.model.openapi.Content;
|
import com.kingsrook.qqq.api.model.openapi.Content;
|
||||||
@ -80,13 +82,15 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
|
|
||||||
QInstance qInstance = QContext.getQInstance();
|
QInstance qInstance = QContext.getQInstance();
|
||||||
|
|
||||||
|
ApiInstanceMetaData apiInstanceMetaData = ApiMiddlewareType.getApiInstanceMetaData(qInstance);
|
||||||
|
|
||||||
OpenAPI openAPI = new OpenAPI()
|
OpenAPI openAPI = new OpenAPI()
|
||||||
.withVersion("3.0.3")
|
.withVersion("3.0.3")
|
||||||
.withInfo(new Info()
|
.withInfo(new Info()
|
||||||
.withTitle("QQQ API")
|
.withTitle(apiInstanceMetaData.getName())
|
||||||
.withDescription("This is an openAPI built by QQQ")
|
.withDescription(apiInstanceMetaData.getDescription())
|
||||||
.withContact(new Contact()
|
.withContact(new Contact()
|
||||||
.withEmail("contact@kingsrook.com")
|
.withEmail(apiInstanceMetaData.getContactEmail())
|
||||||
)
|
)
|
||||||
.withVersion(version)
|
.withVersion(version)
|
||||||
)
|
)
|
||||||
@ -110,6 +114,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
LinkedHashMap<String, String> scopes = new LinkedHashMap<>();
|
LinkedHashMap<String, String> scopes = new LinkedHashMap<>();
|
||||||
securitySchemes.put("OAuth2", new OAuth2()
|
securitySchemes.put("OAuth2", new OAuth2()
|
||||||
.withFlows(MapBuilder.of("authorizationCode", new OAuth2Flow()
|
.withFlows(MapBuilder.of("authorizationCode", new OAuth2Flow()
|
||||||
|
// todo - get from auth metadata
|
||||||
.withAuthorizationUrl("https://nutrifresh-one-development.us.auth0.com/authorize")
|
.withAuthorizationUrl("https://nutrifresh-one-development.us.auth0.com/authorize")
|
||||||
.withTokenUrl("https://nutrifresh-one-development.us.auth0.com/oauth/token")
|
.withTokenUrl("https://nutrifresh-one-development.us.auth0.com/oauth/token")
|
||||||
.withScopes(scopes)
|
.withScopes(scopes)
|
||||||
@ -162,13 +167,13 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
scopes.put(tableUpdatePermissionName, "Permission to update records in the " + tableLabel + " table");
|
scopes.put(tableUpdatePermissionName, "Permission to update records in the " + tableLabel + " table");
|
||||||
}
|
}
|
||||||
|
|
||||||
String tableInsertPermissionName = PermissionsHelper.getTablePermissionName(tableName, TablePermissionSubType.EDIT);
|
String tableInsertPermissionName = PermissionsHelper.getTablePermissionName(tableName, TablePermissionSubType.INSERT);
|
||||||
if(StringUtils.hasContent(tableInsertPermissionName))
|
if(StringUtils.hasContent(tableInsertPermissionName))
|
||||||
{
|
{
|
||||||
scopes.put(tableInsertPermissionName, "Permission to insert records in the " + tableLabel + " table");
|
scopes.put(tableInsertPermissionName, "Permission to insert records in the " + tableLabel + " table");
|
||||||
}
|
}
|
||||||
|
|
||||||
String tableDeletePermissionName = PermissionsHelper.getTablePermissionName(tableName, TablePermissionSubType.EDIT);
|
String tableDeletePermissionName = PermissionsHelper.getTablePermissionName(tableName, TablePermissionSubType.DELETE);
|
||||||
if(StringUtils.hasContent(tableDeletePermissionName))
|
if(StringUtils.hasContent(tableDeletePermissionName))
|
||||||
{
|
{
|
||||||
scopes.put(tableDeletePermissionName, "Permission to delete records in the " + tableLabel + " table");
|
scopes.put(tableDeletePermissionName, "Permission to delete records in the " + tableLabel + " table");
|
||||||
@ -403,25 +408,56 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
.withPost(slashPost)
|
.withPost(slashPost)
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
////////////////
|
||||||
|
// bulk paths //
|
||||||
|
////////////////
|
||||||
|
Method bulkPost = new Method()
|
||||||
|
.withSummary("Create multiple " + tableLabel + " records.")
|
||||||
|
.withRequestBody(new RequestBody()
|
||||||
|
.withRequired(true)
|
||||||
|
.withDescription("Values for the " + tableLabel + " records to create.")
|
||||||
|
.withContent(MapBuilder.of("application/json", new Content()
|
||||||
|
.withSchema(new Schema()
|
||||||
|
.withType("array")
|
||||||
|
.withItems(new Schema().withRef("#/components/schemas/" + tableName + "WithoutPrimaryKey"))))))
|
||||||
|
.withResponses(buildStandardErrorResponses())
|
||||||
|
.withResponse(HttpStatus.MULTI_STATUS.getCode(), buildMultiStatusResponse(tableLabel, primaryKeyName, primaryKeyField, "post"))
|
||||||
|
.withTags(ListBuilder.of(tableLabel))
|
||||||
|
.withSecurity(ListBuilder.of(MapBuilder.of("OAuth2", List.of(tableInsertPermissionName))));
|
||||||
|
|
||||||
|
Method bulkPatch = new Method()
|
||||||
|
.withSummary("Update multiple " + tableLabel + " records.")
|
||||||
|
.withRequestBody(new RequestBody()
|
||||||
|
.withRequired(true)
|
||||||
|
.withDescription("Values for the " + tableLabel + " records to update.")
|
||||||
|
.withContent(MapBuilder.of("application/json", new Content()
|
||||||
|
.withSchema(new Schema()
|
||||||
|
.withType("array")
|
||||||
|
.withItems(new Schema().withRef("#/components/schemas/" + tableName))))))
|
||||||
|
.withResponses(buildStandardErrorResponses())
|
||||||
|
.withResponse(HttpStatus.MULTI_STATUS.getCode(), buildMultiStatusResponse(tableLabel, primaryKeyName, primaryKeyField, "patch"))
|
||||||
|
.withTags(ListBuilder.of(tableLabel))
|
||||||
|
.withSecurity(ListBuilder.of(MapBuilder.of("OAuth2", List.of(tableUpdatePermissionName))));
|
||||||
|
|
||||||
|
Method bulkDelete = new Method()
|
||||||
|
.withSummary("Delete multiple " + tableLabel + " records.")
|
||||||
|
.withRequestBody(new RequestBody()
|
||||||
|
.withRequired(true)
|
||||||
|
.withDescription(primaryKeyLabel + " values for the " + tableLabel + " records to delete.")
|
||||||
|
.withContent(MapBuilder.of("application/json", new Content()
|
||||||
|
.withSchema(new Schema()
|
||||||
|
.withType("array")
|
||||||
|
.withItems(new Schema().withType(getFieldType(primaryKeyField)))
|
||||||
|
.withExample(List.of(42, 47))))))
|
||||||
|
.withResponses(buildStandardErrorResponses())
|
||||||
|
.withResponse(HttpStatus.MULTI_STATUS.getCode(), buildMultiStatusResponse(tableLabel, primaryKeyName, primaryKeyField, "delete"))
|
||||||
|
.withTags(ListBuilder.of(tableLabel))
|
||||||
|
.withSecurity(ListBuilder.of(MapBuilder.of("OAuth2", List.of(tableDeletePermissionName))));
|
||||||
|
|
||||||
openAPI.getPaths().put("/" + tableName + "/bulk", new Path()
|
openAPI.getPaths().put("/" + tableName + "/bulk", new Path()
|
||||||
.withPatch(new Method()
|
.withPost(bulkPost)
|
||||||
.withSummary("Update multiple " + tableLabel + " records.")
|
.withPatch(bulkPatch)
|
||||||
.withTags(ListBuilder.of(tableLabel))
|
.withDelete(bulkDelete));
|
||||||
.withResponses(buildStandardErrorResponses())
|
|
||||||
)
|
|
||||||
.withDelete(new Method()
|
|
||||||
.withSummary("Delete multiple " + tableLabel + " records.")
|
|
||||||
.withTags(ListBuilder.of(tableLabel))
|
|
||||||
.withResponses(buildStandardErrorResponses())
|
|
||||||
)
|
|
||||||
.withPost(new Method()
|
|
||||||
.withSummary("Create multiple " + tableLabel + " records.")
|
|
||||||
.withTags(ListBuilder.of(tableLabel))
|
|
||||||
.withResponses(buildStandardErrorResponses())
|
|
||||||
)
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentResponses.put(HttpStatus.BAD_REQUEST.getCode(), buildStandardErrorResponse("Bad Request. Some portion of the request's content was not acceptable to the server. See error message in body for details.", "Parameter id should be given an integer value, but received string: \"Foo\""));
|
componentResponses.put(HttpStatus.BAD_REQUEST.getCode(), buildStandardErrorResponse("Bad Request. Some portion of the request's content was not acceptable to the server. See error message in body for details.", "Parameter id should be given an integer value, but received string: \"Foo\""));
|
||||||
@ -438,6 +474,68 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@SuppressWarnings("checkstyle:indentation")
|
||||||
|
private Response buildMultiStatusResponse(String tableLabel, String primaryKeyName, QFieldMetaData primaryKeyField, String method)
|
||||||
|
{
|
||||||
|
List<Object> example = switch(method.toLowerCase())
|
||||||
|
{
|
||||||
|
case "post" -> ListBuilder.of(
|
||||||
|
MapBuilder.of(LinkedHashMap::new)
|
||||||
|
.with("statusCode", HttpStatus.CREATED.getCode())
|
||||||
|
.with("statusText", HttpStatus.CREATED.getMessage())
|
||||||
|
.with(primaryKeyName, "47").build(),
|
||||||
|
MapBuilder.of(LinkedHashMap::new)
|
||||||
|
.with("statusCode", HttpStatus.BAD_REQUEST.getCode())
|
||||||
|
.with("statusText", HttpStatus.BAD_REQUEST.getMessage())
|
||||||
|
.with("error", "Could not create " + tableLabel + ": Duplicate value in unique key field.").build()
|
||||||
|
);
|
||||||
|
case "patch" -> ListBuilder.of(
|
||||||
|
MapBuilder.of(LinkedHashMap::new)
|
||||||
|
.with("statusCode", HttpStatus.NO_CONTENT.getCode())
|
||||||
|
.with("statusText", HttpStatus.NO_CONTENT.getMessage()).build(),
|
||||||
|
MapBuilder.of(LinkedHashMap::new)
|
||||||
|
.with("statusCode", HttpStatus.BAD_REQUEST.getCode())
|
||||||
|
.with("statusText", HttpStatus.BAD_REQUEST.getMessage())
|
||||||
|
.with("error", "Could not update " + tableLabel + ": Duplicate value in unique key field.").build()
|
||||||
|
);
|
||||||
|
case "delete" -> ListBuilder.of(
|
||||||
|
MapBuilder.of(LinkedHashMap::new)
|
||||||
|
.with("statusCode", HttpStatus.NO_CONTENT.getCode())
|
||||||
|
.with("statusText", HttpStatus.NO_CONTENT.getMessage()).build(),
|
||||||
|
MapBuilder.of(LinkedHashMap::new)
|
||||||
|
.with("statusCode", HttpStatus.BAD_REQUEST.getCode())
|
||||||
|
.with("statusText", HttpStatus.BAD_REQUEST.getMessage())
|
||||||
|
.with("error", "Could not delete " + tableLabel + ": Foreign key constraint violation.").build()
|
||||||
|
);
|
||||||
|
default -> throw (new IllegalArgumentException("Unrecognized method: " + method));
|
||||||
|
};
|
||||||
|
|
||||||
|
Map<String, Schema> properties = new LinkedHashMap<>();
|
||||||
|
properties.put("status", new Schema().withType("integer"));
|
||||||
|
properties.put("error", new Schema().withType("string"));
|
||||||
|
if(method.equalsIgnoreCase("post"))
|
||||||
|
{
|
||||||
|
properties.put(primaryKeyName, new Schema().withType(getFieldType(primaryKeyField)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response()
|
||||||
|
.withDescription("Multiple statuses. See body for details.")
|
||||||
|
.withContent(MapBuilder.of("application/json", new Content()
|
||||||
|
.withSchema(new Schema()
|
||||||
|
.withType("array")
|
||||||
|
.withItems(new Schema()
|
||||||
|
.withType("object")
|
||||||
|
.withProperties(properties))
|
||||||
|
.withExample(example)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -91,6 +91,7 @@ import io.javalin.apibuilder.EndpointGroup;
|
|||||||
import io.javalin.http.ContentType;
|
import io.javalin.http.ContentType;
|
||||||
import io.javalin.http.Context;
|
import io.javalin.http.Context;
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
|
import org.json.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||||
import static com.kingsrook.qqq.backend.javalin.QJavalinImplementation.SLOW_LOG_THRESHOLD_MS;
|
import static com.kingsrook.qqq.backend.javalin.QJavalinImplementation.SLOW_LOG_THRESHOLD_MS;
|
||||||
@ -140,7 +141,7 @@ public class QJavalinApiHandler
|
|||||||
ApiBuilder.patch("/{primaryKey}", QJavalinApiHandler::doUpdate);
|
ApiBuilder.patch("/{primaryKey}", QJavalinApiHandler::doUpdate);
|
||||||
ApiBuilder.delete("/{primaryKey}", QJavalinApiHandler::doDelete);
|
ApiBuilder.delete("/{primaryKey}", QJavalinApiHandler::doDelete);
|
||||||
|
|
||||||
// post("/bulk", QJavalinApiHandler::bulkInsert);
|
ApiBuilder.post("/bulk", QJavalinApiHandler::bulkInsert);
|
||||||
// patch("/bulk", QJavalinApiHandler::bulkUpdate);
|
// patch("/bulk", QJavalinApiHandler::bulkUpdate);
|
||||||
// delete("/bulk", QJavalinApiHandler::bulkDelete);
|
// delete("/bulk", QJavalinApiHandler::bulkDelete);
|
||||||
});
|
});
|
||||||
@ -514,8 +515,9 @@ public class QJavalinApiHandler
|
|||||||
throw (new QNotFoundException("Could not find any resources at path " + context.path()));
|
throw (new QNotFoundException("Could not find any resources at path " + context.path()));
|
||||||
}
|
}
|
||||||
|
|
||||||
APIVersion requestApiVersion = new APIVersion(version);
|
APIVersion requestApiVersion = new APIVersion(version);
|
||||||
if(!ApiMiddlewareType.getApiInstanceMetaData(qInstance).getSupportedVersions().contains(requestApiVersion))
|
List<APIVersion> supportedVersions = ApiMiddlewareType.getApiInstanceMetaData(qInstance).getSupportedVersions();
|
||||||
|
if(CollectionUtils.nullSafeIsEmpty(supportedVersions) || !supportedVersions.contains(requestApiVersion))
|
||||||
{
|
{
|
||||||
throw (new QNotFoundException("This version of this API does not contain the resource path " + context.path()));
|
throw (new QNotFoundException("This version of this API does not contain the resource path " + context.path()));
|
||||||
}
|
}
|
||||||
@ -717,6 +719,104 @@ public class QJavalinApiHandler
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static void bulkInsert(Context context)
|
||||||
|
{
|
||||||
|
String version = context.pathParam("version");
|
||||||
|
String tableName = context.pathParam("tableName");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
QTableMetaData table = qInstance.getTable(tableName);
|
||||||
|
validateTableAndVersion(context, version, table);
|
||||||
|
|
||||||
|
InsertInput insertInput = new InsertInput();
|
||||||
|
|
||||||
|
setupSession(context, insertInput);
|
||||||
|
QJavalinAccessLogger.logStart("bulkInsert", logPair("table", tableName));
|
||||||
|
|
||||||
|
insertInput.setTableName(tableName);
|
||||||
|
|
||||||
|
PermissionsHelper.checkTablePermissionThrowing(insertInput, TablePermissionSubType.INSERT);
|
||||||
|
|
||||||
|
/////////////////
|
||||||
|
// build input //
|
||||||
|
/////////////////
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(!StringUtils.hasContent(context.body()))
|
||||||
|
{
|
||||||
|
throw (new QBadRequestException("Missing required POST body"));
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<QRecord> recordList = new ArrayList<>();
|
||||||
|
insertInput.setRecords(recordList);
|
||||||
|
JSONArray jsonArray = new JSONArray(context.body());
|
||||||
|
for(int i = 0; i < jsonArray.length(); i++)
|
||||||
|
{
|
||||||
|
JSONObject jsonObject = jsonArray.getJSONObject(i);
|
||||||
|
recordList.add(toQRecord(jsonObject, tableName, version));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(recordList.isEmpty())
|
||||||
|
{
|
||||||
|
throw (new QBadRequestException("No records were found in the POST body"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(QBadRequestException qbre)
|
||||||
|
{
|
||||||
|
throw (qbre);
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
throw (new QBadRequestException("Body could not be parsed as a JSON array: " + e.getMessage(), e));
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////
|
||||||
|
// execute! //
|
||||||
|
//////////////
|
||||||
|
InsertAction insertAction = new InsertAction();
|
||||||
|
InsertOutput insertOutput = insertAction.execute(insertInput);
|
||||||
|
|
||||||
|
///////////////////////////////////////
|
||||||
|
// process records to build response //
|
||||||
|
///////////////////////////////////////
|
||||||
|
List<Map<String, Serializable>> response = new ArrayList<>();
|
||||||
|
for(QRecord record : insertOutput.getRecords())
|
||||||
|
{
|
||||||
|
LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>();
|
||||||
|
response.add(outputRecord);
|
||||||
|
|
||||||
|
List<String> errors = record.getErrors();
|
||||||
|
if(CollectionUtils.nullSafeHasContents(errors))
|
||||||
|
{
|
||||||
|
outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode());
|
||||||
|
outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
|
||||||
|
outputRecord.put("error", "Error inserting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outputRecord.put("statusCode", HttpStatus.Code.CREATED.getCode());
|
||||||
|
outputRecord.put("statusText", HttpStatus.Code.CREATED.getMessage());
|
||||||
|
outputRecord.put(table.getPrimaryKeyField(), record.getValue(table.getPrimaryKeyField()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QJavalinAccessLogger.logEndSuccess();
|
||||||
|
context.status(HttpStatus.Code.MULTI_STATUS.getCode());
|
||||||
|
context.result(JsonUtils.toJson(response));
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
QJavalinAccessLogger.logEndFail(e);
|
||||||
|
handleException(context, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -22,11 +22,18 @@
|
|||||||
package com.kingsrook.qqq.api.model.metadata;
|
package com.kingsrook.qqq.api.model.metadata;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import com.kingsrook.qqq.api.ApiMiddlewareType;
|
import com.kingsrook.qqq.api.ApiMiddlewareType;
|
||||||
import com.kingsrook.qqq.api.model.APIVersion;
|
import com.kingsrook.qqq.api.model.APIVersion;
|
||||||
|
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
|
||||||
|
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.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QMiddlewareInstanceMetaData;
|
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;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -34,6 +41,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.QMiddlewareInstanceMetaData
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class ApiInstanceMetaData extends QMiddlewareInstanceMetaData
|
public class ApiInstanceMetaData extends QMiddlewareInstanceMetaData
|
||||||
{
|
{
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private String contactEmail;
|
||||||
|
|
||||||
private APIVersion currentVersion;
|
private APIVersion currentVersion;
|
||||||
private List<APIVersion> supportedVersions;
|
private List<APIVersion> supportedVersions;
|
||||||
private List<APIVersion> pastVersions;
|
private List<APIVersion> pastVersions;
|
||||||
@ -56,11 +67,49 @@ public class ApiInstanceMetaData extends QMiddlewareInstanceMetaData
|
|||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@Override
|
@Override
|
||||||
public void validate(QInstance qInstance)
|
public void validate(QInstance qInstance, QInstanceValidator validator)
|
||||||
{
|
{
|
||||||
// todo - version is set
|
validator.assertCondition(StringUtils.hasContent(name), "Missing name for instance api");
|
||||||
// todo - past versions all < current < all future
|
validator.assertCondition(StringUtils.hasContent(description), "Missing description for instance api");
|
||||||
// todo - any version specified anywhere is one of the known
|
validator.assertCondition(StringUtils.hasContent(contactEmail), "Missing contactEmail for instance api");
|
||||||
|
|
||||||
|
Set<APIVersion> allVersions = new HashSet<>();
|
||||||
|
|
||||||
|
if(validator.assertCondition(currentVersion != null, "Missing currentVersion for instance api"))
|
||||||
|
{
|
||||||
|
allVersions.add(currentVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(validator.assertCondition(supportedVersions != null, "Missing supportedVersions for instance api"))
|
||||||
|
{
|
||||||
|
validator.assertCondition(supportedVersions.contains(currentVersion), "supportedVersions [" + supportedVersions + "] does not contain currentVersion [" + currentVersion + "] for instance api");
|
||||||
|
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");
|
||||||
|
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");
|
||||||
|
allVersions.add(futureVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////
|
||||||
|
// validate all table versions //
|
||||||
|
/////////////////////////////////
|
||||||
|
for(QTableMetaData table : qInstance.getTables().values())
|
||||||
|
{
|
||||||
|
ApiTableMetaData apiTableMetaData = ApiMiddlewareType.getApiTableMetaData(table);
|
||||||
|
if(apiTableMetaData != null)
|
||||||
|
{
|
||||||
|
validator.assertCondition(allVersions.contains(new APIVersion(apiTableMetaData.getInitialVersion())), "Table " + table.getName() + "'s initial API version is not a recognized version.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -187,4 +236,97 @@ public class ApiInstanceMetaData extends QMiddlewareInstanceMetaData
|
|||||||
return (this);
|
return (this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for name
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getName()
|
||||||
|
{
|
||||||
|
return (this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for name
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setName(String name)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for name
|
||||||
|
*******************************************************************************/
|
||||||
|
public ApiInstanceMetaData withName(String name)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for description
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getDescription()
|
||||||
|
{
|
||||||
|
return (this.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for description
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setDescription(String description)
|
||||||
|
{
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for description
|
||||||
|
*******************************************************************************/
|
||||||
|
public ApiInstanceMetaData withDescription(String description)
|
||||||
|
{
|
||||||
|
this.description = description;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for contactEmail
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getContactEmail()
|
||||||
|
{
|
||||||
|
return (this.contactEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for contactEmail
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setContactEmail(String contactEmail)
|
||||||
|
{
|
||||||
|
this.contactEmail = contactEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for contactEmail
|
||||||
|
*******************************************************************************/
|
||||||
|
public ApiInstanceMetaData withContactEmail(String contactEmail)
|
||||||
|
{
|
||||||
|
this.contactEmail = contactEmail;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ public class Schema
|
|||||||
private List<String> enumValues;
|
private List<String> enumValues;
|
||||||
private Schema items;
|
private Schema items;
|
||||||
private Map<String, Schema> properties;
|
private Map<String, Schema> properties;
|
||||||
private String example;
|
private Object example;
|
||||||
private String ref;
|
private String ref;
|
||||||
private List<Schema> allOf;
|
private List<Schema> allOf;
|
||||||
|
|
||||||
@ -171,7 +171,7 @@ public class Schema
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Getter for example
|
** Getter for example
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public String getExample()
|
public Object getExample()
|
||||||
{
|
{
|
||||||
return (this.example);
|
return (this.example);
|
||||||
}
|
}
|
||||||
@ -199,6 +199,27 @@ public class Schema
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for example
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setExample(List<?> example)
|
||||||
|
{
|
||||||
|
this.example = example;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for example
|
||||||
|
*******************************************************************************/
|
||||||
|
public Schema withExample(List<?> example)
|
||||||
|
{
|
||||||
|
this.example = example;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Getter for ref
|
** Getter for ref
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -34,6 +34,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
|
|||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||||
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
|
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
|
||||||
|
|
||||||
|
|
||||||
@ -93,6 +94,7 @@ public class TestUtils
|
|||||||
.withBackendName(MEMORY_BACKEND_NAME)
|
.withBackendName(MEMORY_BACKEND_NAME)
|
||||||
.withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(API_VERSION))
|
.withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(API_VERSION))
|
||||||
.withPrimaryKeyField("id")
|
.withPrimaryKeyField("id")
|
||||||
|
.withUniqueKey(new UniqueKey("email"))
|
||||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
|
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
|
||||||
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
|
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
|
||||||
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
|
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
|
||||||
|
@ -443,6 +443,104 @@ class QJavalinApiHandlerTest extends BaseTest
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testBulkInsert207() throws QException
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/bulk")
|
||||||
|
.body("""
|
||||||
|
[
|
||||||
|
{"firstName": "Moe", "email": "moe@moes.com"},
|
||||||
|
{"firstName": "Barney", "email": "barney@moes.com"},
|
||||||
|
{"firstName": "CM", "email": "boss@snpp.com"},
|
||||||
|
{"firstName": "Waylon", "email": "boss@snpp.com"}
|
||||||
|
]
|
||||||
|
""")
|
||||||
|
.asString();
|
||||||
|
assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus());
|
||||||
|
JSONArray jsonArray = new JSONArray(response.getBody());
|
||||||
|
assertEquals(4, jsonArray.length());
|
||||||
|
|
||||||
|
assertEquals(HttpStatus.CREATED_201, jsonArray.getJSONObject(0).getInt("statusCode"));
|
||||||
|
assertEquals(1, jsonArray.getJSONObject(0).getInt("id"));
|
||||||
|
|
||||||
|
assertEquals(HttpStatus.CREATED_201, jsonArray.getJSONObject(1).getInt("statusCode"));
|
||||||
|
assertEquals(2, jsonArray.getJSONObject(1).getInt("id"));
|
||||||
|
|
||||||
|
assertEquals(HttpStatus.CREATED_201, jsonArray.getJSONObject(2).getInt("statusCode"));
|
||||||
|
assertEquals(3, jsonArray.getJSONObject(2).getInt("id"));
|
||||||
|
|
||||||
|
assertEquals(HttpStatus.BAD_REQUEST_400, jsonArray.getJSONObject(3).getInt("statusCode"));
|
||||||
|
assertEquals("Error inserting Person: Another record already exists with this Email", jsonArray.getJSONObject(3).getString("error"));
|
||||||
|
|
||||||
|
QRecord record = getPersonRecord(1);
|
||||||
|
assertEquals("Moe", record.getValueString("firstName"));
|
||||||
|
|
||||||
|
record = getPersonRecord(2);
|
||||||
|
assertEquals("Barney", record.getValueString("firstName"));
|
||||||
|
|
||||||
|
record = getPersonRecord(3);
|
||||||
|
assertEquals("CM", record.getValueString("firstName"));
|
||||||
|
|
||||||
|
record = getPersonRecord(4);
|
||||||
|
assertNull(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testBulkInsert400s() throws QException
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/bulk")
|
||||||
|
.body("""
|
||||||
|
{"firstName": "Moe"}
|
||||||
|
""")
|
||||||
|
.asString();
|
||||||
|
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Body could not be parsed as a JSON array: A JSONArray text must start with '['", response);
|
||||||
|
|
||||||
|
response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/bulk")
|
||||||
|
// no body
|
||||||
|
.asString();
|
||||||
|
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Missing required POST body", response);
|
||||||
|
|
||||||
|
response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/bulk")
|
||||||
|
.body("[]")
|
||||||
|
.asString();
|
||||||
|
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "No records were found in the POST body", response);
|
||||||
|
|
||||||
|
response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/bulk")
|
||||||
|
.body("""
|
||||||
|
[{"firstName": "Moe", "foo": "bar"}]
|
||||||
|
""")
|
||||||
|
.asString();
|
||||||
|
assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Request body contained 1 unrecognized field name: foo", response);
|
||||||
|
|
||||||
|
/////////////////////////////////
|
||||||
|
// assert nothing got inserted //
|
||||||
|
/////////////////////////////////
|
||||||
|
QRecord personRecord = getPersonRecord(1);
|
||||||
|
assertNull(personRecord);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// apparently, as long as the body *starts with* json, the JSONObject constructor builds //
|
||||||
|
// a json object out of it?? so... this in this case we expected 400, but get 201... //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/bulk")
|
||||||
|
.body("""
|
||||||
|
[{"firstName": "Moe"}]
|
||||||
|
Not json
|
||||||
|
""")
|
||||||
|
.asString();
|
||||||
|
assertErrorResponse(HttpStatus.MULTI_STATUS_207, null, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
* 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.List;
|
||||||
|
import com.kingsrook.qqq.api.model.APIVersion;
|
||||||
|
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
|
||||||
|
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;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Unit test for ApiInstanceMetaData
|
||||||
|
*******************************************************************************/
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testValidationMissingThings()
|
||||||
|
{
|
||||||
|
assertValidationErrors(new ApiInstanceMetaData(), List.of(
|
||||||
|
"Missing name",
|
||||||
|
"Missing description",
|
||||||
|
"Missing contactEmail",
|
||||||
|
"Missing currentVersion",
|
||||||
|
"Missing supportedVersions"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testValidationInstanceVersionIssues()
|
||||||
|
{
|
||||||
|
assertValidationErrors(makeBaselineValidApiInstanceMetaData()
|
||||||
|
.withCurrentVersion(new APIVersion("2023.Q1"))
|
||||||
|
.withSupportedVersions(List.of(new APIVersion("2022.Q3"), new APIVersion("2022.Q4")))
|
||||||
|
, List.of("supportedVersions [[2022.Q3, 2022.Q4]] does not contain currentVersion [2023.Q1]"));
|
||||||
|
|
||||||
|
assertValidationErrors(makeBaselineValidApiInstanceMetaData()
|
||||||
|
.withCurrentVersion(new APIVersion("2023.Q1"))
|
||||||
|
.withSupportedVersions(List.of(new APIVersion("2023.Q1")))
|
||||||
|
.withPastVersions(List.of(new APIVersion("2022.Q4"), new APIVersion("2023.Q1"), new APIVersion("2023.Q2"))),
|
||||||
|
List.of(
|
||||||
|
"pastVersion [2023.Q2] is not lexicographically before currentVersion",
|
||||||
|
"pastVersion [2023.Q1] is not lexicographically before currentVersion"
|
||||||
|
));
|
||||||
|
|
||||||
|
assertValidationErrors(makeBaselineValidApiInstanceMetaData()
|
||||||
|
.withCurrentVersion(new APIVersion("2023.Q1"))
|
||||||
|
.withSupportedVersions(List.of(new APIVersion("2023.Q1")))
|
||||||
|
.withFutureVersions(List.of(new APIVersion("2022.Q4"), new APIVersion("2023.Q1"), new APIVersion("2023.Q2"))),
|
||||||
|
List.of(
|
||||||
|
"futureVersion [2022.Q4] is not lexicographically after currentVersion",
|
||||||
|
"futureVersion [2023.Q1] is not lexicographically after currentVersion"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testValidationTableVersionIssues()
|
||||||
|
{
|
||||||
|
QInstance qInstance = new QInstance();
|
||||||
|
|
||||||
|
qInstance.addTable(new QTableMetaData()
|
||||||
|
.withName("myValidTable")
|
||||||
|
.withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion("2023.Q1")));
|
||||||
|
|
||||||
|
qInstance.addTable(new QTableMetaData()
|
||||||
|
.withName("myInvalidTable")
|
||||||
|
.withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion("2022.Q1")));
|
||||||
|
|
||||||
|
assertValidationErrors(qInstance, makeBaselineValidApiInstanceMetaData()
|
||||||
|
.withCurrentVersion(new APIVersion("2023.Q1"))
|
||||||
|
.withSupportedVersions(List.of(new APIVersion("2022.Q4"), new APIVersion("2023.Q1"))),
|
||||||
|
List.of("Table myInvalidTable's initial API version is not a recognized version"));
|
||||||
|
|
||||||
|
qInstance.addTable(new QTableMetaData()
|
||||||
|
.withName("myFutureValidTable")
|
||||||
|
.withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion("2024.Q1")));
|
||||||
|
|
||||||
|
assertValidationErrors(qInstance, makeBaselineValidApiInstanceMetaData()
|
||||||
|
.withCurrentVersion(new APIVersion("2023.Q1"))
|
||||||
|
.withSupportedVersions(List.of(new APIVersion("2023.Q1")))
|
||||||
|
.withFutureVersions(List.of(new APIVersion("2024.Q1"))),
|
||||||
|
List.of("Table myInvalidTable's initial API version is not a recognized version"));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static ApiInstanceMetaData makeBaselineValidApiInstanceMetaData()
|
||||||
|
{
|
||||||
|
return (new ApiInstanceMetaData()
|
||||||
|
.withName("QQQ API")
|
||||||
|
.withDescription("Test API for QQQ")
|
||||||
|
.withContactEmail("contact@kingsrook.com"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void assertValidationErrors(ApiInstanceMetaData apiInstanceMetaData, List<String> expectedErrors)
|
||||||
|
{
|
||||||
|
QInstance qInstance = new QInstance();
|
||||||
|
assertValidationErrors(qInstance, apiInstanceMetaData, expectedErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void assertValidationErrors(QInstance qInstance, ApiInstanceMetaData apiInstanceMetaData, List<String> expectedErrors)
|
||||||
|
{
|
||||||
|
qInstance.withMiddlewareMetaData(apiInstanceMetaData);
|
||||||
|
|
||||||
|
QInstanceValidator validator = new QInstanceValidator();
|
||||||
|
apiInstanceMetaData.validate(qInstance, validator);
|
||||||
|
|
||||||
|
List<String> errors = validator.getErrors();
|
||||||
|
assertEquals(expectedErrors.size(), errors.size(), "Expected # of validation errors (got: " + errors + ")");
|
||||||
|
|
||||||
|
for(String expectedError : expectedErrors)
|
||||||
|
{
|
||||||
|
assertThat(errors).withFailMessage("Expected any of:\n " + StringUtils.join("\n ", errors) + "\nto contain:\n " + expectedError).anyMatch(e -> e.contains(expectedError));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user