Initial checkin of API module

This commit is contained in:
2022-10-12 17:54:00 -05:00
parent b91273a53a
commit 471954e8b9
23 changed files with 1614 additions and 9 deletions

View File

@ -179,9 +179,7 @@
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="1"/>
</module>
-->
<module name="OverloadMethodsDeclarationOrder"/>
<!--
<module name="VariableDeclarationUsageDistance"/>
<module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/>

View File

@ -30,8 +30,9 @@
<modules>
<module>qqq-backend-core</module>
<module>qqq-backend-module-rdbms</module>
<module>qqq-backend-module-api</module>
<module>qqq-backend-module-filesystem</module>
<module>qqq-backend-module-rdbms</module>
<module>qqq-middleware-picocli</module>
<module>qqq-middleware-javalin</module>
<module>qqq-sample-project</module>

View File

@ -59,7 +59,7 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.2.1</version>
<version>2.14.0-rc1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>

View File

@ -43,6 +43,7 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface
private String pluralFutureMessage;
private String singularPastMessage;
private String pluralPastMessage;
private String messageSuffix;
//////////////////////////////////////////////////////////////////////////
// using ArrayList, because we need to be Serializable, and List is not //
@ -394,11 +395,13 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface
{
if(count.equals(1))
{
setMessage(isPast ? getSingularPastMessage() : getSingularFutureMessage());
setMessage((isPast ? getSingularPastMessage() : getSingularFutureMessage())
+ (messageSuffix == null ? "" : messageSuffix));
}
else
{
setMessage(isPast ? getPluralPastMessage() : getPluralFutureMessage());
setMessage((isPast ? getPluralPastMessage() : getPluralFutureMessage())
+ (messageSuffix == null ? "" : messageSuffix));
}
}
}
@ -417,4 +420,38 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface
}
}
/*******************************************************************************
** Getter for messageSuffix
**
*******************************************************************************/
public String getMessageSuffix()
{
return messageSuffix;
}
/*******************************************************************************
** Setter for messageSuffix
**
*******************************************************************************/
public void setMessageSuffix(String messageSuffix)
{
this.messageSuffix = messageSuffix;
}
/*******************************************************************************
** Fluent setter for messageSuffix
**
*******************************************************************************/
public ProcessSummaryLine withMessageSuffix(String messageSuffix)
{
this.messageSuffix = messageSuffix;
return (this);
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
@ -241,4 +242,18 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
return (ValueUtils.getValueAsBigDecimal(getValue(fieldName)));
}
/*******************************************************************************
**
*******************************************************************************/
public void addRecord(QRecord record)
{
if(this.processState.getRecords() == null)
{
this.processState.setRecords(new ArrayList<>());
}
this.processState.getRecords().add(record);
}
}

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.insert;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -54,4 +55,19 @@ public class InsertOutput extends AbstractActionOutput
{
this.records = records;
}
/*******************************************************************************
**
*******************************************************************************/
public void addRecord(QRecord record)
{
if(this.records == null)
{
this.records = new ArrayList<>();
}
this.records.add(record);
}
}

View File

@ -75,7 +75,8 @@ public class QBackendModuleDispatcher
"com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule",
"com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendModule",
"com.kingsrook.qqq.backend.module.filesystem.local.FilesystemBackendModule",
"com.kingsrook.qqq.backend.module.filesystem.s3.S3BackendModule"
"com.kingsrook.qqq.backend.module.filesystem.s3.S3BackendModule",
"com.kingsrook.qqq.backend.module.api.APIBackendModule"
};
for(String moduleClassName : moduleClassNames)

View File

