updated api backend to support count and query

This commit is contained in:
Tim Chamberlain
2022-10-19 10:43:39 -05:00
parent bf3835bd4c
commit 18a3f72c4a
9 changed files with 348 additions and 33 deletions

View File

@ -22,6 +22,9 @@
package com.kingsrook.qqq.backend.core.actions;
import java.io.Serializable;
import java.util.List;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
@ -41,11 +44,23 @@ public class ActionHelper
public static void validateSession(AbstractActionInput request) throws QException
{
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData());
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData());
if(!authenticationModule.isSessionValid(request.getInstance(), request.getSession()))
{
throw new QAuthenticationException("Invalid session in request");
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void editFirstValue(List<Serializable> values, Function<String, String> editFunction)
{
if(values.size() > 0)
{
values.set(0, editFunction.apply(String.valueOf(values.get(0))));
}
}
}

View File

@ -23,6 +23,9 @@ package com.kingsrook.qqq.backend.core.utils;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
@ -31,6 +34,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONArray;
@ -219,4 +224,44 @@ public class JsonUtils
return (json != null && json.matches("(?s)\\s*\\[.*"));
}
/*******************************************************************************
** Convert a json object into a QRecord
**
*******************************************************************************/
public static QRecord parseQRecord(JSONObject jsonObject, Map<String, QFieldMetaData> fields)
{
QRecord record = new QRecord();
for(String fieldName : fields.keySet())
{
QFieldMetaData metaData = fields.get(fieldName);
String backendName = metaData.getBackendName() != null ? metaData.getBackendName() : fieldName;
switch(metaData.getType())
{
case INTEGER -> record.setValue(fieldName, jsonObject.optInt(backendName));
case DECIMAL -> record.setValue(fieldName, jsonObject.optBigDecimal(backendName, null));
case BOOLEAN -> record.setValue(fieldName, jsonObject.optBoolean(backendName));
case DATE_TIME ->
{
String dateTimeString = jsonObject.optString(backendName);
if(StringUtils.hasContent(dateTimeString))
{
try
{
record.setValue(fieldName, LocalDateTime.parse(dateTimeString, DateTimeFormatter.ISO_ZONED_DATE_TIME));
}
catch(DateTimeParseException dtpe1)
{
record.setValue(fieldName, LocalDateTime.parse(dateTimeString, DateTimeFormatter.ISO_DATE_TIME));
}
}
}
default -> record.setValue(fieldName, jsonObject.optString(backendName));
}
}
return (record);
}
}

View File

