mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-17 20:50:44 +00:00
Checkpoint, semi-working query endpoint
This commit is contained in:
@ -118,6 +118,11 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
///////////////////
|
||||
for(QTableMetaData table : qInstance.getTables().values())
|
||||
{
|
||||
if(table.getIsHidden())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
String tableName = table.getName();
|
||||
String tableNameUcFirst = StringUtils.ucFirst(table.getName());
|
||||
String tableLabel = table.getLabel();
|
||||
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.javalin;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class QBadRequestException extends QException
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QBadRequestException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
@ -23,20 +23,28 @@ package com.kingsrook.qqq.api.javalin;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.api.ApiMiddlewareType;
|
||||
import com.kingsrook.qqq.api.actions.GenerateOpenApiSpecAction;
|
||||
import com.kingsrook.qqq.api.actions.GetTableApiFieldsAction;
|
||||
import com.kingsrook.qqq.api.model.APIVersion;
|
||||
import com.kingsrook.qqq.api.model.APIVersionRange;
|
||||
import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput;
|
||||
import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput;
|
||||
import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsInput;
|
||||
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
|
||||
@ -44,8 +52,13 @@ import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
||||
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.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
@ -56,11 +69,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import com.kingsrook.qqq.backend.javalin.QJavalinAccessLogger;
|
||||
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
|
||||
import com.kingsrook.qqq.backend.javalin.QJavalinUtils;
|
||||
import io.javalin.apibuilder.ApiBuilder;
|
||||
import io.javalin.apibuilder.EndpointGroup;
|
||||
import io.javalin.http.ContentType;
|
||||
import io.javalin.http.Context;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
@ -92,12 +107,14 @@ public class QJavalinApiHandler
|
||||
/*******************************************************************************
|
||||
** Define the routes
|
||||
*******************************************************************************/
|
||||
public static EndpointGroup getRoutes()
|
||||
public EndpointGroup getRoutes()
|
||||
{
|
||||
return (() ->
|
||||
{
|
||||
ApiBuilder.path("/api/{version}", () -> // todo - configurable, that /api/ bit?
|
||||
{
|
||||
ApiBuilder.get("/openapi.yaml", QJavalinApiHandler::doSpec);
|
||||
|
||||
ApiBuilder.path("/{tableName}", () ->
|
||||
{
|
||||
ApiBuilder.post("/", QJavalinApiHandler::doInsert);
|
||||
@ -119,6 +136,27 @@ public class QJavalinApiHandler
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void doSpec(Context context)
|
||||
{
|
||||
try
|
||||
{
|
||||
QContext.init(qInstance, null);
|
||||
String version = context.pathParam("version");
|
||||
GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion(version));
|
||||
context.contentType(ContentType.APPLICATION_YAML);
|
||||
context.result(output.getYaml());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
QJavalinImplementation.handleException(context, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -151,7 +189,7 @@ public class QJavalinApiHandler
|
||||
private static void doGet(Context context)
|
||||
{
|
||||
String version = context.pathParam("version");
|
||||
String tableName = context.pathParam("table");
|
||||
String tableName = context.pathParam("tableName");
|
||||
String primaryKey = context.pathParam("primaryKey");
|
||||
|
||||
try
|
||||
@ -200,14 +238,8 @@ public class QJavalinApiHandler
|
||||
+ table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
|
||||
}
|
||||
|
||||
List<? extends QFieldMetaData> tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version)).getFields();
|
||||
Map<String, Serializable> outputRecord = new LinkedHashMap<>();
|
||||
for(QFieldMetaData tableApiField : tableApiFields)
|
||||
{
|
||||
// todo - what about display values / possible values
|
||||
// todo - handle removed-from-this-version fields!!
|
||||
outputRecord.put(tableApiField.getName(), record.getValue(tableApiField.getName()));
|
||||
}
|
||||
List<? extends QFieldMetaData> tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version)).getFields();
|
||||
LinkedHashMap<String, Serializable> outputRecord = toApiRecord(record, tableApiFields);
|
||||
|
||||
QJavalinAccessLogger.logEndSuccess();
|
||||
context.result(JsonUtils.toJson(outputRecord));
|
||||
@ -226,40 +258,220 @@ public class QJavalinApiHandler
|
||||
*******************************************************************************/
|
||||
private static void doQuery(Context context)
|
||||
{
|
||||
String table = context.pathParam("table");
|
||||
String filter = null;
|
||||
String version = context.pathParam("version");
|
||||
String tableName = context.pathParam("tableName");
|
||||
QQueryFilter filter = null;
|
||||
|
||||
try
|
||||
{
|
||||
List<String> badRequestMessages = new ArrayList<>();
|
||||
|
||||
// todo - make sure version is known in this instance
|
||||
// todo - make sure table is supported in this version
|
||||
|
||||
QTableMetaData table = qInstance.getTable(tableName);
|
||||
|
||||
if(table == null)
|
||||
{
|
||||
throw (new QNotFoundException("Could not find any resources at path " + context.path()));
|
||||
}
|
||||
|
||||
if(!getApiVersionRange(table).includes(new APIVersion(version)))
|
||||
{
|
||||
throw (new QNotFoundException("This version of this API does not contain the resource path " + context.path()));
|
||||
}
|
||||
|
||||
QueryInput queryInput = new QueryInput();
|
||||
setupSession(context, queryInput);
|
||||
QJavalinAccessLogger.logStart("query", logPair("table", table));
|
||||
QJavalinAccessLogger.logStart("apiQuery", logPair("table", tableName));
|
||||
|
||||
queryInput.setTableName(table);
|
||||
queryInput.setShouldGenerateDisplayValues(true);
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.setSkip(QJavalinUtils.integerQueryParam(context, "skip"));
|
||||
queryInput.setLimit(QJavalinUtils.integerQueryParam(context, "limit"));
|
||||
queryInput.setTableName(tableName);
|
||||
//? queryInput.setShouldGenerateDisplayValues(true);
|
||||
//? queryInput.setShouldTranslatePossibleValues(true);
|
||||
|
||||
PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ);
|
||||
|
||||
filter = QJavalinUtils.stringQueryParam(context, "filter");
|
||||
if(!StringUtils.hasContent(filter))
|
||||
Integer pageSize = 50;
|
||||
if(StringUtils.hasContent(context.queryParam("pageSize")))
|
||||
{
|
||||
filter = context.formParam("filter");
|
||||
try
|
||||
{
|
||||
pageSize = ValueUtils.getValueAsInteger(context.queryParam("pageSize"));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
badRequestMessages.add("Could not parse pageSize as an integer");
|
||||
}
|
||||
}
|
||||
if(filter != null)
|
||||
if(pageSize < 1 || pageSize > 1000)
|
||||
{
|
||||
queryInput.setFilter(JsonUtils.toObject(filter, QQueryFilter.class));
|
||||
badRequestMessages.add("pageSize must be between 1 and 1000.");
|
||||
}
|
||||
|
||||
Integer pageNo = Objects.requireNonNullElse(QJavalinUtils.integerQueryParam(context, "pageNo"), 1);
|
||||
if(pageNo < 1)
|
||||
{
|
||||
badRequestMessages.add("pageNo must be greater than 0.");
|
||||
}
|
||||
|
||||
queryInput.setLimit(pageSize);
|
||||
queryInput.setSkip((pageNo - 1) * pageSize);
|
||||
|
||||
// queryInput.setQueryJoins(processQueryJoinsParam(context));
|
||||
|
||||
filter = new QQueryFilter();
|
||||
if("and".equalsIgnoreCase(context.queryParam("booleanOperator")))
|
||||
{
|
||||
filter.setBooleanOperator(QQueryFilter.BooleanOperator.AND);
|
||||
}
|
||||
else if("or".equalsIgnoreCase(context.queryParam("booleanOperator")))
|
||||
{
|
||||
filter.setBooleanOperator(QQueryFilter.BooleanOperator.AND);
|
||||
}
|
||||
else if(StringUtils.hasContent(context.queryParam("booleanOperator")))
|
||||
{
|
||||
badRequestMessages.add("booleanOperator must be either AND or OR.");
|
||||
}
|
||||
|
||||
boolean includeCount = true;
|
||||
if("true".equalsIgnoreCase(context.queryParam("includeCount")))
|
||||
{
|
||||
includeCount = true;
|
||||
}
|
||||
else if("false".equalsIgnoreCase(context.queryParam("includeCount")))
|
||||
{
|
||||
includeCount = false;
|
||||
}
|
||||
else if(StringUtils.hasContent(context.queryParam("includeCount")))
|
||||
{
|
||||
badRequestMessages.add("includeCount must be either true or false");
|
||||
}
|
||||
|
||||
String orderBy = context.queryParam("orderBy");
|
||||
if(StringUtils.hasContent(orderBy))
|
||||
{
|
||||
for(String orderByPart : orderBy.split(","))
|
||||
{
|
||||
orderByPart = orderByPart.trim();
|
||||
String[] orderByNameDirection = orderByPart.split(" +");
|
||||
boolean asc = true;
|
||||
if(orderByNameDirection.length == 2)
|
||||
{
|
||||
if("asc".equalsIgnoreCase(orderByNameDirection[1]))
|
||||
{
|
||||
asc = true;
|
||||
}
|
||||
else if("desc".equalsIgnoreCase(orderByNameDirection[1]))
|
||||
{
|
||||
asc = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
badRequestMessages.add("orderBy direction for field " + orderByNameDirection[0] + " must be either ASC or DESC.");
|
||||
}
|
||||
}
|
||||
else if(orderByNameDirection.length > 2)
|
||||
{
|
||||
badRequestMessages.add("unrecognized format for orderBy clause: " + orderByPart + ". Expected: fieldName [ASC|DESC].");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
QFieldMetaData field = table.getField(orderByNameDirection[0]);
|
||||
filter.withOrderBy(new QFilterOrderBy(field.getName(), asc));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
badRequestMessages.add("unrecognized orderBy field name: " + orderByNameDirection[0] + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> nonFilterParams = Set.of("pageSize", "pageNo", "orderBy", "booleanOperator", "includeCount");
|
||||
|
||||
////////////////////////////
|
||||
// look for filter params //
|
||||
////////////////////////////
|
||||
for(Map.Entry<String, List<String>> entry : context.queryParamMap().entrySet())
|
||||
{
|
||||
String name = entry.getKey();
|
||||
List<String> values = entry.getValue();
|
||||
|
||||
if(nonFilterParams.contains(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
QFieldMetaData field = table.getField(name);
|
||||
for(String value : values)
|
||||
{
|
||||
if(StringUtils.hasContent(value))
|
||||
{
|
||||
QCriteriaOperator operator = getCriteriaOperator(value);
|
||||
List<Serializable> criteriaValues = getCriteriaValues(field, value);
|
||||
filter.addCriteria(new QFilterCriteria(name, operator, criteriaValues));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
badRequestMessages.add("Unrecognized filter criteria field: " + name);
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////
|
||||
// no more badRequest checks below here //
|
||||
//////////////////////////////////////////
|
||||
if(!badRequestMessages.isEmpty())
|
||||
{
|
||||
if(badRequestMessages.size() == 1)
|
||||
{
|
||||
throw (new QBadRequestException(badRequestMessages.get(0)));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw (new QBadRequestException("Requested failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join(" \n", badRequestMessages)));
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////
|
||||
// do the query //
|
||||
//////////////////
|
||||
QueryAction queryAction = new QueryAction();
|
||||
queryInput.setFilter(filter);
|
||||
QueryOutput queryOutput = queryAction.execute(queryInput);
|
||||
|
||||
Map<String, Serializable> output = new HashMap<>();
|
||||
output.put("pageSize", pageSize);
|
||||
output.put("pageNo", pageNo);
|
||||
|
||||
///////////////////////////////
|
||||
// map record fields for api //
|
||||
///////////////////////////////
|
||||
List<? extends QFieldMetaData> tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version)).getFields();
|
||||
ArrayList<Map<String, Serializable>> records = new ArrayList<>();
|
||||
for(QRecord record : queryOutput.getRecords())
|
||||
{
|
||||
records.add(toApiRecord(record, tableApiFields));
|
||||
}
|
||||
output.put("records", records);
|
||||
|
||||
/////////////////////////////
|
||||
// optionally do the count //
|
||||
/////////////////////////////
|
||||
if(includeCount)
|
||||
{
|
||||
CountInput countInput = new CountInput();
|
||||
countInput.setTableName(tableName);
|
||||
countInput.setFilter(filter);
|
||||
CountOutput countOutput = new CountAction().execute(countInput);
|
||||
output.put("count", countOutput.getCount());
|
||||
}
|
||||
|
||||
QJavalinAccessLogger.logEndSuccess(logPair("recordCount", queryOutput.getRecords().size()), QJavalinAccessLogger.logPairIfSlow("filter", filter, SLOW_LOG_THRESHOLD_MS));
|
||||
context.result(JsonUtils.toJson(queryOutput));
|
||||
context.result(JsonUtils.toJson(output));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@ -270,6 +482,28 @@ public class QJavalinApiHandler
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static QCriteriaOperator getCriteriaOperator(String value)
|
||||
{
|
||||
// todo - all other operators
|
||||
return (QCriteriaOperator.EQUALS);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static List<Serializable> getCriteriaValues(QFieldMetaData field, String value)
|
||||
{
|
||||
// todo - parse the thing, do stuff
|
||||
return (List.of(value));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -300,6 +534,23 @@ public class QJavalinApiHandler
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static LinkedHashMap<String, Serializable> toApiRecord(QRecord record, List<? extends QFieldMetaData> tableApiFields)
|
||||
{
|
||||
LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>();
|
||||
for(QFieldMetaData tableApiField : tableApiFields)
|
||||
{
|
||||
// todo - what about display values / possible values
|
||||
// todo - handle removed-from-this-version fields!!
|
||||
outputRecord.put(tableApiField.getName(), record.getValue(tableApiField.getName()));
|
||||
}
|
||||
return outputRecord;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -315,6 +566,14 @@ public class QJavalinApiHandler
|
||||
*******************************************************************************/
|
||||
public static void handleException(HttpStatus.Code statusCode, Context context, Exception e)
|
||||
{
|
||||
QBadRequestException badRequestException = ExceptionUtils.findClassInRootChain(e, QBadRequestException.class);
|
||||
if(badRequestException != null)
|
||||
{
|
||||
statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.BAD_REQUEST); // 400
|
||||
respondWithError(context, statusCode, badRequestException.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class);
|
||||
if(userFacingException != null)
|
||||
{
|
||||
|
Reference in New Issue
Block a user