@ -71,4 +71,18 @@ public class StreamedBackendStepOutput extends RunBackendStepOutput
return (outputRecords);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addRecord(QRecord record)
{
if(this.outputRecords == null)
{
this.outputRecords = new ArrayList<>();
}
this.outputRecords.add(record);
}
}

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.utils;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@ -29,6 +30,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
@ -422,4 +424,57 @@ public class CollectionUtils
}
return (list);
}
/*******************************************************************************
** Returns the input collection, unless it was null - in which case a new array list is returned.
**
** Meant to help avoid null checks on foreach loops.
*******************************************************************************/
public static <T> Collection<T> nonNullCollection(Collection<T> list)
{
if(list == null)
{
return (new ArrayList<>());
}
return (list);
}
/*******************************************************************************
** Convert a collection of QRecords to a map, from one field's values out of
** those records, to another field's value from those records
*******************************************************************************/
public static Map<Serializable, Serializable> recordsToMap(Collection<QRecord> records, String keyFieldName, String valueFieldName)
{
Map<Serializable, Serializable> rs = new HashMap<>();
for(QRecord record : nonNullCollection(records))
{
rs.put(record.getValue(keyFieldName), record.getValue(valueFieldName));
}
return (rs);
}
/*******************************************************************************
** Convert a collection of QRecords to a map, from one field's values out of
** those records, to the records themselves.
*******************************************************************************/
public static Map<Serializable, QRecord> recordsToMap(Collection<QRecord> records, String keyFieldName)
{
Map<Serializable, QRecord> rs = new HashMap<>();
for(QRecord record : nonNullCollection(records))
{
rs.put(record.getValue(keyFieldName), record);
}
return (rs);
}
}

View File