@ -228,6 +228,10 @@ public class ValueUtils
ZoneId zid = (tz == null) ? ZoneId.systemDefault() : tz.toZoneId();
return LocalDateTime.ofInstant(c.toInstant(), zid);
}
else if(value instanceof String s)
{
return LocalDateTime.parse(s);
}
else
{
throw (new QValueException("Unsupported class " + value.getClass().getName() + " for converting to LocalDateTime."));

View File

@ -30,15 +30,9 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.module.api.actions.APICountAction;
import com.kingsrook.qqq.backend.module.api.actions.APIInsertAction;
// import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSCountAction;
// import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSDeleteAction;
// import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSInsertAction;
// import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSQueryAction;
// import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSUpdateAction;
// import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
// import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails;
import com.kingsrook.qqq.backend.module.api.actions.APIQueryAction;
/*******************************************************************************
@ -84,7 +78,7 @@ public class APIBackendModule implements QBackendModuleInterface
@Override
public CountInterface getCountInterface()
{
return (null); //return (new RDBMSCountAction());
return (new APICountAction());
}
@ -95,7 +89,7 @@ public class APIBackendModule implements QBackendModuleInterface
@Override
public QueryInterface getQueryInterface()
{
return (null); //return (new RDBMSQueryAction());
return (new APIQueryAction());
}

View File

@ -0,0 +1,86 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. 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.backend.module.api.actions;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
**
*******************************************************************************/
public class APICountAction extends AbstractAPIAction implements CountInterface
{
private static final Logger LOG = LogManager.getLogger(APICountAction.class);
/*******************************************************************************
**
*******************************************************************************/
public CountOutput execute(CountInput countInput) throws QException
{
QTableMetaData table = countInput.getTable();
preAction(countInput);
try
{
QQueryFilter filter = countInput.getFilter();
String paramString = apiActionUtil.buildQueryString(filter, null, null, table.getFields());
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
HttpClient client = httpClientBuilder.build();
String url = apiActionUtil.buildTableUrl(table);
HttpGet request = new HttpGet(url + paramString);
apiActionUtil.setupAuthorizationInRequest(request);
apiActionUtil.setupContentTypeInRequest(request);
apiActionUtil.setupAdditionalHeaders(request);
HttpResponse response = client.execute(request);
List<QRecord> queryResults = apiActionUtil.processGetResponse(table, response);
CountOutput rs = new CountOutput();
rs.setCount(queryResults.size());
return rs;
}
catch(Exception e)
{
LOG.warn("Error in API count", e);
throw new QException("Error executing count: " + e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,89 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. 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.backend.module.api.actions;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
**
*******************************************************************************/
public class APIQueryAction extends AbstractAPIAction implements QueryInterface
{
private static final Logger LOG = LogManager.getLogger(APIQueryAction.class);
/*******************************************************************************
**
*******************************************************************************/
public QueryOutput execute(QueryInput queryInput) throws QException
{
QTableMetaData table = queryInput.getTable();
preAction(queryInput);
try
{
QQueryFilter filter = queryInput.getFilter();
String paramString = apiActionUtil.buildQueryString(filter, queryInput.getLimit(), queryInput.getSkip(), table.getFields());
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
HttpClient client = httpClientBuilder.build();
String url = apiActionUtil.buildTableUrl(table) + paramString;
HttpGet request = new HttpGet(url);
LOG.info("API URL: " + url);
apiActionUtil.setupAuthorizationInRequest(request);
apiActionUtil.setupContentTypeInRequest(request);
apiActionUtil.setupAdditionalHeaders(request);
HttpResponse response = client.execute(request);
List<QRecord> queryResults = apiActionUtil.processGetResponse(table, response);
QueryOutput queryOutput = new QueryOutput(queryInput);
queryOutput.addRecords(queryResults);
return (queryOutput);
}
catch(Exception e)
{
LOG.warn("Error in API Query", e);
throw new QException("Error executing query: " + e.getMessage(), e);
}
}
}

View File

@ -24,10 +24,17 @@ package com.kingsrook.qqq.backend.module.api.actions;
import java.io.IOException;
import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -43,6 +50,7 @@ import org.apache.http.entity.StringEntity;
import org.apache.http.util.EntityUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONObject;
@ -89,6 +97,18 @@ public class BaseAPIActionUtil
/*******************************************************************************
** method to build up a query string based on a given QFilter object
**
*******************************************************************************/
protected String buildQueryString(QQueryFilter filter, Integer limit, Integer skip, Map<String, QFieldMetaData> fields) throws QException
{
// todo: reasonable default action
return (null);
}
/*******************************************************************************
** As part of making a request - set up its authorization header (not just
** strictly "Authorization", but whatever is needed for auth).
@ -107,6 +127,10 @@ public class BaseAPIActionUtil
request.addHeader("Authorization", getBasicAuthenticationHeader(backendMetaData.getUsername(), backendMetaData.getPassword()));
break;
case API_KEY_HEADER:
request.addHeader("API-Key", backendMetaData.getApiKey());
break;
default:
throw new IllegalArgumentException("Unexpected authorization type: " + backendMetaData.getAuthorizationType());
}
@ -141,7 +165,7 @@ public class BaseAPIActionUtil
*******************************************************************************/
public void setupAdditionalHeaders(HttpRequestBase request)
{
request.addHeader("Accept", "application/json");
}
@ -219,6 +243,66 @@ public class BaseAPIActionUtil
/*******************************************************************************
**
*******************************************************************************/
protected QRecord jsonObjectToRecord(JSONObject jsonObject, Map<String, QFieldMetaData> fields) throws IOException
{
QRecord record = JsonUtils.parseQRecord(jsonObject, fields);
return (record);
}
/*******************************************************************************
**
*******************************************************************************/
protected List<QRecord> processGetResponse(QTableMetaData table, HttpResponse response) throws IOException
{
int statusCode = response.getStatusLine().getStatusCode();
System.out.println(statusCode);
HttpEntity entity = response.getEntity();
String resultString = EntityUtils.toString(entity);
List<QRecord> recordList = new ArrayList<>();
if(StringUtils.hasContent(resultString))
{
JSONArray resultList = null;
JSONObject jsonObject = null;
if(resultString.startsWith("["))
{
resultList = JsonUtils.toJSONArray(resultString);
}
else
{
String tablePath = getBackendDetails(table).getTablePath();
jsonObject = JsonUtils.toJSONObject(resultString);
if(jsonObject.has(tablePath))
{
resultList = jsonObject.getJSONArray(getBackendDetails(table).getTablePath());
}
}
if(resultList != null)
{
for(int i = 0; i < resultList.length(); i++)
{
recordList.add(jsonObjectToRecord(resultList.getJSONObject(i), table.getFields()));
}
}
else
{
recordList.add(jsonObjectToRecord(jsonObject, table.getFields()));
}
}
return (recordList);
}
/*******************************************************************************
**
*******************************************************************************/
@ -328,4 +412,14 @@ public class BaseAPIActionUtil
{
this.actionInput = actionInput;
}
/*******************************************************************************
**
*******************************************************************************/
protected String urlEncode(String s)
{
return (URLEncoder.encode(s, StandardCharsets.UTF_8));
}
}

View File

@ -27,7 +27,8 @@ package com.kingsrook.qqq.backend.module.api.model;
*******************************************************************************/
public enum AuthorizationType
{
API_KEY_HEADER,
BASIC_AUTH_API_KEY,
BASIC_AUTH_USERNAME_PASSWORD
BASIC_AUTH_USERNAME_PASSWORD,
}

View File

@ -27,8 +27,8 @@ import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.interfaces.QActionInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@ -255,42 +255,42 @@ public abstract class AbstractRDBMSAction implements QActionInterface
case STARTS_WITH:
{
clause += " LIKE ?";
editFirstValue(values, (s -> s + "%"));
ActionHelper.editFirstValue(values, (s -> s + "%"));
expectedNoOfParams = 1;
break;
}
case ENDS_WITH:
{
clause += " LIKE ?";
editFirstValue(values, (s -> "%" + s));
ActionHelper.editFirstValue(values, (s -> "%" + s));
expectedNoOfParams = 1;
break;
}
case CONTAINS:
{
clause += " LIKE ?";
editFirstValue(values, (s -> "%" + s + "%"));
ActionHelper.editFirstValue(values, (s -> "%" + s + "%"));
expectedNoOfParams = 1;
break;
}
case NOT_STARTS_WITH:
{
clause += " NOT LIKE ?";
editFirstValue(values, (s -> s + "%"));
ActionHelper.editFirstValue(values, (s -> s + "%"));
expectedNoOfParams = 1;
break;
}
case NOT_ENDS_WITH:
{
clause += " NOT LIKE ?";
editFirstValue(values, (s -> "%" + s));
ActionHelper.editFirstValue(values, (s -> "%" + s));
expectedNoOfParams = 1;
break;
}
case NOT_CONTAINS:
{
clause += " NOT LIKE ?";
editFirstValue(values, (s -> "%" + s + "%"));
ActionHelper.editFirstValue(values, (s -> "%" + s + "%"));
expectedNoOfParams = 1;
break;
}
@ -372,19 +372,6 @@ public abstract class AbstractRDBMSAction implements QActionInterface
/*******************************************************************************
**
*******************************************************************************/
private static void editFirstValue(List<Serializable> values, Function<String, String> editFunction)
{
if(values.size() > 0)
{
values.set(0, editFunction.apply(String.valueOf(values.get(0))));
}
}
/*******************************************************************************
**
*******************************************************************************/