CE-881 - First round of adjustments to api-middleware for rendering saved-reports:

- add path params as concept
- add customizeHttpApiResponse to ApiProcessOutputInterface
- add contentType and needsFormattedAsJson in HttpApiResponse
This commit is contained in:
2024-03-29 08:42:34 -05:00
parent b37e26e03b
commit 98e9d1bf57
10 changed files with 284 additions and 28 deletions

View File

@ -1000,6 +1000,7 @@ public class ApiImplementation
ApiProcessInput apiProcessInput = apiProcessMetaData.getInput(); ApiProcessInput apiProcessInput = apiProcessMetaData.getInput();
if(apiProcessInput != null) if(apiProcessInput != null)
{ {
processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getPathParams());
processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getQueryStringParams()); processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getQueryStringParams());
processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getFormParams()); processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getFormParams());
processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getObjectBodyParams()); processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getObjectBodyParams());
@ -1143,7 +1144,10 @@ public class ApiImplementation
ApiProcessOutputInterface output = apiProcessMetaData.getOutput(); ApiProcessOutputInterface output = apiProcessMetaData.getOutput();
if(output != null) if(output != null)
{ {
return (new HttpApiResponse(output.getSuccessStatusCode(runProcessInput, runProcessOutput), output.getOutputForProcess(runProcessInput, runProcessOutput))); Serializable outputForProcess = output.getOutputForProcess(runProcessInput, runProcessOutput);
HttpApiResponse httpApiResponse = new HttpApiResponse(output.getSuccessStatusCode(runProcessInput, runProcessOutput), outputForProcess);
output.customizeHttpApiResponse(httpApiResponse, runProcessInput, runProcessOutput);
return httpApiResponse;
} }
else else
{ {

View File

@ -816,7 +816,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
openAPI.getPaths().put(basePath + processApiPath, path); openAPI.getPaths().put(basePath + processApiPath, path);
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
// if the process can run async, then do the status checkin endpoitn // // if the process can run async, then do the status checkin endpoint //
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
if(!ApiProcessMetaData.AsyncMode.NEVER.equals(apiProcessMetaData.getAsyncMode())) if(!ApiProcessMetaData.AsyncMode.NEVER.equals(apiProcessMetaData.getAsyncMode()))
{ {
@ -900,12 +900,27 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
String apiName = apiInstanceMetaData.getName(); String apiName = apiInstanceMetaData.getName();
if(apiProcessInput != null) if(apiProcessInput != null)
{ {
/////////////////
// path params //
/////////////////
ApiProcessInputFieldsContainer pathParams = apiProcessInput.getPathParams();
if(pathParams != null)
{
for(QFieldMetaData field : CollectionUtils.nonNullList(pathParams.getFields()))
{
parameters.add(processFieldToParameter(apiInstanceMetaData, field).withIn("path"));
}
}
/////////////////////////
// query string params //
/////////////////////////
ApiProcessInputFieldsContainer queryStringParams = apiProcessInput.getQueryStringParams(); ApiProcessInputFieldsContainer queryStringParams = apiProcessInput.getQueryStringParams();
if(queryStringParams != null) if(queryStringParams != null)
{ {
if(queryStringParams.getRecordIdsField() != null) if(queryStringParams.getRecordIdsField() != null)
{ {
parameters.add(processFieldToParameter(apiInstanceMetaData, queryStringParams.getRecordIdsField()).withIn("query")); parameters.add(processFieldToParameter(apiInstanceMetaData, queryStringParams.getRecordIdsField()).withIn("path"));
} }
for(QFieldMetaData field : CollectionUtils.nonNullList(queryStringParams.getFields())) for(QFieldMetaData field : CollectionUtils.nonNullList(queryStringParams.getFields()))
@ -914,6 +929,9 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
} }
} }
/////////////////////
// Body as content //
/////////////////////
QFieldMetaData bodyField = apiProcessInput.getBodyField(); QFieldMetaData bodyField = apiProcessInput.getBodyField();
if(bodyField != null) if(bodyField != null)
{ {
@ -939,8 +957,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
content.withSchema(new Schema() content.withSchema(new Schema()
.withDescription(bodyDescription) .withDescription(bodyDescription)
.withType("string") .withType("string")
.withExample(exampleWithSingleValue.getValue()) .withExample(exampleWithSingleValue.getValue()));
);
} }
methodForProcess.withRequestBody(new RequestBody() methodForProcess.withRequestBody(new RequestBody()
@ -963,8 +980,9 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withIn("query") .withIn("query")
.withDescription(""" .withDescription("""
Indicates if the job should be ran asynchronously. Indicates if the job should be ran asynchronously.
If false, or not specified, job is ran synchronously, and returns with response status of 207 (Multi-Status) or 204 (No Content). If false or not specified, then the job is ran synchronously and returns with an appropriate response status when completed.
If true, request returns immediately with response status of 202 (Accepted). If true, then the request returns immediately with response status of 202 (Accepted), and a jobId in the response body,
which can then be sent to the corresponding .../status/{jobId} endpoint to follow-up on the status of the job.
""") """)
.withExamples(MapBuilder.of( .withExamples(MapBuilder.of(
"false", new ExampleWithSingleValue().withValue(false).withSummary("Run the job synchronously."), "false", new ExampleWithSingleValue().withValue(false).withSummary("Run the job synchronously."),
@ -988,6 +1006,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
{ {
responses.putAll(output.getSpecResponses(apiName)); responses.putAll(output.getSpecResponses(apiName));
} }
if(!ApiProcessMetaData.AsyncMode.NEVER.equals(apiProcessMetaData.getAsyncMode())) if(!ApiProcessMetaData.AsyncMode.NEVER.equals(apiProcessMetaData.getAsyncMode()))
{ {
responses.put(HttpStatus.ACCEPTED.getCode(), new Response() responses.put(HttpStatus.ACCEPTED.getCode(), new Response()
@ -1046,16 +1065,28 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withDescription("Get the status for a previous asynchronous call to the process named " + processMetaData.getLabel()) .withDescription("Get the status for a previous asynchronous call to the process named " + processMetaData.getLabel())
.withSecurity(getSecurity(apiInstanceMetaData, processMetaData.getName())); .withSecurity(getSecurity(apiInstanceMetaData, processMetaData.getName()));
//////////////////////////////////////////////////////// List<Parameter> parameters = new ArrayList<>();
// add the async input for optionally-async processes // ApiProcessInput apiProcessInput = apiProcessMetaData.getInput();
//////////////////////////////////////////////////////// ApiProcessInputFieldsContainer pathParams = apiProcessInput.getPathParams();
methodForProcess.setParameters(ListBuilder.of(new Parameter() if(pathParams != null)
{
for(QFieldMetaData field : CollectionUtils.nonNullList(pathParams.getFields()))
{
parameters.add(processFieldToParameter(apiInstanceMetaData, field).withIn("path"));
}
}
parameters.add(new Parameter()
.withName("jobId") .withName("jobId")
.withIn("path") .withIn("path")
.withRequired(true) .withRequired(true)
.withDescription("Id of the job, as returned by the API call that started it.") .withDescription("Id of the job, as returned by the API call that started it.")
.withSchema(new Schema().withType("string").withFormat("uuid")) .withSchema(new Schema().withType("string").withFormat("uuid")));
));
////////////////////////////////////////////////////////
// add the async input for optionally-async processes //
////////////////////////////////////////////////////////
methodForProcess.setParameters(parameters);
////////////////////////////////// //////////////////////////////////
// build all possible responses // // build all possible responses //
@ -1126,7 +1157,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
{ {
if(apiFieldMetaData.getExample() != null) if(apiFieldMetaData.getExample() != null)
{ {
parameter.withExample(apiFieldMetaData.getExample()); parameter.withExamples(Map.of("example", apiFieldMetaData.getExample()));
} }
else if(apiFieldMetaData.getExamples() != null) else if(apiFieldMetaData.getExamples() != null)
{ {

View File

@ -316,6 +316,7 @@ public class QJavalinApiHandler
ApiProcessInput input = apiProcessMetaData.getInput(); ApiProcessInput input = apiProcessMetaData.getInput();
if(input != null) if(input != null)
{ {
processProcessInputFieldsContainer(context, parameters, input.getPathParams(), Context::pathParam);
processProcessInputFieldsContainer(context, parameters, input.getQueryStringParams(), Context::queryParam); processProcessInputFieldsContainer(context, parameters, input.getQueryStringParams(), Context::queryParam);
processProcessInputFieldsContainer(context, parameters, input.getFormParams(), Context::formParam); processProcessInputFieldsContainer(context, parameters, input.getFormParams(), Context::formParam);
@ -347,9 +348,7 @@ public class QJavalinApiHandler
////////////////// //////////////////
QJavalinAccessLogger.logEndSuccess(); QJavalinAccessLogger.logEndSuccess();
context.status(response.getStatusCode().getCode()); context.status(response.getStatusCode().getCode());
String resultString = toJson(Objects.requireNonNullElse(response.getResponseBodyObject(), "")); handleProcessResponse(context, response, apiLog);
context.result(resultString);
storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString));
} }
catch(Exception e) catch(Exception e)
{ {
@ -360,6 +359,52 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
private static void handleProcessResponse(Context context, HttpApiResponse response, APILog apiLog)
{
if(response.getNeedsFormattedAsJson())
{
/////////////////////////////////////////////////////////////////////////////////////////
// if the response object says that we should format the response as json, then do so. //
/////////////////////////////////////////////////////////////////////////////////////////
String resultString = toJson(Objects.requireNonNullElse(response.getResponseBodyObject(), ""));
context.result(resultString);
storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString));
}
else
{
if(StringUtils.hasContent(response.getContentType()))
{
context.contentType(response.getContentType());
}
////////////////////////////////////////////////////////////////////////////////////
// else, try to return it raw - as byte[], or String, or as a converted-to-String //
////////////////////////////////////////////////////////////////////////////////////
Serializable result = Objects.requireNonNullElse(response.getResponseBodyObject(), "");
if(result instanceof byte[] ba)
{
context.result(ba);
storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody("Byte array of length: " + ba.length));
}
else if(result instanceof String s)
{
context.result(s);
storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(s));
}
else
{
String resultString = String.valueOf(result);
context.result(resultString);
storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString));
}
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -381,9 +426,7 @@ public class QJavalinApiHandler
////////////////// //////////////////
QJavalinAccessLogger.logEndSuccess(); QJavalinAccessLogger.logEndSuccess();
context.status(response.getStatusCode().getCode()); context.status(response.getStatusCode().getCode());
String resultString = toJson(Objects.requireNonNullElse(response.getResponseBodyObject(), "")); handleProcessResponse(context, response, apiLog);
context.result(resultString);
storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString));
} }
catch(Exception e) catch(Exception e)
{ {

View File

@ -35,6 +35,14 @@ public class HttpApiResponse
private HttpStatus.Code statusCode; private HttpStatus.Code statusCode;
private Serializable responseBodyObject; private Serializable responseBodyObject;
private String contentType;
////////////////////////////////////////////////////////////////////////////////////////////////////////
// by default - QJavalinApiHandler will format the responseBodyObject as JSON. //
// set this field to false if you don't want it to do that (e.g., if your response is, say, a byte[]) //
////////////////////////////////////////////////////////////////////////////////////////////////////////
private boolean needsFormattedAsJson = true;
/******************************************************************************* /*******************************************************************************
@ -119,4 +127,66 @@ public class HttpApiResponse
return (this); return (this);
} }
/*******************************************************************************
** Getter for needsFormattedAsJson
*******************************************************************************/
public boolean getNeedsFormattedAsJson()
{
return (this.needsFormattedAsJson);
}
/*******************************************************************************
** Setter for needsFormattedAsJson
*******************************************************************************/
public void setNeedsFormattedAsJson(boolean needsFormattedAsJson)
{
this.needsFormattedAsJson = needsFormattedAsJson;
}
/*******************************************************************************
** Fluent setter for needsFormattedAsJson
*******************************************************************************/
public HttpApiResponse withNeedsFormattedAsJson(boolean needsFormattedAsJson)
{
this.needsFormattedAsJson = needsFormattedAsJson;
return (this);
}
/*******************************************************************************
** Getter for contentType
*******************************************************************************/
public String getContentType()
{
return (this.contentType);
}
/*******************************************************************************
** Setter for contentType
*******************************************************************************/
public void setContentType(String contentType)
{
this.contentType = contentType;
}
/*******************************************************************************
** Fluent setter for contentType
*******************************************************************************/
public HttpApiResponse withContentType(String contentType)
{
this.contentType = contentType;
return (this);
}
} }

View File

@ -30,6 +30,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
*******************************************************************************/ *******************************************************************************/
public class ApiProcessInput public class ApiProcessInput
{ {
private ApiProcessInputFieldsContainer pathParams;
private ApiProcessInputFieldsContainer queryStringParams; private ApiProcessInputFieldsContainer queryStringParams;
private ApiProcessInputFieldsContainer formParams; private ApiProcessInputFieldsContainer formParams;
private ApiProcessInputFieldsContainer recordBodyParams; private ApiProcessInputFieldsContainer recordBodyParams;
@ -44,6 +45,11 @@ public class ApiProcessInput
*******************************************************************************/ *******************************************************************************/
public String getRecordIdsParamName() public String getRecordIdsParamName()
{ {
if(pathParams != null && pathParams.getRecordIdsField() != null)
{
return (pathParams.getRecordIdsField().getName());
}
if(queryStringParams != null && queryStringParams.getRecordIdsField() != null) if(queryStringParams != null && queryStringParams.getRecordIdsField() != null)
{ {
return (queryStringParams.getRecordIdsField().getName()); return (queryStringParams.getRecordIdsField().getName());
@ -217,4 +223,35 @@ public class ApiProcessInput
return (this); return (this);
} }
/*******************************************************************************
** Getter for pathParams
*******************************************************************************/
public ApiProcessInputFieldsContainer getPathParams()
{
return (this.pathParams);
}
/*******************************************************************************
** Setter for pathParams
*******************************************************************************/
public void setPathParams(ApiProcessInputFieldsContainer pathParams)
{
this.pathParams = pathParams;
}
/*******************************************************************************
** Fluent setter for pathParams
*******************************************************************************/
public ApiProcessInput withPathParams(ApiProcessInputFieldsContainer pathParams)
{
this.pathParams = pathParams;
return (this);
}
} }

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.api.model.metadata.processes;
import java.io.Serializable; import java.io.Serializable;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.api.model.actions.HttpApiResponse;
import com.kingsrook.qqq.api.model.openapi.Response; import com.kingsrook.qqq.api.model.openapi.Response;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
@ -61,4 +62,13 @@ public interface ApiProcessOutputInterface
.withDescription("Process has been successfully executed.") .withDescription("Process has been successfully executed.")
)); ));
} }
/*******************************************************************************
**
*******************************************************************************/
default void customizeHttpApiResponse(HttpApiResponse httpApiResponse, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException
{
}
} }

View File

@ -34,9 +34,11 @@ import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.logging.LogPair; import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
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.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
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.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.Pair; import com.kingsrook.qqq.backend.core.utils.Pair;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.BooleanUtils;
@ -162,9 +164,18 @@ public class ApiProcessUtils
*******************************************************************************/ *******************************************************************************/
public static String getProcessApiPath(QInstance qInstance, QProcessMetaData process, ApiProcessMetaData apiProcessMetaData, ApiInstanceMetaData apiInstanceMetaData) public static String getProcessApiPath(QInstance qInstance, QProcessMetaData process, ApiProcessMetaData apiProcessMetaData, ApiInstanceMetaData apiInstanceMetaData)
{ {
StringBuilder pathParams = new StringBuilder();
if(ObjectUtils.ifCan(() -> CollectionUtils.nullSafeHasContents(apiProcessMetaData.getInput().getPathParams().getFields())))
{
for(QFieldMetaData field : apiProcessMetaData.getInput().getPathParams().getFields())
{
pathParams.append("/{").append(field.getName()).append("}");
}
}
if(StringUtils.hasContent(apiProcessMetaData.getPath())) if(StringUtils.hasContent(apiProcessMetaData.getPath()))
{ {
return apiProcessMetaData.getPath() + "/" + apiProcessMetaData.getApiProcessName(); return apiProcessMetaData.getPath() + "/" + apiProcessMetaData.getApiProcessName() + pathParams;
} }
else if(StringUtils.hasContent(process.getTableName())) else if(StringUtils.hasContent(process.getTableName()))
{ {
@ -182,11 +193,11 @@ public class ApiProcessUtils
} }
} }
} }
return tablePathPart + "/" + apiProcessMetaData.getApiProcessName(); return tablePathPart + "/" + apiProcessMetaData.getApiProcessName() + pathParams;
} }
else else
{ {
return apiProcessMetaData.getApiProcessName(); return apiProcessMetaData.getApiProcessName() + pathParams;
} }
} }

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.api;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
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.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QSession;
@ -56,7 +57,7 @@ public class BaseTest
** **
*******************************************************************************/ *******************************************************************************/
@BeforeEach @BeforeEach
void baseBeforeEach() void baseBeforeEach() throws QException
{ {
QContext.init(TestUtils.defineInstance(), new QSession()); QContext.init(TestUtils.defineInstance(), new QSession());
} }

