From 4da3cc9206368f0cf01d2d24dd3d0cc93fc8b08a Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 22 Mar 2023 15:12:12 -0500 Subject: [PATCH] Checkpoint; bulkInsert working; some api instance validation --- .../core/instances/QInstanceValidator.java | 2 +- .../metadata/QMiddlewareInstanceMetaData.java | 3 +- .../core/utils/collections/MapBuilder.java | 47 ++++- .../actions/GenerateOpenApiSpecAction.java | 144 +++++++++++--- .../qqq/api/javalin/QJavalinApiHandler.java | 106 +++++++++- .../model/metadata/ApiInstanceMetaData.java | 150 +++++++++++++- .../qqq/api/model/openapi/Schema.java | 25 ++- .../java/com/kingsrook/qqq/api/TestUtils.java | 2 + .../api/javalin/QJavalinApiHandlerTest.java | 98 +++++++++ .../metadata/ApiInstanceMetaDataTest.java | 188 ++++++++++++++++++ 10 files changed, 730 insertions(+), 35 deletions(-) create mode 100644 qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 07d2a954..8d90492a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -168,7 +168,7 @@ public class QInstanceValidator { for(QMiddlewareInstanceMetaData middlewareInstanceMetaData : CollectionUtils.nonNullMap(qInstance.getMiddlewareMetaData()).values()) { - middlewareInstanceMetaData.validate(qInstance); + middlewareInstanceMetaData.validate(qInstance, this); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMiddlewareInstanceMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMiddlewareInstanceMetaData.java index 9e34a689..5c20f8bf 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMiddlewareInstanceMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMiddlewareInstanceMetaData.java @@ -22,6 +22,7 @@ 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; @@ -80,7 +81,7 @@ public abstract class QMiddlewareInstanceMetaData /******************************************************************************* ** *******************************************************************************/ - public void validate(QInstance qInstance) + public void validate(QInstance qInstance, QInstanceValidator validator) { //////////////////////// // noop in base class // diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilder.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilder.java index 2647db3d..21b5c667 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilder.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilder.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.utils.collections; import java.util.HashMap; 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 ** "don't suck" *******************************************************************************/ -public class MapBuilder +public class MapBuilder { + private Map map; + + + + /******************************************************************************* + ** + *******************************************************************************/ + private MapBuilder(Map map) + { + this.map = map; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static MapBuilder of(Supplier> mapSupplier) + { + return (new MapBuilder<>(mapSupplier.get())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public MapBuilder with(K key, V value) + { + map.put(key, value); + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Map build() + { + return (this.map); + } + + /******************************************************************************* ** diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java index 6397fc6b..332a8ba4 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java @@ -26,9 +26,11 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import com.kingsrook.qqq.api.ApiMiddlewareType; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput; import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsInput; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.openapi.Components; import com.kingsrook.qqq.api.model.openapi.Contact; import com.kingsrook.qqq.api.model.openapi.Content; @@ -80,13 +82,15 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction scopes = new LinkedHashMap<>(); securitySchemes.put("OAuth2", new OAuth2() .withFlows(MapBuilder.of("authorizationCode", new OAuth2Flow() + // todo - get from auth metadata .withAuthorizationUrl("https://nutrifresh-one-development.us.auth0.com/authorize") .withTokenUrl("https://nutrifresh-one-development.us.auth0.com/oauth/token") .withScopes(scopes) @@ -162,13 +167,13 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction 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 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) + ) + )); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java index dad79cf9..be6607de 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java @@ -91,6 +91,7 @@ import io.javalin.apibuilder.EndpointGroup; import io.javalin.http.ContentType; import io.javalin.http.Context; import org.eclipse.jetty.http.HttpStatus; +import org.json.JSONArray; import org.json.JSONObject; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; 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.delete("/{primaryKey}", QJavalinApiHandler::doDelete); - // post("/bulk", QJavalinApiHandler::bulkInsert); + ApiBuilder.post("/bulk", QJavalinApiHandler::bulkInsert); // patch("/bulk", QJavalinApiHandler::bulkUpdate); // delete("/bulk", QJavalinApiHandler::bulkDelete); }); @@ -514,8 +515,9 @@ public class QJavalinApiHandler throw (new QNotFoundException("Could not find any resources at path " + context.path())); } - APIVersion requestApiVersion = new APIVersion(version); - if(!ApiMiddlewareType.getApiInstanceMetaData(qInstance).getSupportedVersions().contains(requestApiVersion)) + APIVersion requestApiVersion = new APIVersion(version); + List 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())); } @@ -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 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> response = new ArrayList<>(); + for(QRecord record : insertOutput.getRecords()) + { + LinkedHashMap outputRecord = new LinkedHashMap<>(); + response.add(outputRecord); + + List 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); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java index cff8108f..b67daea2 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java @@ -22,11 +22,18 @@ package com.kingsrook.qqq.api.model.metadata; +import java.util.HashSet; import java.util.List; +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.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; /******************************************************************************* @@ -34,6 +41,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.QMiddlewareInstanceMetaData *******************************************************************************/ public class ApiInstanceMetaData extends QMiddlewareInstanceMetaData { + private String name; + private String description; + private String contactEmail; + private APIVersion currentVersion; private List supportedVersions; private List pastVersions; @@ -56,11 +67,49 @@ public class ApiInstanceMetaData extends QMiddlewareInstanceMetaData ** *******************************************************************************/ @Override - public void validate(QInstance qInstance) + public void validate(QInstance qInstance, QInstanceValidator validator) { - // todo - version is set - // todo - past versions all < current < all future - // todo - any version specified anywhere is one of the known + 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"); + + Set 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); } + + + /******************************************************************************* + ** 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); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java index 3641f66b..eb55dc2e 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java @@ -38,7 +38,7 @@ public class Schema private List enumValues; private Schema items; private Map properties; - private String example; + private Object example; private String ref; private List allOf; @@ -171,7 +171,7 @@ public class Schema /******************************************************************************* ** Getter for example *******************************************************************************/ - public String getExample() + public Object getExample() { 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 *******************************************************************************/ diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java index f4dc2722..302b93cb 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java @@ -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.QFieldType; 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; @@ -93,6 +94,7 @@ public class TestUtils .withBackendName(MEMORY_BACKEND_NAME) .withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(API_VERSION)) .withPrimaryKeyField("id") + .withUniqueKey(new UniqueKey("email")) .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java index 77307c8a..f1fdb477 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java @@ -443,6 +443,104 @@ class QJavalinApiHandlerTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBulkInsert207() throws QException + { + HttpResponse 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 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); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataTest.java new file mode 100644 index 00000000..2ab9d635 --- /dev/null +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataTest.java @@ -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 . + */ + +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 expectedErrors) + { + QInstance qInstance = new QInstance(); + assertValidationErrors(qInstance, apiInstanceMetaData, expectedErrors); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void assertValidationErrors(QInstance qInstance, ApiInstanceMetaData apiInstanceMetaData, List expectedErrors) + { + qInstance.withMiddlewareMetaData(apiInstanceMetaData); + + QInstanceValidator validator = new QInstanceValidator(); + apiInstanceMetaData.validate(qInstance, validator); + + List 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)); + } + } +} \ No newline at end of file