From 471954e8b98be010b84fa54c22c31c845ab87f60 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 12 Oct 2022 17:54:00 -0500 Subject: [PATCH] Initial checkin of API module --- checkstyle/config.xml | 2 - pom.xml | 3 +- qqq-backend-core/pom.xml | 2 +- .../actions/processes/ProcessSummaryLine.java | 41 ++- .../processes/RunBackendStepOutput.java | 15 + .../actions/tables/insert/InsertOutput.java | 16 + .../backend/QBackendModuleDispatcher.java | 3 +- .../StreamedBackendStepOutput.java | 14 + .../backend/core/utils/CollectionUtils.java | 55 +++ qqq-backend-module-api/README.md | 22 ++ qqq-backend-module-api/pom.xml | 86 +++++ .../backend/module/api/APIBackendModule.java | 134 ++++++++ .../module/api/actions/APIInsertAction.java | 108 ++++++ .../module/api/actions/AbstractAPIAction.java | 63 ++++ .../module/api/actions/BaseAPIActionUtil.java | 297 ++++++++++++++++ .../module/api/model/AuthorizationType.java | 33 ++ .../model/metadata/APIBackendMetaData.java | 321 ++++++++++++++++++ .../metadata/APITableBackendDetails.java | 116 +++++++ .../backend/module/api/EasyPostApiTest.java | 137 ++++++++ .../qqq/backend/module/api/EasyPostUtils.java | 36 ++ .../qqq/backend/module/api/TestUtils.java | 113 ++++++ qqq-backend-module-rdbms/pom.xml | 4 +- qqq-dev-tools/bin/setup-environments.sh | 2 + 23 files changed, 1614 insertions(+), 9 deletions(-) create mode 100644 qqq-backend-module-api/README.md create mode 100644 qqq-backend-module-api/pom.xml create mode 100644 qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java create mode 100644 qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIInsertAction.java create mode 100644 qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/AbstractAPIAction.java create mode 100644 qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java create mode 100644 qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java create mode 100644 qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java create mode 100644 qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APITableBackendDetails.java create mode 100644 qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostApiTest.java create mode 100644 qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostUtils.java create mode 100644 qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java diff --git a/checkstyle/config.xml b/checkstyle/config.xml index 2e563475..c6a1604e 100644 --- a/checkstyle/config.xml +++ b/checkstyle/config.xml @@ -179,9 +179,7 @@ - --> - + + + 4.0.0 + + qqq-backend-module-api + + + com.kingsrook.qqq + qqq-parent-project + ${revision} + + + + + + + + + + + com.kingsrook.qqq + qqq-backend-core + ${revision} + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-core + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + + + + + org.jacoco + jacoco-maven-plugin + + + com/kingsrook/qqq/backend/module/api/model/**/*.class + + + + + + + diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java new file mode 100644 index 00000000..d95ac1f7 --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/APIBackendModule.java @@ -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 . + */ + +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 getBackendMetaDataClass() + { + return (null); //return (RDBMSBackendMetaData.class); + } + + + + /******************************************************************************* + ** Method to identify the class used for table-backend details for this module. + *******************************************************************************/ + @Override + public Class 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()); + } + +} diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIInsertAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIInsertAction.java new file mode 100644 index 00000000..43c9bd8a --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIInsertAction.java @@ -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 . + */ + +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); + } + } + +} diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/AbstractAPIAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/AbstractAPIAction.java new file mode 100644 index 00000000..737eb253 --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/AbstractAPIAction.java @@ -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 . + */ + +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); + } + +} + diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java new file mode 100644 index 00000000..ddf3608b --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java @@ -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 . + */ + +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 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; + } +} diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java new file mode 100644 index 00000000..eb357c02 --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/AuthorizationType.java @@ -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 . + */ + +package com.kingsrook.qqq.backend.module.api.model; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum AuthorizationType +{ + BASIC_AUTH_API_KEY, + BASIC_AUTH_USERNAME_PASSWORD + +} diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java new file mode 100644 index 00000000..70048f77 --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APIBackendMetaData.java @@ -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 . + */ + +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); + } + +} diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APITableBackendDetails.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APITableBackendDetails.java new file mode 100644 index 00000000..510fe540 --- /dev/null +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/metadata/APITableBackendDetails.java @@ -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 . + */ + +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); + } + +} diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostApiTest.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostApiTest.java new file mode 100644 index 00000000..48668dec --- /dev/null +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostApiTest.java @@ -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 . + */ + +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(); + } + +} diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostUtils.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostUtils.java new file mode 100644 index 00000000..f8309bc9 --- /dev/null +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/EasyPostUtils.java @@ -0,0 +1,36 @@ +/* + * Copyright © 2022-2022. Nutrifresh Services . 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); + } +} diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java new file mode 100644 index 00000000..ba76bae2 --- /dev/null +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java @@ -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 . + */ + +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") + ) + ); + } +} diff --git a/qqq-backend-module-rdbms/pom.xml b/qqq-backend-module-rdbms/pom.xml index 817a1e1d..ce8b919b 100644 --- a/qqq-backend-module-rdbms/pom.xml +++ b/qqq-backend-module-rdbms/pom.xml @@ -48,12 +48,12 @@ mysql mysql-connector-java - 8.0.28 + 8.0.30 com.h2database h2 - 2.1.210 + 2.1.214 test diff --git a/qqq-dev-tools/bin/setup-environments.sh b/qqq-dev-tools/bin/setup-environments.sh index e6204cee..b322e442 100755 --- a/qqq-dev-tools/bin/setup-environments.sh +++ b/qqq-dev-tools/bin/setup-environments.sh @@ -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 )