View File

@ -25,7 +25,6 @@ package com.kingsrook.qqq.api.javalin;
import com.kingsrook.qqq.api.BaseTest; import com.kingsrook.qqq.api.BaseTest;
import com.kingsrook.qqq.api.TestUtils; import com.kingsrook.qqq.api.TestUtils;
import com.kingsrook.qqq.api.actions.ApiImplementation; import com.kingsrook.qqq.api.actions.ApiImplementation;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
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.permissions.PermissionLevel; import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
@ -61,7 +60,7 @@ class QJavalinApiHandlerPermissionsTest extends BaseTest
** **
*******************************************************************************/ *******************************************************************************/
@BeforeAll @BeforeAll
static void beforeAll() throws QInstanceValidationException static void beforeAll() throws Exception
{ {
QInstance qInstance = TestUtils.defineInstance(); QInstance qInstance = TestUtils.defineInstance();

View File

@ -36,7 +36,6 @@ 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.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
@ -51,6 +50,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
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.savedreports.SavedReport;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.SleepUtils; import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
@ -66,6 +67,7 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.api.TestUtils.insertPersonRecord; import static com.kingsrook.qqq.api.TestUtils.insertPersonRecord;
import static com.kingsrook.qqq.api.TestUtils.insertSavedReport;
import static com.kingsrook.qqq.api.TestUtils.insertSimpsons; import static com.kingsrook.qqq.api.TestUtils.insertSimpsons;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertArrayEquals;
@ -95,7 +97,7 @@ class QJavalinApiHandlerTest extends BaseTest
** **
*******************************************************************************/ *******************************************************************************/
@BeforeAll @BeforeAll
static void beforeAll() throws QInstanceValidationException static void beforeAll() throws Exception
{ {
QInstance qInstance = TestUtils.defineInstance(); QInstance qInstance = TestUtils.defineInstance();
@ -1404,6 +1406,54 @@ class QJavalinApiHandlerTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGetProcessRenderSavedReport() throws QException
{
insertSimpsons();
Integer reportId = insertSavedReport(new SavedReport()
.withLabel("Person Report")
.withTableName(TestUtils.TABLE_NAME_PERSON)
.withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()))
.withColumnsJson("""
{"columns":[
{"name": "id"},
{"name": "firstName"},
{"name": "lastName"}
]}
"""));
HttpResponse<String> response = Unirest.get(BASE_URL + "/api/" + VERSION + "/savedReport/renderSavedReport/" + reportId + "?reportFormat=CSV").asString();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertThat(response.getHeaders().getFirst("content-type")).contains("csv");
assertEquals("""
"Id","First Name","Last Name"
"1","Homer","Simpson"
"2","Marge","Simpson"
"3","Bart","Simpson"
"4","Lisa","Simpson"
"5","Maggie","Simpson"
""", response.getBody());
response = Unirest.get(BASE_URL + "/api/" + VERSION + "/savedReport/renderSavedReport/" + reportId + "?reportFormat=JSON").asString();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertThat(response.getHeaders().getFirst("content-type")).contains("json");
JSONArray jsonArray = new JSONArray(response.getBody());
assertEquals(5, jsonArray.length());
assertThat(jsonArray.getJSONObject(0).toMap())
.hasFieldOrPropertyWithValue("id", 1)
.hasFieldOrPropertyWithValue("firstName", "Homer")
.hasFieldOrPropertyWithValue("lastName", "Simpson");
response = Unirest.get(BASE_URL + "/api/" + VERSION + "/savedReport/renderSavedReport/" + reportId + "?reportFormat=XLSX").asString();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertThat(response.getHeaders().getFirst("content-type")).contains("openxmlformats-officedocument.spreadsheetml");
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/