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 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());
+ }
+
+}
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
)