@ -0,0 +1,22 @@
# qqq-backend-module-api
This is a backend-module for the qqq framework - specifically, one that
works with an API (as in, a web service API, over http(s)).
## License
QQQ - Low-code Application Framework for Engineers. \
Copyright (C) 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/>.

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>qqq-backend-module-api</artifactId>
<parent>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-parent-project</artifactId>
<version>${revision}</version>
</parent>
<properties>
<!-- props specifically to this module -->
<!-- none at this time -->
</properties>
<dependencies>
<!-- other qqq modules deps -->
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId>
<version>${revision}</version>
</dependency>
<!-- Common deps for all qqq modules -->
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>com/kingsrook/qqq/backend/module/api/model/**/*.class</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,134 @@
/*
* 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;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
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.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;
/*******************************************************************************
** QQQ Backend module for working with API's (e.g., over http(s)).
*******************************************************************************/
public class APIBackendModule implements QBackendModuleInterface
{
/*******************************************************************************
** Method where a backend module must be able to provide its type (name).
*******************************************************************************/
public String getBackendType()
{
return ("api");
}
/*******************************************************************************
** Method to identify the class used for backend meta data for this module.
*******************************************************************************/
@Override
public Class<? extends QBackendMetaData> getBackendMetaDataClass()
{
return (null); //return (RDBMSBackendMetaData.class);
}
/*******************************************************************************
** Method to identify the class used for table-backend details for this module.
*******************************************************************************/
@Override
public Class<? extends QTableBackendDetails> getTableBackendDetailsClass()
{
return (null); //return (RDBMSTableBackendDetails.class);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public CountInterface getCountInterface()
{
return (null); //return (new RDBMSCountAction());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QueryInterface getQueryInterface()
{
return (null); //return (new RDBMSQueryAction());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public InsertInterface getInsertInterface()
{
return (new APIInsertAction());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public UpdateInterface getUpdateInterface()
{
return (null); //return (new RDBMSUpdateAction());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public DeleteInterface getDeleteInterface()
{
return (null); //return (new RDBMSDeleteAction());
}
}

View File

@ -0,0 +1,108 @@
/*
* 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.ArrayList;
import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
**
*******************************************************************************/
public class APIInsertAction extends AbstractAPIAction implements InsertInterface
{
private static final Logger LOG = LogManager.getLogger(APIInsertAction.class);
/*******************************************************************************
**
*******************************************************************************/
@Override
public InsertOutput execute(InsertInput insertInput) throws QException
{
InsertOutput insertOutput = new InsertOutput();
insertOutput.setRecords(new ArrayList<>());
if(CollectionUtils.nullSafeIsEmpty(insertInput.getRecords()))
{
LOG.info("Insert request called with 0 records. Returning with no-op");
return (insertOutput);
}
QTableMetaData table = insertInput.getTable();
preAction(insertInput);
try
{
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
HttpClient client = httpClientBuilder.build();
String url = apiActionUtil.buildTableUrl(table);
HttpPost request = new HttpPost(url);
apiActionUtil.setupAuthorizationInRequest(request);
apiActionUtil.setupContentTypeInRequest(request);
apiActionUtil.setupAdditionalHeaders(request);
// todo - supports bulk post?
for(QRecord record : insertInput.getRecords())
{
try
{
request.setEntity(apiActionUtil.recordToEntity(table, record));
HttpResponse response = client.execute(request);
QRecord outputRecord = apiActionUtil.processPostResponse(table, record, response);
insertOutput.addRecord(outputRecord);
}
catch(Exception e)
{
record.addError("Error: " + e.getMessage());
}
}
return (insertOutput);
}
catch(Exception e)
{
LOG.warn("Error in API Insert", e);
throw new QException("Error executing insert: " + e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,63 @@
/*
* 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 com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
/*******************************************************************************
** Base class for all Backend-module-API Actions
*******************************************************************************/
public abstract class AbstractAPIAction
{
protected APIBackendMetaData backendMetaData;
protected BaseAPIActionUtil apiActionUtil;
/*******************************************************************************
** Setup the s3 utils object to be used for this action.
*******************************************************************************/
public void preAction(AbstractTableActionInput actionInput)
{
QBackendMetaData baseBackendMetaData = actionInput.getInstance().getBackendForTable(actionInput.getTableName());
this.backendMetaData = (APIBackendMetaData) baseBackendMetaData;
if(backendMetaData.getActionUtil() != null)
{
apiActionUtil = QCodeLoader.getAdHoc(BaseAPIActionUtil.class, backendMetaData.getActionUtil());
}
else
{
apiActionUtil = new BaseAPIActionUtil();
}
apiActionUtil.setBackendMetaData(this.backendMetaData);
apiActionUtil.setActionInput(actionInput);
}
}

View File

@ -0,0 +1,297 @@
/*
* 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.io.IOException;
import java.io.Serializable;
import java.util.Base64;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
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;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetails;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.util.EntityUtils;
import org.json.JSONObject;
/*******************************************************************************
** Base class for utility functions that make up the unique ways in which an
** API can be implemented.
*******************************************************************************/
public class BaseAPIActionUtil
{
protected APIBackendMetaData backendMetaData;
protected AbstractTableActionInput actionInput;
/*******************************************************************************
** As part of making a request - set up its authorization header (not just
** strictly "Authorization", but whatever is needed for auth).
**
** Can be overridden if an API uses an authorization type we don't natively support.
*******************************************************************************/
protected void setupAuthorizationInRequest(HttpRequestBase request)
{
switch(backendMetaData.getAuthorizationType())
{
case BASIC_AUTH_API_KEY:
request.addHeader("Authorization", getBasicAuthenticationHeader(backendMetaData.getApiKey()));
break;
case BASIC_AUTH_USERNAME_PASSWORD:
request.addHeader("Authorization", getBasicAuthenticationHeader(backendMetaData.getUsername(), backendMetaData.getPassword()));
break;
default:
throw new IllegalArgumentException("Unexpected authorization type: " + backendMetaData.getAuthorizationType());
}
}
/*******************************************************************************
** As part of making a request - set up its content-type header.
*******************************************************************************/
protected void setupContentTypeInRequest(HttpRequestBase request)
{
request.addHeader("Content-Type", backendMetaData.getContentType());
}
/*******************************************************************************
** Helper method to create a value for an Authentication header, using just an
** apiKey - encoded as Basic + base64(apiKey)
*******************************************************************************/
protected String getBasicAuthenticationHeader(String apiKey)
{
return "Basic " + Base64.getEncoder().encodeToString(apiKey.getBytes());
}
/*******************************************************************************
** As part of making a request - set up additional headers. Noop in base -
** meant to override in subclasses.
*******************************************************************************/
public void setupAdditionalHeaders(HttpRequestBase request)
{
}
/*******************************************************************************
** Helper method to create a value for an Authentication header, using just a
** username & password - encoded as Basic + base64(username:password)
*******************************************************************************/
protected String getBasicAuthenticationHeader(String username, String password)
{
return "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes());
}
/*******************************************************************************
** Helper method to build the URL for a table. Can be overridden, if a
** particular API isn't just "base" + "tablePath".
**
** Note: you may want to look at the actionInput object, to help figure out
** what path you need, depending on your API.
*******************************************************************************/
protected String buildTableUrl(QTableMetaData table)
{
return (backendMetaData.getBaseUrl() + getBackendDetails(table).getTablePath());
}
/*******************************************************************************
** Build an HTTP Entity (e.g., for a PUT or POST) from a QRecord. Can be
** overridden if an API doesn't do a basic json object. Or, can override a
** helper method, such as recordToJsonObject.
**
*******************************************************************************/
protected AbstractHttpEntity recordToEntity(QTableMetaData table, QRecord record) throws IOException
{
JSONObject body = recordToJsonObject(table, record);
String json = body.toString(3);
System.out.println(json);
return (new StringEntity(json));
}
/*******************************************************************************
** Helper for recordToEntity - builds a basic JSON object. Can be
** overridden if an API doesn't do a basic json object.
**
*******************************************************************************/
protected JSONObject recordToJsonObject(QTableMetaData table, QRecord record)
{
JSONObject body = new JSONObject();
for(Map.Entry<String, Serializable> entry : record.getValues().entrySet())
{
String fieldName = entry.getKey();
Serializable value = entry.getValue();
QFieldMetaData field;
try
{
field = table.getField(fieldName);
}
catch(Exception e)
{
////////////////////////////////////
// skip values that aren't fields //
////////////////////////////////////
continue;
}
body.put(getFieldBackendName(field), value);
}
return body;
}
/*******************************************************************************
**
*******************************************************************************/
protected QRecord processPostResponse(QTableMetaData table, QRecord record, HttpResponse response) throws IOException
{
int statusCode = response.getStatusLine().getStatusCode();
System.out.println(statusCode);
HttpEntity entity = response.getEntity();
String resultString = EntityUtils.toString(entity);
System.out.println(resultString);
JSONObject jsonObject = JsonUtils.toJSONObject(resultString);
String primaryKeyFieldName = table.getPrimaryKeyField();
String primaryKeyBackendName = getFieldBackendName(table.getField(primaryKeyFieldName));
if(jsonObject.has(primaryKeyBackendName))
{
Serializable primaryKey = (Serializable) jsonObject.get(primaryKeyBackendName);
record.setValue(primaryKeyFieldName, primaryKey);
}
else
{
if(jsonObject.has("error"))
{
JSONObject errorObject = jsonObject.getJSONObject("error");
if(errorObject.has("message"))
{
record.addError("Error: " + errorObject.getString("message"));
}
}
if(CollectionUtils.nullSafeIsEmpty(record.getErrors()))
{
record.addError("Unspecified error executing insert.");
}
}
return (record);
}
/*******************************************************************************
**
*******************************************************************************/
protected APITableBackendDetails getBackendDetails(QTableMetaData tableMetaData)
{
return (APITableBackendDetails) tableMetaData.getBackendDetails();
}
/*******************************************************************************
**
*******************************************************************************/
protected String getFieldBackendName(QFieldMetaData field)
{
String backendName = field.getBackendName();
if(!StringUtils.hasContent(backendName))
{
backendName = field.getName();
}
return (backendName);
}
/*******************************************************************************
** Getter for backendMetaData
**
*******************************************************************************/
public APIBackendMetaData getBackendMetaData()
{
return backendMetaData;
}
/*******************************************************************************
** Setter for backendMetaData
**
*******************************************************************************/
public void setBackendMetaData(APIBackendMetaData backendMetaData)
{
this.backendMetaData = backendMetaData;
}
/*******************************************************************************
** Getter for actionInput
**
*******************************************************************************/
public AbstractTableActionInput getActionInput()
{
return actionInput;
}
/*******************************************************************************
** Setter for actionInput
**
*******************************************************************************/
public void setActionInput(AbstractTableActionInput actionInput)
{
this.actionInput = actionInput;
}
}

View File

@ -0,0 +1,33 @@
/*
* 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.model;
/*******************************************************************************
**
*******************************************************************************/
public enum AuthorizationType
{
BASIC_AUTH_API_KEY,
BASIC_AUTH_USERNAME_PASSWORD
}

View File

@ -0,0 +1,321 @@
/*
* 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.model.metadata;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.module.api.APIBackendModule;
import com.kingsrook.qqq.backend.module.api.model.AuthorizationType;
/*******************************************************************************
** Meta-data to provide details of an API backend (e.g., connection params)
*******************************************************************************/
public class APIBackendMetaData extends QBackendMetaData
{
private String baseUrl;
private String apiKey;
private String username;
private String password;
private AuthorizationType authorizationType;
private String contentType; // todo enum?
private QCodeReference actionUtil;
/*******************************************************************************
** Default Constructor.
*******************************************************************************/
public APIBackendMetaData()
{
super();
setBackendType(APIBackendModule.class);
}
/*******************************************************************************
** Fluent setter, override to help fluent flows
*******************************************************************************/
@Override
public APIBackendMetaData withName(String name)
{
setName(name);
return this;
}
// todo?
// /*******************************************************************************
// ** Called by the QInstanceEnricher - to do backend-type-specific enrichments.
// ** Original use case is: reading secrets into fields (e.g., passwords).
// *******************************************************************************/
// @Override
// public void enrich()
// {
// super.enrich();
// QMetaDataVariableInterpreter interpreter = new QMetaDataVariableInterpreter();
// username = interpreter.interpret(username);
// password = interpreter.interpret(password);
// }
/*******************************************************************************
** Getter for baseUrl
**
*******************************************************************************/
public String getBaseUrl()
{
return baseUrl;
}
/*******************************************************************************
** Setter for baseUrl
**
*******************************************************************************/
public void setBaseUrl(String baseUrl)
{
this.baseUrl = baseUrl;
}
/*******************************************************************************
** Fluent setter for baseUrl
**
*******************************************************************************/
public APIBackendMetaData withBaseUrl(String baseUrl)
{
this.baseUrl = baseUrl;
return (this);
}
/*******************************************************************************
** Getter for apiKey
**
*******************************************************************************/
public String getApiKey()
{
return apiKey;
}
/*******************************************************************************
** Setter for apiKey
**
*******************************************************************************/
public void setApiKey(String apiKey)
{
this.apiKey = apiKey;
}
/*******************************************************************************
** Fluent setter for apiKey
**
*******************************************************************************/
public APIBackendMetaData withApiKey(String apiKey)
{
this.apiKey = apiKey;
return (this);
}
/*******************************************************************************
** Getter for username
**
*******************************************************************************/
public String getUsername()
{
return username;
}
/*******************************************************************************
** Setter for username
**
*******************************************************************************/
public void setUsername(String username)
{
this.username = username;
}
/*******************************************************************************
** Fluent setter for username
**
*******************************************************************************/
public APIBackendMetaData withUsername(String username)
{
this.username = username;
return (this);
}
/*******************************************************************************
** Getter for password
**
*******************************************************************************/
public String getPassword()
{
return password;
}
/*******************************************************************************
** Setter for password
**
*******************************************************************************/
public void setPassword(String password)
{
this.password = password;
}
/*******************************************************************************
** Fluent setter for password
**
*******************************************************************************/
public APIBackendMetaData withPassword(String password)
{
this.password = password;
return (this);
}
/*******************************************************************************
** Getter for authorizationType
**
*******************************************************************************/
public AuthorizationType getAuthorizationType()
{
return authorizationType;
}
/*******************************************************************************
** Setter for authorizationType
**
*******************************************************************************/
public void setAuthorizationType(AuthorizationType authorizationType)
{
this.authorizationType = authorizationType;
}
/*******************************************************************************
** Fluent setter for authorizationType
**
*******************************************************************************/
public APIBackendMetaData withAuthorizationType(AuthorizationType authorizationType)
{
this.authorizationType = authorizationType;
return (this);
}
/*******************************************************************************
** Getter for contentType
**
*******************************************************************************/
public String getContentType()
{
return contentType;
}
/*******************************************************************************
** Setter for contentType
**
*******************************************************************************/
public void setContentType(String contentType)
{
this.contentType = contentType;
}
/*******************************************************************************
** Fluent setter for contentType
**
*******************************************************************************/
public APIBackendMetaData withContentType(String contentType)
{
this.contentType = contentType;
return (this);
}
/*******************************************************************************
** Getter for actionUtil
**
*******************************************************************************/
public QCodeReference getActionUtil()
{
return actionUtil;
}
/*******************************************************************************
** Setter for actionUtil
**
*******************************************************************************/
public void setActionUtil(QCodeReference actionUtil)
{
this.actionUtil = actionUtil;
}
/*******************************************************************************
** Fluent setter for actionUtil
**
*******************************************************************************/
public APIBackendMetaData withActionUtil(QCodeReference actionUtil)
{
this.actionUtil = actionUtil;
return (this);
}
}

View File

@ -0,0 +1,116 @@
/*
* 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.model.metadata;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
import com.kingsrook.qqq.backend.module.api.APIBackendModule;
/*******************************************************************************
** Extension of QTableBackendDetails, with details specific to an API table.
*******************************************************************************/
public class APITableBackendDetails extends QTableBackendDetails
{
private String tablePath;
private String tableWrapperObjectName;
/*******************************************************************************
** Default Constructor.
*******************************************************************************/
public APITableBackendDetails()
{
super();
setBackendType(APIBackendModule.class);
}
/*******************************************************************************
** Getter for tablePath
**
*******************************************************************************/
public String getTablePath()
{
return tablePath;
}
/*******************************************************************************
** Setter for tablePath
**
*******************************************************************************/
public void setTablePath(String tablePath)
{
this.tablePath = tablePath;
}
/*******************************************************************************
** Fluent Setter for tablePath
**
*******************************************************************************/
public APITableBackendDetails withTablePath(String tablePath)
{
this.tablePath = tablePath;
return (this);
}
/*******************************************************************************
** Getter for tableWrapperObjectName
**
*******************************************************************************/
public String getTableWrapperObjectName()
{
return tableWrapperObjectName;
}
/*******************************************************************************
** Setter for tableWrapperObjectName
**
*******************************************************************************/
public void setTableWrapperObjectName(String tableWrapperObjectName)
{
this.tableWrapperObjectName = tableWrapperObjectName;
}
/*******************************************************************************
** Fluent setter for tableWrapperObjectName
**
*******************************************************************************/
public APITableBackendDetails withTableWrapperObjectName(String tableWrapperObjectName)
{
this.tableWrapperObjectName = tableWrapperObjectName;
return (this);
}
}

View File

@ -0,0 +1,137 @@
/*
* 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;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
**
*******************************************************************************/
public class EasyPostApiTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPostTrackerSuccess() throws QException
{
QRecord record = new QRecord()
.withValue("__ignoreMe", "123")
.withValue("carrierCode", "USPS")
.withValue("trackingNo", "EZ1000000001");
InsertInput insertInput = new InsertInput(TestUtils.defineInstance());
insertInput.setSession(new QSession());
insertInput.setTableName("easypostTracker");
insertInput.setRecords(List.of(record));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
QRecord outputRecord = insertOutput.getRecords().get(0);
assertNotNull(outputRecord.getValue("id"), "Should get a tracker id");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPostTrackerEmptyInput() throws QException
{
InsertInput insertInput = new InsertInput(TestUtils.defineInstance());
insertInput.setSession(new QSession());
insertInput.setTableName("easypostTracker");
insertInput.setRecords(List.of());
new InsertAction().execute(insertInput);
////////////////////////////////////
// just make sure we don't throw. //
////////////////////////////////////
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPostTrackerBadApiKey() throws QException
{
QInstance instance = TestUtils.defineInstance();
QBackendMetaData backend = instance.getBackend(TestUtils.EASYPOST_BACKEND_NAME);
((APIBackendMetaData) backend).setApiKey("not-valid");
QRecord record = new QRecord()
.withValue("carrierCode", "USPS")
.withValue("trackingNo", "EZ1000000001");
InsertInput insertInput = new InsertInput(instance);
insertInput.setSession(new QSession());
insertInput.setTableName("easypostTracker");
insertInput.setRecords(List.of(record));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
QRecord outputRecord = insertOutput.getRecords().get(0);
assertNull(outputRecord.getValue("id"), "Should not get a tracker id");
assertThat(outputRecord.getErrors()).isNotNull().isNotEmpty();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPostTrackerError() throws QException
{
QRecord record = new QRecord()
.withValue("carrierCode", "USPS")
.withValue("trackingNo", "Not-Valid-Tracking-No");
InsertInput insertInput = new InsertInput(TestUtils.defineInstance());
insertInput.setSession(new QSession());
insertInput.setTableName("easypostTracker");
insertInput.setRecords(List.of(record));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
QRecord outputRecord = insertOutput.getRecords().get(0);
assertNull(outputRecord.getValue("id"), "Should not get a tracker id");
assertThat(outputRecord.getErrors()).isNotNull().isNotEmpty();
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright © 2022-2022. Nutrifresh Services <contact@nutrifreshservices.com>. All Rights Reserved.
*/
package com.kingsrook.qqq.backend.module.api;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.module.api.actions.BaseAPIActionUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONObject;
/*******************************************************************************
** Utility methods for working with EasyPost API
*******************************************************************************/
public class EasyPostUtils extends BaseAPIActionUtil
{
private static final Logger LOG = LogManager.getLogger(EasyPostUtils.class);
/*******************************************************************************
**
*******************************************************************************/
@Override
protected JSONObject recordToJsonObject(QTableMetaData table, QRecord record)
{
JSONObject inner = super.recordToJsonObject(table, record);
JSONObject outer = new JSONObject();
outer.put(getBackendDetails(table).getTableWrapperObjectName(), inner);
return (outer);
}
}

View File

@ -0,0 +1,113 @@
/*
* 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;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
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.modules.authentication.metadata.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.module.api.model.AuthorizationType;
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetails;
/*******************************************************************************
**
*******************************************************************************/
public class TestUtils
{
public static final String EASYPOST_BACKEND_NAME = "easypost";
/*******************************************************************************
**
*******************************************************************************/
public static QInstance defineInstance()
{
QInstance qInstance = new QInstance();
qInstance.addBackend(defineBackend());
qInstance.addTable(defineTableEasypostTracker());
qInstance.setAuthentication(defineAuthentication());
return (qInstance);
}
/*******************************************************************************
** Define the authentication used in standard tests - using 'mock' type.
**
*******************************************************************************/
public static QAuthenticationMetaData defineAuthentication()
{
return new QAuthenticationMetaData()
.withName("mock")
.withType(QAuthenticationType.MOCK);
}
/*******************************************************************************
**
*******************************************************************************/
public static QBackendMetaData defineBackend()
{
String apiKey = new QMetaDataVariableInterpreter().interpret("${env.EASYPOST_API_KEY}");
return (new APIBackendMetaData()
.withName("easypost")
.withApiKey(apiKey)
.withAuthorizationType(AuthorizationType.BASIC_AUTH_API_KEY)
.withBaseUrl("https://api.easypost.com/v2/")
.withContentType("application/json")
.withActionUtil(new QCodeReference(EasyPostUtils.class, QCodeUsage.CUSTOMIZER))
);
}
/*******************************************************************************
**
*******************************************************************************/
private static QTableMetaData defineTableEasypostTracker()
{
return (new QTableMetaData()
.withName("easypostTracker")
.withBackendName("easypost")
.withField(new QFieldMetaData("id", QFieldType.STRING))
.withField(new QFieldMetaData("trackingNo", QFieldType.STRING).withBackendName("tracking_code"))
.withField(new QFieldMetaData("carrierCode", QFieldType.STRING).withBackendName("carrier"))
.withPrimaryKeyField("id")
.withBackendDetails(new APITableBackendDetails()
.withTablePath("trackers")
.withTableWrapperObjectName("tracker")
)
);
}
}

View File

@ -48,12 +48,12 @@
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
<version>8.0.30</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.210</version>
<version>2.1.214</version>
<scope>test</scope>
</dependency>

View File

@ -65,6 +65,7 @@ NF_ONE_REPO_NAME="Nutrifresh-One"
QQQ_SAMPLE_PROJECT_MODULE_NAME="qqq-sample-project"
QQQ_BACKEND_CORE_MODULE_NAME="qqq-backend-core"
QQQ_BACKEND_MODULE_RDBMS_MODULE_NAME="qqq-backend-module-rdbms"
QQQ_BACKEND_MODULE_API_MODULE_NAME="qqq-backend-module-api"
QQQ_DEV_TOOLS_MODULE_NAME="qqq-dev-tools"
@ -82,6 +83,7 @@ QQQ_REPO_LIST=(
QQQ_MODULE_LIST=(
$QQQ_SAMPLE_PROJECT_MODULE_NAME
$QQQ_BACKEND_MODULE_RDBMS_MODULE_NAME
$QQQ_BACKEND_MODULE_API_MODULE_NAME
$QQQ_BACKEND_CORE_MODULE_NAME
$QQQ_DEV_TOOLS_MODULE_NAME
)