Merged dev into feature/bulk-upload-v2

This commit is contained in:
2024-11-25 16:49:15 -06:00
265 changed files with 26709 additions and 632 deletions

View File

@ -63,12 +63,10 @@ import com.kingsrook.qqq.backend.core.actions.values.SearchPossibleValueSourceAc
import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.logging.QLogger;
@ -106,7 +104,6 @@ import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSo
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper;
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.fields.AdornmentType;
@ -123,6 +120,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.QStatusMessage;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule;
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
@ -178,7 +176,8 @@ public class QJavalinImplementation
private static int DEFAULT_PORT = 8001;
private static Javalin service;
private static Javalin service;
private static List<EndpointGroup> endpointGroups;
private static long startTime = 0;
@ -241,8 +240,18 @@ public class QJavalinImplementation
{
// todo port from arg
// todo base path from arg? - and then potentially multiple instances too (chosen based on the root path??)
service = Javalin.create().start(port);
service.routes(getRoutes());
service = Javalin.create(config ->
{
config.router.apiBuilder(getRoutes());
for(EndpointGroup endpointGroup : CollectionUtils.nonNullList(endpointGroups))
{
config.router.apiBuilder(endpointGroup);
}
}
).start(port);
service.before(QJavalinImplementation::hotSwapQInstance);
service.before((Context context) -> context.header("Content-Type", "application/json"));
service.after(QJavalinImplementation::clearQContext);
@ -292,7 +301,7 @@ public class QJavalinImplementation
////////////////////////////////////////////////////////////////////////////////
// clear the cache of classes in this class, so that new classes can be found //
////////////////////////////////////////////////////////////////////////////////
MetaDataProducerHelper.clearTopLevelClassCache();
ClassPathUtils.clearTopLevelClassCache();
/////////////////////////////////////////////////
// try to get a new instance from the supplier //
@ -1014,8 +1023,7 @@ public class QJavalinImplementation
QRecord record = getOutput.getRecord();
if(record == null)
{
throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
+ table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
}
QValueFormatter.setBlobValuesToDownloadUrls(table, List.of(record));
@ -1078,8 +1086,7 @@ public class QJavalinImplementation
///////////////////////////////////////////////////////
if(getOutput.getRecord() == null)
{
throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
+ table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
}
String mimeType = null;
@ -1727,7 +1734,7 @@ public class QJavalinImplementation
}
catch(QUserFacingException e)
{
handleException(HttpStatus.Code.BAD_REQUEST, context, e);
QJavalinUtils.handleException(HttpStatus.Code.BAD_REQUEST, context, e);
return null;
}
return reportFormat;
@ -1816,7 +1823,7 @@ public class QJavalinImplementation
if(CollectionUtils.nullSafeHasContents(valuesParamList))
{
String valuesParam = valuesParamList.get(0);
values = JsonUtils.toObject(valuesParam, new TypeReference<>(){});
values = JsonUtils.toObject(valuesParam, new TypeReference<>() {});
}
}
@ -1867,67 +1874,7 @@ public class QJavalinImplementation
*******************************************************************************/
public static void handleException(Context context, Exception e)
{
handleException(null, context, e);
}
/*******************************************************************************
**
*******************************************************************************/
public static void handleException(HttpStatus.Code statusCode, Context context, Exception e)
{
QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class);
if(userFacingException != null)
{
if(userFacingException instanceof QNotFoundException)
{
statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.NOT_FOUND); // 404
respondWithError(context, statusCode, userFacingException.getMessage());
}
else if(userFacingException instanceof QBadRequestException)
{
statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.BAD_REQUEST); // 400
respondWithError(context, statusCode, userFacingException.getMessage());
}
else
{
LOG.info("User-facing exception", e);
statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR); // 500
respondWithError(context, statusCode, userFacingException.getMessage());
}
}
else
{
if(e instanceof QAuthenticationException)
{
respondWithError(context, HttpStatus.Code.UNAUTHORIZED, e.getMessage()); // 401
return;
}
if(e instanceof QPermissionDeniedException)
{
respondWithError(context, HttpStatus.Code.FORBIDDEN, e.getMessage()); // 403
return;
}
////////////////////////////////
// default exception handling //
////////////////////////////////
LOG.warn("Exception in javalin request", e);
respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, e.getClass().getSimpleName() + " (" + e.getMessage() + ")"); // 500
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void respondWithError(Context context, HttpStatus.Code statusCode, String errorMessage)
{
context.status(statusCode.getCode());
context.result(JsonUtils.toJson(Map.of("error", errorMessage)));
QJavalinUtils.handleException(null, context, e);
}
@ -1946,7 +1893,7 @@ public class QJavalinImplementation
** Getter for javalinMetaData
**
*******************************************************************************/
public QJavalinMetaData getJavalinMetaData()
public static QJavalinMetaData getJavalinMetaData()
{
return javalinMetaData;
}
@ -1975,7 +1922,7 @@ public class QJavalinImplementation
/*******************************************************************************
** Getter for qInstanceHotSwapSupplier
** Getter for qInstance
*******************************************************************************/
public static QInstance getQInstance()
{
@ -1984,6 +1931,16 @@ public class QJavalinImplementation
/*******************************************************************************
** Setter for qInstance
*******************************************************************************/
public static void setQInstance(QInstance qInstance)
{
QJavalinImplementation.qInstance = qInstance;
}
/*******************************************************************************
**
*******************************************************************************/
@ -2011,4 +1968,30 @@ public class QJavalinImplementation
{
return (startTime);
}
/***************************************************************************
**
***************************************************************************/
public void addJavalinRoutes(EndpointGroup routes)
{
if(endpointGroups == null)
{
endpointGroups = new ArrayList<>();
}
endpointGroups.add(routes);
}
/***************************************************************************
** if restarting this class, and you want to re-run addJavalinRoutes, but
** not create duplicates, well, you might want to call this method!
***************************************************************************/
public void clearJavalinRoutes()
{
endpointGroups = null;
}
}

View File

@ -41,6 +41,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
@ -231,13 +232,13 @@ public class QJavalinProcessHandler
if(inputField.getIsRequired() && !setValue)
{
QJavalinImplementation.respondWithError(context, HttpStatus.Code.BAD_REQUEST, "Missing query param value for required input field: [" + inputField.getName() + "]");
QJavalinUtils.respondWithError(context, HttpStatus.Code.BAD_REQUEST, "Missing query param value for required input field: [" + inputField.getName() + "]");
return;
}
}
catch(Exception e)
{
QJavalinImplementation.respondWithError(context, HttpStatus.Code.BAD_REQUEST, "Error processing query param [" + inputField.getName() + "]: " + e.getClass().getSimpleName() + " (" + e.getMessage() + ")");
QJavalinUtils.respondWithError(context, HttpStatus.Code.BAD_REQUEST, "Error processing query param [" + inputField.getName() + "]: " + e.getClass().getSimpleName() + " (" + e.getMessage() + ")");
return;
}
}
@ -428,7 +429,18 @@ public class QJavalinProcessHandler
QJavalinAccessLogger.logEndSuccess();
}
context.result(JsonUtils.toJson(resultForCaller));
///////////////////////////////////////////////////////////////////////////////////
// Note: originally we did not have this serializationInclusion:ALWAYS here - //
// which meant that null and empty values from backend would not go to frontend, //
// which made things like clearing out a value in a field not happen. //
// So, this is added to get that beneficial effect. Unclear if there are any //
// negative side-effects - but be aware. //
// One could imagine that we'd need this to be configurable in the future? //
///////////////////////////////////////////////////////////////////////////////////
context.result(JsonUtils.toJson(resultForCaller, mapper ->
{
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
}));
}
@ -449,11 +461,19 @@ public class QJavalinProcessHandler
resultForCaller.put("values", runProcessOutput.getValues());
runProcessOutput.getProcessState().getNextStepName().ifPresent(nextStep -> resultForCaller.put("nextStep", nextStep));
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - delete after all frontends look for processMetaDataAdjustment instead of updatedFrontendStepList //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<QFrontendStepMetaData> updatedFrontendStepList = runProcessOutput.getUpdatedFrontendStepList();
if(updatedFrontendStepList != null)
{
resultForCaller.put("updatedFrontendStepList", updatedFrontendStepList);
}
if(runProcessOutput.getProcessMetaDataAdjustment() != null)
{
resultForCaller.put("processMetaDataAdjustment", runProcessOutput.getProcessMetaDataAdjustment());
}
}

View File

@ -22,11 +22,21 @@
package com.kingsrook.qqq.backend.javalin;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import io.javalin.http.Context;
import org.eclipse.jetty.http.HttpStatus;
/*******************************************************************************
@ -34,6 +44,10 @@ import io.javalin.http.Context;
*******************************************************************************/
public class QJavalinUtils
{
private static final QLogger LOG = QLogger.getLogger(QJavalinUtils.class);
/*******************************************************************************
** Returns Integer if context has a valid int query parameter by the given name,
@ -178,4 +192,64 @@ public class QJavalinUtils
return value;
}
/*******************************************************************************
**
*******************************************************************************/
public static void handleException(HttpStatus.Code statusCode, Context context, Exception e)
{
QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class);
if(userFacingException != null)
{
if(userFacingException instanceof QNotFoundException)
{
statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.NOT_FOUND); // 404
respondWithError(context, statusCode, userFacingException.getMessage());
}
else if(userFacingException instanceof QBadRequestException)
{
statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.BAD_REQUEST); // 400
respondWithError(context, statusCode, userFacingException.getMessage());
}
else
{
LOG.info("User-facing exception", e);
statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR); // 500
respondWithError(context, statusCode, userFacingException.getMessage());
}
}
else
{
if(e instanceof QAuthenticationException)
{
respondWithError(context, HttpStatus.Code.UNAUTHORIZED, e.getMessage()); // 401
return;
}
if(e instanceof QPermissionDeniedException)
{
respondWithError(context, HttpStatus.Code.FORBIDDEN, e.getMessage()); // 403
return;
}
////////////////////////////////
// default exception handling //
////////////////////////////////
LOG.warn("Exception in javalin request", e);
respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, e.getClass().getSimpleName() + " (" + e.getMessage() + ")"); // 500
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void respondWithError(Context context, HttpStatus.Code statusCode, String errorMessage)
{
context.status(statusCode.getCode());
context.result(JsonUtils.toJson(Map.of("error", errorMessage)));
}
}

View File

@ -0,0 +1,529 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin;
import java.util.List;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion;
import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
import io.javalin.Javalin;
import io.javalin.http.Context;
import org.apache.commons.lang.BooleanUtils;
/*******************************************************************************
** Second-generation qqq javalin server.
**
** An evolution over the original QJavalinImplementation, which both managed
** the javalin instance itself, but also provided all of the endpoint handlers...
** This class instead just configures & starts the server.
**
** Makes several setters available, to let application-developer choose what
** standard qqq endpoints are served (e.g., frontend-material-dashboard, the
** legacy-unversioned middleware, newer versioned-middleware, and additional qqq
** modules or application-defined services (both provided as instances of
** QJavalinRouteProviderInterface).
**
** System property `qqq.javalin.hotSwapInstance` (defaults to false), causes the
** QInstance to be re-loaded every X millis, to avoid some server restarts while
** doing dev.
*******************************************************************************/
public class QApplicationJavalinServer
{
private static final QLogger LOG = QLogger.getLogger(QApplicationJavalinServer.class);
private final AbstractQQQApplication application;
private Integer port = 8000;
private boolean serveFrontendMaterialDashboard = true;
private boolean serveLegacyUnversionedMiddlewareAPI = true;
private List<AbstractMiddlewareVersion> middlewareVersionList = List.of(new MiddlewareVersionV1());
private List<QJavalinRouteProviderInterface> additionalRouteProviders = null;
private Consumer<Javalin> javalinConfigurationCustomizer = null;
private long lastQInstanceHotSwapMillis;
private long millisBetweenHotSwaps = 2500;
private Consumer<QInstance> hotSwapCustomizer = null;
private Javalin service;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public QApplicationJavalinServer(AbstractQQQApplication application)
{
this.application = application;
}
/***************************************************************************
**
***************************************************************************/
public void start() throws QException
{
QInstance qInstance = application.defineValidatedQInstance();
service = Javalin.create(config ->
{
if(serveFrontendMaterialDashboard)
{
////////////////////////////////////////////////////////////////////////////////////////
// If you have any assets to add to the web server (e.g., logos, icons) place them at //
// src/main/resources/material-dashboard-overlay (or a directory of your choice //
// under src/main/resources) and use this line of code to tell javalin about it. //
// Make sure to add your app-specific directory to the javalin config before the core //
// material-dashboard directory, so in case the same file exists in both (e.g., //
// favicon.png), the app-specific one will be used. //
////////////////////////////////////////////////////////////////////////////////////////
config.staticFiles.add("/material-dashboard-overlay");
/////////////////////////////////////////////////////////////////////
// tell javalin where to find material-dashboard static web assets //
/////////////////////////////////////////////////////////////////////
config.staticFiles.add("/material-dashboard");
////////////////////////////////////////////////////////////
// set the index page for the SPA from material dashboard //
////////////////////////////////////////////////////////////
config.spaRoot.addFile("/", "material-dashboard/index.html");
}
///////////////////////////////////////////
// add qqq routes to the javalin service //
///////////////////////////////////////////
if(serveLegacyUnversionedMiddlewareAPI)
{
try
{
QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(qInstance);
config.router.apiBuilder(qJavalinImplementation.getRoutes());
}
catch(QInstanceValidationException e)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// we should be pretty comfortable that this won't happen, because we've pre-validated the instance above... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
throw new RuntimeException(e);
}
}
/////////////////////////////////////
// versioned qqq middleware routes //
/////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(middlewareVersionList))
{
config.router.apiBuilder(new QMiddlewareApiSpecHandler(middlewareVersionList).defineJavalinEndpointGroup());
for(AbstractMiddlewareVersion version : middlewareVersionList)
{
version.setQInstance(qInstance);
config.router.apiBuilder(version.getJavalinEndpointGroup(qInstance));
}
}
////////////////////////////////////////////////////////////////////////////
// additional route providers (e.g., application-apis, other middlewares) //
////////////////////////////////////////////////////////////////////////////
for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders))
{
routeProvider.setQInstance(qInstance);
config.router.apiBuilder(routeProvider.getJavalinEndpointGroup());
}
});
//////////////////////////////////////////////////////////////////////////////////////
// per system property, set the server to hot-swap the q instance before all routes //
//////////////////////////////////////////////////////////////////////////////////////
String hotSwapPropertyValue = System.getProperty("qqq.javalin.hotSwapInstance", "false");
if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(hotSwapPropertyValue)))
{
LOG.info("Server will hotSwap QInstance before requests every [" + millisBetweenHotSwaps + "] millis.");
service.before(context -> hotSwapQInstance());
}
service.before((Context context) -> context.header("Content-Type", "application/json"));
service.after(QJavalinImplementation::clearQContext);
////////////////////////////////////////////////
// allow a configuration-customizer to be run //
////////////////////////////////////////////////
if(javalinConfigurationCustomizer != null)
{
javalinConfigurationCustomizer.accept(service);
}
service.start(port);
}
/***************************************************************************
**
***************************************************************************/
public void stop()
{
if(this.service == null)
{
LOG.info("Stop called, but there is no javalin service, so noop.");
return;
}
this.service.stop();
}
/*******************************************************************************
** If there's a qInstanceHotSwapSupplier, and its been a little while, replace
** the qInstance with a new one from the supplier. Meant to be used while doing
** development.
*******************************************************************************/
public void hotSwapQInstance()
{
long now = System.currentTimeMillis();
if(now - lastQInstanceHotSwapMillis < millisBetweenHotSwaps)
{
return;
}
lastQInstanceHotSwapMillis = now;
try
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// clear the cache of classes in this class, so that new classes can be found if a meta-data-producer is being used //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ClassPathUtils.clearTopLevelClassCache();
///////////////////////////////////////////////////////////////
// try to get a new, validated instance from the application //
///////////////////////////////////////////////////////////////
QInstance newQInstance = application.defineValidatedQInstance();
if(newQInstance == null)
{
LOG.warn("Got a null qInstance from the application.defineQInstance(). Not hot-swapping.");
return;
}
////////////////////////////////////////
// allow a hot-swap customizer to run //
////////////////////////////////////////
if(hotSwapCustomizer != null)
{
hotSwapCustomizer.accept(newQInstance);
}
///////////////////////////////////////////////////////////////////////
// pass the new qInstance into all of the objects serving qqq routes //
///////////////////////////////////////////////////////////////////////
if(serveLegacyUnversionedMiddlewareAPI)
{
QJavalinImplementation.setQInstance(newQInstance);
}
if(CollectionUtils.nullSafeHasContents(middlewareVersionList))
{
for(AbstractMiddlewareVersion spec : CollectionUtils.nonNullList(middlewareVersionList))
{
spec.setQInstance(newQInstance);
}
}
for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders))
{
routeProvider.setQInstance(newQInstance);
}
LOG.info("Swapped qInstance");
}
catch(QInstanceValidationException e)
{
LOG.error("Validation Error while hot-swapping QInstance", e);
}
catch(Exception e)
{
LOG.error("Error hot-swapping QInstance", e);
}
}
/*******************************************************************************
** Getter for port
*******************************************************************************/
public Integer getPort()
{
return (this.port);
}
/*******************************************************************************
** Setter for port
*******************************************************************************/
public void setPort(Integer port)
{
this.port = port;
}
/*******************************************************************************
** Fluent setter for port
*******************************************************************************/
public QApplicationJavalinServer withPort(Integer port)
{
this.port = port;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void setMillisBetweenHotSwaps(long millisBetweenHotSwaps)
{
this.millisBetweenHotSwaps = millisBetweenHotSwaps;
}
/*******************************************************************************
** Getter for serveFrontendMaterialDashboard
*******************************************************************************/
public boolean getServeFrontendMaterialDashboard()
{
return (this.serveFrontendMaterialDashboard);
}
/*******************************************************************************
** Setter for serveFrontendMaterialDashboard
*******************************************************************************/
public void setServeFrontendMaterialDashboard(boolean serveFrontendMaterialDashboard)
{
this.serveFrontendMaterialDashboard = serveFrontendMaterialDashboard;
}
/*******************************************************************************
** Fluent setter for serveFrontendMaterialDashboard
*******************************************************************************/
public QApplicationJavalinServer withServeFrontendMaterialDashboard(boolean serveFrontendMaterialDashboard)
{
this.serveFrontendMaterialDashboard = serveFrontendMaterialDashboard;
return (this);
}
/*******************************************************************************
** Getter for serveLegacyUnversionedMiddlewareAPI
*******************************************************************************/
public boolean getServeLegacyUnversionedMiddlewareAPI()
{
return (this.serveLegacyUnversionedMiddlewareAPI);
}
/*******************************************************************************
** Setter for serveLegacyUnversionedMiddlewareAPI
*******************************************************************************/
public void setServeLegacyUnversionedMiddlewareAPI(boolean serveLegacyUnversionedMiddlewareAPI)
{
this.serveLegacyUnversionedMiddlewareAPI = serveLegacyUnversionedMiddlewareAPI;
}
/*******************************************************************************
** Fluent setter for serveLegacyUnversionedMiddlewareAPI
*******************************************************************************/
public QApplicationJavalinServer withServeLegacyUnversionedMiddlewareAPI(boolean serveLegacyUnversionedMiddlewareAPI)
{
this.serveLegacyUnversionedMiddlewareAPI = serveLegacyUnversionedMiddlewareAPI;
return (this);
}
/*******************************************************************************
** Getter for middlewareVersionList
*******************************************************************************/
public List<AbstractMiddlewareVersion> getMiddlewareVersionList()
{
return (this.middlewareVersionList);
}
/*******************************************************************************
** Setter for middlewareVersionList
*******************************************************************************/
public void setMiddlewareVersionList(List<AbstractMiddlewareVersion> middlewareVersionList)
{
this.middlewareVersionList = middlewareVersionList;
}
/*******************************************************************************
** Fluent setter for middlewareVersionList
*******************************************************************************/
public QApplicationJavalinServer withMiddlewareVersionList(List<AbstractMiddlewareVersion> middlewareVersionList)
{
this.middlewareVersionList = middlewareVersionList;
return (this);
}
/*******************************************************************************
** Getter for additionalRouteProviders
*******************************************************************************/
public List<QJavalinRouteProviderInterface> getAdditionalRouteProviders()
{
return (this.additionalRouteProviders);
}
/*******************************************************************************
** Setter for additionalRouteProviders
*******************************************************************************/
public void setAdditionalRouteProviders(List<QJavalinRouteProviderInterface> additionalRouteProviders)
{
this.additionalRouteProviders = additionalRouteProviders;
}
/*******************************************************************************
** Fluent setter for additionalRouteProviders
*******************************************************************************/
public QApplicationJavalinServer withAdditionalRouteProviders(List<QJavalinRouteProviderInterface> additionalRouteProviders)
{
this.additionalRouteProviders = additionalRouteProviders;
return (this);
}
/*******************************************************************************
** Getter for MILLIS_BETWEEN_HOT_SWAPS
*******************************************************************************/
public long getMillisBetweenHotSwaps()
{
return (millisBetweenHotSwaps);
}
/*******************************************************************************
** Fluent setter for MILLIS_BETWEEN_HOT_SWAPS
*******************************************************************************/
public QApplicationJavalinServer withMillisBetweenHotSwaps(long millisBetweenHotSwaps)
{
this.millisBetweenHotSwaps = millisBetweenHotSwaps;
return (this);
}
/*******************************************************************************
** Getter for hotSwapCustomizer
*******************************************************************************/
public Consumer<QInstance> getHotSwapCustomizer()
{
return (this.hotSwapCustomizer);
}
/*******************************************************************************
** Setter for hotSwapCustomizer
*******************************************************************************/
public void setHotSwapCustomizer(Consumer<QInstance> hotSwapCustomizer)
{
this.hotSwapCustomizer = hotSwapCustomizer;
}
/*******************************************************************************
** Fluent setter for hotSwapCustomizer
*******************************************************************************/
public QApplicationJavalinServer withHotSwapCustomizer(Consumer<QInstance> hotSwapCustomizer)
{
this.hotSwapCustomizer = hotSwapCustomizer;
return (this);
}
/*******************************************************************************
** Getter for javalinConfigurationCustomizer
*******************************************************************************/
public Consumer<Javalin> getJavalinConfigurationCustomizer()
{
return (this.javalinConfigurationCustomizer);
}
/*******************************************************************************
** Setter for javalinConfigurationCustomizer
*******************************************************************************/
public void setJavalinConfigurationCustomizer(Consumer<Javalin> javalinConfigurationCustomizer)
{
this.javalinConfigurationCustomizer = javalinConfigurationCustomizer;
}
/*******************************************************************************
** Fluent setter for javalinConfigurationCustomizer
*******************************************************************************/
public QApplicationJavalinServer withJavalinConfigurationCustomizer(Consumer<Javalin> javalinConfigurationCustomizer)
{
this.javalinConfigurationCustomizer = javalinConfigurationCustomizer;
return (this);
}
}

View File

@ -0,0 +1,47 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import io.javalin.apibuilder.EndpointGroup;
/*******************************************************************************
** Interface for classes that can provide a list of endpoints to a javalin
** server.
*******************************************************************************/
public interface QJavalinRouteProviderInterface
{
/***************************************************************************
** For initial setup when server boots, set the qInstance - but also,
** e.g., for development, to do a hot-swap.
***************************************************************************/
void setQInstance(QInstance qInstance);
/***************************************************************************
**
***************************************************************************/
EndpointGroup getJavalinEndpointGroup();
}

View File

@ -0,0 +1,269 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
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.core.utils.YamlUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion;
import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
import com.kingsrook.qqq.openapi.model.OpenAPI;
import io.javalin.apibuilder.ApiBuilder;
import io.javalin.apibuilder.EndpointGroup;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import org.apache.commons.io.IOUtils;
/*******************************************************************************
** javalin-handler that serves both rapidoc static html/css/js files, and
** dynamically generated openapi json/yaml, for a given list of qqq middleware
** versions
*******************************************************************************/
public class QMiddlewareApiSpecHandler
{
private final List<AbstractMiddlewareVersion> middlewareVersionList;
private final String basePath;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public QMiddlewareApiSpecHandler(List<AbstractMiddlewareVersion> middlewareVersionList)
{
this(middlewareVersionList, "qqq");
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public QMiddlewareApiSpecHandler(List<AbstractMiddlewareVersion> middlewareVersionList, String basePath)
{
this.middlewareVersionList = middlewareVersionList;
this.basePath = basePath.replaceFirst("^/+", "").replaceFirst("/+$", "");;
}
/***************************************************************************
**
***************************************************************************/
public EndpointGroup defineJavalinEndpointGroup()
{
return (() ->
{
ApiBuilder.get("/api/docs/js/rapidoc.min.js", (context) -> serveResource(context, "rapidoc/rapidoc-9.3.8.min.js", MapBuilder.of("Content-Type", ContentType.JAVASCRIPT)));
ApiBuilder.get("/api/docs/css/qqq-api-styles.css", (context) -> serveResource(context, "rapidoc/rapidoc-overrides.css", MapBuilder.of("Content-Type", ContentType.CSS)));
ApiBuilder.get("/images/qqq-api-logo.png", (context) -> serveResource(context, "images/qqq-on-crown-trans-160x80.png", MapBuilder.of("Content-Type", ContentType.IMAGE_PNG.getMimeType())));
//////////////////////////////////////////////
// default page is the current version spec //
//////////////////////////////////////////////
ApiBuilder.get("/" + basePath + "/", context -> doSpecHtml(context));
ApiBuilder.get("/" + basePath + "/versions.json", context -> doVersions(context));
////////////////////////////////////////////
// default page for a version is its spec //
////////////////////////////////////////////
for(AbstractMiddlewareVersion middlewareSpec : middlewareVersionList)
{
String version = middlewareSpec.getVersion();
String versionPath = "/" + basePath + "/" + version;
ApiBuilder.get(versionPath + "/", context -> doSpecHtml(context, version));
///////////////////////////////////////////
// add known paths for specs & docs page //
///////////////////////////////////////////
ApiBuilder.get(versionPath + "/openapi.yaml", context -> doSpecYaml(context, version));
ApiBuilder.get(versionPath + "/openapi.json", context -> doSpecJson(context, version));
ApiBuilder.get(versionPath + "/openapi.html", context -> doSpecHtml(context, version));
}
});
}
/*******************************************************************************
** list the versions in this api
*******************************************************************************/
private void doVersions(Context context)
{
Map<String, Object> rs = new HashMap<>();
List<String> supportedVersions = middlewareVersionList.stream().map(msi -> msi.getVersion()).toList();
String currentVersion = supportedVersions.get(supportedVersions.size() - 1);
rs.put("supportedVersions", supportedVersions);
rs.put("currentVersion", currentVersion);
context.contentType(ContentType.APPLICATION_JSON);
context.result(JsonUtils.toJson(rs));
}
/*******************************************************************************
**
*******************************************************************************/
private void serveResource(Context context, String resourcePath, Map<String, String> headers)
{
InputStream resourceAsStream = QJavalinImplementation.class.getClassLoader().getResourceAsStream(resourcePath);
for(Map.Entry<String, String> entry : CollectionUtils.nonNullMap(headers).entrySet())
{
context.header(entry.getKey(), entry.getValue());
}
context.result(resourceAsStream);
}
/*******************************************************************************
**
*******************************************************************************/
private void doSpecYaml(Context context, String version)
{
try
{
OpenAPI openAPI = new MiddlewareVersionV1().generateOpenAPIModel(basePath);
context.contentType(ContentType.APPLICATION_YAML);
context.result(YamlUtils.toYaml(openAPI));
}
catch(Exception e)
{
QJavalinImplementation.handleException(context, e);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void doSpecJson(Context context, String version)
{
try
{
OpenAPI openAPI = new MiddlewareVersionV1().generateOpenAPIModel(basePath);
context.contentType(ContentType.APPLICATION_JSON);
context.result(JsonUtils.toJson(openAPI));
}
catch(Exception e)
{
QJavalinImplementation.handleException(context, e);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void doSpecHtml(Context context)
{
String version = null;
try
{
version = context.pathParam("version");
}
catch(Exception e)
{
////////////////
// leave null //
////////////////
}
if(!StringUtils.hasContent(version))
{
List<String> supportedVersions = middlewareVersionList.stream().map(msi -> msi.getVersion()).toList();
version = supportedVersions.get(supportedVersions.size() - 1);
}
doSpecHtml(context, version);
}
/*******************************************************************************
**
*******************************************************************************/
private void doSpecHtml(Context context, String version)
{
try
{
//////////////////////////////////
// read html from resource file //
//////////////////////////////////
InputStream resourceAsStream = QMiddlewareApiSpecHandler.class.getClassLoader().getResourceAsStream("rapidoc/rapidoc-container.html");
String html = IOUtils.toString(resourceAsStream, StandardCharsets.UTF_8);
/////////////////////////////////
// do replacements in the html //
/////////////////////////////////
html = html.replace("{spec-url}", "/" + basePath + "/" + version + "/openapi.json");
html = html.replace("{version}", version);
html = html.replace("{primaryColor}", "#444444");
html = html.replace("{navLogoImg}", "<img id=\"navLogo\" slot=\"nav-logo\" src=\"/images/qqq-api-logo.png\" />");
Optional<AbstractMiddlewareVersion> middlewareSpec = middlewareVersionList.stream().filter(msi -> msi.getVersion().equals(version)).findFirst();
if(middlewareSpec.isEmpty())
{
throw (new QUserFacingException("Unrecognized version: " + version));
}
OpenAPI openAPI = middlewareSpec.get().generateOpenAPIModel(basePath);
html = html.replace("{title}", openAPI.getInfo().getTitle() + " - " + version);
StringBuilder otherVersionOptions = new StringBuilder();
for(AbstractMiddlewareVersion otherVersionSpec : middlewareVersionList)
{
otherVersionOptions.append("<option value=\"/").append(basePath).append("/").append(otherVersionSpec.getVersion()).append("/openapi.html\">").append(otherVersionSpec.getVersion()).append("</option>");
}
html = html.replace("{otherVersionOptions}", otherVersionOptions.toString());
context.contentType(ContentType.HTML);
context.result(html);
}
catch(Exception e)
{
QJavalinImplementation.handleException(context, e);
}
}
}

View File

@ -0,0 +1,41 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.middleware.javalin.executors.io.AbstractMiddlewareInput;
import com.kingsrook.qqq.middleware.javalin.executors.io.AbstractMiddlewareOutputInterface;
/*******************************************************************************
**
*******************************************************************************/
public abstract class AbstractMiddlewareExecutor<INPUT extends AbstractMiddlewareInput, OUTPUT extends AbstractMiddlewareOutputInterface>
{
/***************************************************************************
**
***************************************************************************/
public abstract void execute(INPUT input, OUTPUT output) throws QException;
}

View File

@ -0,0 +1,46 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.middleware.javalin.executors.io.AuthenticationMetaDataInput;
import com.kingsrook.qqq.middleware.javalin.executors.io.AuthenticationMetaDataOutputInterface;
/*******************************************************************************
**
*******************************************************************************/
public class AuthenticationMetaDataExecutor extends AbstractMiddlewareExecutor<AuthenticationMetaDataInput, AuthenticationMetaDataOutputInterface>
{
/***************************************************************************
**
***************************************************************************/
@Override
public void execute(AuthenticationMetaDataInput input, AuthenticationMetaDataOutputInterface output) throws QException
{
output.setAuthenticationMetaData(QContext.getQInstance().getAuthentication());
}
}

View File

@ -0,0 +1,224 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors;
import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import io.javalin.http.Context;
/*******************************************************************************
**
*******************************************************************************/
public class ExecutorSessionUtils
{
private static final QLogger LOG = QLogger.getLogger(ExecutorSessionUtils.class);
public static final int SESSION_COOKIE_AGE = 60 * 60 * 24;
public static final String SESSION_ID_COOKIE_NAME = "sessionId";
public static final String API_KEY_NAME = "apiKey";
/*******************************************************************************
**
*******************************************************************************/
public static QSession setupSession(Context context, QInstance qInstance) throws QModuleDispatchException, QAuthenticationException
{
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication());
try
{
Map<String, String> authenticationContext = new HashMap<>();
String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME);
String sessionUuidCookieValue = context.cookie(Auth0AuthenticationModule.SESSION_UUID_KEY);
String authorizationHeaderValue = context.header("Authorization");
String apiKeyHeaderValue = context.header("x-api-key");
if(StringUtils.hasContent(sessionIdCookieValue))
{
///////////////////////////////////////////////////////
// sessionId - maybe used by table-based auth module //
///////////////////////////////////////////////////////
authenticationContext.put(SESSION_ID_COOKIE_NAME, sessionIdCookieValue);
}
else if(StringUtils.hasContent(sessionUuidCookieValue))
{
///////////////////////////////////////////////////////////////////////////
// session UUID - known to be used by auth0 module (in aug. 2023 update) //
///////////////////////////////////////////////////////////////////////////
authenticationContext.put(Auth0AuthenticationModule.SESSION_UUID_KEY, sessionUuidCookieValue);
}
else if(apiKeyHeaderValue != null)
{
/////////////////////////////////////////////////////////////////
// next, look for an api key header: //
// this will be used to look up auth0 values via an auth table //
/////////////////////////////////////////////////////////////////
authenticationContext.put(API_KEY_NAME, apiKeyHeaderValue);
}
else if(authorizationHeaderValue != null)
{
/////////////////////////////////////////////////////////////////////////////////////////////////
// second, look for the authorization header: //
// either with a "Basic " prefix (for a username:password pair) //
// or with a "Bearer " prefix (for a token that can be handled the same as a sessionId cookie) //
/////////////////////////////////////////////////////////////////////////////////////////////////
processAuthorizationValue(authenticationContext, authorizationHeaderValue);
}
else
{
try
{
String authorizationFormValue = context.formParam("Authorization");
if(StringUtils.hasContent(authorizationFormValue))
{
processAuthorizationValue(authenticationContext, authorizationFormValue);
}
}
catch(Exception e)
{
LOG.info("Exception looking for Authorization formParam", e);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// put the qInstance into context - but no session yet (since, the whole point of this call is to setup the session!) //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QContext.init(qInstance, null);
QSession session = authenticationModule.createSession(qInstance, authenticationContext);
QContext.init(qInstance, session, null, null);
///////////////////////////////////////////////////////////////////////////////
// todo - this must be moved ... not exactly sure where, but into some spec. //
///////////////////////////////////////////////////////////////////////////////
// String tableVariant = QJavalinUtils.getFormParamOrQueryParam(context, "tableVariant");
// if(StringUtils.hasContent(tableVariant))
// {
// JSONObject variant = new JSONObject(tableVariant);
// QContext.getQSession().setBackendVariants(MapBuilder.of(variant.getString("type"), variant.getInt("id")));
// }
/////////////////////////////////////////////////////////////////////////////////
// if we got a session id cookie in, then send it back with updated cookie age //
/////////////////////////////////////////////////////////////////////////////////
if(authenticationModule.usesSessionIdCookie())
{
context.cookie(SESSION_ID_COOKIE_NAME, session.getIdReference(), SESSION_COOKIE_AGE);
}
setUserTimezoneOffsetMinutesInSession(context, session);
setUserTimezoneInSession(context, session);
return (session);
}
catch(QAuthenticationException qae)
{
////////////////////////////////////////////////////////////////////////////////
// if exception caught, clear out the cookie so the frontend will reauthorize //
////////////////////////////////////////////////////////////////////////////////
if(authenticationModule.usesSessionIdCookie())
{
context.removeCookie(SESSION_ID_COOKIE_NAME);
}
throw (qae);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void processAuthorizationValue(Map<String, String> authenticationContext, String authorizationHeaderValue)
{
String basicPrefix = "Basic ";
String bearerPrefix = "Bearer ";
if(authorizationHeaderValue.startsWith(basicPrefix))
{
authorizationHeaderValue = authorizationHeaderValue.replaceFirst(basicPrefix, "");
authenticationContext.put(Auth0AuthenticationModule.BASIC_AUTH_KEY, authorizationHeaderValue);
}
else if(authorizationHeaderValue.startsWith(bearerPrefix))
{
authorizationHeaderValue = authorizationHeaderValue.replaceFirst(bearerPrefix, "");
authenticationContext.put(Auth0AuthenticationModule.ACCESS_TOKEN_KEY, authorizationHeaderValue);
}
else
{
LOG.debug("Authorization value did not have Basic or Bearer prefix. [" + authorizationHeaderValue + "]");
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void setUserTimezoneOffsetMinutesInSession(Context context, QSession session)
{
String userTimezoneOffsetMinutes = context.header("X-QQQ-UserTimezoneOffsetMinutes");
if(StringUtils.hasContent(userTimezoneOffsetMinutes))
{
try
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// even though we're putting it in the session as a string, go through parse int, to make sure it's a valid int. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
session.setValue(QSession.VALUE_KEY_USER_TIMEZONE_OFFSET_MINUTES, String.valueOf(Integer.parseInt(userTimezoneOffsetMinutes)));
}
catch(Exception e)
{
LOG.debug("Received non-integer value for X-QQQ-UserTimezoneOffsetMinutes header: " + userTimezoneOffsetMinutes);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void setUserTimezoneInSession(Context context, QSession session)
{
String userTimezone = context.header("X-QQQ-UserTimezone");
if(StringUtils.hasContent(userTimezone))
{
session.setValue(QSession.VALUE_KEY_USER_TIMEZONE, userTimezone);
}
}
}

View File

@ -0,0 +1,75 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors;
import java.io.Serializable;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule;
import com.kingsrook.qqq.middleware.javalin.executors.io.ManageSessionInput;
import com.kingsrook.qqq.middleware.javalin.executors.io.ManageSessionOutputInterface;
/*******************************************************************************
**
*******************************************************************************/
public class ManageSessionExecutor extends AbstractMiddlewareExecutor<ManageSessionInput, ManageSessionOutputInterface>
{
/***************************************************************************
**
***************************************************************************/
@Override
public void execute(ManageSessionInput input, ManageSessionOutputInterface output) throws QException
{
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(QContext.getQInstance().getAuthentication());
Map<String, String> authContext = new HashMap<>();
authContext.put(Auth0AuthenticationModule.ACCESS_TOKEN_KEY, input.getAccessToken());
authContext.put(Auth0AuthenticationModule.DO_STORE_USER_SESSION_KEY, "true");
/////////////////////////////////
// (try to) create the session //
/////////////////////////////////
QSession session = authenticationModule.createSession(QContext.getQInstance(), authContext);
//////////////////
// build output //
//////////////////
output.setUuid(session.getUuid());
if(session.getValuesForFrontend() != null)
{
LinkedHashMap<String, Serializable> valuesForFrontend = new LinkedHashMap<>(session.getValuesForFrontend());
output.setValues(valuesForFrontend);
}
}
}

View File

@ -0,0 +1,58 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors;
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
import com.kingsrook.qqq.middleware.javalin.executors.io.MetaDataInput;
import com.kingsrook.qqq.middleware.javalin.executors.io.MetaDataOutputInterface;
/*******************************************************************************
**
*******************************************************************************/
public class MetaDataExecutor extends AbstractMiddlewareExecutor<MetaDataInput, MetaDataOutputInterface>
{
/***************************************************************************
**
***************************************************************************/
@Override
public void execute(MetaDataInput input, MetaDataOutputInterface output) throws QException
{
com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput actionInput = new com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput();
actionInput.setMiddlewareName(input.getMiddlewareName());
actionInput.setMiddlewareVersion(input.getMiddlewareVersion());
actionInput.setFrontendName(input.getFrontendName());
actionInput.setFrontendVersion(input.getFrontendVersion());
actionInput.setApplicationName(input.getApplicationName());
actionInput.setApplicationVersion(input.getApplicationVersion());
MetaDataAction metaDataAction = new MetaDataAction();
MetaDataOutput metaDataOutput = metaDataAction.execute(actionInput);
output.setMetaDataOutput(metaDataOutput);
}
}

View File

@ -0,0 +1,186 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
import com.kingsrook.qqq.backend.core.actions.async.JobGoingAsyncException;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.javalin.QJavalinAccessLogger;
import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepInput;
import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepOrStatusOutputInterface;
import com.kingsrook.qqq.middleware.javalin.executors.utils.ProcessExecutorUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessInitOrStepExecutor extends AbstractMiddlewareExecutor<ProcessInitOrStepInput, ProcessInitOrStepOrStatusOutputInterface>
{
private static final QLogger LOG = QLogger.getLogger(ProcessInitOrStepExecutor.class);
/***************************************************************************
** Note: implementation of the output interface here, it wants to know what
** type it's going to be first, so, be polite and always call .setType before
** any other setters.
***************************************************************************/
@Override
public void execute(ProcessInitOrStepInput input, ProcessInitOrStepOrStatusOutputInterface output) throws QException
{
Exception returningException = null;
String processName = input.getProcessName();
String startAfterStep = input.getStartAfterStep();
String processUUID = input.getProcessUUID();
if(processUUID == null)
{
processUUID = UUID.randomUUID().toString();
}
LOG.info(startAfterStep == null ? "Initiating process [" + processName + "] [" + processUUID + "]"
: "Resuming process [" + processName + "] [" + processUUID + "] after step [" + startAfterStep + "]");
try
{
RunProcessInput runProcessInput = new RunProcessInput();
QContext.pushAction(runProcessInput);
runProcessInput.setProcessName(processName);
runProcessInput.setFrontendStepBehavior(input.getFrontendStepBehavior());
runProcessInput.setProcessUUID(processUUID);
runProcessInput.setStartAfterStep(startAfterStep);
runProcessInput.setValues(Objects.requireNonNullElseGet(input.getValues(), HashMap::new));
if(input.getRecordsFilter() != null)
{
runProcessInput.setCallback(new QProcessCallback()
{
/***************************************************************************
**
***************************************************************************/
@Override
public QQueryFilter getQueryFilter()
{
return (input.getRecordsFilter());
}
/***************************************************************************
**
***************************************************************************/
@Override
public Map<String, Serializable> getFieldValues(List<QFieldMetaData> fields)
{
return (Collections.emptyMap());
}
});
}
String reportName = ValueUtils.getValueAsString(runProcessInput.getValue("reportName"));
QJavalinAccessLogger.logStart(startAfterStep == null ? "processInit" : "processStep", logPair("processName", processName), logPair("processUUID", processUUID),
StringUtils.hasContent(startAfterStep) ? logPair("startAfterStep", startAfterStep) : null,
StringUtils.hasContent(reportName) ? logPair("reportName", reportName) : null);
//////////////////////////////////////////////////////////////////////////////////////////////////
// important to do this check AFTER the runProcessInput is populated with values from context - //
// e.g., in case things like a reportName are set in here //
//////////////////////////////////////////////////////////////////////////////////////////////////
PermissionsHelper.checkProcessPermissionThrowing(runProcessInput, processName);
////////////////////////////////////////
// run the process as an async action //
////////////////////////////////////////
RunProcessOutput runProcessOutput = new AsyncJobManager().startJob(processName, input.getStepTimeoutMillis(), TimeUnit.MILLISECONDS, (callback) ->
{
runProcessInput.setAsyncJobCallback(callback);
return (new RunProcessAction().execute(runProcessInput));
});
LOG.debug("Process result error? " + runProcessOutput.getException());
for(QFieldMetaData outputField : QContext.getQInstance().getProcess(runProcessInput.getProcessName()).getOutputFields())
{
LOG.debug("Process result output value: " + outputField.getName() + ": " + runProcessOutput.getValues().get(outputField.getName()));
}
ProcessExecutorUtils.serializeRunProcessResultForCaller(output, processName, runProcessOutput);
QJavalinAccessLogger.logProcessSummary(processName, processUUID, runProcessOutput);
}
catch(JobGoingAsyncException jgae)
{
output.setType(ProcessInitOrStepOrStatusOutputInterface.Type.JOB_STARTED);
output.setJobUUID(jgae.getJobUUID());
}
catch(QPermissionDeniedException | QAuthenticationException e)
{
throw (e);
}
catch(Exception e)
{
//////////////////////////////////////////////////////////////////////////////
// our other actions in here would do: handleException(context, e); //
// which would return a 500 to the client. //
// but - other process-step actions, they always return a 200, just with an //
// optional error message - so - keep all of the processes consistent. //
//////////////////////////////////////////////////////////////////////////////
returningException = e;
ProcessExecutorUtils.serializeRunProcessExceptionForCaller(output, e);
}
output.setProcessUUID(processUUID);
if(returningException != null)
{
QJavalinAccessLogger.logEndFail(returningException);
}
else
{
QJavalinAccessLogger.logEndSuccess();
}
}
}

View File

@ -0,0 +1,65 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors;
import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataOutput;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessMetaDataInput;
import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessMetaDataOutputInterface;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessMetaDataExecutor extends AbstractMiddlewareExecutor<ProcessMetaDataInput, ProcessMetaDataOutputInterface>
{
/***************************************************************************
**
***************************************************************************/
@Override
public void execute(ProcessMetaDataInput input, ProcessMetaDataOutputInterface output) throws QException
{
com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInput processMetaDataInput = new com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInput();
String processName = input.getProcessName();
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
if(process == null)
{
throw (new QNotFoundException("Process [" + processName + "] was not found."));
}
PermissionsHelper.checkProcessPermissionThrowing(processMetaDataInput, processName);
processMetaDataInput.setProcessName(processName);
ProcessMetaDataAction processMetaDataAction = new ProcessMetaDataAction();
ProcessMetaDataOutput processMetaDataOutput = processMetaDataAction.execute(processMetaDataInput);
output.setProcessMetaData(processMetaDataOutput.getProcess());
}
}

View File

@ -0,0 +1,121 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.javalin.QJavalinAccessLogger;
import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepOrStatusOutputInterface;
import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessStatusInput;
import com.kingsrook.qqq.middleware.javalin.executors.utils.ProcessExecutorUtils;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessStatusExecutor extends AbstractMiddlewareExecutor<ProcessStatusInput, ProcessInitOrStepOrStatusOutputInterface>
{
private static final QLogger LOG = QLogger.getLogger(ProcessStatusExecutor.class);
/***************************************************************************
** Note: implementation of the output interface here, it wants to know what
** type it's going to be first, so, be polite and always call .setType before
** any other setters.
***************************************************************************/
@Override
public void execute(ProcessStatusInput input, ProcessInitOrStepOrStatusOutputInterface output) throws QException
{
try
{
String processName = input.getProcessName();
String processUUID = input.getProcessUUID();
String jobUUID = input.getJobUUID();
LOG.debug("Request for status of process " + processUUID + ", job " + jobUUID);
Optional<AsyncJobStatus> optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID);
if(optionalJobStatus.isEmpty())
{
ProcessExecutorUtils.serializeRunProcessExceptionForCaller(output, new RuntimeException("Could not find status of process step job"));
}
else
{
AsyncJobStatus jobStatus = optionalJobStatus.get();
// resultForCaller.put("jobStatus", jobStatus);
LOG.debug("Job status is " + jobStatus.getState() + " for " + jobUUID);
if(jobStatus.getState().equals(AsyncJobState.COMPLETE))
{
///////////////////////////////////////////////////////////////////////////////////////
// if the job is complete, get the process result from state provider, and return it //
// this output should look like it did if the job finished synchronously!! //
///////////////////////////////////////////////////////////////////////////////////////
Optional<ProcessState> processState = RunProcessAction.getState(processUUID);
if(processState.isPresent())
{
RunProcessOutput runProcessOutput = new RunProcessOutput(processState.get());
ProcessExecutorUtils.serializeRunProcessResultForCaller(output, processName, runProcessOutput);
QJavalinAccessLogger.logProcessSummary(processName, processUUID, runProcessOutput);
}
else
{
ProcessExecutorUtils.serializeRunProcessExceptionForCaller(output, new RuntimeException("Could not find results for process " + processUUID));
}
}
else if(jobStatus.getState().equals(AsyncJobState.ERROR))
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the job had an error (e.g., a process step threw), "nicely" serialize its exception for the caller //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
if(jobStatus.getCaughtException() != null)
{
ProcessExecutorUtils.serializeRunProcessExceptionForCaller(output, jobStatus.getCaughtException());
}
}
else
{
output.setType(ProcessInitOrStepOrStatusOutputInterface.Type.RUNNING);
output.setMessage(jobStatus.getMessage());
output.setCurrent(jobStatus.getCurrent());
output.setTotal(jobStatus.getTotal());
}
}
output.setProcessUUID(processUUID);
}
catch(Exception e)
{
ProcessExecutorUtils.serializeRunProcessExceptionForCaller(output, e);
}
}
}

View File

@ -0,0 +1,30 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
/*******************************************************************************
**
*******************************************************************************/
public class AbstractMiddlewareInput
{
}

View File

@ -0,0 +1,30 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
/*******************************************************************************
**
*******************************************************************************/
public interface AbstractMiddlewareOutputInterface
{
}

View File

@ -0,0 +1,31 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
/*******************************************************************************
**
*******************************************************************************/
public class AuthenticationMetaDataInput extends AbstractMiddlewareInput
{
}

View File

@ -0,0 +1,37 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
/*******************************************************************************
**
*******************************************************************************/
public interface AuthenticationMetaDataOutputInterface extends AbstractMiddlewareOutputInterface
{
/***************************************************************************
**
***************************************************************************/
void setAuthenticationMetaData(QAuthenticationMetaData qAuthenticationMetaData);
}

View File

@ -0,0 +1,31 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
/*******************************************************************************
** generic middleware input that has no fields.
*******************************************************************************/
public class EmptyMiddlewareInput extends AbstractMiddlewareInput
{
}

View File

@ -0,0 +1,31 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
/*******************************************************************************
**
*******************************************************************************/
public interface EmptyMiddlewareOutputInterface extends AbstractMiddlewareOutputInterface
{
}

View File

@ -0,0 +1,63 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
/*******************************************************************************
**
*******************************************************************************/
public class ManageSessionInput extends AbstractMiddlewareInput
{
private String accessToken;
/*******************************************************************************
** Getter for accessToken
*******************************************************************************/
public String getAccessToken()
{
return (this.accessToken);
}
/*******************************************************************************
** Setter for accessToken
*******************************************************************************/
public void setAccessToken(String accessToken)
{
this.accessToken = accessToken;
}
/*******************************************************************************
** Fluent setter for accessToken
*******************************************************************************/
public ManageSessionInput withAccessToken(String accessToken)
{
this.accessToken = accessToken;
return (this);
}
}

View File

@ -0,0 +1,45 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
import java.io.Serializable;
import java.util.Map;
/*******************************************************************************
**
*******************************************************************************/
public interface ManageSessionOutputInterface extends AbstractMiddlewareOutputInterface
{
/***************************************************************************
** Setter for Uuid
***************************************************************************/
void setUuid(String uuid);
/*******************************************************************************
** Setter for values
*******************************************************************************/
void setValues(Map<String, Serializable> values);
}

View File

@ -0,0 +1,225 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
/*******************************************************************************
**
*******************************************************************************/
public class MetaDataInput extends AbstractMiddlewareInput
{
private String frontendName;
private String frontendVersion;
private String middlewareName;
private String middlewareVersion;
private String applicationName;
private String applicationVersion;
/*******************************************************************************
** Getter for frontendName
*******************************************************************************/
public String getFrontendName()
{
return (this.frontendName);
}
/*******************************************************************************
** Setter for frontendName
*******************************************************************************/
public void setFrontendName(String frontendName)
{
this.frontendName = frontendName;
}
/*******************************************************************************
** Fluent setter for frontendName
*******************************************************************************/
public MetaDataInput withFrontendName(String frontendName)
{
this.frontendName = frontendName;
return (this);
}
/*******************************************************************************
** Getter for frontendVersion
*******************************************************************************/
public String getFrontendVersion()
{
return (this.frontendVersion);
}
/*******************************************************************************
** Setter for frontendVersion
*******************************************************************************/
public void setFrontendVersion(String frontendVersion)
{
this.frontendVersion = frontendVersion;
}
/*******************************************************************************
** Fluent setter for frontendVersion
*******************************************************************************/
public MetaDataInput withFrontendVersion(String frontendVersion)
{
this.frontendVersion = frontendVersion;
return (this);
}
/*******************************************************************************
** Getter for middlewareName
*******************************************************************************/
public String getMiddlewareName()
{
return (this.middlewareName);
}
/*******************************************************************************
** Setter for middlewareName
*******************************************************************************/
public void setMiddlewareName(String middlewareName)
{
this.middlewareName = middlewareName;
}
/*******************************************************************************
** Fluent setter for middlewareName
*******************************************************************************/
public MetaDataInput withMiddlewareName(String middlewareName)
{
this.middlewareName = middlewareName;
return (this);
}
/*******************************************************************************
** Getter for middlewareVersion
*******************************************************************************/
public String getMiddlewareVersion()
{
return (this.middlewareVersion);
}
/*******************************************************************************
** Setter for middlewareVersion
*******************************************************************************/
public void setMiddlewareVersion(String middlewareVersion)
{
this.middlewareVersion = middlewareVersion;
}
/*******************************************************************************
** Fluent setter for middlewareVersion
*******************************************************************************/
public MetaDataInput withMiddlewareVersion(String middlewareVersion)
{
this.middlewareVersion = middlewareVersion;
return (this);
}
/*******************************************************************************
** Getter for applicationName
*******************************************************************************/
public String getApplicationName()
{
return (this.applicationName);
}
/*******************************************************************************
** Setter for applicationName
*******************************************************************************/
public void setApplicationName(String applicationName)
{
this.applicationName = applicationName;
}
/*******************************************************************************
** Fluent setter for applicationName
*******************************************************************************/
public MetaDataInput withApplicationName(String applicationName)
{
this.applicationName = applicationName;
return (this);
}
/*******************************************************************************
** Getter for applicationVersion
*******************************************************************************/
public String getApplicationVersion()
{
return (this.applicationVersion);
}
/*******************************************************************************
** Setter for applicationVersion
*******************************************************************************/
public void setApplicationVersion(String applicationVersion)
{
this.applicationVersion = applicationVersion;
}
/*******************************************************************************
** Fluent setter for applicationVersion
*******************************************************************************/
public MetaDataInput withApplicationVersion(String applicationVersion)
{
this.applicationVersion = applicationVersion;
return (this);
}
}

View File

@ -0,0 +1,39 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
/*******************************************************************************
**
*******************************************************************************/
public interface MetaDataOutputInterface extends AbstractMiddlewareOutputInterface
{
/*******************************************************************************
** Setter for metaDataOutput
*******************************************************************************/
void setMetaDataOutput(MetaDataOutput metaDataOutput);
}

View File

@ -0,0 +1,295 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessInitOrStepInput extends AbstractMiddlewareInput
{
private String processName;
private Integer stepTimeoutMillis = 3000;
private QQueryFilter recordsFilter;
private Map<String, Serializable> values = new LinkedHashMap<>();
/////////////////////////////////////
// used only for 'step' (not init) //
/////////////////////////////////////
private String processUUID;
private String startAfterStep;
private RunProcessInput.FrontendStepBehavior frontendStepBehavior = RunProcessInput.FrontendStepBehavior.BREAK;
// todo - file??
/***************************************************************************
**
***************************************************************************/
public enum RecordsParam
{
FILTER_JSON("filterJSON"),
RECORD_IDS("recordIds");
private final String value;
/***************************************************************************
**
***************************************************************************/
RecordsParam(String value)
{
this.value = value;
}
}
/*******************************************************************************
** Getter for processName
*******************************************************************************/
public String getProcessName()
{
return (this.processName);
}
/*******************************************************************************
** Setter for processName
*******************************************************************************/
public void setProcessName(String processName)
{
this.processName = processName;
}
/*******************************************************************************
** Fluent setter for processName
*******************************************************************************/
public ProcessInitOrStepInput withProcessName(String processName)
{
this.processName = processName;
return (this);
}
/*******************************************************************************
** Getter for stepTimeoutMillis
*******************************************************************************/
public Integer getStepTimeoutMillis()
{
return (this.stepTimeoutMillis);
}
/*******************************************************************************
** Setter for stepTimeoutMillis
*******************************************************************************/
public void setStepTimeoutMillis(Integer stepTimeoutMillis)
{
this.stepTimeoutMillis = stepTimeoutMillis;
}
/*******************************************************************************
** Fluent setter for stepTimeoutMillis
*******************************************************************************/
public ProcessInitOrStepInput withStepTimeoutMillis(Integer stepTimeoutMillis)
{
this.stepTimeoutMillis = stepTimeoutMillis;
return (this);
}
/*******************************************************************************
** Getter for recordsFilter
*******************************************************************************/
public QQueryFilter getRecordsFilter()
{
return (this.recordsFilter);
}
/*******************************************************************************
** Setter for recordsFilter
*******************************************************************************/
public void setRecordsFilter(QQueryFilter recordsFilter)
{
this.recordsFilter = recordsFilter;
}
/*******************************************************************************
** Fluent setter for recordsFilter
*******************************************************************************/
public ProcessInitOrStepInput withRecordsFilter(QQueryFilter recordsFilter)
{
this.recordsFilter = recordsFilter;
return (this);
}
/*******************************************************************************
** Getter for values
*******************************************************************************/
public Map<String, Serializable> getValues()
{
return (this.values);
}
/*******************************************************************************
** Setter for values
*******************************************************************************/
public void setValues(Map<String, Serializable> values)
{
this.values = values;
}
/*******************************************************************************
** Fluent setter for values
*******************************************************************************/
public ProcessInitOrStepInput withValues(Map<String, Serializable> values)
{
this.values = values;
return (this);
}
/*******************************************************************************
** Getter for frontendStepBehavior
*******************************************************************************/
public RunProcessInput.FrontendStepBehavior getFrontendStepBehavior()
{
return (this.frontendStepBehavior);
}
/*******************************************************************************
** Setter for frontendStepBehavior
*******************************************************************************/
public void setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior frontendStepBehavior)
{
this.frontendStepBehavior = frontendStepBehavior;
}
/*******************************************************************************
** Fluent setter for frontendStepBehavior
*******************************************************************************/
public ProcessInitOrStepInput withFrontendStepBehavior(RunProcessInput.FrontendStepBehavior frontendStepBehavior)
{
this.frontendStepBehavior = frontendStepBehavior;
return (this);
}
/*******************************************************************************
** Getter for processUUID
*******************************************************************************/
public String getProcessUUID()
{
return (this.processUUID);
}
/*******************************************************************************
** Setter for processUUID
*******************************************************************************/
public void setProcessUUID(String processUUID)
{
this.processUUID = processUUID;
}
/*******************************************************************************
** Fluent setter for processUUID
*******************************************************************************/
public ProcessInitOrStepInput withProcessUUID(String processUUID)
{
this.processUUID = processUUID;
return (this);
}
/*******************************************************************************
** Getter for startAfterStep
*******************************************************************************/
public String getStartAfterStep()
{
return (this.startAfterStep);
}
/*******************************************************************************
** Setter for startAfterStep
*******************************************************************************/
public void setStartAfterStep(String startAfterStep)
{
this.startAfterStep = startAfterStep;
}
/*******************************************************************************
** Fluent setter for startAfterStep
*******************************************************************************/
public ProcessInitOrStepInput withStartAfterStep(String startAfterStep)
{
this.startAfterStep = startAfterStep;
return (this);
}
}

View File

@ -0,0 +1,100 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessMetaDataAdjustment;
/*******************************************************************************
**
*******************************************************************************/
public interface ProcessInitOrStepOrStatusOutputInterface extends AbstractMiddlewareOutputInterface
{
/***************************************************************************
**
***************************************************************************/
enum Type
{
COMPLETE, JOB_STARTED, RUNNING, ERROR;
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
void setType(Type type);
/*******************************************************************************
** Setter for processUUID
*******************************************************************************/
void setProcessUUID(String processUUID);
/*******************************************************************************
** Setter for nextStep
*******************************************************************************/
void setNextStep(String nextStep);
/*******************************************************************************
** Setter for values
*******************************************************************************/
void setValues(Map<String, Serializable> values);
/*******************************************************************************
** Setter for jobUUID
*******************************************************************************/
void setJobUUID(String jobUUID);
/*******************************************************************************
** Setter for message
*******************************************************************************/
void setMessage(String message);
/*******************************************************************************
** Setter for current
*******************************************************************************/
void setCurrent(Integer current);
/*******************************************************************************
** Setter for total
*******************************************************************************/
void setTotal(Integer total);
/*******************************************************************************
** Setter for error
*******************************************************************************/
void setError(String error);
/*******************************************************************************
** Setter for userFacingError
*******************************************************************************/
void setUserFacingError(String userFacingError);
/*******************************************************************************
** Setter for processMetaDataAdjustment
*******************************************************************************/
void setProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment);
}

View File

@ -0,0 +1,66 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessMetaDataInput extends AbstractMiddlewareInput
{
private String processName;
/*******************************************************************************
** Getter for processName
**
*******************************************************************************/
public String getProcessName()
{
return processName;
}
/*******************************************************************************
** Setter for processName
**
*******************************************************************************/
public void setProcessName(String processName)
{
this.processName = processName;
}
/*******************************************************************************
** Fluent setter for processName
**
*******************************************************************************/
public ProcessMetaDataInput withProcessName(String processName)
{
this.processName = processName;
return (this);
}
}

View File

@ -0,0 +1,37 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData;
/*******************************************************************************
**
*******************************************************************************/
public interface ProcessMetaDataOutputInterface extends AbstractMiddlewareOutputInterface
{
/***************************************************************************
**
***************************************************************************/
void setProcessMetaData(QFrontendProcessMetaData processMetaData);
}

View File

@ -0,0 +1,127 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessStatusInput extends AbstractMiddlewareInput
{
private String processName;
private String processUUID;
private String jobUUID;
/*******************************************************************************
** Getter for processName
*******************************************************************************/
public String getProcessName()
{
return (this.processName);
}
/*******************************************************************************
** Setter for processName
*******************************************************************************/
public void setProcessName(String processName)
{
this.processName = processName;
}
/*******************************************************************************
** Fluent setter for processName
*******************************************************************************/
public ProcessStatusInput withProcessName(String processName)
{
this.processName = processName;
return (this);
}
/*******************************************************************************
** Getter for processUUID
*******************************************************************************/
public String getProcessUUID()
{
return (this.processUUID);
}
/*******************************************************************************
** Setter for processUUID
*******************************************************************************/
public void setProcessUUID(String processUUID)
{
this.processUUID = processUUID;
}
/*******************************************************************************
** Fluent setter for processUUID
*******************************************************************************/
public ProcessStatusInput withProcessUUID(String processUUID)
{
this.processUUID = processUUID;
return (this);
}
/*******************************************************************************
** Getter for jobUUID
*******************************************************************************/
public String getJobUUID()
{
return (this.jobUUID);
}
/*******************************************************************************
** Setter for jobUUID
*******************************************************************************/
public void setJobUUID(String jobUUID)
{
this.jobUUID = jobUUID;
}
/*******************************************************************************
** Fluent setter for jobUUID
*******************************************************************************/
public ProcessStatusInput withJobUUID(String jobUUID)
{
this.jobUUID = jobUUID;
return (this);
}
}

View File

@ -0,0 +1,31 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
/*******************************************************************************
**
*******************************************************************************/
public interface ProcessStatusOutputInterface extends AbstractMiddlewareOutputInterface
{
}

View File

@ -0,0 +1,132 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.io;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
/*******************************************************************************
**
*******************************************************************************/
public class QueryMiddlewareInput extends AbstractMiddlewareInput
{
private String table;
private QQueryFilter filter;
private List<QueryJoin> queryJoins;
/*******************************************************************************
** Getter for table
*******************************************************************************/
public String getTable()
{
return (this.table);
}
/*******************************************************************************
** Setter for table
*******************************************************************************/
public void setTable(String table)
{
this.table = table;
}
/*******************************************************************************
** Fluent setter for table
*******************************************************************************/
public QueryMiddlewareInput withTable(String table)
{
this.table = table;
return (this);
}
/*******************************************************************************
** Getter for filter
*******************************************************************************/
public QQueryFilter getFilter()
{
return (this.filter);
}
/*******************************************************************************
** Setter for filter
*******************************************************************************/
public void setFilter(QQueryFilter filter)
{
this.filter = filter;
}
/*******************************************************************************
** Fluent setter for filter
*******************************************************************************/
public QueryMiddlewareInput withFilter(QQueryFilter filter)
{
this.filter = filter;
return (this);
}
/*******************************************************************************
** Getter for queryJoins
*******************************************************************************/
public List<QueryJoin> getQueryJoins()
{
return (this.queryJoins);
}
/*******************************************************************************
** Setter for queryJoins
*******************************************************************************/
public void setQueryJoins(List<QueryJoin> queryJoins)
{
this.queryJoins = queryJoins;
}
/*******************************************************************************
** Fluent setter for queryJoins
*******************************************************************************/
public QueryMiddlewareInput withQueryJoins(List<QueryJoin> queryJoins)
{
this.queryJoins = queryJoins;
return (this);
}
}

View File

@ -0,0 +1,29 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*******************************************************************************
** These classes are input/output wrappers for middleware executors.
**
** Some "empty" implementations are provided, for executors that (more likely)
** take no inputs, or (less likely?) return no outputs.
**
*******************************************************************************/
package com.kingsrook.qqq.middleware.javalin.executors.io;

View File

@ -0,0 +1,40 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*******************************************************************************
** This package contains (hopefully generally) api-version-agnostic classes
** that implement the actual QQQ Middleware. That is to say, subclasses of
** `AbstractMiddlewareExecutor`, which use classes from the `.io` subpackage
** for I/O, to run code in a QQQ server.
**
** As new versions of the middleware evolve, the idea is that the spec classes
** for new versions will be responsible for appropriately marshalling data
** in and out of the executors, via the I/O classes, with "feature flags", etc
** added to those input classes as needed (say if v N+1 adds a new feature,
** then a request for v N may omit the feature-flag that turns that feature on).
**
** As functionality continues to evolve, the time may come when it's appropriate
** to fork an Executor. Hypothetically, if version 5 of the QueryExecutor
** bears very little resemblance to versions 1 through 4 (due to additional
** pizzazz?) spawn a new QueryWithPizzazzExecutor. Of course, naming here
** will be the hardest part (e.g., avoid NewNewQueryExecutorFinal2B...)
*******************************************************************************/
package com.kingsrook.qqq.middleware.javalin.executors;

View File

@ -0,0 +1,118 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.executors.utils;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepOrStatusOutputInterface;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessExecutorUtils
{
private static final QLogger LOG = QLogger.getLogger(ProcessExecutorUtils.class);
/*******************************************************************************
** Whether a step finished synchronously or asynchronously, return its data
** to the caller the same way.
*******************************************************************************/
public static void serializeRunProcessResultForCaller(ProcessInitOrStepOrStatusOutputInterface processInitOrStepOutput, String processName, RunProcessOutput runProcessOutput)
{
processInitOrStepOutput.setType(ProcessInitOrStepOrStatusOutputInterface.Type.COMPLETE);
if(runProcessOutput.getException().isPresent())
{
////////////////////////////////////////////////////////////////
// per code coverage, this path may never actually get hit... //
////////////////////////////////////////////////////////////////
serializeRunProcessExceptionForCaller(processInitOrStepOutput, runProcessOutput.getException().get());
}
processInitOrStepOutput.setValues(runProcessOutput.getValues());
// processInitOrStepOutput.setValues(getValuesForCaller(processName, runProcessOutput));
runProcessOutput.getProcessState().getNextStepName().ifPresent(nextStep -> processInitOrStepOutput.setNextStep(nextStep));
if(runProcessOutput.getProcessMetaDataAdjustment() != null)
{
processInitOrStepOutput.setProcessMetaDataAdjustment(runProcessOutput.getProcessMetaDataAdjustment());
}
}
// /***************************************************************************
// ** maybe good idea here, but... to only return fields that the frontend steps
// ** say they care about. yeah.
// ***************************************************************************/
// private static Map<String, Serializable> getValuesForCaller(String processName, RunProcessOutput runProcessOutput)
// {
// QProcessMetaData process = QContext.getQInstance().getProcess(processName);
// Map<String, QFieldMetaData> frontendFields = new LinkedHashMap<>();
// for(QStepMetaData step : process.getAllSteps().values())
// {
// if(step instanceof QFrontendStepMetaData frontendStepMetaData)
// {
// frontendFields.addAll(frontendStepMetaData.getAllFields());
// }
// else if(step instanceof QStateMachineStep stateMachineStep)
// {
// for(QStepMetaData subStep : stateMachineStep.getSubSteps())
// {
// // recur, etc
// }
// }
// }
//
// // then, only return ones in the map, eh
// }
/*******************************************************************************
**
*******************************************************************************/
public static void serializeRunProcessExceptionForCaller(ProcessInitOrStepOrStatusOutputInterface processInitOrStepOutput, Exception exception)
{
processInitOrStepOutput.setType(ProcessInitOrStepOrStatusOutputInterface.Type.ERROR);
QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(exception, QUserFacingException.class);
if(userFacingException != null)
{
LOG.info("User-facing exception in process", userFacingException);
processInitOrStepOutput.setError(userFacingException.getMessage());
processInitOrStepOutput.setUserFacingError(userFacingException.getMessage());
}
else
{
Throwable rootException = ExceptionUtils.getRootException(exception);
LOG.warn("Uncaught Exception in process", exception);
processInitOrStepOutput.setError("Error message: " + rootException.getMessage());
}
}
}

View File

@ -0,0 +1,347 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.schemabuilder;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIHasAdditionalProperties;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIIncludeProperties;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapKnownEntries;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapValueType;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIOneOf;
import com.kingsrook.qqq.openapi.model.Schema;
import com.kingsrook.qqq.openapi.model.Type;
/*******************************************************************************
** This class facilitates generating OpenAPI Schema objects based on reflectively
** reading classes and annotations
*******************************************************************************/
public class SchemaBuilder
{
/***************************************************************************
**
***************************************************************************/
public Schema classToSchema(Class<?> c)
{
return classToSchema(c, c);
}
/***************************************************************************
**
***************************************************************************/
public static class SchemaFromBuilder extends Schema
{
private Class<?> originalClass;
/*******************************************************************************
** Getter for originalClass
**
*******************************************************************************/
@JsonIgnore
public Class<?> getOriginalClass()
{
return originalClass;
}
/*******************************************************************************
** Setter for originalClass
**
*******************************************************************************/
public void setOriginalClass(Class<?> originalClass)
{
this.originalClass = originalClass;
}
/*******************************************************************************
** Fluent setter for originalClass
**
*******************************************************************************/
public SchemaFromBuilder withOriginalClass(Class<?> originalClass)
{
this.originalClass = originalClass;
return (this);
}
}
/***************************************************************************
**
***************************************************************************/
private Schema classToSchema(Class<?> c, AnnotatedElement element)
{
SchemaFromBuilder schema = new SchemaFromBuilder();
schema.setOriginalClass(c);
if(c.isEnum())
{
schema.withType(Type.STRING);
schema.withEnumValues(Arrays.stream(c.getEnumConstants()).map(e -> String.valueOf(e)).collect(Collectors.toList()));
}
else if(c.equals(String.class))
{
schema.withType(Type.STRING);
}
else if(c.equals(Integer.class) || c.equals(BigDecimal.class))
{
schema.withType(Type.NUMBER);
}
else if(c.equals(Boolean.class))
{
schema.withType(Type.BOOLEAN);
}
else if(c.equals(List.class))
{
schema.withType(Type.ARRAY);
// Class<?> itemType = field.getType().getTypeParameters()[0].getBounds().getClass();
OpenAPIListItems openAPIListItemsAnnotation = element.getAnnotation(OpenAPIListItems.class);
if(openAPIListItemsAnnotation == null)
{
// todo - can this be allowed, to make a generic list? maybe.
// throw (new QRuntimeException("A List field [" + field.getName() + "] was missing its @OpenAPIItems annotation"));
}
else
{
if(openAPIListItemsAnnotation.useRef())
{
schema.withItems(new Schema().withRef("#/components/schemas/" + openAPIListItemsAnnotation.value().getSimpleName()));
}
else
{
Class<?> itemType = openAPIListItemsAnnotation.value();
schema.withItems(classToSchema(itemType));
}
}
}
else if(c.equals(Map.class))
{
schema.withType(Type.OBJECT);
OpenAPIMapKnownEntries openAPIMapKnownEntriesAnnotation = element.getAnnotation(OpenAPIMapKnownEntries.class);
if(openAPIMapKnownEntriesAnnotation != null)
{
schema.withRef("#/components/schemas/" + openAPIMapKnownEntriesAnnotation.value().getSimpleName());
// if(openAPIMapKnownEntriesAnnotation.additionalProperties())
// {
// schema.withAdditionalProperties(true);
// }
}
OpenAPIMapValueType openAPIMapValueTypeAnnotation = element.getAnnotation(OpenAPIMapValueType.class);
if(openAPIMapValueTypeAnnotation != null)
{
if(openAPIMapValueTypeAnnotation.useRef())
{
schema.withAdditionalProperties(new Schema().withRef("#/components/schemas/" + openAPIMapValueTypeAnnotation.value().getSimpleName()));
}
else
{
schema.withAdditionalProperties(classToSchema(openAPIMapValueTypeAnnotation.value()));
}
}
}
else
{
OpenAPIOneOf openAPIOneOfAnnotation = element.getAnnotation(OpenAPIOneOf.class);
if(openAPIOneOfAnnotation != null)
{
String description = "[" + element + "]";
List<Schema> oneOfList = processOneOfAnnotation(openAPIOneOfAnnotation, c, description);
schema.withOneOf(oneOfList);
}
else
{
/////////////////////////////////////////////////////////////////////////////////////////////
// else, if not a one-of then assume the schema is an object, and build out its properties //
/////////////////////////////////////////////////////////////////////////////////////////////
schema.withType(Type.OBJECT);
Map<String, Schema> properties = new TreeMap<>();
schema.withProperties(properties);
//////////////////////////////////////////////////////////////////////////////////////////
// if we're told to includeProperties (e.g., from ancestor classes), then go find those //
//////////////////////////////////////////////////////////////////////////////////////////
OpenAPIIncludeProperties openAPIIncludePropertiesAnnotation = c.getAnnotation(OpenAPIIncludeProperties.class);
if(openAPIIncludePropertiesAnnotation != null)
{
Set<Class<?>> ancestorClasses = Arrays.stream(openAPIIncludePropertiesAnnotation.ancestorClasses()).collect(Collectors.toSet());
Class<?> superClass = c.getSuperclass();
do
{
if(ancestorClasses.contains(superClass))
{
addDeclaredFieldsToProperties(superClass, properties);
addDeclaredMethodsToProperties(superClass, properties);
}
superClass = superClass.getSuperclass();
}
while(superClass != null);
}
///////////////////////////////////////////////////////////////////////
// make all declared-fields and getters in the class into properties //
///////////////////////////////////////////////////////////////////////
addDeclaredFieldsToProperties(c, properties);
addDeclaredMethodsToProperties(c, properties);
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// now (after schema may have been replaced, e.g., in a recursive call), add more details to it //
//////////////////////////////////////////////////////////////////////////////////////////////////
OpenAPIDescription openAPIDescriptionAnnotation = element.getAnnotation(OpenAPIDescription.class);
if(openAPIDescriptionAnnotation != null)
{
schema.setDescription(openAPIDescriptionAnnotation.value());
}
if(element.isAnnotationPresent(OpenAPIHasAdditionalProperties.class))
{
schema.withAdditionalProperties(true);
}
return (schema);
}
/***************************************************************************
** Getter methods with an annotation
***************************************************************************/
private void addDeclaredMethodsToProperties(Class<?> c, Map<String, Schema> properties)
{
for(Method method : c.getDeclaredMethods())
{
OpenAPIDescription methodDescription = method.getAnnotation(OpenAPIDescription.class);
OpenAPIExclude openAPIExclude = method.getAnnotation(OpenAPIExclude.class);
if(method.getName().startsWith("get") && method.getParameterCount() == 0 && methodDescription != null && openAPIExclude == null)
{
String name = StringUtils.lcFirst(method.getName().substring(3));
properties.put(name, getMemberSchema(method));
}
}
}
/***************************************************************************
**
***************************************************************************/
private void addDeclaredFieldsToProperties(Class<?> c, Map<String, Schema> properties)
{
for(Field declaredField : c.getDeclaredFields())
{
OpenAPIExclude openAPIExclude = declaredField.getAnnotation(OpenAPIExclude.class);
if(openAPIExclude == null)
{
properties.put(declaredField.getName(), getMemberSchema(declaredField));
}
}
}
/***************************************************************************
**
***************************************************************************/
private Schema getMemberSchema(AccessibleObject member)
{
Class<?> type;
if(member instanceof Field field)
{
type = field.getType();
}
else if(member instanceof Method method)
{
type = method.getReturnType();
}
else
{
throw (new IllegalArgumentException("Unsupported AccessibleObject: " + member));
}
return (classToSchema(type, member));
}
/***************************************************************************
**
***************************************************************************/
private List<Schema> processOneOfAnnotation(OpenAPIOneOf openAPIOneOfAnnotation, Class<?> type, String description)
{
List<Schema> oneOfList = new ArrayList<>();
if(openAPIOneOfAnnotation.mode().equals(OpenAPIOneOf.Mode.PERMITTED_SUBCLASSES))
{
Class<?>[] permittedSubclasses = type.getPermittedSubclasses();
for(Class<?> permittedSubclass : permittedSubclasses)
{
oneOfList.add(classToSchema(permittedSubclass));
}
}
else if(openAPIOneOfAnnotation.mode().equals(OpenAPIOneOf.Mode.SPECIFIED_LIST))
{
for(Class<?> oneOfClass : openAPIOneOfAnnotation.options())
{
oneOfList.add(classToSchema(oneOfClass));
}
}
if(oneOfList.isEmpty())
{
throw (new QRuntimeException("Could not find any options to use for an @OpenAPIOneOf annotation on " + description));
}
return oneOfList;
}
}

View File

@ -0,0 +1,46 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.schemabuilder;
import com.kingsrook.qqq.openapi.model.Schema;
/*******************************************************************************
** Mark a class as eligible for running through the SchemaBuilder.
**
** Actually not really necessary, as schemaBuilder can run on any class - but
** does provide a method that a class might use to customize how it gets
** schemafied.
*******************************************************************************/
public interface ToSchema
{
/***************************************************************************
**
***************************************************************************/
default Schema toSchema()
{
return new SchemaBuilder().classToSchema(getClass());
}
}

View File

@ -0,0 +1,42 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/***************************************************************************
**
***************************************************************************/
@Target({ ElementType.FIELD, ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenAPIDescription
{
/***************************************************************************
**
***************************************************************************/
String value();
}

View File

@ -0,0 +1,55 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.EnumSet;
/*******************************************************************************
**
*******************************************************************************/
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenAPIEnumSubSet
{
/***************************************************************************
**
***************************************************************************/
Class<? extends EnumSubSet<?>> value();
/***************************************************************************
**
***************************************************************************/
interface EnumSubSet<E extends Enum<E>>
{
/***************************************************************************
**
***************************************************************************/
EnumSet<E> getSubSet();
}
}

View File

@ -0,0 +1,38 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*******************************************************************************
**
*******************************************************************************/
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenAPIExclude
{
}

View File

@ -0,0 +1,38 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*******************************************************************************
**
*******************************************************************************/
@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenAPIHasAdditionalProperties
{
}

View File

@ -0,0 +1,42 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/***************************************************************************
**
***************************************************************************/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenAPIIncludeProperties
{
/***************************************************************************
**
***************************************************************************/
Class<?>[] ancestorClasses() default { };
}

View File

@ -0,0 +1,47 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/***************************************************************************
**
***************************************************************************/
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenAPIListItems
{
/***************************************************************************
**
***************************************************************************/
Class<?> value();
/***************************************************************************
**
***************************************************************************/
boolean useRef() default false;
}

View File

@ -0,0 +1,47 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/***************************************************************************
**
***************************************************************************/
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenAPIMapKnownEntries
{
/***************************************************************************
**
***************************************************************************/
Class<?> value();
/***************************************************************************
**
***************************************************************************/
boolean useRef() default false;
}

View File

@ -0,0 +1,47 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/***************************************************************************
**
***************************************************************************/
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenAPIMapValueType
{
/***************************************************************************
**
***************************************************************************/
Class<?> value();
/***************************************************************************
**
***************************************************************************/
boolean useRef() default false;
}

View File

@ -0,0 +1,57 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/***************************************************************************
**
***************************************************************************/
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenAPIOneOf
{
/***************************************************************************
**
***************************************************************************/
Mode mode() default Mode.PERMITTED_SUBCLASSES;
/***************************************************************************
**
***************************************************************************/
Class<?>[] options() default { };
/***************************************************************************
**
***************************************************************************/
enum Mode
{
PERMITTED_SUBCLASSES,
SPECIFIED_LIST
}
}

View File

@ -0,0 +1,546 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs;
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.javalin.QJavalinUtils;
import com.kingsrook.qqq.middleware.javalin.executors.AbstractMiddlewareExecutor;
import com.kingsrook.qqq.middleware.javalin.executors.ExecutorSessionUtils;
import com.kingsrook.qqq.middleware.javalin.executors.io.AbstractMiddlewareInput;
import com.kingsrook.qqq.middleware.javalin.executors.io.AbstractMiddlewareOutputInterface;
import com.kingsrook.qqq.openapi.model.Content;
import com.kingsrook.qqq.openapi.model.Method;
import com.kingsrook.qqq.openapi.model.Parameter;
import com.kingsrook.qqq.openapi.model.RequestBody;
import com.kingsrook.qqq.openapi.model.Response;
import com.kingsrook.qqq.openapi.model.Schema;
import io.javalin.apibuilder.ApiBuilder;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import org.apache.commons.lang.NotImplementedException;
import org.json.JSONObject;
/*******************************************************************************
** Base class for individual endpoint specs.
** e.g., one path, that has one "spec" (a "Method" in openapi structure),
** with one implementation (executor + input & output)
*******************************************************************************/
public abstract class AbstractEndpointSpec<
INPUT extends AbstractMiddlewareInput,
OUTPUT extends AbstractMiddlewareOutputInterface,
EXECUTOR extends AbstractMiddlewareExecutor<INPUT, ? super OUTPUT>>
{
private static final QLogger LOG = QLogger.getLogger(AbstractEndpointSpec.class);
protected QInstance qInstance;
private List<Parameter> memoizedRequestParameters = null;
private RequestBody memoizedRequestBody = null;
/***************************************************************************
** build the endpoint's input object from a javalin context
***************************************************************************/
public abstract INPUT buildInput(Context context) throws Exception;
/***************************************************************************
** build the endpoint's http response (written to the javalin context) from
** an execution output object
***************************************************************************/
public abstract void handleOutput(Context context, OUTPUT output) throws Exception;
/***************************************************************************
** Construct a new instance of the executor class, based on type-argument
***************************************************************************/
@SuppressWarnings("unchecked")
public EXECUTOR newExecutor()
{
Object object = newObjectFromTypeArgument(2);
return (EXECUTOR) object;
}
/***************************************************************************
** Construct a new instance of the output class, based on type-argument
***************************************************************************/
@SuppressWarnings("unchecked")
public OUTPUT newOutput()
{
Object object = newObjectFromTypeArgument(1);
return (OUTPUT) object;
}
/***************************************************************************
** credit: https://www.baeldung.com/java-generic-type-find-class-runtime
***************************************************************************/
private Object newObjectFromTypeArgument(int argumentIndex)
{
try
{
Type superClass = getClass().getGenericSuperclass();
Type actualTypeArgument = ((ParameterizedType) superClass).getActualTypeArguments()[argumentIndex];
String className = actualTypeArgument.getTypeName().replaceAll("<.*", "");
Class<?> aClass = Class.forName(className);
return (aClass.getConstructor().newInstance());
}
catch(Exception e)
{
throw (new QRuntimeException("Failed to reflectively create new object from type argument", e));
}
}
/***************************************************************************
** define a javalin route for the spec
***************************************************************************/
public void defineRoute(String versionBasePath)
{
CompleteOperation completeOperation = defineCompleteOperation();
String fullPath = "/qqq/" + versionBasePath + completeOperation.getPath();
fullPath = fullPath.replaceAll("/+", "/");
final Handler handler = context -> serveRequest(context);
switch(completeOperation.getHttpMethod())
{
case GET -> ApiBuilder.get(fullPath, handler);
case POST -> ApiBuilder.post(fullPath, handler);
case PUT -> ApiBuilder.put(fullPath, handler);
case PATCH -> ApiBuilder.patch(fullPath, handler);
case DELETE -> ApiBuilder.delete(fullPath, handler);
default -> throw new IllegalStateException("Unexpected value: " + completeOperation.getHttpMethod());
}
}
/***************************************************************************
**
***************************************************************************/
public OUTPUT serveRequest(Context context) throws Exception
{
try
{
if(isSecured())
{
ExecutorSessionUtils.setupSession(context, qInstance);
}
else
{
QContext.setQInstance(qInstance);
}
INPUT input = buildInput(context);
EXECUTOR executor = newExecutor();
OUTPUT output = newOutput();
executor.execute(input, output);
handleOutput(context, output);
return (output);
}
catch(Exception e)
{
handleException(context, e);
return (null);
}
finally
{
QContext.clear();
}
}
/***************************************************************************
**
***************************************************************************/
protected void handleException(Context context, Exception e)
{
QJavalinUtils.handleException(null, context, e);
}
/***************************************************************************
**
***************************************************************************/
public Method defineMethod()
{
BasicOperation basicOperation = defineBasicOperation();
Method method = new Method()
.withTag(basicOperation.getTag().getText())
.withSummary(basicOperation.getShortSummary())
.withDescription(basicOperation.getLongDescription())
.withParameters(defineRequestParameters())
.withRequestBody(defineRequestBody())
.withResponses(defineResponses());
customizeMethod(method);
return (method);
}
/***************************************************************************
**
***************************************************************************/
protected void customizeMethod(Method method)
{
}
/***************************************************************************
**
***************************************************************************/
public BasicOperation defineBasicOperation()
{
throw new NotImplementedException(getClass().getSimpleName() + " did not implement defineBasicOperation or defineMethod");
}
/***************************************************************************
**
***************************************************************************/
public CompleteOperation defineCompleteOperation()
{
CompleteOperation completeOperation = new CompleteOperation(defineBasicOperation());
completeOperation.setMethod(defineMethod());
return completeOperation;
}
/***************************************************************************
**
***************************************************************************/
public boolean isSecured()
{
return (true);
}
/***************************************************************************
**
***************************************************************************/
public BasicResponse defineBasicSuccessResponse()
{
return (null);
}
/***************************************************************************
**
***************************************************************************/
public List<BasicResponse> defineAdditionalBasicResponses()
{
return (Collections.emptyList());
}
/***************************************************************************
**
***************************************************************************/
public Map<Integer, Response> defineResponses()
{
BasicResponse standardSuccessResponse = defineBasicSuccessResponse();
List<BasicResponse> basicResponseList = defineAdditionalBasicResponses();
List<BasicResponse> allBasicResponses = new ArrayList<>();
if(standardSuccessResponse != null)
{
allBasicResponses.add(standardSuccessResponse);
}
if(basicResponseList != null)
{
allBasicResponses.addAll(basicResponseList);
}
Map<Integer, Response> rs = new HashMap<>();
for(BasicResponse basicResponse : allBasicResponses)
{
Response responseObject = rs.computeIfAbsent(basicResponse.status().getCode(), (k) -> new Response());
responseObject.withDescription(basicResponse.description());
Map<String, Content> content = responseObject.getContent();
if(content == null)
{
content = new HashMap<>();
responseObject.setContent(content);
}
content.put(basicResponse.contentType(), new Content()
.withSchema(new Schema().withRefToSchema(basicResponse.schemaRefName()))
.withExamples(basicResponse.examples())
);
}
return rs;
}
/***************************************************************************
**
***************************************************************************/
public List<Parameter> defineRequestParameters()
{
return Collections.emptyList();
}
/***************************************************************************
**
***************************************************************************/
public RequestBody defineRequestBody()
{
return null;
}
/***************************************************************************
**
***************************************************************************/
public Map<String, Schema> defineComponentSchemas()
{
return Collections.emptyMap();
}
/***************************************************************************
**
***************************************************************************/
protected String getRequestParam(Context context, String name)
{
for(Parameter parameter : CollectionUtils.nonNullList(getMemoizedRequestParameters()))
{
if(parameter.getName().equals(name))
{
String value = switch(parameter.getIn())
{
case "path" -> context.pathParam(parameter.getName());
case "query" -> context.queryParam(parameter.getName());
default -> throw new IllegalStateException("Unexpected 'in' value for parameter [" + parameter.getName() + "]: " + parameter.getIn());
};
// todo - validate value vs. required?
// todo - validate value vs. schema?
return (value);
}
}
RequestBody requestBody = getMemoizedRequestBody();
if(requestBody != null)
{
String requestContentType = context.contentType();
if(requestContentType != null)
{
requestContentType = requestContentType.toLowerCase().replaceAll(" *;.*", "");
}
Content contentSpec = requestBody.getContent().get(requestContentType);
if(contentSpec != null && "object".equals(contentSpec.getSchema().getType()))
{
if(contentSpec.getSchema().getProperties() != null && contentSpec.getSchema().getProperties().containsKey(name))
{
String value = null;
if(ContentType.MULTIPART_FORM_DATA.getMimeType().equals(requestContentType))
{
value = context.formParam(name);
}
else if(ContentType.APPLICATION_JSON.getMimeType().equals(requestContentType))
{
/////////////////////////////////////////////////////////////////////////////
// avoid re-parsing the JSON object if getting multiple attributes from it //
// by stashing it in a (request) attribute. //
/////////////////////////////////////////////////////////////////////////////
Object jsonBodyAttribute = context.attribute("jsonBody");
JSONObject jsonObject = null;
if(jsonBodyAttribute instanceof JSONObject jo)
{
jsonObject = jo;
}
if(jsonObject == null)
{
jsonObject = new JSONObject(context.body());
context.attribute("jsonBody", jsonObject);
}
if(jsonObject.has(name))
{
value = jsonObject.getString(name);
}
}
else
{
LOG.warn("Unhandled content type: " + requestContentType);
}
return (value);
}
}
}
return (null);
}
/***************************************************************************
**
***************************************************************************/
protected Map<String, Serializable> getRequestParamMap(Context context, String name)
{
String requestParam = getRequestParam(context, name);
if(requestParam == null)
{
return (null);
}
JSONObject jsonObject = new JSONObject(requestParam);
Map<String, Serializable> map = new LinkedHashMap<>();
for(String key : jsonObject.keySet())
{
Object value = jsonObject.get(key);
if(value instanceof Serializable s)
{
map.put(key, s);
}
else
{
throw (new QRuntimeException("Non-serializable value in param map under key [" + name + "][" + key + "]: " + value.getClass().getSimpleName()));
}
}
return (map);
}
/***************************************************************************
**
***************************************************************************/
protected Integer getRequestParamInteger(Context context, String name)
{
String requestParam = getRequestParam(context, name);
return ValueUtils.getValueAsInteger(requestParam);
}
/***************************************************************************
** For initial setup when server boots, set the qInstance - but also,
** e.g., for development, to do a hot-swap.
***************************************************************************/
public void setQInstance(QInstance qInstance)
{
this.qInstance = qInstance;
//////////////////////////////////////////////////////////////////
// if we did a hot swap, we should clear these memoizations too //
//////////////////////////////////////////////////////////////////
memoizedRequestParameters = null;
memoizedRequestBody = null;
}
/***************************************************************************
** An original implementation here was prone to race-condition-based errors:
*
** if(memoizedRequestParameters == null)
** {
** memoizedRequestParameters = CollectionUtils.nonNullList(defineRequestParameters());
** }
** return (memoizedRequestParameters);
**
** where between the defineX call and the return, if another thread cleared the
** memoizedX field, then a null would be returned, which isn't supposed to happen.
** Thus, this implementation which looks a bit more convoluted, but should
** be safe(r).
***************************************************************************/
private List<Parameter> getMemoizedRequestParameters()
{
List<Parameter> rs = memoizedRequestParameters;
if(rs == null)
{
rs = CollectionUtils.nonNullList(defineRequestParameters());
memoizedRequestParameters = rs;
}
return (rs);
}
/***************************************************************************
**
***************************************************************************/
private RequestBody getMemoizedRequestBody()
{
RequestBody rs = memoizedRequestBody;
if(rs == null)
{
rs = defineRequestBody();
memoizedRequestBody = rs;
}
return (rs);
}
}

View File

@ -0,0 +1,361 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.SchemaBuilder;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.openapi.model.Components;
import com.kingsrook.qqq.openapi.model.Contact;
import com.kingsrook.qqq.openapi.model.Content;
import com.kingsrook.qqq.openapi.model.Example;
import com.kingsrook.qqq.openapi.model.Info;
import com.kingsrook.qqq.openapi.model.Method;
import com.kingsrook.qqq.openapi.model.OpenAPI;
import com.kingsrook.qqq.openapi.model.Path;
import com.kingsrook.qqq.openapi.model.Response;
import com.kingsrook.qqq.openapi.model.Schema;
import com.kingsrook.qqq.openapi.model.SecurityScheme;
import com.kingsrook.qqq.openapi.model.SecuritySchemeType;
import com.kingsrook.qqq.openapi.model.Type;
import io.javalin.apibuilder.EndpointGroup;
/*******************************************************************************
** Baseclass that combines multiple specs together into a single "version" of
** the full qqq middleware.
*******************************************************************************/
public abstract class AbstractMiddlewareVersion
{
public static final QLogger LOG = QLogger.getLogger(AbstractMiddlewareVersion.class);
/***************************************************************************
**
***************************************************************************/
public abstract String getVersion();
/***************************************************************************
** hey - don't re-construct the endpoint-spec objects inside this method...
***************************************************************************/
public abstract List<AbstractEndpointSpec<?, ?, ?>> getEndpointSpecs();
/***************************************************************************
**
***************************************************************************/
public EndpointGroup getJavalinEndpointGroup(QInstance qInstance)
{
return (() ->
{
for(AbstractEndpointSpec<?, ?, ?> spec : CollectionUtils.nonNullList(getEndpointSpecs()))
{
spec.defineRoute("/" + getVersion() + "/");
}
});
}
/***************************************************************************
** For initial setup when server boots, set the qInstance - but also,
** e.g., for development, to do a hot-swap.
***************************************************************************/
public void setQInstance(QInstance qInstance)
{
getEndpointSpecs().forEach(spec -> spec.setQInstance(qInstance));
}
/*******************************************************************************
**
*******************************************************************************/
public OpenAPI generateOpenAPIModel(String basePath) throws QException
{
List<AbstractEndpointSpec<?, ?, ?>> list = getEndpointSpecs();
Map<String, Path> paths = new LinkedHashMap<>();
Map<String, Example> componentExamples = new LinkedHashMap<>();
Set<Class<?>> componentClasses = new HashSet<>();
Map<String, Schema> componentSchemas = new TreeMap<>();
buildComponentSchemasFromComponentsPackage(componentSchemas, componentClasses);
String sessionUuidCookieSchemeName = "sessionUuidCookie";
SecurityScheme sessionUuidCookieScheme = new SecurityScheme()
.withType(SecuritySchemeType.API_KEY)
.withName("sessionUUID")
.withIn("cookie");
for(AbstractEndpointSpec<?, ?, ?> spec : list)
{
CompleteOperation completeOperation = spec.defineCompleteOperation();
String fullPath = ("/" + basePath + "/" + getVersion() + "/" + completeOperation.getPath()).replaceAll("/+", "/");
Path path = paths.computeIfAbsent(fullPath, (k) -> new Path());
Method method = completeOperation.getMethod();
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if this spec is supposed to be secured, but no security has been applied, then add our default security //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(spec.isSecured() && method.getSecurity() == null)
{
//////////////////////////////////////////////////////////////////////////////
// the N/A here refers to the lack of a 'scope' for this kind of permission //
//////////////////////////////////////////////////////////////////////////////
method.withSecurity(List.of(Map.of(sessionUuidCookieSchemeName, List.of("N/A"))));
}
convertMethodSchemasToRefs(method, componentClasses);
switch(completeOperation.getHttpMethod())
{
case GET ->
{
warnIfPathMethodAlreadyUsed(path.getGet(), completeOperation, spec);
path.withGet(method);
}
case POST ->
{
warnIfPathMethodAlreadyUsed(path.getPost(), completeOperation, spec);
path.withPost(method);
}
case PUT ->
{
warnIfPathMethodAlreadyUsed(path.getPut(), completeOperation, spec);
path.withPut(method);
}
case PATCH ->
{
warnIfPathMethodAlreadyUsed(path.getPatch(), completeOperation, spec);
path.withPatch(method);
}
case DELETE ->
{
warnIfPathMethodAlreadyUsed(path.getDelete(), completeOperation, spec);
path.withDelete(method);
}
default -> throw new IllegalStateException("Unexpected value: " + completeOperation.getHttpMethod());
}
for(Map.Entry<String, Schema> entry : CollectionUtils.nonNullMap(spec.defineComponentSchemas()).entrySet())
{
if(componentSchemas.containsKey(entry.getKey()))
{
LOG.warn("More than one endpoint spec defined a componentSchema named: " + entry.getKey() + ". The last one encountered (from " + spec.getClass().getSimpleName() + ") will be used.");
}
componentSchemas.put(entry.getKey(), entry.getValue());
}
}
OpenAPI openAPI = new OpenAPI();
openAPI.withInfo(new Info()
.withVersion(getVersion())
.withTitle("QQQ Middleware API")
.withDescription(getDescription())
.withContact(new Contact().withEmail("contact@kingsrook.com"))
);
openAPI.withPaths(paths);
openAPI.withComponents(new Components()
.withSchemas(componentSchemas)
.withExamples(componentExamples)
.withSecuritySchemes(Map.of(sessionUuidCookieSchemeName, sessionUuidCookieScheme))
);
return openAPI;
}
/***************************************************************************
**
***************************************************************************/
private void buildComponentSchemasFromComponentsPackage(Map<String, Schema> componentSchemas, Set<Class<?>> componentClasses) throws QException
{
try
{
////////////////////////////////////////////////////
// find all classes in the components sub-package //
////////////////////////////////////////////////////
String packageName = getClass().getPackageName();
List<Class<?>> classesInPackage = ClassPathUtils.getClassesInPackage(packageName);
for(Class<?> c : classesInPackage)
{
if(c.getPackageName().matches(".*\\bcomponents\\b.*"))
{
componentClasses.add(c);
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
// now that we know that full set, make any references to others schemas in those objects be via Ref //
///////////////////////////////////////////////////////////////////////////////////////////////////////
for(Class<?> c : componentClasses)
{
Object o = null;
try
{
o = c.getConstructor().newInstance();
}
catch(Exception nsme)
{
///////////////////////////////////////
// fine, assume we can't do toSchema //
///////////////////////////////////////
}
Schema schema = null;
if(o instanceof ToSchema toSchema)
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// just in case a custom implementation of toSchema is provided (e.g., to go around a wrapped object or some-such) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
schema = toSchema.toSchema();
}
else
{
schema = new SchemaBuilder().classToSchema(c);
}
convertSchemaToRefs(schema, componentClasses);
componentSchemas.put(c.getSimpleName(), schema);
}
}
catch(Exception e)
{
throw (new QException("Error building component schemas from components package", e));
}
}
/***************************************************************************
**
***************************************************************************/
private void convertMethodSchemasToRefs(Method method, Set<Class<?>> componentClasses)
{
for(Response response : method.getResponses().values())
{
for(Content content : response.getContent().values())
{
Schema schema = content.getSchema();
convertSchemaToRefs(schema, componentClasses);
}
}
}
/***************************************************************************
**
***************************************************************************/
private void convertSchemaToRefs(Schema schema, Set<Class<?>> componentClasses)
{
if(schema.getItems() instanceof SchemaBuilder.SchemaFromBuilder itemSchemaFromBuilder && componentClasses.contains(itemSchemaFromBuilder.getOriginalClass()))
{
schema.getItems().withRefToSchema(itemSchemaFromBuilder.getOriginalClass().getSimpleName());
schema.getItems().setProperties(null);
schema.getItems().setType((Type) null);
}
else if(schema.getItems() != null)
{
convertSchemaToRefs(schema.getItems(), componentClasses);
}
if(schema.getProperties() != null)
{
for(Schema propertySchema : schema.getProperties().values())
{
if(propertySchema instanceof SchemaBuilder.SchemaFromBuilder propertySchemaFromBuilder && componentClasses.contains(propertySchemaFromBuilder.getOriginalClass()))
{
propertySchema.withRefToSchema(propertySchemaFromBuilder.getOriginalClass().getSimpleName());
propertySchema.setProperties(null);
propertySchema.setType((Type) null);
}
else
{
convertSchemaToRefs(propertySchema, componentClasses);
}
}
}
}
/***************************************************************************
**
***************************************************************************/
private static String getDescription()
{
return """
## Intro
This is the definition of the standard API implemented by QQQ Middleware.
Developers of QQQ Frontends (e.g., javascript libraries, or native applications) use this API to access
a QQQ Backend server.
As such, this API itself is not concerned with any of the application-level details of any particular
application built using QQQ. Instead, this API is all about the generic endpoints used for any application
built on QQQ. For example, many endpoints work with a `${table}` path parameter - whose possible values
are defined by the application - but which are not known to this API.
## Flow
The typical flow of a user (as implemented in a frontend that utilizes this API) looks like:
1. Frontend calls `.../metaData/authentication`, to know what type of authentication provider is used by the backend, and display an appropriate UI to the user for authenticating.
2. User authenticates in frontend, as required for the authentication provider.
3. Frontend calls `.../manageSession`, providing authentication details (e.g., an accessToken or other credentials) to the backend.
4. The response from the `manageSession` call (assuming success), sets the `sessionUUID` Cookie, which should be included in all subsequent requests for authentication.
5. After the user is authenticated, the frontend calls `.../metaData`, to discover the apps, tables, processes, etc, that the application is made up of (and that the authenticated user has permission to access).
6. As the user interacts with apps, tables, process, etc, the frontend utilizes the appropriate endpoints as required.
""";
}
/***************************************************************************
**
***************************************************************************/
public void warnIfPathMethodAlreadyUsed(Method existing, CompleteOperation completeOperation, AbstractEndpointSpec<?, ?, ?> spec)
{
if(existing != null)
{
LOG.warn("More than one endpoint spec for version " + getVersion() + " defined a " + completeOperation.getHttpMethod() + " at path: " + completeOperation.getPath() + ". The last one encountered (from " + spec.getClass().getSimpleName() + ") will be used.");
}
}
}

View File

@ -0,0 +1,194 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs;
import com.kingsrook.qqq.openapi.model.HttpMethod;
/*******************************************************************************
** Basic definition of an operation (e.g., an endpoint exposed in the API).
*******************************************************************************/
public class BasicOperation
{
private String path;
private HttpMethod httpMethod;
private TagsInterface tag;
private String shortSummary;
private String longDescription;
/*******************************************************************************
** Getter for path
*******************************************************************************/
public String getPath()
{
return (this.path);
}
/*******************************************************************************
** Setter for path
*******************************************************************************/
public void setPath(String path)
{
this.path = path;
}
/*******************************************************************************
** Fluent setter for path
*******************************************************************************/
public BasicOperation withPath(String path)
{
this.path = path;
return (this);
}
/*******************************************************************************
** Getter for httpMethod
*******************************************************************************/
public HttpMethod getHttpMethod()
{
return (this.httpMethod);
}
/*******************************************************************************
** Setter for httpMethod
*******************************************************************************/
public void setHttpMethod(HttpMethod httpMethod)
{
this.httpMethod = httpMethod;
}
/*******************************************************************************
** Fluent setter for httpMethod
*******************************************************************************/
public BasicOperation withHttpMethod(HttpMethod httpMethod)
{
this.httpMethod = httpMethod;
return (this);
}
/*******************************************************************************
** Getter for tag
*******************************************************************************/
public TagsInterface getTag()
{
return (this.tag);
}
/*******************************************************************************
** Setter for tag
*******************************************************************************/
public void setTag(TagsInterface tag)
{
this.tag = tag;
}
/*******************************************************************************
** Fluent setter for tag
*******************************************************************************/
public BasicOperation withTag(TagsInterface tag)
{
this.tag = tag;
return (this);
}
/*******************************************************************************
** Getter for shortSummary
*******************************************************************************/
public String getShortSummary()
{
return (this.shortSummary);
}
/*******************************************************************************
** Setter for shortSummary
*******************************************************************************/
public void setShortSummary(String shortSummary)
{
this.shortSummary = shortSummary;
}
/*******************************************************************************
** Fluent setter for shortSummary
*******************************************************************************/
public BasicOperation withShortSummary(String shortSummary)
{
this.shortSummary = shortSummary;
return (this);
}
/*******************************************************************************
** Getter for longDescription
*******************************************************************************/
public String getLongDescription()
{
return (this.longDescription);
}
/*******************************************************************************
** Setter for longDescription
*******************************************************************************/
public void setLongDescription(String longDescription)
{
this.longDescription = longDescription;
}
/*******************************************************************************
** Fluent setter for longDescription
*******************************************************************************/
public BasicOperation withLongDescription(String longDescription)
{
this.longDescription = longDescription;
return (this);
}
}

View File

@ -0,0 +1,79 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs;
import java.util.Map;
import com.kingsrook.qqq.openapi.model.Example;
import io.javalin.http.ContentType;
import io.javalin.http.HttpStatus;
/***************************************************************************
** Basic version of a response from a spec/endpoint.
***************************************************************************/
public record BasicResponse(String contentType, HttpStatus status, String description, String schemaRefName, Map<String, Example> examples)
{
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public BasicResponse(String description, String schemaRefName)
{
this(ContentType.APPLICATION_JSON.getMimeType(), HttpStatus.OK, description, schemaRefName, null);
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public BasicResponse(String description, String schemaRefName, Map<String, Example> examples)
{
this(ContentType.APPLICATION_JSON.getMimeType(), HttpStatus.OK, description, schemaRefName, examples);
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public BasicResponse(HttpStatus status, String description, String schemaRefName)
{
this(ContentType.APPLICATION_JSON.getMimeType(), status, description, schemaRefName, null);
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public BasicResponse(HttpStatus status, String description, String schemaRefName, Map<String, Example> examples)
{
this(ContentType.APPLICATION_JSON.getMimeType(), status, description, schemaRefName, examples);
}
}

View File

@ -0,0 +1,80 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs;
import com.kingsrook.qqq.openapi.model.Method;
/*******************************************************************************
** Extension of a BasicOperation that adds the full openAPI Method object.
*******************************************************************************/
public class CompleteOperation extends BasicOperation
{
private Method method;
/***************************************************************************
**
***************************************************************************/
public CompleteOperation(BasicOperation basicOperation)
{
setPath(basicOperation.getPath());
setHttpMethod(basicOperation.getHttpMethod());
setTag(basicOperation.getTag());
setLongDescription(basicOperation.getLongDescription());
setShortSummary(basicOperation.getShortSummary());
}
/*******************************************************************************
** Getter for method
*******************************************************************************/
public Method getMethod()
{
return (this.method);
}
/*******************************************************************************
** Setter for method
*******************************************************************************/
public void setMethod(Method method)
{
this.method = method;
}
/*******************************************************************************
** Fluent setter for method
*******************************************************************************/
public CompleteOperation withMethod(Method method)
{
this.method = method;
return (this);
}
}

View File

@ -0,0 +1,34 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs;
/*******************************************************************************
**
*******************************************************************************/
public interface TagsInterface
{
/***************************************************************************
**
***************************************************************************/
String getText();
}

View File

@ -0,0 +1,137 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1;
import java.util.LinkedHashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.middleware.javalin.executors.AuthenticationMetaDataExecutor;
import com.kingsrook.qqq.middleware.javalin.executors.io.AuthenticationMetaDataInput;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.AuthenticationMetaDataResponseV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
import com.kingsrook.qqq.openapi.model.Example;
import com.kingsrook.qqq.openapi.model.HttpMethod;
import com.kingsrook.qqq.openapi.model.Schema;
import io.javalin.http.Context;
/*******************************************************************************
**
*******************************************************************************/
public class AuthenticationMetaDataSpecV1 extends AbstractEndpointSpec<AuthenticationMetaDataInput, AuthenticationMetaDataResponseV1, AuthenticationMetaDataExecutor>
{
/***************************************************************************
**
***************************************************************************/
public BasicOperation defineBasicOperation()
{
return new BasicOperation()
.withPath("/metaData/authentication")
.withHttpMethod(HttpMethod.GET)
.withTag(TagsV1.AUTHENTICATION)
.withShortSummary("Get authentication metaData")
.withLongDescription("""
For a frontend to determine which authentication provider or mechanism to use, it should begin its lifecycle
by requesting this metaData object, and inspecting the `type` property in the response.
Note that this endpoint is not secured, as its purpose is to be called as part of the workflow that results
in a user being authenticated."""
);
}
/***************************************************************************
**
***************************************************************************/
public boolean isSecured()
{
return (false);
}
/***************************************************************************
**
***************************************************************************/
@Override
public AuthenticationMetaDataInput buildInput(Context context) throws Exception
{
AuthenticationMetaDataInput input = new AuthenticationMetaDataInput();
return (input);
}
/***************************************************************************
**
***************************************************************************/
@Override
public Map<String, Schema> defineComponentSchemas()
{
return Map.of(AuthenticationMetaDataResponseV1.class.getSimpleName(), new AuthenticationMetaDataResponseV1().toSchema());
}
/***************************************************************************
**
***************************************************************************/
@Override
public BasicResponse defineBasicSuccessResponse()
{
Map<String, Example> examples = new LinkedHashMap<>();
examples.put("For FULLY_ANONYMOUS type", new Example()
.withValue(new AuthenticationMetaDataResponseV1()
.withType(QAuthenticationType.FULLY_ANONYMOUS.name())
.withName("anonymous")));
examples.put("For AUTH_0 type", new Example()
.withValue(new AuthenticationMetaDataResponseV1()
.withType(QAuthenticationType.AUTH_0.name())
.withName("auth0")
.withValues(new AuthenticationMetaDataResponseV1.Auth0Values()
.withClientId("abcdefg1234567")
.withBaseUrl("https://myapp.auth0.com/")
.withAudience("myapp.mydomain.com"))));
return new BasicResponse("Successful Response", AuthenticationMetaDataResponseV1.class.getSimpleName(), examples);
}
/***************************************************************************
**
***************************************************************************/
@Override
public void handleOutput(Context context, AuthenticationMetaDataResponseV1 output) throws Exception
{
context.result(JsonUtils.toJson(output));
}
}

View File

@ -0,0 +1,213 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import com.kingsrook.qqq.middleware.javalin.executors.ManageSessionExecutor;
import com.kingsrook.qqq.middleware.javalin.executors.io.ManageSessionInput;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.BasicErrorResponseV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.ManageSessionResponseV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.ProcessSpecUtilsV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
import com.kingsrook.qqq.openapi.model.Content;
import com.kingsrook.qqq.openapi.model.Example;
import com.kingsrook.qqq.openapi.model.HttpMethod;
import com.kingsrook.qqq.openapi.model.RequestBody;
import com.kingsrook.qqq.openapi.model.Schema;
import com.kingsrook.qqq.openapi.model.Type;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.HttpStatus;
/*******************************************************************************
**
*******************************************************************************/
public class ManageSessionSpecV1 extends AbstractEndpointSpec<ManageSessionInput, ManageSessionResponseV1, ManageSessionExecutor>
{
/***************************************************************************
**
***************************************************************************/
@Override
public BasicOperation defineBasicOperation()
{
return new BasicOperation()
.withPath("/manageSession")
.withHttpMethod(HttpMethod.POST)
.withTag(TagsV1.AUTHENTICATION)
.withShortSummary("Create a session")
.withLongDescription("""
After a frontend authenticates the user as per the requirements of the authentication provider specified by the
`type` field in the `metaData/authentication` response, data from that authentication provider should be posted
to this endpoint, to create a session within the QQQ application.
The response object will include a session identifier (`uuid`) to authenticate the user in subsequent API calls.""");
}
/***************************************************************************
**
***************************************************************************/
public boolean isSecured()
{
return (false);
}
/***************************************************************************
**
***************************************************************************/
@Override
public ManageSessionResponseV1 serveRequest(Context context) throws Exception
{
ManageSessionResponseV1 result = super.serveRequest(context);
if(result != null)
{
String sessionUuid = result.getUuid();
context.cookie(QJavalinImplementation.SESSION_UUID_COOKIE_NAME, sessionUuid, QJavalinImplementation.SESSION_COOKIE_AGE);
}
return (result);
}
/***************************************************************************
**
***************************************************************************/
@Override
public RequestBody defineRequestBody()
{
return new RequestBody()
.withRequired(true)
.withContent(MapBuilder.of(ContentType.JSON, new Content()
.withSchema(new Schema()
.withDescription("Data required to create the session. Specific needs may vary based on the AuthenticationModule type in the QQQ Backend.")
.withType(Type.OBJECT)
.withProperty("accessToken", new Schema()
.withType(Type.STRING)
.withDescription("An access token from a downstream authentication provider (e.g., Auth0), to use as the basis for authentication and authorization.")
)
)
));
}
/***************************************************************************
**
***************************************************************************/
@Override
public ManageSessionInput buildInput(Context context) throws Exception
{
ManageSessionInput manageSessionInput = new ManageSessionInput();
manageSessionInput.setAccessToken(getRequestParam(context, "accessToken"));
return (manageSessionInput);
}
/***************************************************************************
**
***************************************************************************/
@Override
public BasicResponse defineBasicSuccessResponse()
{
Map<String, Example> examples = new LinkedHashMap<>();
examples.put("With no custom values", new Example().withValue(new ManageSessionResponseV1()
.withUuid(ProcessSpecUtilsV1.EXAMPLE_PROCESS_UUID)
));
examples.put("With custom values", new Example().withValue(new ManageSessionResponseV1()
.withUuid(ProcessSpecUtilsV1.EXAMPLE_JOB_UUID)
.withValues(MapBuilder.of(LinkedHashMap<String, Serializable>::new)
.with("region", "US")
.with("userCategoryId", 47)
.build()
)
));
return new BasicResponse("Successful response - session has been created",
ManageSessionResponseV1.class.getSimpleName(),
examples);
}
/***************************************************************************
**
***************************************************************************/
@Override
public Map<String, Schema> defineComponentSchemas()
{
return Map.of(
ManageSessionResponseV1.class.getSimpleName(), new ManageSessionResponseV1().toSchema(),
BasicErrorResponseV1.class.getSimpleName(), new BasicErrorResponseV1().toSchema()
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public List<BasicResponse> defineAdditionalBasicResponses()
{
Map<String, Example> examples = new LinkedHashMap<>();
examples.put("Invalid token", new Example().withValue(new BasicErrorResponseV1().withError("Unable to decode access token.")));
return List.of(
new BasicResponse(HttpStatus.UNAUTHORIZED,
"Authentication error - session was not created",
BasicErrorResponseV1.class.getSimpleName(),
examples
)
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public void handleOutput(Context context, ManageSessionResponseV1 output) throws Exception
{
context.result(JsonUtils.toJson(output));
}
}

View File

@ -0,0 +1,282 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
import com.kingsrook.qqq.backend.core.context.CapturedContext;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
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.authentication.QAuthenticationMetaData;
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.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.middleware.javalin.executors.MetaDataExecutor;
import com.kingsrook.qqq.middleware.javalin.executors.io.MetaDataInput;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.MetaDataResponseV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
import com.kingsrook.qqq.openapi.model.Example;
import com.kingsrook.qqq.openapi.model.HttpMethod;
import com.kingsrook.qqq.openapi.model.In;
import com.kingsrook.qqq.openapi.model.Parameter;
import com.kingsrook.qqq.openapi.model.Schema;
import com.kingsrook.qqq.openapi.model.Type;
import io.javalin.http.Context;
/*******************************************************************************
**
*******************************************************************************/
public class MetaDataSpecV1 extends AbstractEndpointSpec<MetaDataInput, MetaDataResponseV1, MetaDataExecutor>
{
/***************************************************************************
**
***************************************************************************/
public BasicOperation defineBasicOperation()
{
return new BasicOperation()
.withPath("/metaData")
.withHttpMethod(HttpMethod.GET)
.withTag(TagsV1.GENERAL)
.withShortSummary("Get instance metaData")
.withLongDescription("""
Load the overall metadata, as is relevant to a frontend, for the entire application, with permissions applied, as per the
authenticated user.
This includes:
- Apps (both as a map of name to AppMetaData (`apps`), but also as a tree (`appTree`), for presenting
hierarchical navigation),
- Tables (but without all details, e.g., fields),
- Processes (also without full details, e.g., screens),
- Reports
- Widgets
- Branding
- Help Contents
- Environment values
"""
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public List<Parameter> defineRequestParameters()
{
return List.of(
new Parameter()
.withName("frontendName")
.withDescription("""
Name of the frontend requesting the meta-data.
Generally a QQQ frontend library, unless a custom application frontend has been built.""")
.withIn(In.QUERY)
.withSchema(new Schema().withType(Type.STRING))
.withExample("qqq-frontend-material-dashboard"),
new Parameter()
.withName("frontendVersion")
.withDescription("Version of the frontend requesting the meta-data.")
.withIn(In.QUERY)
.withSchema(new Schema().withType(Type.STRING))
.withExample("0.23.0"),
new Parameter()
.withName("applicationName")
.withDescription("""
Name of the application requesting the meta-data. e.g., an instance of a specific frontend
(i.e., an application might be deployed with 2 different qqq-frontend-material-dashboard frontends,
in which case this attribute allows differentiation between them).""")
.withIn(In.QUERY)
.withSchema(new Schema().withType(Type.STRING))
.withExample("my-admin-web-app"),
new Parameter()
.withName("applicationVersion")
.withDescription("Version of the application requesting the meta-data.")
.withIn(In.QUERY)
.withSchema(new Schema().withType(Type.STRING))
.withExample("20241021")
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public MetaDataInput buildInput(Context context) throws Exception
{
MetaDataInput input = new MetaDataInput();
input.setMiddlewareName("qqq-middleware-javalin");
input.setMiddlewareVersion("v1");
input.setFrontendName(getRequestParam(context, "frontendName"));
input.setFrontendVersion(getRequestParam(context, "frontendVersion"));
input.setApplicationName(getRequestParam(context, "applicationName"));
input.setApplicationVersion(getRequestParam(context, "applicationVersion"));
return (input);
}
/***************************************************************************
**
***************************************************************************/
@Override
public Map<String, Schema> defineComponentSchemas()
{
return Map.of(MetaDataResponseV1.class.getSimpleName(), new MetaDataResponseV1().toSchema());
}
/***************************************************************************
**
***************************************************************************/
@Override
public BasicResponse defineBasicSuccessResponse()
{
Map<String, Example> examples = new HashMap<>();
QInstance exampleInstance = new QInstance();
exampleInstance.setAuthentication(new QAuthenticationMetaData().withName("anonymous").withType(QAuthenticationType.FULLY_ANONYMOUS));
QBackendMetaData exampleBackend = new QBackendMetaData()
.withName("example")
.withBackendType(MemoryBackendModule.class);
exampleInstance.addBackend(exampleBackend);
//////////////////////////////////////
// create stable sorting of entries //
//////////////////////////////////////
TreeSet<Capability> capabilities = new TreeSet<>(Comparator.comparing((Capability c) -> c.name()));
capabilities.addAll(Capability.allReadCapabilities());
capabilities.addAll(Capability.allWriteCapabilities());
QTableMetaData exampleTable = new QTableMetaData()
.withName("person")
.withLabel("Person")
.withBackendName("example")
.withPrimaryKeyField("id")
.withIsHidden(false)
.withIcon(new QIcon().withName("person_outline"))
.withEnabledCapabilities(capabilities)
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
.withField(new QFieldMetaData("id", QFieldType.INTEGER));
exampleInstance.addTable(exampleTable);
QProcessMetaData exampleProcess = new QProcessMetaData()
.withName("samplePersonProcess")
.withLabel("Sample Person Process")
.withTableName("person")
.withIsHidden(false)
.withIcon(new QIcon().withName("person_add"))
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
.withStep(new QFrontendStepMetaData().withName("example"));
exampleInstance.addProcess(exampleProcess);
QAppMetaData childApp = new QAppMetaData()
.withName("childApp")
.withLabel("Child App")
.withIcon(new QIcon().withName("child_friendly"))
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
.withChild(exampleProcess);
exampleInstance.addApp(childApp);
QAppMetaData exampleApp = new QAppMetaData()
.withName("homeApp")
.withLabel("Home App")
.withIcon(new QIcon().withName("home"))
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
.withChild(childApp)
.withChild(exampleTable);
exampleInstance.addApp(exampleApp);
QContext.withTemporaryContext(new CapturedContext(exampleInstance, new QSystemUserSession()), () ->
{
try
{
MetaDataAction metaDataAction = new MetaDataAction();
MetaDataOutput output = metaDataAction.execute(new com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput());
examples.put("Example", new Example()
.withValue(new MetaDataResponseV1()
.withMetaDataOutput(output)
)
);
}
catch(Exception e)
{
examples.put("Example", new Example().withValue("Error building example: " + e.getMessage())
);
}
});
return new BasicResponse("""
Overall metadata for the application.""",
MetaDataResponseV1.class.getSimpleName(),
examples
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public void handleOutput(Context context, MetaDataResponseV1 output) throws Exception
{
context.result(JsonUtils.toJson(output));
}
}

View File

@ -0,0 +1,70 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion;
/*******************************************************************************
**
*******************************************************************************/
public class MiddlewareVersionV1 extends AbstractMiddlewareVersion
{
private static List<AbstractEndpointSpec<?, ?, ?>> list = new ArrayList<>();
static
{
list.add(new AuthenticationMetaDataSpecV1());
list.add(new ManageSessionSpecV1());
list.add(new MetaDataSpecV1());
list.add(new ProcessMetaDataSpecV1());
list.add(new ProcessInitSpecV1());
list.add(new ProcessStepSpecV1());
list.add(new ProcessStatusSpecV1());
}
/***************************************************************************
**
***************************************************************************/
@Override
public String getVersion()
{
return "v1";
}
/***************************************************************************
**
***************************************************************************/
public List<AbstractEndpointSpec<?, ?, ?>> getEndpointSpecs()
{
return (list);
}
}

View File

@ -0,0 +1,268 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1;
import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.middleware.javalin.executors.ProcessInitOrStepExecutor;
import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepInput;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.ProcessInitOrStepOrStatusResponseV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.ProcessSpecUtilsV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
import com.kingsrook.qqq.openapi.model.Content;
import com.kingsrook.qqq.openapi.model.Example;
import com.kingsrook.qqq.openapi.model.HttpMethod;
import com.kingsrook.qqq.openapi.model.In;
import com.kingsrook.qqq.openapi.model.Parameter;
import com.kingsrook.qqq.openapi.model.RequestBody;
import com.kingsrook.qqq.openapi.model.Schema;
import com.kingsrook.qqq.openapi.model.Type;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import org.apache.commons.lang.NotImplementedException;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessInitSpecV1 extends AbstractEndpointSpec<ProcessInitOrStepInput, ProcessInitOrStepOrStatusResponseV1, ProcessInitOrStepExecutor>
{
private static final QLogger LOG = QLogger.getLogger(ProcessInitSpecV1.class);
public static int DEFAULT_ASYNC_STEP_TIMEOUT_MILLIS = 3_000;
/***************************************************************************
**
***************************************************************************/
public BasicOperation defineBasicOperation()
{
return new BasicOperation()
.withPath("/processes/{processName}/init")
.withHttpMethod(HttpMethod.POST)
.withTag(TagsV1.PROCESSES)
.withShortSummary("Initialize a process")
.withLongDescription("""
For a user to start running a process, this endpoint should be called, to start the process
and run its first step(s) (any backend steps before the first frontend step).
Additional process-specific values should posted in a form param named `values`, as JSON object
with keys defined by the process in question.
For a process which needs to operate on a set of records that a user selected, see
`recordsParam`, and `recordIds` or `filterJSON`.
The response will include a `processUUID`, to be included in all subsequent requests relevant
to the process.
Note that this request, if it takes longer than a given threshold* to complete, will return a
a `jobUUID`, which should be sent to the `/processes/{processName}/{processUUID}/status/{jobUUID}`
endpoint, to poll for a status update.
*This threshold has a default value of 3,000 ms., but can be set per-request via the form
parameter `stepTimeoutMillis`.
""");
}
/***************************************************************************
**
***************************************************************************/
@Override
public List<Parameter> defineRequestParameters()
{
return List.of(
new Parameter()
.withName("processName")
.withDescription("Name of the process to initialize")
.withRequired(true)
.withSchema(new Schema().withType(Type.STRING))
.withExample("samplePersonProcess")
.withIn(In.PATH)
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public RequestBody defineRequestBody()
{
return new RequestBody()
.withContent(
ContentType.MULTIPART_FORM_DATA.getMimeType(), new Content()
.withSchema(new Schema()
.withType(Type.OBJECT)
.withProperty("values", new Schema()
.withType(Type.OBJECT)
.withDescription("Process-specific field names and values."))
.withProperty("recordsParam", new Schema()
.withDescription("Specifies which other query-param will contain the indicator of initial records to pass in to the process.")
.withType(Type.STRING)
.withExample("recordIds", new Example().withValue("recordIds"))
.withExample("filterJSON", new Example().withValue("recordIds")))
.withProperty("recordIds", new Schema()
.withDescription("Comma-separated list of ids from the table this process is based on, to use as input records for the process. Needs `recordsParam=recordIds` value to be given as well.")
.withType(Type.STRING)
.withExample("one id", new Example().withValue("1701"))
.withExample("multiple ids", new Example().withValue("42,47")))
.withProperty("filterJSON", new Schema()
.withDescription("JSON encoded QQueryFilter object, to execute against the table this process is based on, to find input records for the process. Needs `recordsParam=filterJSON` value to be given as well.")
.withType(Type.STRING)
.withExample("empty filter (all records)", new Example().withValue("{}"))
.withExample("filter by a condition", new Example().withValue(
JsonUtils.toJson(new QQueryFilter().withCriteria(new QFilterCriteria("id", QCriteriaOperator.LESS_THAN, 10))))
))
.withProperty("stepTimeoutMillis", new Schema()
.withDescription("Optionally change the time that the server will wait for the job before letting it go asynchronous. Default value is 3000.")
.withType(Type.INTEGER)
.withExample("shorter timeout", new Example().withValue("500"))
.withExample("longer timeout", new Example().withValue("60000")))
.withProperty("file", new Schema()
.withType(Type.STRING)
.withFormat("binary")
.withDescription("A file upload, for processes which expect to be initialized with an uploaded file.")
)
)
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public ProcessInitOrStepInput buildInput(Context context) throws Exception
{
ProcessInitOrStepInput processInitOrStepInput = new ProcessInitOrStepInput();
processInitOrStepInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
processInitOrStepInput.setProcessName(getRequestParam(context, "processName"));
processInitOrStepInput.setStepTimeoutMillis(Objects.requireNonNullElse(getRequestParamInteger(context, "stepTimeoutMillis"), DEFAULT_ASYNC_STEP_TIMEOUT_MILLIS));
processInitOrStepInput.setValues(getRequestParamMap(context, "values"));
String recordsParam = getRequestParam(context, "recordsParam");
String recordIds = getRequestParam(context, "recordIds");
String filterJSON = getRequestParam(context, "filterJSON");
QQueryFilter initialRecordsFilter = buildProcessInitRecordsFilter(recordsParam, recordIds, filterJSON, processInitOrStepInput);
processInitOrStepInput.setRecordsFilter(initialRecordsFilter);
// todo - uploaded files
// todo - archive uploaded files?
return (processInitOrStepInput);
}
/*******************************************************************************
**
*******************************************************************************/
private static QQueryFilter buildProcessInitRecordsFilter(String recordsParam, String recordIds, String filterJSON, ProcessInitOrStepInput processInitOrStepInput) throws IOException
{
QProcessMetaData process = QContext.getQInstance().getProcess(processInitOrStepInput.getProcessName());
QTableMetaData table = QContext.getQInstance().getTable(process.getTableName());
if(table == null)
{
LOG.info("No table found in process - so not building an init records filter.");
return (null);
}
String primaryKeyField = table.getPrimaryKeyField();
if(StringUtils.hasContent(recordsParam))
{
return switch(recordsParam)
{
case "recordIds" ->
{
Serializable[] idStrings = recordIds.split(",");
yield (new QQueryFilter().withCriteria(new QFilterCriteria()
.withFieldName(primaryKeyField)
.withOperator(QCriteriaOperator.IN)
.withValues(Arrays.stream(idStrings).toList())));
}
case "filterJSON" -> (JsonUtils.toObject(filterJSON, QQueryFilter.class));
case "filterId" -> throw (new NotImplementedException("Saved filters are not yet implemented."));
default -> throw (new IllegalArgumentException("Unrecognized value [" + recordsParam + "] for query parameter: recordsParam"));
};
}
return (null);
}
/***************************************************************************
**
***************************************************************************/
@Override
public BasicResponse defineBasicSuccessResponse()
{
return new BasicResponse("""
State of the initialization of the job, with different fields set, based on the
status of the task.""",
ProcessSpecUtilsV1.getResponseSchemaRefName(),
ProcessSpecUtilsV1.buildResponseExample()
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public void handleOutput(Context context, ProcessInitOrStepOrStatusResponseV1 output) throws Exception
{
ProcessSpecUtilsV1.handleOutput(context, output);
}
}

View File

@ -0,0 +1,140 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.middleware.javalin.executors.ProcessMetaDataExecutor;
import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessMetaDataInput;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.ProcessMetaDataResponseV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
import com.kingsrook.qqq.openapi.model.Example;
import com.kingsrook.qqq.openapi.model.HttpMethod;
import com.kingsrook.qqq.openapi.model.In;
import com.kingsrook.qqq.openapi.model.Parameter;
import com.kingsrook.qqq.openapi.model.Schema;
import com.kingsrook.qqq.openapi.model.Type;
import io.javalin.http.Context;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessMetaDataSpecV1 extends AbstractEndpointSpec<ProcessMetaDataInput, ProcessMetaDataResponseV1, ProcessMetaDataExecutor>
{
/***************************************************************************
**
***************************************************************************/
public BasicOperation defineBasicOperation()
{
return new BasicOperation()
.withPath("/metaData/process/{processName}")
.withHttpMethod(HttpMethod.GET)
.withTag(TagsV1.PROCESSES)
.withShortSummary("Get process metaData")
.withLongDescription("""
Load the full metadata for a single process, including all screens (aka, frontend steps), which a frontend
needs to display to users."""
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public List<Parameter> defineRequestParameters()
{
return List.of(
new Parameter()
.withName("processName")
.withDescription("Name of the process to load.")
.withRequired(true)
.withSchema(new Schema().withType(Type.STRING))
.withExample("samplePersonProcess")
.withIn(In.PATH)
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public ProcessMetaDataInput buildInput(Context context) throws Exception
{
ProcessMetaDataInput input = new ProcessMetaDataInput();
input.setProcessName(getRequestParam(context, "processName"));
return (input);
}
/***************************************************************************
**
***************************************************************************/
@Override
public Map<String, Schema> defineComponentSchemas()
{
return Map.of(ProcessMetaDataResponseV1.class.getSimpleName(), new ProcessMetaDataResponseV1().toSchema());
}
/***************************************************************************
**
***************************************************************************/
@Override
public BasicResponse defineBasicSuccessResponse()
{
Map<String, Example> examples = new LinkedHashMap<>();
examples.put("TODO", new Example()
.withValue(new ProcessMetaDataResponseV1())); // todo do
return new BasicResponse("""
The full process metadata""",
ProcessMetaDataResponseV1.class.getSimpleName(),
examples
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public void handleOutput(Context context, ProcessMetaDataResponseV1 output) throws Exception
{
context.result(JsonUtils.toJson(output.getProcessMetaData()));
}
}

View File

@ -0,0 +1,164 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.middleware.javalin.executors.ProcessStatusExecutor;
import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessStatusInput;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.ProcessInitOrStepOrStatusResponseV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.ProcessSpecUtilsV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
import com.kingsrook.qqq.openapi.model.HttpMethod;
import com.kingsrook.qqq.openapi.model.In;
import com.kingsrook.qqq.openapi.model.Parameter;
import com.kingsrook.qqq.openapi.model.Schema;
import com.kingsrook.qqq.openapi.model.Type;
import io.javalin.http.Context;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessStatusSpecV1 extends AbstractEndpointSpec<ProcessStatusInput, ProcessInitOrStepOrStatusResponseV1, ProcessStatusExecutor>
{
/***************************************************************************
**
***************************************************************************/
public BasicOperation defineBasicOperation()
{
return new BasicOperation()
.withPath("/processes/{processName}/{processUUID}/status/{jobUUID}")
.withHttpMethod(HttpMethod.GET)
.withTag(TagsV1.PROCESSES)
.withShortSummary("Get job status")
.withLongDescription("""
Get the status of a running job for a process.
Response is the same format as for an init or step call that completed synchronously.
"""
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public List<Parameter> defineRequestParameters()
{
return List.of(
new Parameter()
.withName("processName")
.withDescription("Name of the process that is being ran")
.withRequired(true)
.withSchema(new Schema().withType(Type.STRING))
.withExample("samplePersonProcess")
.withIn(In.PATH),
new Parameter()
.withName("processUUID")
.withDescription("Unique identifier for this run of the process - as was returned by the `init` call.")
.withRequired(true)
.withSchema(new Schema().withType(Type.STRING).withFormat("uuid"))
.withExample(ProcessSpecUtilsV1.EXAMPLE_PROCESS_UUID)
.withIn(In.PATH),
new Parameter()
.withName("jobUUID")
.withDescription("Unique identifier for the asynchronous job being executed, as returned by an `init` or `step` call that went asynch.")
.withRequired(true)
.withSchema(new Schema().withType(Type.STRING).withFormat("uuid"))
.withExample(ProcessSpecUtilsV1.EXAMPLE_JOB_UUID)
.withIn(In.PATH)
);
}
/***************************************************************************
** These aren't in the components sub-package, so they don't get auto-found.
***************************************************************************/
@Override
public Map<String, Schema> defineComponentSchemas()
{
return Map.of(
ProcessSpecUtilsV1.getResponseSchemaRefName(), new ProcessInitOrStepOrStatusResponseV1().toSchema(),
"ProcessStepComplete", new ProcessInitOrStepOrStatusResponseV1.ProcessStepComplete().toSchema(),
"ProcessStepJobStarted", new ProcessInitOrStepOrStatusResponseV1.ProcessStepJobStarted().toSchema(),
"ProcessStepRunning", new ProcessInitOrStepOrStatusResponseV1.ProcessStepRunning().toSchema(),
"ProcessStepError", new ProcessInitOrStepOrStatusResponseV1.ProcessStepError().toSchema()
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public ProcessStatusInput buildInput(Context context) throws Exception
{
ProcessStatusInput input = new ProcessStatusInput();
input.setProcessName(getRequestParam(context, "processName"));
input.setProcessUUID(getRequestParam(context, "processUUID"));
input.setJobUUID(getRequestParam(context, "jobUUID"));
return (input);
}
/***************************************************************************
**
***************************************************************************/
@Override
public BasicResponse defineBasicSuccessResponse()
{
return new BasicResponse("""
State of the backend's running of the specified job, with different fields set,
based on the status of the job.""",
// new ProcessInitOrStepOrStatusResponseV1().toSchema(),
ProcessSpecUtilsV1.getResponseSchemaRefName(),
ProcessSpecUtilsV1.buildResponseExample()
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public void handleOutput(Context context, ProcessInitOrStepOrStatusResponseV1 output) throws Exception
{
ProcessSpecUtilsV1.handleOutput(context, output);
}
}

View File

@ -0,0 +1,201 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.middleware.javalin.executors.ProcessInitOrStepExecutor;
import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepInput;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.ProcessInitOrStepOrStatusResponseV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.ProcessSpecUtilsV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
import com.kingsrook.qqq.openapi.model.Content;
import com.kingsrook.qqq.openapi.model.Example;
import com.kingsrook.qqq.openapi.model.HttpMethod;
import com.kingsrook.qqq.openapi.model.In;
import com.kingsrook.qqq.openapi.model.Parameter;
import com.kingsrook.qqq.openapi.model.RequestBody;
import com.kingsrook.qqq.openapi.model.Schema;
import com.kingsrook.qqq.openapi.model.Type;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessStepSpecV1 extends AbstractEndpointSpec<ProcessInitOrStepInput, ProcessInitOrStepOrStatusResponseV1, ProcessInitOrStepExecutor>
{
public static int DEFAULT_ASYNC_STEP_TIMEOUT_MILLIS = 3_000;
/***************************************************************************
**
***************************************************************************/
public BasicOperation defineBasicOperation()
{
return new BasicOperation()
.withPath("/processes/{processName}/{processUUID}/step/{stepName}")
.withHttpMethod(HttpMethod.POST)
.withTag(TagsV1.PROCESSES)
.withShortSummary("Run a step in a process")
.withLongDescription("""
To run the next step in a process, this endpoint should be called, with the `processName`
and existing `processUUID`, as well as the step that was just completed in the frontend,
given as `stepName`.
Additional process-specific values should posted in a form param named `values`, as JSON object
with keys defined by the process in question.
Note that this request, if it takes longer than a given threshold* to complete, will return a
a `jobUUID`, which should be sent to the `/processes/{processName}/{processUUID}/status/{jobUUID}`
endpoint, to poll for a status update.
*This threshold has a default value of 3,000 ms., but can be set per-request via the form
parameter `stepTimeoutMillis`.
""");
}
/***************************************************************************
**
***************************************************************************/
@Override
public List<Parameter> defineRequestParameters()
{
return List.of(
new Parameter()
.withName("processName")
.withDescription("Name of the process to perform the step in.")
.withRequired(true)
.withExample("samplePersonProcess")
.withSchema(new Schema().withType(Type.STRING))
.withIn(In.PATH),
new Parameter()
.withName("processUUID")
.withDescription("Unique identifier for this run of the process - as was returned by the `init` call.")
.withRequired(true)
.withSchema(new Schema().withType(Type.STRING))
.withExample(ProcessSpecUtilsV1.EXAMPLE_PROCESS_UUID)
.withIn(In.PATH),
new Parameter()
.withName("stepName")
.withDescription("Name of the frontend step that the user has just completed.")
.withRequired(true)
.withSchema(new Schema().withType(Type.STRING))
.withExample("inputForm")
.withIn(In.PATH)
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public RequestBody defineRequestBody()
{
return new RequestBody()
.withContent(ContentType.MULTIPART_FORM_DATA.getMimeType(), new Content()
.withSchema(new Schema()
.withType(Type.OBJECT)
.withProperty("values", new Schema()
.withType(Type.OBJECT)
.withDescription("Process-specific field names and values."))
.withProperty("stepTimeoutMillis", new Schema()
.withDescription("Optionally change the time that the server will wait for the job before letting it go asynchronous. Default value is 3000.")
.withType(Type.INTEGER)
.withExample("shorter timeout", new Example().withValue("500"))
.withExample("longer timeout", new Example().withValue("60000")))
.withProperty("file", new Schema()
.withType(Type.STRING)
.withFormat("binary")
.withDescription("A file upload, for process steps which expect an uploaded file."))
)
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public ProcessInitOrStepInput buildInput(Context context) throws Exception
{
ProcessInitOrStepInput processInitOrStepInput = new ProcessInitOrStepInput();
processInitOrStepInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
processInitOrStepInput.setProcessName(getRequestParam(context, "processName"));
processInitOrStepInput.setProcessUUID(getRequestParam(context, "processUUID"));
processInitOrStepInput.setStartAfterStep(getRequestParam(context, "stepName"));
processInitOrStepInput.setStepTimeoutMillis(Objects.requireNonNullElse(getRequestParamInteger(context, "stepTimeoutMillis"), DEFAULT_ASYNC_STEP_TIMEOUT_MILLIS));
processInitOrStepInput.setValues(getRequestParamMap(context, "values"));
// todo - uploaded files
// todo - archive uploaded files?
return (processInitOrStepInput);
}
/***************************************************************************
**
***************************************************************************/
@Override
public BasicResponse defineBasicSuccessResponse()
{
return new BasicResponse("""
State of the backend's running of the next step(s) of the job, with different fields set,
based on the status of the job.""",
ProcessSpecUtilsV1.getResponseSchemaRefName(),
ProcessSpecUtilsV1.buildResponseExample()
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public void handleOutput(Context context, ProcessInitOrStepOrStatusResponseV1 output) throws Exception
{
ProcessSpecUtilsV1.handleOutput(context, output);
}
}

View File

@ -0,0 +1,340 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0AuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.middleware.javalin.executors.io.AuthenticationMetaDataOutputInterface;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIOneOf;
/*******************************************************************************
**
*******************************************************************************/
public class AuthenticationMetaDataResponseV1 implements AuthenticationMetaDataOutputInterface, ToSchema
{
@OpenAPIDescription("""
Specifier for the type of authentication module being used.
Frontends should use this value to determine how to prompt the user for authentication credentials.
In addition, depending on this value, additional properties will be included in this object, as
may be needed to complete the authorization workflow with the provider (e.g., a baseUrl, clientId,
and audience for an OAuth type workflow).""")
private String type;
@OpenAPIDescription("""
Unique name for the authentication metaData object within the QInstance.
""")
private String name;
@OpenAPIDescription("""
Additional values, as determined by the type of authentication provider.
""")
@OpenAPIOneOf()
private Values values;
/***************************************************************************
**
***************************************************************************/
public sealed interface Values permits EmptyValues, Auth0Values
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("No additional values are used for some authentication providers.")
public static final class EmptyValues implements Values
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Additional values used by the Auth0 type authentication provider.")
public static final class Auth0Values implements Values
{
@OpenAPIDescription("ClientId for auth0")
private String clientId;
@OpenAPIDescription("BaseUrl for auth0")
private String baseUrl;
@OpenAPIDescription("Audience for auth0")
private String audience;
/*******************************************************************************
** Getter for clientId
**
*******************************************************************************/
public String getClientId()
{
return clientId;
}
/*******************************************************************************
** Setter for clientId
**
*******************************************************************************/
public void setClientId(String clientId)
{
this.clientId = clientId;
}
/*******************************************************************************
** Fluent setter for clientId
**
*******************************************************************************/
public Auth0Values withClientId(String clientId)
{
this.clientId = clientId;
return (this);
}
/*******************************************************************************
** Getter for baseUrl
**
*******************************************************************************/
public String getBaseUrl()
{
return baseUrl;
}
/*******************************************************************************
** Setter for baseUrl
**
*******************************************************************************/
public void setBaseUrl(String baseUrl)
{
this.baseUrl = baseUrl;
}
/*******************************************************************************
** Fluent setter for baseUrl
**
*******************************************************************************/
public Auth0Values withBaseUrl(String baseUrl)
{
this.baseUrl = baseUrl;
return (this);
}
/*******************************************************************************
** Getter for audience
**
*******************************************************************************/
public String getAudience()
{
return audience;
}
/*******************************************************************************
** Setter for audience
**
*******************************************************************************/
public void setAudience(String audience)
{
this.audience = audience;
}
/*******************************************************************************
** Fluent setter for audience
**
*******************************************************************************/
public Auth0Values withAudience(String audience)
{
this.audience = audience;
return (this);
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setAuthenticationMetaData(QAuthenticationMetaData qAuthenticationMetaData)
{
setType(qAuthenticationMetaData.getType().name());
setName(qAuthenticationMetaData.getName());
if(qAuthenticationMetaData instanceof Auth0AuthenticationMetaData auth0MetaData)
{
// values = new LinkedHashMap<>();
// values.put("clientId", auth0MetaData.getClientId());
// values.put("baseUrl", auth0MetaData.getBaseUrl());
// values.put("audience", auth0MetaData.getAudience());
Auth0Values auth0Values = new Auth0Values();
values = auth0Values;
auth0Values.setClientId(auth0MetaData.getClientId());
auth0Values.setBaseUrl(auth0MetaData.getBaseUrl());
auth0Values.setAudience(auth0MetaData.getAudience());
}
/*
JSONObject jsonObject = new JSONObject(JsonUtils.toJson(qAuthenticationMetaData));
for(String key : jsonObject.keySet())
{
if("name".equals(key) || "type".equals(key))
{
continue;
}
if(values == null)
{
values = new LinkedHashMap<>();
}
Object value = jsonObject.get(key);
if(value instanceof Serializable s)
{
values.put(key, s);
}
}
*/
}
/*******************************************************************************
** Getter for type
*******************************************************************************/
public String getType()
{
return (this.type);
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
public void setType(String type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
*******************************************************************************/
public AuthenticationMetaDataResponseV1 withType(String type)
{
this.type = type;
return (this);
}
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public AuthenticationMetaDataResponseV1 withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Getter for values
*******************************************************************************/
public Values getValues()
{
return (this.values);
}
/*******************************************************************************
** Setter for values
*******************************************************************************/
public void setValues(Values values)
{
this.values = values;
}
/*******************************************************************************
** Fluent setter for values
*******************************************************************************/
public AuthenticationMetaDataResponseV1 withValues(Values values)
{
this.values = values;
return (this);
}
}

View File

@ -0,0 +1,69 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
/*******************************************************************************
**
*******************************************************************************/
public class BasicErrorResponseV1 implements ToSchema
{
@OpenAPIDescription("Description of the error")
private String error;
/*******************************************************************************
** Getter for error
*******************************************************************************/
public String getError()
{
return (this.error);
}
/*******************************************************************************
** Setter for error
*******************************************************************************/
public void setError(String error)
{
this.error = error;
}
/*******************************************************************************
** Fluent setter for error
*******************************************************************************/
public BasicErrorResponseV1 withError(String error)
{
this.error = error;
return (this);
}
}

View File

@ -0,0 +1,107 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.middleware.javalin.executors.io.ManageSessionOutputInterface;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIHasAdditionalProperties;
/*******************************************************************************
**
*******************************************************************************/
public class ManageSessionResponseV1 implements ManageSessionOutputInterface, ToSchema
{
@OpenAPIDescription("Unique identifier of the session. Required to be returned on subsequent requests in the sessionUUID Cookie, to prove authentication.")
private String uuid;
@OpenAPIDescription("Optional object with application-defined values.")
@OpenAPIHasAdditionalProperties()
private Map<String, Serializable> values;
/*******************************************************************************
** Getter for uuid
*******************************************************************************/
public String getUuid()
{
return (this.uuid);
}
/*******************************************************************************
** Setter for uuid
*******************************************************************************/
public void setUuid(String uuid)
{
this.uuid = uuid;
}
/*******************************************************************************
** Fluent setter for uuid
*******************************************************************************/
public ManageSessionResponseV1 withUuid(String uuid)
{
this.uuid = uuid;
return (this);
}
/*******************************************************************************
** Getter for values
*******************************************************************************/
public Map<String, Serializable> getValues()
{
return (this.values);
}
/*******************************************************************************
** Setter for values
*******************************************************************************/
public void setValues(Map<String, Serializable> values)
{
this.values = values;
}
/*******************************************************************************
** Fluent setter for values
*******************************************************************************/
public ManageSessionResponseV1 withValues(Map<String, Serializable> values)
{
this.values = values;
return (this);
}
}

View File

@ -0,0 +1,177 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendWidgetMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.middleware.javalin.executors.io.MetaDataOutputInterface;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapValueType;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.AppMetaData;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.AppTreeNode;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.ProcessMetaDataLight;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.TableMetaDataLight;
/*******************************************************************************
**
*******************************************************************************/
public class MetaDataResponseV1 implements MetaDataOutputInterface, ToSchema
{
@OpenAPIDescription("Map of all apps within the QQQ Instance (that the user has permission to see that they exist).")
@OpenAPIMapValueType(value = AppMetaData.class, useRef = true)
private Map<String, AppMetaData> apps;
@OpenAPIDescription("Tree of apps within the QQQ Instance, sorted and organized hierarchically, for presentation to a user.")
@OpenAPIListItems(value = AppTreeNode.class, useRef = true)
private List<AppTreeNode> appTree;
@OpenAPIDescription("Map of all tables within the QQQ Instance (that the user has permission to see that they exist).")
@OpenAPIMapValueType(value = TableMetaDataLight.class, useRef = true)
private Map<String, TableMetaDataLight> tables;
@OpenAPIDescription("Map of all processes within the QQQ Instance (that the user has permission to see that they exist).")
@OpenAPIMapValueType(value = ProcessMetaDataLight.class, useRef = true)
private Map<String, ProcessMetaDataLight> processes;
@OpenAPIDescription("Map of all widgets within the QQQ Instance (that the user has permission to see that they exist).")
@OpenAPIMapValueType(value = ProcessMetaDataLight.class, useRef = true)
private Map<String, WidgetMetaData> widgets;
/***************************************************************************
**
***************************************************************************/
@Override
public void setMetaDataOutput(MetaDataOutput metaDataOutput)
{
apps = new HashMap<>();
for(QFrontendAppMetaData app : CollectionUtils.nonNullMap(metaDataOutput.getApps()).values())
{
apps.put(app.getName(), new AppMetaData(app));
}
appTree = new ArrayList<>();
for(com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode app : CollectionUtils.nonNullList(metaDataOutput.getAppTree()))
{
appTree.add(new AppTreeNode(app));
}
tables = new HashMap<>();
for(QFrontendTableMetaData table : CollectionUtils.nonNullMap(metaDataOutput.getTables()).values())
{
tables.put(table.getName(), new TableMetaDataLight(table));
}
processes = new HashMap<>();
for(QFrontendProcessMetaData process : CollectionUtils.nonNullMap(metaDataOutput.getProcesses()).values())
{
processes.put(process.getName(), new ProcessMetaDataLight(process));
}
widgets = new HashMap<>();
for(QFrontendWidgetMetaData widget : CollectionUtils.nonNullMap(metaDataOutput.getWidgets()).values())
{
widgets.put(widget.getName(), new WidgetMetaData(widget));
}
}
/*******************************************************************************
** Fluent setter for MetaDataOutput
**
*******************************************************************************/
public MetaDataResponseV1 withMetaDataOutput(MetaDataOutput metaDataOutput)
{
setMetaDataOutput(metaDataOutput);
return (this);
}
/*******************************************************************************
** Getter for apps
**
*******************************************************************************/
public Map<String, AppMetaData> getApps()
{
return apps;
}
/*******************************************************************************
** Getter for appTree
**
*******************************************************************************/
public List<AppTreeNode> getAppTree()
{
return appTree;
}
/*******************************************************************************
** Getter for tables
**
*******************************************************************************/
public Map<String, TableMetaDataLight> getTables()
{
return tables;
}
/*******************************************************************************
** Getter for processes
**
*******************************************************************************/
public Map<String, ProcessMetaDataLight> getProcesses()
{
return processes;
}
/*******************************************************************************
** Getter for widgets
**
*******************************************************************************/
public Map<String, WidgetMetaData> getWidgets()
{
return widgets;
}
}

View File

@ -0,0 +1,444 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepOrStatusOutputInterface;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.SchemaBuilder;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIIncludeProperties;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIOneOf;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.FieldMetaData;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.FrontendStep;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.ProcessMetaDataAdjustment;
import com.kingsrook.qqq.openapi.model.Schema;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessInitOrStepOrStatusResponseV1 implements ProcessInitOrStepOrStatusOutputInterface, ToSchema
{
private TypedResponse typedResponse;
/***************************************************************************
**
***************************************************************************/
@OpenAPIOneOf()
public static sealed class TypedResponse implements ToSchema permits ProcessStepComplete, ProcessStepJobStarted, ProcessStepRunning, ProcessStepError
{
@OpenAPIDescription("What kind of response has been received. Determines what additional fields will be set.")
private String type;
@OpenAPIDescription("Unique identifier for a running instance the process.")
private String processUUID;
/*******************************************************************************
** Getter for type
**
*******************************************************************************/
public String getType()
{
return type;
}
/*******************************************************************************
** Getter for processUUID
**
*******************************************************************************/
public String getProcessUUID()
{
return processUUID;
}
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIIncludeProperties(ancestorClasses = { TypedResponse.class })
@OpenAPIDescription("Data returned after the job is complete (whether it was synchronous, or asynchronous)")
public static final class ProcessStepComplete extends TypedResponse
{
@OpenAPIDescription("Name of the next process step that needs to run (a frontend step). If there are no more steps in the process, this field will not be included. ")
private String nextStep;
@OpenAPIDescription("Current values for fields used by the process.Keys are Strings, values can be any type, as determined by the application & process.")
private Map<String, Serializable> values;
@OpenAPIDescription("Changes to be made to the process's metaData.")
private ProcessMetaDataAdjustment processMetaDataAdjustment;
/*******************************************************************************
** Getter for nextStep
**
*******************************************************************************/
public String getNextStep()
{
return nextStep;
}
/*******************************************************************************
** Getter for values
**
*******************************************************************************/
public Map<String, Serializable> getValues()
{
return values;
}
/*******************************************************************************
** Getter for processMetaDataAdjustment
**
*******************************************************************************/
public ProcessMetaDataAdjustment getProcessMetaDataAdjustment()
{
return processMetaDataAdjustment;
}
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIIncludeProperties(ancestorClasses = { TypedResponse.class })
@OpenAPIDescription("In case the backend needs more time, this is a UUID of the background job that has been started.")
public static final class ProcessStepJobStarted extends TypedResponse
{
@OpenAPIDescription("Unique identifier for a running step of the process. Must be passed into `status` check calls.")
private String jobUUID;
/*******************************************************************************
** Getter for jobUUID
**
*******************************************************************************/
public String getJobUUID()
{
return jobUUID;
}
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIIncludeProperties(ancestorClasses = { TypedResponse.class })
@OpenAPIDescription("Response to a status check for a backgrounded job.")
public static final class ProcessStepRunning extends TypedResponse
{
@OpenAPIDescription("Status message regarding the running process step.")
private String message;
@OpenAPIDescription("Optional indicator of progress (e.g., `current` of `total`, as in (`1 of 10`).")
private Integer current;
@OpenAPIDescription("Optional indicator of progress (e.g., `current` of `total`, as in (`1 of 10`).")
private Integer total;
/*******************************************************************************
** Getter for message
**
*******************************************************************************/
public String getMessage()
{
return message;
}
/*******************************************************************************
** Getter for current
**
*******************************************************************************/
public Integer getCurrent()
{
return current;
}
/*******************************************************************************
** Getter for total
**
*******************************************************************************/
public Integer getTotal()
{
return total;
}
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIIncludeProperties(ancestorClasses = { TypedResponse.class })
@OpenAPIDescription("In case an error is thrown in the backend job.")
public static final class ProcessStepError extends TypedResponse
{
@OpenAPIDescription("Exception message, in case the process step threw an error.")
private String error;
@OpenAPIDescription("Optional user-facing exception message, in case the process step threw a user-facing error.")
private String userFacingError;
/*******************************************************************************
** Getter for error
**
*******************************************************************************/
public String getError()
{
return error;
}
/*******************************************************************************
** Getter for userFacingError
**
*******************************************************************************/
public String getUserFacingError()
{
return userFacingError;
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setType(Type type)
{
this.typedResponse = switch(type)
{
case COMPLETE -> new ProcessStepComplete();
case JOB_STARTED -> new ProcessStepJobStarted();
case RUNNING -> new ProcessStepRunning();
case ERROR -> new ProcessStepError();
};
this.typedResponse.type = type.toString();
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setProcessUUID(String processUUID)
{
this.typedResponse.processUUID = processUUID;
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setNextStep(String nextStep)
{
if(this.typedResponse instanceof ProcessStepComplete complete)
{
complete.nextStep = nextStep;
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setValues(Map<String, Serializable> values)
{
if(this.typedResponse instanceof ProcessStepComplete complete)
{
complete.values = values;
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setProcessMetaDataAdjustment(com.kingsrook.qqq.backend.core.model.actions.processes.ProcessMetaDataAdjustment processMetaDataAdjustment)
{
if(this.typedResponse instanceof ProcessStepComplete complete)
{
if(processMetaDataAdjustment == null)
{
complete.processMetaDataAdjustment = null;
}
else
{
complete.processMetaDataAdjustment = new ProcessMetaDataAdjustment();
Map<String, FieldMetaData> updatedFields = processMetaDataAdjustment.getUpdatedFields().entrySet()
.stream().collect(Collectors.toMap(e -> e.getKey(), f -> new FieldMetaData(f.getValue())));
complete.processMetaDataAdjustment.setUpdatedFields(updatedFields);
List<FrontendStep> updatedFrontendSteps = processMetaDataAdjustment.getUpdatedFrontendStepList()
.stream().map(f -> new FrontendStep(f)).toList();
complete.processMetaDataAdjustment.setUpdatedFrontendStepList(updatedFrontendSteps);
}
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setJobUUID(String jobUUID)
{
if(this.typedResponse instanceof ProcessStepJobStarted jobStarted)
{
jobStarted.jobUUID = jobUUID;
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setMessage(String message)
{
if(this.typedResponse instanceof ProcessStepRunning running)
{
running.message = message;
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setCurrent(Integer current)
{
if(this.typedResponse instanceof ProcessStepRunning running)
{
running.current = current;
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setTotal(Integer total)
{
if(this.typedResponse instanceof ProcessStepRunning running)
{
running.total = total;
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setError(String errorString)
{
if(this.typedResponse instanceof ProcessStepError error)
{
error.error = errorString;
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setUserFacingError(String userFacingError)
{
if(this.typedResponse instanceof ProcessStepError error)
{
error.userFacingError = userFacingError;
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public Schema toSchema()
{
return new SchemaBuilder().classToSchema(TypedResponse.class);
}
/*******************************************************************************
** Getter for typedResponse
**
*******************************************************************************/
public TypedResponse getTypedResponse()
{
return typedResponse;
}
}

View File

@ -0,0 +1,72 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData;
import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessMetaDataOutputInterface;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.SchemaBuilder;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.ProcessMetaData;
import com.kingsrook.qqq.openapi.model.Schema;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessMetaDataResponseV1 implements ProcessMetaDataOutputInterface, ToSchema
{
private ProcessMetaData processMetaData;
/***************************************************************************
**
***************************************************************************/
@Override
public void setProcessMetaData(QFrontendProcessMetaData frontendProcessMetaData)
{
this.processMetaData = new ProcessMetaData(frontendProcessMetaData);
}
/***************************************************************************
**
***************************************************************************/
@Override
public Schema toSchema()
{
return new SchemaBuilder().classToSchema(ProcessMetaData.class);
}
/*******************************************************************************
** Getter for processMetaData
**
*******************************************************************************/
public ProcessMetaData getProcessMetaData()
{
return processMetaData;
}
}

View File

@ -0,0 +1,92 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendWidgetMetaData;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
/*******************************************************************************
**
*******************************************************************************/
public class WidgetMetaData
{
@OpenAPIExclude()
private QFrontendWidgetMetaData wrapped;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetMetaData(QFrontendWidgetMetaData wrapped)
{
this.wrapped = wrapped;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetMetaData()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Unique name for this widget within the QQQ Instance")
public String getName()
{
return (this.wrapped.getName());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("User-facing name for this widget")
public String getLabel()
{
return (this.wrapped.getLabel());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("The type of this widget.")
// todo enum of the NAMES of the widget types?? or, can we just f'ing change to return the enum.name's?
public String getType()
{
return this.wrapped.getType();
}
}

View File

@ -0,0 +1,161 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapKnownEntries;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapValueType;
/***************************************************************************
**
***************************************************************************/
public class AppMetaData implements ToSchema
{
@OpenAPIExclude()
private QFrontendAppMetaData wrapped;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public AppMetaData(QFrontendAppMetaData wrapped)
{
this.wrapped = wrapped;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public AppMetaData()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Unique name for this app within the QQQ Instance")
public String getName()
{
return (this.wrapped.getName());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("User-facing name for this app")
public String getLabel()
{
return (this.wrapped.getLabel());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Icon to display for the app.")
@OpenAPIMapKnownEntries(value = Icon.class, useRef = true)
public Icon getIcon()
{
return (this.wrapped.getIcon() == null ? null : new Icon(this.wrapped.getIcon()));
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("List of widgets names that are part of this app. These strings should be keys to the widgets map in the QQQ Instance.")
@OpenAPIListItems(value = String.class)
public List<String> getWidgets()
{
return (this.wrapped.getWidgets());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("List of other apps, tables, process, and reports, which are contained within this app.")
@OpenAPIListItems(value = AppTreeNode.class, useRef = true)
public List<AppTreeNode> getChildren()
{
return (CollectionUtils.nonNullList(this.wrapped.getChildren()).stream().map(a -> new AppTreeNode(a)).toList());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Map of other apps, tables, process, and reports, which are contained within this app. Same contents as the children list, just structured as a map.")
@OpenAPIMapValueType(value = AppTreeNode.class, useRef = true)
public Map<String, AppTreeNode> getChildMap()
{
return (CollectionUtils.nonNullMap(this.wrapped.getChildMap()).entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> new AppTreeNode(e.getValue()))));
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("List of sections - sub-divisions of the app, to further organize its children.")
@OpenAPIListItems(value = AppSection.class, useRef = true) // todo local type
public List<AppSection> getSections()
{
return (CollectionUtils.nonNullList(this.wrapped.getSections()).stream().map(s -> new AppSection(s)).toList());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Additional meta-data describing the app, which may not be known to the QQQ backend core module.")
public Map<String, Object> getSupplementalAppMetaData()
{
return (new LinkedHashMap<>(CollectionUtils.nonNullMap(this.wrapped.getSupplementalAppMetaData())));
}
}

View File

@ -0,0 +1,133 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapKnownEntries;
/***************************************************************************
**
***************************************************************************/
public class AppSection implements ToSchema
{
@OpenAPIExclude()
private QAppSection wrapped;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public AppSection(QAppSection wrapped)
{
this.wrapped = wrapped;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public AppSection()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Unique (within the app) name for this section.")
public String getName()
{
return (this.wrapped.getName());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("User-facing name of the section.")
public String getLabel()
{
return (this.wrapped.getLabel());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Icon to display for the section.")
@OpenAPIMapKnownEntries(value = Icon.class, useRef = true)
public Icon getIcon()
{
return (this.wrapped.getIcon() == null ? null : new Icon(this.wrapped.getIcon()));
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("List of table names for the section")
@OpenAPIListItems(value = String.class)
public List<String> getTables()
{
return (this.wrapped.getTables());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("List of process names for the section")
@OpenAPIListItems(value = String.class)
public List<String> getProcesses()
{
return (this.wrapped.getProcesses());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("List of report names for the section")
@OpenAPIListItems(value = String.class)
public List<String> getReports()
{
return (this.wrapped.getReports());
}
}

View File

@ -0,0 +1,120 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import java.util.List;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapKnownEntries;
/***************************************************************************
**
***************************************************************************/
public class AppTreeNode implements ToSchema
{
@OpenAPIExclude()
private com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode wrapped;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public AppTreeNode(com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode wrapped)
{
this.wrapped = wrapped;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public AppTreeNode()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("The type of node (table, process, report, app)")
public String getType()
{
return (this.wrapped.getType() == null ? null : this.wrapped.getType().name());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Unique (within its type) name for this element. e.g., for type = 'table', the table's name.")
public String getName()
{
return (this.wrapped.getName());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("User-facing name of the element.")
public String getLabel()
{
return (this.wrapped.getLabel());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Child elements. Only applies for type='app', which contains additional apps under it")
@OpenAPIListItems(value = AppTreeNode.class, useRef = true)
public List<AppTreeNode> getChildren()
{
return (CollectionUtils.nonNullList(this.wrapped.getChildren()).stream().map(a -> new AppTreeNode(a)).toList());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Icon to display for the item.")
@OpenAPIMapKnownEntries(value = Icon.class, useRef = true)
public Icon getIcon()
{
return (this.wrapped.getIcon() == null ? null : new Icon(this.wrapped.getIcon()));
}
}

View File

@ -0,0 +1,194 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
/*******************************************************************************
**
*******************************************************************************/
public class FieldMetaData implements ToSchema
{
@OpenAPIExclude()
private QFieldMetaData wrapped;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public FieldMetaData(QFieldMetaData wrapped)
{
this.wrapped = wrapped;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public FieldMetaData()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Unique name for this field within its container (table or process)")
public String getName()
{
return (this.wrapped.getName());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("User-facing name for this field")
public String getLabel()
{
return (this.wrapped.getLabel());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Data-type for this field") // todo enum
public String getType()
{
return (this.wrapped.getType() == null ? null : this.wrapped.getType().name());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Indicate if a value in this field is required.")
public Boolean getIsRequired()
{
return (this.wrapped.getIsRequired());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Indicate if user may edit the value in this field.")
public Boolean getIsEditable()
{
return (this.wrapped.getIsEditable());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Indicate if this field should be hidden from users")
public Boolean getIsHidden()
{
return (this.wrapped.getIsHidden());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Indicator of 'heavy' fields, which are not loaded by default. e.g., some blobs or long-texts")
public Boolean getIsHeavy()
{
return (this.wrapped.getIsHeavy());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("C-style format specifier for displaying values in this field.")
public String getDisplayFormat()
{
return (this.wrapped.getDisplayFormat());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Default value to use in this field.")
public String getDefaultValue()
{
return (this.wrapped.getDefaultValue() == null ? null : String.valueOf(this.wrapped.getDefaultValue()));
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("If this field's values should come from a possible value source, then that PVS is named here.")
public String getPossibleValueSourceName()
{
return (this.wrapped.getPossibleValueSourceName());
}
// todo - PVS filter!!
// todo - inline PVS
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("For String fields, the max length the field supports.")
public Integer getMaxLength()
{
return (this.wrapped.getMaxLength());
}
// todo behaviors?
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Special UI dressings to add to the field.")
@OpenAPIListItems(value = FieldAdornment.class) // todo!
public List<FieldAdornment> getAdornments()
{
return (this.wrapped.getAdornments());
}
// todo help content
// todo supplemental...
}

View File

@ -0,0 +1,88 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapKnownEntries;
/*******************************************************************************
**
*******************************************************************************/
public class FrontendComponent implements ToSchema
{
@OpenAPIExclude()
private QFrontendComponentMetaData wrapped;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public FrontendComponent(QFrontendComponentMetaData wrapped)
{
this.wrapped = wrapped;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public FrontendComponent()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("The type of this component. e.g., what kind of UI element(s) should be presented to the user.")
public QComponentType getType()
{
return (this.wrapped.getType());
}
/*******************************************************************************
** Getter for values
**
*******************************************************************************/
@OpenAPIDescription("Name-value pairs specific to the type of component.")
@OpenAPIMapKnownEntries(value = FrontendComponentValues.class, useRef = true)
public Map<String, Serializable> getValues()
{
return (this.wrapped.getValues() == null ? null : new FrontendComponentValues(this.wrapped.getValues()).toMap());
}
}

View File

@ -0,0 +1,145 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIHasAdditionalProperties;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
/*******************************************************************************
**
*******************************************************************************/
@OpenAPIDescription("""
These are the known values that can appear in the values map under a FrontendComponent, to control
how that component should be presented to the user.
Note that additional properties may appear as well.
In addition, components are expected to use values from an active process's `values` map (e.g., as included in
a `ProcessStepComplete` object), with the following contract between component-types and expected values:
- For component type=`HTML`, there will be a process value with key=`${stepName}.html` (e.g., `resultScreen.html`),
whose value is the HTML to display on that screen.
- For component type=`HELP_TEXT`: There will be a process value with key=`text`, whose value is the text to display on that screen.
There may also be a process value with key=`previewText`, which, if present, can be shown before the full text is shown,
e.g., with a toggle control to hide/show the `text` value.
""")
@OpenAPIHasAdditionalProperties()
public class FrontendComponentValues implements ToSchema
{
@OpenAPIExclude()
private Map<String, Serializable> wrapped;
@OpenAPIDescription("""
Components of type=`WIDGET`, which do not reference a widget defined in the QQQ Instance, but instead,
are defined as a list of blocks within a frontend step component, will have a this value set to true.""")
private Boolean isAdHocWidget;
@OpenAPIDescription("""
Components of type=`WIDGET`, which are set as `isAdHocWidget=true`, should include a list of WidgetBlocks in this value.""")
@OpenAPIListItems(value = WidgetBlock.class, useRef = true)
private List<WidgetBlock> blocks;
@OpenAPIDescription("""
Components of type=`WIDGET`, which should render a widget defined in the QQQ instance, this value specifies
the name of that widget. Contrast with ad-hoc widgets.
""")
private String widgetName;
@OpenAPIDescription("""
Components of type=`EDIT_FORM` can specify a subset of field names to include. This can be used to break a form up into
sections, by including multiple EDIT_FORM components, with different lists of `includeFieldNames`.
""")
@OpenAPIListItems(String.class)
private List<String> includeFieldNames;
@OpenAPIDescription("""
Components of type=`EDIT_FORM` can specify a user-facing text label to show on screen.
""")
private String sectionLabel;
/***************************************************************************
**
***************************************************************************/
public FrontendComponentValues(Map<String, Serializable> values)
{
this.wrapped = values;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public FrontendComponentValues()
{
}
/***************************************************************************
**
***************************************************************************/
public Map<String, Serializable> toMap()
{
if(wrapped == null)
{
return (null);
}
Map<String, Serializable> rs = new HashMap<>();
for(Map.Entry<String, Serializable> entry : wrapped.entrySet())
{
String key = entry.getKey();
Serializable value = entry.getValue();
if(key.equals("blocks"))
{
ArrayList<WidgetBlock> resultList = new ArrayList<>();
List<AbstractBlockWidgetData<?, ?, ?, ?>> sourceList = (List<AbstractBlockWidgetData<?, ?, ?, ?>>) value;
for(AbstractBlockWidgetData<?, ?, ?, ?> abstractBlockWidgetData : sourceList)
{
resultList.add(new WidgetBlock(abstractBlockWidgetData));
}
value = resultList;
}
rs.put(key, value);
}
return (rs);
}
}

View File

@ -0,0 +1,144 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
/*******************************************************************************
**
*******************************************************************************/
public class FrontendStep implements ToSchema
{
@OpenAPIExclude()
private QFrontendStepMetaData wrapped;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public FrontendStep(QFrontendStepMetaData wrapped)
{
this.wrapped = wrapped;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public FrontendStep()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("The unique name for this step within its process")
public String getName()
{
return (this.wrapped.getName());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("The user-facing name for this step")
public String getLabel()
{
return (this.wrapped.getLabel());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("An optional indicator of the screen format preferred by the application to be used for this screen. Different frontends may support different formats, and implement them differently.")
public String getFormat()
{
return (this.wrapped.getFormat());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("The components that make up this screen")
@OpenAPIListItems(value = FrontendComponent.class, useRef = true)
public List<FrontendComponent> getComponents()
{
return (CollectionUtils.nonNullList(this.wrapped.getComponents()).stream().map(f -> new FrontendComponent(f)).toList());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Fields used as form fields (inputs) on this step/screen")
@OpenAPIListItems(value = FieldMetaData.class, useRef = true)
public List<FieldMetaData> getFormFields()
{
return (CollectionUtils.nonNullList(this.wrapped.getFormFields()).stream().map(f -> new FieldMetaData(f)).toList());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Fields used as view-only fields on this step/screen")
@OpenAPIListItems(value = FieldMetaData.class, useRef = true)
public List<FieldMetaData> getViewFields()
{
return (CollectionUtils.nonNullList(this.wrapped.getViewFields()).stream().map(f -> new FieldMetaData(f)).toList());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Fields used in record-lists shown on the step/screen.")
@OpenAPIListItems(value = FieldMetaData.class, useRef = true)
public List<FieldMetaData> getRecordListFields()
{
return (CollectionUtils.nonNullList(this.wrapped.getRecordListFields()).stream().map(f -> new FieldMetaData(f)).toList());
}
}

View File

@ -0,0 +1,92 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
/***************************************************************************
**
***************************************************************************/
public class Icon implements ToSchema
{
@OpenAPIExclude()
private QIcon wrapped;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public Icon(QIcon wrapped)
{
this.wrapped = wrapped;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public Icon()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("A material UI icon name.")
public String getName()
{
return (this.wrapped.getName());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("A color code to use for displaying the icon")
public String getColor()
{
return (this.wrapped.getColor());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("A path to an image file that can be requested from the server, to serve as the icon image instead of a material UI icon.")
public String getPath()
{
return (this.wrapped.getPath());
}
}

View File

@ -0,0 +1,74 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIIncludeProperties;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
/***************************************************************************
**
***************************************************************************/
@OpenAPIIncludeProperties(ancestorClasses = ProcessMetaDataLight.class)
public class ProcessMetaData extends ProcessMetaDataLight implements ToSchema
{
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ProcessMetaData(QFrontendProcessMetaData wrapped)
{
super(wrapped);
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ProcessMetaData()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Frontend steps (aka, Screens) for this process.")
@OpenAPIListItems(value = FrontendStep.class, useRef = true)
public List<FrontendStep> getFrontendSteps()
{
return (CollectionUtils.nonNullList(this.wrapped.getFrontendSteps()).stream().map(f -> new FrontendStep(f)).toList());
}
}

View File

@ -0,0 +1,112 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapValueType;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessMetaDataAdjustment
{
@OpenAPIDescription("""
In case the backend has changed the list of frontend steps, it will be set in this field.""")
@OpenAPIListItems(FrontendStep.class)
private List<FrontendStep> updatedFrontendStepList = null;
@OpenAPIDescription("""
Fields whose meta-data has changed. e.g., changing a label, or required status, or inline-possible-values.""")
@OpenAPIMapValueType(value = FieldMetaData.class, useRef = true)
private Map<String, FieldMetaData> updatedFields = null;
/*******************************************************************************
** Getter for updatedFrontendStepList
**
*******************************************************************************/
public List<FrontendStep> getUpdatedFrontendStepList()
{
return updatedFrontendStepList;
}
/*******************************************************************************
** Setter for updatedFrontendStepList
**
*******************************************************************************/
public void setUpdatedFrontendStepList(List<FrontendStep> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
}
/*******************************************************************************
** Fluent setter for updatedFrontendStepList
**
*******************************************************************************/
public ProcessMetaDataAdjustment withUpdatedFrontendStepList(List<FrontendStep> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
return (this);
}
/*******************************************************************************
** Getter for updatedFields
*******************************************************************************/
public Map<String, FieldMetaData> getUpdatedFields()
{
return (this.updatedFields);
}
/*******************************************************************************
** Setter for updatedFields
*******************************************************************************/
public void setUpdatedFields(Map<String, FieldMetaData> updatedFields)
{
this.updatedFields = updatedFields;
}
/*******************************************************************************
** Fluent setter for updatedFields
*******************************************************************************/
public ProcessMetaDataAdjustment withUpdatedFields(Map<String, FieldMetaData> updatedFields)
{
this.updatedFields = updatedFields;
return (this);
}
}

View File

@ -0,0 +1,139 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapKnownEntries;
/***************************************************************************
**
***************************************************************************/
public class ProcessMetaDataLight implements ToSchema
{
@OpenAPIExclude()
protected QFrontendProcessMetaData wrapped;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ProcessMetaDataLight(QFrontendProcessMetaData wrapped)
{
this.wrapped = wrapped;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ProcessMetaDataLight()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Unique name for this process within the QQQ Instance")
public String getName()
{
return (this.wrapped.getName());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("User-facing name for this process")
public String getLabel()
{
return (this.wrapped.getLabel());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("If this process is associated with a table, the table name is given here")
public String getTableName()
{
return (this.wrapped.getTableName());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Boolean indicator of whether the process should be shown to users or not")
public Boolean getIsHidden()
{
return (this.wrapped.getIsHidden());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Indicator of the Step Flow used by the process. Possible values are: LINEAR, STATE_MACHINE.")
public String getStepFlow()
{
return (this.wrapped.getStepFlow());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Icon to display for the process.")
@OpenAPIMapKnownEntries(value = Icon.class, useRef = true)
public Icon getIcon()
{
return (this.wrapped.getIcon() == null ? null : new Icon(this.wrapped.getIcon()));
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Boolean to indicate if the user has permission for the process.")
public Boolean getHasPermission()
{
return (this.wrapped.getHasPermission());
}
}

View File

@ -0,0 +1,189 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapKnownEntries;
/***************************************************************************
**
***************************************************************************/
public class TableMetaDataLight implements ToSchema
{
@OpenAPIExclude()
private QFrontendTableMetaData wrapped;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public TableMetaDataLight(QFrontendTableMetaData wrapped)
{
this.wrapped = wrapped;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public TableMetaDataLight()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Unique name for this table within the QQQ Instance")
public String getName()
{
return (this.wrapped.getName());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("User-facing name for this table")
public String getLabel()
{
return (this.wrapped.getLabel());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Boolean indicator of whether the table should be shown to users or not")
public Boolean getIsHidden()
{
return (this.wrapped.getIsHidden());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Icon to display for the table")
@OpenAPIMapKnownEntries(value = Icon.class, useRef = true)
public Icon getIcon()
{
return (this.wrapped.getIcon() == null ? null : new Icon(this.wrapped.getIcon()));
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("List of strings describing actions that are supported by the backend application for the table.")
@OpenAPIListItems(value = String.class) // todo - better, enum
public List<String> getCapabilities()
{
return (new ArrayList<>(this.wrapped.getCapabilities()));
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Boolean to indicate if the user has read permission for the table.")
public Boolean getReadPermission()
{
return (this.wrapped.getReadPermission());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Boolean to indicate if the user has insert permission for the table.")
public Boolean getInsertPermission()
{
return (this.wrapped.getInsertPermission());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Boolean to indicate if the user has edit permission for the table.")
public Boolean getEditPermission()
{
return (this.wrapped.getEditPermission());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Boolean to indicate if the user has delete permission for the table.")
public Boolean getDeletePermission()
{
return (this.wrapped.getDeletePermission());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("If the table uses variants, this is the user-facing label for the table that supplies variants for this table.")
public String getVariantTableLabel()
{
return (this.wrapped.getVariantTableLabel());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Help Contents for this table.") // todo describe more
public Map<String, List<QHelpContent>> getHelpContents()
{
return (this.wrapped.getHelpContents());
}
}

View File

@ -0,0 +1,232 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import java.io.Serializable;
import java.util.EnumSet;
import java.util.List;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.CompositeWidgetData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIEnumSubSet;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIOneOf;
/*******************************************************************************
**
*******************************************************************************/
public class WidgetBlock implements Serializable, ToSchema
{
@OpenAPIExclude()
private static final QLogger LOG = QLogger.getLogger(WidgetBlock.class);
@OpenAPIExclude()
private final AbstractBlockWidgetData<?, ?, ?, ?> wrapped;
/***************************************************************************
**
***************************************************************************/
public WidgetBlock(AbstractBlockWidgetData<?, ?, ?, ?> abstractBlockWidgetData)
{
this.wrapped = abstractBlockWidgetData;
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Unique identifier for this block within it widget. Used as a key for helpContents.")
public String getBlockId()
{
return (this.wrapped.getBlockId());
}
/***************************************************************************
**
***************************************************************************/
public enum BlockType
{
BUTTON,
AUDIO,
BIG_NUMBER,
COMPOSITE,
DIVIDER,
IMAGE,
INPUT_FIELD,
NUMBER_ICON_BADGE,
PROGRESS_BAR,
// todo? REVEAL,
TABLE_SUB_ROW_DETAIL_ROW,
TEXT,
UP_OR_DOWN_NUMBER
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("What type of block to render.")
public BlockType getBlockType()
{
return (BlockType.valueOf(this.wrapped.getBlockTypeName()));
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Values to show in the block, or otherwise control its behavior. Different fields based on blockType.")
@OpenAPIOneOf()
public WidgetBlockValues getValues()
{
return (WidgetBlockValues.of(this.wrapped.getValues()));
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Styles to apply to the block. Different fields based on blockType.")
@OpenAPIOneOf()
public WidgetBlockStyles getStyles()
{
return (WidgetBlockStyles.of(this.wrapped.getStyles()));
}
// todo link, links
// todo tooltip, tooltips
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Optional field name (e.g,. from a process's set of fields) to act as a 'guard' for the block - e.g., only include it in the UI if the value for this field is true")
public String getConditional()
{
return (this.wrapped.getConditional());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("For COMPOSITE type blocks, a list of sub-blocks.")
@OpenAPIListItems(value = WidgetBlock.class, useRef = true)
public List<WidgetBlock> getSubBlocks()
{
if(this.wrapped instanceof CompositeWidgetData compositeWidgetData)
{
return (compositeWidgetData.getBlocks() == null ? null : compositeWidgetData.getBlocks().stream().map(b -> new WidgetBlock(b)).toList());
}
return (null);
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("For COMPOSITE type blocks, optional control to make the widget appear modally")
public CompositeWidgetData.ModalMode getModalMode()
{
if(this.wrapped instanceof CompositeWidgetData compositeWidgetData)
{
return (compositeWidgetData.getModalMode());
}
return (null);
}
/***************************************************************************
**
***************************************************************************/
public static class LayoutSubSet implements OpenAPIEnumSubSet.EnumSubSet<CompositeWidgetData.Layout>
{
/***************************************************************************
**
***************************************************************************/
@Override
public EnumSet<CompositeWidgetData.Layout> getSubSet()
{
return EnumSet.of(
CompositeWidgetData.Layout.FLEX_COLUMN,
CompositeWidgetData.Layout.FLEX_ROW_WRAPPED,
CompositeWidgetData.Layout.FLEX_ROW_SPACE_BETWEEN,
CompositeWidgetData.Layout.FLEX_ROW_CENTER,
CompositeWidgetData.Layout.TABLE_SUB_ROW_DETAILS,
CompositeWidgetData.Layout.BADGES_WRAPPER
);
}
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("For COMPOSITE type blocks, an indicator of how the sub-blocks should be laid out")
@OpenAPIEnumSubSet(LayoutSubSet.class)
public CompositeWidgetData.Layout getLayout()
{
if(this.wrapped instanceof CompositeWidgetData compositeWidgetData && compositeWidgetData.getLayout() != null)
{
CompositeWidgetData.Layout layout = compositeWidgetData.getLayout();
if(new LayoutSubSet().getSubSet().contains(layout))
{
return (layout);
}
else
{
LOG.info("Layout [" + layout + "] is not in the subset used by this version. It will not be returned.");
}
}
return (null);
}
/* todo
private Map<String, Serializable> styleOverrides = new HashMap<>();
private String overlayHtml;
private Map<String, Serializable> overlayStyleOverrides = new HashMap<>();
*/
}

View File

@ -0,0 +1,92 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.audio.AudioValues;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
/*******************************************************************************
**
*******************************************************************************/
@OpenAPIDescription("Values used for an AUDIO type widget block")
public final class WidgetBlockAudioValues implements WidgetBlockValues
{
@OpenAPIExclude()
private AudioValues wrapped;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetBlockAudioValues(AudioValues textValues)
{
this.wrapped = textValues;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetBlockAudioValues()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("The path to the audio file on the server")
public String getPath()
{
return (this.wrapped.getPath());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Control if the file should automatically play when the block is rendered")
public Boolean getAutoPlay()
{
return (this.wrapped.getAutoPlay());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Control if on-screen controls should be shown to allow the user to control playback")
public Boolean getShowControls()
{
return (this.wrapped.getShowControls());
}
}

View File

@ -0,0 +1,79 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
/*******************************************************************************
**
*******************************************************************************/
public final class WidgetBlockBaseStyles implements WidgetBlockStyles
{
@OpenAPIExclude()
private BaseStyles wrapped;
/***************************************************************************
**
***************************************************************************/
public WidgetBlockBaseStyles(BaseStyles baseStyles)
{
this.wrapped = baseStyles;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetBlockBaseStyles()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Optional padding to apply to the block")
public BaseStyles.Directional<String> getPadding()
{
return (this.wrapped.getPadding());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("A background color to use for the block")
public String getBackgroundColor()
{
return (this.wrapped.getBackgroundColor());
}
}

View File

@ -0,0 +1,84 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.button.ButtonStyles;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
/*******************************************************************************
**
*******************************************************************************/
public final class WidgetBlockButtonStyles implements WidgetBlockStyles
{
@OpenAPIExclude()
private ButtonStyles wrapped;
/***************************************************************************
**
***************************************************************************/
public WidgetBlockButtonStyles(ButtonStyles buttonStyles)
{
this.wrapped = buttonStyles;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetBlockButtonStyles()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("A Color to use for the button. May be specified as a StandardColor (one of: "
+ "SUCCESS, WARNING, ERROR, INFO, MUTED) or an RGB code.")
public String getColor()
{
return (this.wrapped.getColor());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("An optional indicator of the screen format preferred by the application to be used for this block, "
+ "such as OUTLINED, FILLED, or TEXT. Different frontends may support different formats, and implement them differently.")
public String getFormat()
{
return (this.wrapped.getFormat());
}
}

View File

@ -0,0 +1,123 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.button.ButtonValues;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
/*******************************************************************************
**
*******************************************************************************/
@OpenAPIDescription("Values used for a BUTTON type widget block")
public final class WidgetBlockButtonValues implements WidgetBlockValues
{
@OpenAPIExclude()
private ButtonValues wrapped;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetBlockButtonValues(ButtonValues buttonValues)
{
this.wrapped = buttonValues;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetBlockButtonValues()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("User-facing label to display in the button")
public String getLabel()
{
return (this.wrapped.getLabel());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Code used within the app as the value submitted when the button is clicked")
public String getActionCode()
{
return (this.wrapped.getActionCode());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("""
Instructions for what should happen in the frontend (e.g., within a screen), when the button is clicked.
To show a modal composite block, use format: `showModal:${blockId}` (e.g., `showModal:myBlock`)
To hide a modal composite block, use format: `hideModal:${blockId}` (e.g., `hideModal:myBlock`)
To toggle visibility of a modal composite block, use format: `toggleModal:${blockId}` (e.g., `toggleModal:myBlock`)
""")
public String getControlCode()
{
return (this.wrapped.getControlCode());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("An optional icon to display before the text in the button")
public Icon getStartIcon()
{
return (this.wrapped.getStartIcon() == null ? null : new Icon(this.wrapped.getStartIcon()));
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("An optional icon to display after the text in the button")
public Icon getEndIcon()
{
return (this.wrapped.getEndIcon() == null ? null : new Icon(this.wrapped.getEndIcon()));
}
}

View File

@ -0,0 +1,92 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.image.ImageStyles;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
/*******************************************************************************
**
*******************************************************************************/
public final class WidgetBlockImageStyles implements WidgetBlockStyles
{
@OpenAPIExclude()
private ImageStyles wrapped;
/***************************************************************************
**
***************************************************************************/
public WidgetBlockImageStyles(ImageStyles imageStyles)
{
this.wrapped = imageStyles;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetBlockImageStyles()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("A request to render the image at a specified width.")
public String getWidth()
{
return (this.wrapped.getWidth());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("A request to render the image at a specified height.")
public String getHeight()
{
return (this.wrapped.getHeight());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Optional padding to apply to the image")
public BaseStyles.Directional<String> getPadding()
{
return (this.wrapped.getPadding());
}
}

View File

@ -0,0 +1,71 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.image.ImageValues;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
/*******************************************************************************
**
*******************************************************************************/
@OpenAPIDescription("Values used for an IMAGE type widget block")
public final class WidgetBlockImageValues implements WidgetBlockValues
{
@OpenAPIExclude()
private ImageValues wrapped;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetBlockImageValues(ImageValues textValues)
{
this.wrapped = textValues;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetBlockImageValues()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("The path to the image on the server")
public String getPath()
{
return (this.wrapped.getPath());
}
}

View File

@ -0,0 +1,115 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.inputfield.InputFieldValues;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
/*******************************************************************************
**
*******************************************************************************/
@OpenAPIDescription("Values used for an INPUT_FIELD type widget block")
public final class WidgetBlockInputFieldValues implements WidgetBlockValues
{
@OpenAPIExclude()
private InputFieldValues wrapped;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetBlockInputFieldValues(InputFieldValues textValues)
{
this.wrapped = textValues;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetBlockInputFieldValues()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Metadata to define the field that this block controls")
public FieldMetaData getFieldMetaData()
{
return (new FieldMetaData(this.wrapped.getFieldMetaData()));
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Indicate whether this field should auto-focus when it is rendered")
public Boolean getAutoFocus()
{
return (this.wrapped.getAutoFocus());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Indicate whether the form that this field is on should be submitted when Enter is pressed")
public Boolean getSubmitOnEnter()
{
return (this.wrapped.getSubmitOnEnter());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Indicate if the frontend uses a software/on-screen keyboard, if the application should try to hide it (e.g., upon auto-focus).")
public Boolean getHideSoftKeyboard()
{
return (this.wrapped.getHideSoftKeyboard());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Optional placeholder text to display in the input box.")
public String getPlaceholder()
{
return (this.wrapped.getPlaceholder());
}
}

View File

@ -0,0 +1,82 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStylesInterface;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.button.ButtonStyles;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.image.ImageStyles;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.text.TextStyles;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
/*******************************************************************************
**
*******************************************************************************/
public sealed interface WidgetBlockStyles extends ToSchema permits
WidgetBlockBaseStyles,
WidgetBlockButtonStyles,
WidgetBlockImageStyles,
WidgetBlockTextStyles
{
@OpenAPIExclude
QLogger LOG = QLogger.getLogger(WidgetBlockStyles.class);
/***************************************************************************
**
***************************************************************************/
static WidgetBlockStyles of(BlockStylesInterface blockStyles)
{
if(blockStyles == null)
{
return (null);
}
if(blockStyles instanceof ButtonStyles s)
{
return (new WidgetBlockButtonStyles(s));
}
else if(blockStyles instanceof ImageStyles s)
{
return (new WidgetBlockImageStyles(s));
}
else if(blockStyles instanceof TextStyles s)
{
return (new WidgetBlockTextStyles(s));
}
//////////////////////////////////////////////////////////////////////////////////////////////
// note - important for this one to be last, since it's a base class to some of the above!! //
//////////////////////////////////////////////////////////////////////////////////////////////
else if(blockStyles instanceof BaseStyles s)
{
return (new WidgetBlockBaseStyles(s));
}
LOG.warn("Unrecognized block value type: " + blockStyles.getClass().getName());
return (null);
}
}

View File

@ -0,0 +1,106 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.text.TextStyles;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
/*******************************************************************************
**
*******************************************************************************/
public final class WidgetBlockTextStyles implements WidgetBlockStyles
{
@OpenAPIExclude()
private TextStyles wrapped;
/***************************************************************************
**
***************************************************************************/
public WidgetBlockTextStyles(TextStyles textStyles)
{
this.wrapped = textStyles;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetBlockTextStyles()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("A Color to display the text in. May be specified as a StandardColor (one of: "
+ "SUCCESS, WARNING, ERROR, INFO, MUTED) or an RGB code.")
public String getColor()
{
return (this.wrapped.getColor());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("An optional indicator of the screen format preferred by the application to be used for this block. "
+ "Different frontends may support different formats, and implement them differently.")
public String getFormat()
{
return (this.wrapped.getFormat());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("An optional indicator of the weight at which the text should be rendered. May be a named value (one of"
+ "extralight, thin, medium, black, semibold, bold, extrabold) or a numeric, e.g., 100, 200, ..., 900")
public String getWeight()
{
return (this.wrapped.getWeight());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("An optional indicator of the size at which the text should be rendered. May be a named value (one of"
+ "largest, headline, title, body, smallest) or a numeric size - both are up to the frontend to interpret.")
public String getSize()
{
return (this.wrapped.getSize());
}
}

View File

@ -0,0 +1,93 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.text.TextValues;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
/*******************************************************************************
**
*******************************************************************************/
@OpenAPIDescription("Values used for a TEXT type widget block")
public final class WidgetBlockTextValues implements WidgetBlockValues
{
@OpenAPIExclude()
private TextValues wrapped;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetBlockTextValues(TextValues textValues)
{
this.wrapped = textValues;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetBlockTextValues()
{
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("The text to display in the block")
public String getText()
{
return (this.wrapped.getText());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("An optional icon to display before the text")
public Icon getStartIcon()
{
return (this.wrapped.getStartIcon() == null ? null : new Icon(this.wrapped.getStartIcon()));
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("An optional icon to display after the text")
public Icon getEndIcon()
{
return (this.wrapped.getEndIcon() == null ? null : new Icon(this.wrapped.getEndIcon()));
}
}

View File

@ -0,0 +1,85 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.audio.AudioValues;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.button.ButtonValues;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.image.ImageValues;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.inputfield.InputFieldValues;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.text.TextValues;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
/*******************************************************************************
**
*******************************************************************************/
public sealed interface WidgetBlockValues extends ToSchema permits
WidgetBlockAudioValues,
WidgetBlockButtonValues,
WidgetBlockImageValues,
WidgetBlockInputFieldValues,
WidgetBlockTextValues
{
@OpenAPIExclude
QLogger LOG = QLogger.getLogger(WidgetBlockValues.class);
/***************************************************************************
**
***************************************************************************/
static WidgetBlockValues of(BlockValuesInterface blockValues)
{
if(blockValues == null)
{
return (null);
}
if(blockValues instanceof AudioValues v)
{
return (new WidgetBlockAudioValues(v));
}
else if(blockValues instanceof ButtonValues v)
{
return (new WidgetBlockButtonValues(v));
}
else if(blockValues instanceof ImageValues v)
{
return (new WidgetBlockImageValues(v));
}
else if(blockValues instanceof InputFieldValues v)
{
return (new WidgetBlockInputFieldValues(v));
}
else if(blockValues instanceof TextValues v)
{
return (new WidgetBlockTextValues(v));
}
LOG.warn("Unrecognized block value type: " + blockValues.getClass().getName());
return (null);
}
}

View File

@ -0,0 +1,281 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.utils;
import java.io.File;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessMetaDataAdjustment;
import com.kingsrook.qqq.backend.core.model.actions.processes.QUploadedFile;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
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.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepOrStatusOutputInterface;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.ProcessInitOrStepOrStatusResponseV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.WidgetBlock;
import com.kingsrook.qqq.openapi.model.Example;
import com.kingsrook.qqq.openapi.model.Schema;
import io.javalin.http.Context;
import org.json.JSONArray;
import org.json.JSONObject;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessSpecUtilsV1
{
private static final QLogger LOG = QLogger.getLogger(ProcessSpecUtilsV1.class);
public static final String EXAMPLE_PROCESS_UUID = "01234567-89AB-CDEF-0123-456789ABCDEF";
public static final String EXAMPLE_JOB_UUID = "98765432-10FE-DCBA-9876-543210FEDCBA";
/***************************************************************************
**
***************************************************************************/
public static String getResponseSchemaRefName()
{
return ("ProcessStepResponseV1");
}
/***************************************************************************
**
***************************************************************************/
public static Schema buildResponseSchema()
{
return new Schema().withOneOf(List.of(
new Schema().withRef("#/components/schemas/ProcessStepComplete"),
new Schema().withRef("#/components/schemas/ProcessStepJobStarted"),
new Schema().withRef("#/components/schemas/ProcessStepRunning"),
new Schema().withRef("#/components/schemas/ProcessStepError")
));
}
/***************************************************************************
**
***************************************************************************/
public static LinkedHashMap<String, Example> buildResponseExample()
{
ProcessInitOrStepOrStatusResponseV1 completeResponse = new ProcessInitOrStepOrStatusResponseV1();
completeResponse.setType(ProcessInitOrStepOrStatusOutputInterface.Type.COMPLETE);
completeResponse.setProcessUUID(EXAMPLE_PROCESS_UUID);
Map<String, Serializable> values = new LinkedHashMap<>();
values.put("totalAge", 32768);
values.put("firstLastName", "Aabramson");
completeResponse.setValues(values);
completeResponse.setNextStep("reviewScreen");
ProcessInitOrStepOrStatusResponseV1 completeResponseWithMetaDataAdjustment = new ProcessInitOrStepOrStatusResponseV1();
completeResponseWithMetaDataAdjustment.setType(ProcessInitOrStepOrStatusOutputInterface.Type.COMPLETE);
completeResponseWithMetaDataAdjustment.setProcessUUID(EXAMPLE_PROCESS_UUID);
completeResponseWithMetaDataAdjustment.setValues(values);
completeResponseWithMetaDataAdjustment.setNextStep("inputScreen");
completeResponseWithMetaDataAdjustment.setProcessMetaDataAdjustment(new ProcessMetaDataAdjustment()
.withUpdatedField(new QFieldMetaData("someField", QFieldType.STRING).withIsRequired(true))
.withUpdatedFrontendStepList(List.of(
new QFrontendStepMetaData()
.withName("inputScreen")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
.withFormField(new QFieldMetaData("someField", QFieldType.STRING)),
new QFrontendStepMetaData()
.withName("resultScreen")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.PROCESS_SUMMARY_RESULTS))
)));
ProcessInitOrStepOrStatusResponseV1 jobStartedResponse = new ProcessInitOrStepOrStatusResponseV1();
jobStartedResponse.setType(ProcessInitOrStepOrStatusOutputInterface.Type.JOB_STARTED);
jobStartedResponse.setProcessUUID(EXAMPLE_PROCESS_UUID);
jobStartedResponse.setJobUUID(EXAMPLE_JOB_UUID);
ProcessInitOrStepOrStatusResponseV1 runningResponse = new ProcessInitOrStepOrStatusResponseV1();
runningResponse.setType(ProcessInitOrStepOrStatusOutputInterface.Type.RUNNING);
runningResponse.setProcessUUID(EXAMPLE_PROCESS_UUID);
runningResponse.setMessage("Processing person records");
runningResponse.setCurrent(47);
runningResponse.setTotal(1701);
ProcessInitOrStepOrStatusResponseV1 errorResponse = new ProcessInitOrStepOrStatusResponseV1();
errorResponse.setType(ProcessInitOrStepOrStatusOutputInterface.Type.RUNNING);
errorResponse.setProcessUUID(EXAMPLE_PROCESS_UUID);
errorResponse.setError("Illegal Argument Exception: NaN");
errorResponse.setUserFacingError("The process could not be completed due to invalid input.");
return MapBuilder.of(() -> new LinkedHashMap<String, Example>())
.with("COMPLETE", new Example().withValue(completeResponse))
.with("COMPLETE with metaDataAdjustment", new Example().withValue(completeResponseWithMetaDataAdjustment))
.with("JOB_STARTED", new Example().withValue(jobStartedResponse))
.with("RUNNING", new Example().withValue(runningResponse))
.with("ERROR", new Example().withValue(errorResponse))
.build();
}
/***************************************************************************
**
***************************************************************************/
public static void handleOutput(Context context, ProcessInitOrStepOrStatusResponseV1 output)
{
////////////////////////////////////////////////////////////////////////////////
// normally, we like the JsonUtils behavior of excluding null/empty elements. //
// but it turns out we want those in the values sub-map. //
// so, go through a loop of object → JSON String → JSONObject → String... //
// also - work with the TypedResponse sub-object within this response class //
////////////////////////////////////////////////////////////////////////////////
ProcessInitOrStepOrStatusResponseV1.TypedResponse typedOutput = output.getTypedResponse();
String outputJson = JsonUtils.toJson(typedOutput);
JSONObject outputJsonObject = new JSONObject(outputJson);
if(typedOutput instanceof ProcessInitOrStepOrStatusResponseV1.ProcessStepComplete complete)
{
////////////////////////////////////////////////////////////////////////////////////
// here's where we'll handle the values map specially - note - that we'll also //
// be mapping some specific object types into their API-versioned responses types //
////////////////////////////////////////////////////////////////////////////////////
Map<String, Serializable> values = complete.getValues();
if(values != null)
{
JSONObject valuesAsJsonObject = new JSONObject();
for(Map.Entry<String, Serializable> valueEntry : values.entrySet())
{
String name = valueEntry.getKey();
Serializable value = valueEntry.getValue();
Serializable valueToMakeIntoJson = value;
if(value instanceof String s)
{
valuesAsJsonObject.put(name, s);
continue;
}
else if(value instanceof Boolean b)
{
valuesAsJsonObject.put(name, b);
continue;
}
else if(value instanceof Number n)
{
valuesAsJsonObject.put(name, n);
continue;
}
else if(value == null)
{
valuesAsJsonObject.put(name, (Object) null);
continue;
}
//////////////////////////////////////////////////////////////////////////////////
// if there are any types that we want to make sure we send back using this API //
// version's mapped objects, then add cases for them here, and wrap them. //
//////////////////////////////////////////////////////////////////////////////////
else if(value instanceof AbstractBlockWidgetData<?, ?, ?, ?> abstractBlockWidgetData)
{
valueToMakeIntoJson = new WidgetBlock(abstractBlockWidgetData);
}
String valueAsJsonString = JsonUtils.toJson(valueToMakeIntoJson, mapper ->
{
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
});
if(valueAsJsonString.startsWith("["))
{
valuesAsJsonObject.put(name, new JSONArray(valueAsJsonString));
}
else if(valueAsJsonString.startsWith("{"))
{
valuesAsJsonObject.put(name, new JSONObject(valueAsJsonString));
}
else
{
///////////////////////////////////////////////////////////////////////////////////
// curious, if/when this ever happens, since we should get all "primitive" types //
// above, and everything else, I think, would be an object or an array, right? //
///////////////////////////////////////////////////////////////////////////////////
valuesAsJsonObject.put(name, valueAsJsonString);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ... this might be a concept for us at some point in time - but might be better to not do as a value itself? //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Serializable backendOnlyValues = values.get("_qqqBackendOnlyValues");
// if(backendOnlyValues instanceof String backendOnlyValuesString)
// {
// for(String key : backendOnlyValuesString.split(","))
// {
// jsonObject.remove(key);
// }
// jsonObject.remove("_qqqBackendOnlyValues");
// }
outputJsonObject.put("values", valuesAsJsonObject);
}
}
String json = outputJsonObject.toString(3);
System.out.println(json);
context.result(json);
}
/*******************************************************************************
**
*******************************************************************************/
private static void archiveUploadedFile(String processName, QUploadedFile qUploadedFile)
{
String fileName = QValueFormatter.formatDate(LocalDate.now())
+ File.separator + processName
+ File.separator + qUploadedFile.getFilename();
InsertInput insertInput = new InsertInput();
insertInput.setTableName(QJavalinImplementation.getJavalinMetaData().getUploadedFileArchiveTableName());
insertInput.setRecords(List.of(new QRecord()
.withValue("fileName", fileName)
.withValue("contents", qUploadedFile.getBytes())
));
new InsertAction().executeAsync(insertInput);
}
}

View File

@ -0,0 +1,208 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import com.kingsrook.qqq.middleware.javalin.executors.io.QueryMiddlewareInput;
import com.kingsrook.qqq.openapi.model.Schema;
import com.kingsrook.qqq.openapi.model.Type;
import org.json.JSONArray;
import org.json.JSONObject;
/*******************************************************************************
**
*******************************************************************************/
public class QuerySpecUtils
{
/***************************************************************************
**
***************************************************************************/
public static Schema defineQueryJoinsSchema()
{
Schema queryJoinsSchema = new Schema()
.withType(Type.ARRAY)
.withItems(new Schema()
.withProperties(MapBuilder.of(
"joinTable", new Schema()
.withType(Type.STRING),
"select", new Schema()
.withType(Type.BOOLEAN),
"type", new Schema()
.withType(Type.STRING)
.withEnumValues(Arrays.stream(QueryJoin.Type.values()).map(o -> o.name()).toList()),
"alias", new Schema()
.withType(Type.STRING),
"baseTableOrAlias", new Schema()
.withType(Type.STRING)
))
);
return queryJoinsSchema;
}
/***************************************************************************
**
***************************************************************************/
public static Schema defineQQueryFilterSchema()
{
Schema qQueryFilterSchema = new Schema()
.withType(Type.OBJECT)
.withExample(List.of(
JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.LESS_THAN, 5)))
))
.withProperties(MapBuilder.of(
"criteria", new Schema()
.withType(Type.ARRAY)
.withItems(new Schema()
.withProperties(MapBuilder.of(
"fieldName", new Schema()
.withType(Type.STRING),
"operator", new Schema()
.withType(Type.STRING)
.withEnumValues(Arrays.stream(QCriteriaOperator.values()).map(o -> o.name()).toList()),
"values", new Schema()
.withType(Type.ARRAY)
.withItems(new Schema().withOneOf(List.of(
new Schema().withType(Type.INTEGER),
new Schema().withType(Type.STRING)
)))
))
),
"orderBys", new Schema()
.withType(Type.ARRAY)
.withItems(new Schema()
.withProperties(MapBuilder.of(
"fieldName", new Schema()
.withType(Type.STRING),
"isAscending", new Schema()
.withType(Type.BOOLEAN)))
),
"booleanOperator", new Schema().withType(Type.STRING).withEnumValues(Arrays.stream(QQueryFilter.BooleanOperator.values()).map(o -> o.name()).toList()),
"skip", new Schema().withType(Type.INTEGER),
"limit", new Schema().withType(Type.INTEGER)
// todo - subfilters??
));
return qQueryFilterSchema;
}
/***************************************************************************
**
***************************************************************************/
public static Schema getQueryResponseSchema()
{
Schema schema = new Schema()
.withDescription("Records found by the query. May be empty.")
.withType(Type.OBJECT)
.withProperties(MapBuilder.of(
"records", new Schema()
.withType(Type.ARRAY)
.withItems(new Schema()
.withType(Type.OBJECT)
.withProperties(MapBuilder.of(
"recordLabel", new Schema().withType(Type.STRING),
"tableName", new Schema().withType(Type.STRING),
"values", new Schema().withType(Type.OBJECT).withDescription("Keys for each field in the table"),
"displayValues", new Schema().withType(Type.OBJECT)
))
)
));
return schema;
}
/***************************************************************************
**
***************************************************************************/
public static QueryMiddlewareInput buildInput(Map<String, String> paramMap) throws IOException
{
QQueryFilter filter = null;
String filterParam = paramMap.get("filter");
if(StringUtils.hasContent(filterParam))
{
filter = JsonUtils.toObject(filterParam, QQueryFilter.class);
}
List<QueryJoin> queryJoins = null;
String queryJoinsParam = paramMap.get("queryJoins");
if(StringUtils.hasContent(queryJoinsParam))
{
queryJoins = new ArrayList<>();
JSONArray queryJoinsJSON = new JSONArray(queryJoinsParam);
for(int i = 0; i < queryJoinsJSON.length(); i++)
{
QueryJoin queryJoin = new QueryJoin();
queryJoins.add(queryJoin);
JSONObject jsonObject = queryJoinsJSON.getJSONObject(i);
queryJoin.setJoinTable(jsonObject.optString("joinTable"));
if(jsonObject.has("baseTableOrAlias") && !jsonObject.isNull("baseTableOrAlias"))
{
queryJoin.setBaseTableOrAlias(jsonObject.optString("baseTableOrAlias"));
}
if(jsonObject.has("alias") && !jsonObject.isNull("alias"))
{
queryJoin.setAlias(jsonObject.optString("alias"));
}
queryJoin.setSelect(jsonObject.optBoolean("select"));
if(jsonObject.has("type") && !jsonObject.isNull("type"))
{
queryJoin.setType(QueryJoin.Type.valueOf(jsonObject.getString("type")));
}
if(jsonObject.has("joinName") && !jsonObject.isNull("joinName"))
{
queryJoin.setJoinMetaData(QContext.getQInstance().getJoin(jsonObject.getString("joinName")));
}
}
}
return new QueryMiddlewareInput()
.withTable(paramMap.get("table"))
.withFilter(filter)
.withQueryJoins(queryJoins);
}
}

View File

@ -0,0 +1,66 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.specs.v1.utils;
import com.kingsrook.qqq.middleware.javalin.specs.TagsInterface;
/*******************************************************************************
**
*******************************************************************************/
public enum TagsV1 implements TagsInterface
{
AUTHENTICATION("Authentication"),
GENERAL("General"),
QUERY("Query"),
INSERT("Insert"),
UPDATE("Update"),
DELETE("Delete"),
PROCESSES("Processes"),
REPORTS("Reports"),
WIDGETS("Widgets");
private final String text;
/***************************************************************************
**
***************************************************************************/
TagsV1(String text)
{
this.text = text;
}
/*******************************************************************************
** Getter for text
**
*******************************************************************************/
public String getText()
{
return text;
}
}

View File

@ -0,0 +1,160 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.tools;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.yaml.snakeyaml.LoaderOptions;
/*******************************************************************************
**
*******************************************************************************/
public class APIUtils
{
public static final String PUBLISHED_API_LOCATION = "qqq-middleware-javalin/src/main/resources/openapi/";
public static final List<String> FILE_FORMATS = List.of("json", "yaml");
/*******************************************************************************
**
*******************************************************************************/
public static String getPublishedAPIFile(String apiPath, String name, String fileFormat) throws Exception
{
String fileLocation = apiPath + "/" + name + "." + fileFormat;
File file = new File(fileLocation);
if(!file.exists())
{
throw (new Exception("Error: The file [" + file.getPath() + "] could not be found."));
}
Path path = Paths.get(fileLocation);
return (StringUtils.join("\n", Files.readAllLines(path)));
}
/*******************************************************************************
** get a map representation of the yaml.
** we'll remove things from that map, writing out specific sub-files that we know we want (e.g., per-table & process).
** also, there are some objects we just don't care about (e.g., tags, they'll always be in lock-step with the tables).
** then we'll write out everything else left in the map at the end.
*******************************************************************************/
@SuppressWarnings("unchecked")
static Map<String, Map<String, Object>> splitUpYamlForPublishing(String openApiYaml) throws JsonProcessingException
{
Map<String, Object> apiYaml = parseYaml(openApiYaml);
Map<String, Object> components = (Map<String, Object>) apiYaml.get("components");
Map<String, Object> schemas = (Map<String, Object>) components.get("schemas");
Map<String, Object> paths = (Map<String, Object>) apiYaml.remove("paths");
apiYaml.remove("tags");
Map<String, Map<String, Object>> groupedPaths = new HashMap<>();
for(Map.Entry<String, Object> entry : paths.entrySet())
{
///////////////////////////////////////////////////////////////////////////////
// keys here look like: apiName/apiVersion/table-or-process/<maybe-more> //
// let's discard the apiName & version, and group under the table-or-process //
///////////////////////////////////////////////////////////////////////////////
String key = entry.getKey();
String[] parts = key.split("/");
String uniquePart = parts[3];
groupedPaths.computeIfAbsent(uniquePart, k -> new TreeMap<>());
groupedPaths.get(uniquePart).put(entry.getKey(), entry.getValue());
}
for(Map.Entry<String, Map<String, Object>> entry : groupedPaths.entrySet())
{
String name = entry.getKey();
Map<String, Object> subMap = entry.getValue();
if(schemas.containsKey(name))
{
subMap.put("schema", schemas.remove(name));
}
name += "SearchResult";
if(schemas.containsKey(name))
{
subMap.put("searchResultSchema", schemas.remove(name));
}
}
////////////////////////////////////////////////////////
// put the left-over yaml as a final entry in the map //
////////////////////////////////////////////////////////
groupedPaths.put("openapi", apiYaml);
return groupedPaths;
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
private static Map<String, Object> parseYaml(String yaml) throws JsonProcessingException
{
////////////////////////////////////////////////////////////////////////////////////////////////////////
// need a larger limit than you get by default (and qqq's yamlUtils doens't let us customize this...) //
////////////////////////////////////////////////////////////////////////////////////////////////////////
LoaderOptions loaderOptions = new LoaderOptions();
loaderOptions.setCodePointLimit(100 * 1024 * 1024); // 100 MB
YAMLFactory yamlFactory = YAMLFactory.builder()
.loaderOptions(loaderOptions)
.build();
YAMLMapper mapper = new YAMLMapper(yamlFactory);
mapper.findAndRegisterModules();
return (mapper.readValue(yaml, Map.class));
}
/*******************************************************************************
**
*******************************************************************************/
public static File[] listPublishedAPIFiles(String path) throws Exception
{
File file = new File(path);
if(!file.exists())
{
throw (new Exception("Error: API Directory [" + file.getPath() + "] could not be found."));
}
File[] files = file.listFiles();
return (files);
}
}

View File

@ -0,0 +1,150 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.tools;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.Callable;
import com.fasterxml.jackson.databind.MapperFeature;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.YamlUtils;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion;
import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
import com.kingsrook.qqq.openapi.model.OpenAPI;
import picocli.CommandLine;
/*******************************************************************************
**
*******************************************************************************/
@CommandLine.Command(name = "publishAPI")
public class PublishAPI implements Callable<Integer>
{
@CommandLine.Option(names = { "-r", "--repoRoot" })
private String repoRoot;
@CommandLine.Option(names = { "--sortFileContentsForHuman" }, description = "By default, properties in the yaml are sorted alphabetically, to help with stability (for diffing). This option preserves the 'natural' order of the file, so may look a little bette for human consumption")
private boolean sortFileContentsForHuman = false;
/*******************************************************************************
**
*******************************************************************************/
public static void main(String[] args) throws Exception
{
// for a run from the IDE, to override args... args = new String[] { "-r", "/Users/dkelkhoff/git/kingsrook/qqq/" };
int exitCode = new CommandLine(new PublishAPI()).execute(args);
System.exit(exitCode);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Integer call() throws Exception
{
AbstractMiddlewareVersion middlewareVersion = new MiddlewareVersionV1();
if(!StringUtils.hasContent(repoRoot))
{
throw (new QException("Repo root argument was not given."));
}
if(!new File(repoRoot).exists())
{
throw (new QException("Repo root directory [" + repoRoot + "] was not found."));
}
String allApisPath = repoRoot + "/" + APIUtils.PUBLISHED_API_LOCATION + "/";
if(!new File(allApisPath).exists())
{
throw (new QException("APIs directory [" + allApisPath + "] was not found."));
}
File versionDirectory = new File(allApisPath + middlewareVersion.getVersion() + "/");
if(!versionDirectory.exists())
{
if(!versionDirectory.mkdirs())
{
// CTEngCliUtils.printError("Error: An error occurred creating directory [" + apiDirectory.getPath() + "].");
System.err.println("Error: An error occurred creating directory [" + versionDirectory.getPath() + "].");
return (1);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////
// build the openapi spec - then run it through a "grouping" function, which will make several //
// subsets of it (e.g., grouped by table mostly) - then we'll write out each such file //
/////////////////////////////////////////////////////////////////////////////////////////////////
OpenAPI openAPI = middlewareVersion.generateOpenAPIModel("qqq");
String yaml = YamlUtils.toYaml(openAPI, mapper ->
{
if(sortFileContentsForHuman)
{
////////////////////////////////////////////////
// this is actually the default mapper config //
////////////////////////////////////////////////
}
else
{
mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
}
});
writeFile(yaml, versionDirectory, "openapi.yaml");
//////////////////////////////////////////////////////////////////////////////////////
// if we want to split up by some paths, components, we could use a version of this //
//////////////////////////////////////////////////////////////////////////////////////
// Map<String, Map<String, Object>> groupedPaths = APIUtils.splitUpYamlForPublishing(yaml);
// for(String name : groupedPaths.keySet())
// {
// writeFile(groupedPaths.get(name), versionDirectory, name + ".yaml");
// }
// CTEngCliUtils.printSuccess("Files for [" + apiInstanceMetaData.getName() + "] [" + apiVersion + "] have been successfully published.");
// System.out.println("Files for [" + middlewareVersion.getClass().getSimpleName() + "] have been successfully published.");
return (0);
}
/*******************************************************************************
**
*******************************************************************************/
private void writeFile(String yaml, File directory, String fileBaseName) throws IOException
{
String yamlFileName = directory.getAbsolutePath() + "/" + fileBaseName;
Path yamlPath = Paths.get(yamlFileName);
Files.write(yamlPath, yaml.getBytes());
System.out.println("Wrote [" + yamlPath + "]");
}
}

View File

@ -0,0 +1,253 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.tools;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import com.fasterxml.jackson.databind.MapperFeature;
import com.kingsrook.qqq.backend.core.utils.YamlUtils;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion;
import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
import com.kingsrook.qqq.openapi.model.OpenAPI;
import picocli.CommandLine;
/*******************************************************************************
**
*******************************************************************************/
@CommandLine.Command(name = "validateApiVersions")
public class ValidateAPIVersions implements Callable<Integer>
{
@CommandLine.Option(names = { "-r", "--repoRoot" })
String repoRoot;
/*******************************************************************************
**
*******************************************************************************/
public static void main(String[] args) throws Exception
{
// for a run from the IDE, to override args... args = new String[] { "-r", "/Users/dkelkhoff/git/kingsrook/qqq/" };
int exitCode = new CommandLine(new ValidateAPIVersions()).execute(args);
System.out.println("Exiting with code: " + exitCode);
System.exit(exitCode);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Integer call() throws Exception
{
String fileFormat = "yaml";
boolean hadErrors = false;
List<String> errorHeaders = new ArrayList<>();
List<AbstractMiddlewareVersion> specList = List.of(new MiddlewareVersionV1());
for(AbstractMiddlewareVersion middlewareVersion : specList)
{
String version = middlewareVersion.getVersion();
boolean hadErrorsThisVersion = false;
//////////
// todo //
//////////
// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// // if this api version is in the list of "future" versions, consider it a "beta" and don't do any validation //
// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// for(APIVersion futureAPIVersion : apiInstanceMetaData.getFutureVersions())
// {
// if(apiVersion.equals(futureAPIVersion))
// {
// continue versionLoop;
// }
// }
try
{
////////////////////////////////////////////////////////////
// list current files - so we can tell if all get diff'ed //
////////////////////////////////////////////////////////////
Set<String> existingFileNames = new HashSet<>();
String versionPath = repoRoot + "/" + APIUtils.PUBLISHED_API_LOCATION + "/" + version + "/";
versionPath = versionPath.replaceAll("/+", "/");
for(File file : APIUtils.listPublishedAPIFiles(versionPath))
{
existingFileNames.add(file.getPath().replaceFirst(versionPath, ""));
}
///////////////////////////////////////////////////////////
// generate a new spec based on current code in codebase //
///////////////////////////////////////////////////////////
OpenAPI openAPI = middlewareVersion.generateOpenAPIModel("qqq");
String yaml = YamlUtils.toYaml(openAPI, mapper ->
{
mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
});
/////////////////////////////////////////////////////////////////////
// get the published API file - then diff it to what we just wrote //
/////////////////////////////////////////////////////////////////////
String publishedAPI = APIUtils.getPublishedAPIFile(versionPath, "openapi", fileFormat);
String newFileName = "/tmp/" + version + "-new." + fileFormat;
String publishedFileName = "/tmp/" + version + "-published." + fileFormat;
Files.write(Path.of(newFileName), yaml.getBytes());
Files.write(Path.of(publishedFileName), publishedAPI.getBytes());
Runtime rt = Runtime.getRuntime();
String[] commands = { "diff", "-w", publishedFileName, newFileName };
Process proc = rt.exec(commands);
BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
StringBuilder diffOutput = new StringBuilder();
String s;
while((s = stdInput.readLine()) != null)
{
diffOutput.append(s).append("\n");
}
if(!"".contentEquals(diffOutput))
{
String message = "Error: Differences were found in openapi.yaml file between the published docs and the newly generated file for API Version [" + version + "].";
errorHeaders.add(message);
System.out.println(message);
System.out.println(diffOutput);
hadErrors = true;
hadErrorsThisVersion = true;
}
//////////////////////////////////////////////////////////////////////////////////////
// if we want to split up by some paths, components, we could use a version of this //
//////////////////////////////////////////////////////////////////////////////////////
/*
Map<String, Map<String, Object>> groupedPaths = APIUtils.splitUpYamlForPublishing(yaml);
///////////////////////////////////////////////////////////////////////////////////////
// for each of the groupings (e.g., files), compare to what was previously published //
///////////////////////////////////////////////////////////////////////////////////////
for(Map.Entry<String, Map<String, Object>> entry : groupedPaths.entrySet())
{
try
{
String name = entry.getKey();
String newFileToDiff = YamlUtils.toYaml(entry.getValue());
/////////////////////////////////////////////////////////////////////
// get the published API file - then diff it to what we just wrote //
/////////////////////////////////////////////////////////////////////
String publishedAPI = APIUtils.getPublishedAPIFile(versionPath, name, fileFormat);
existingFileNames.remove(name + "." + fileFormat);
String newFileName = "/tmp/" + version + "-new." + fileFormat;
String publishedFileName = "/tmp/" + version + "-published." + fileFormat;
Files.write(Path.of(newFileName), newFileToDiff.getBytes());
Files.write(Path.of(publishedFileName), publishedAPI.getBytes());
Runtime rt = Runtime.getRuntime();
String[] commands = { "diff", "-w", publishedFileName, newFileName };
Process proc = rt.exec(commands);
BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
StringBuilder diffOutput = new StringBuilder();
String s;
while((s = stdInput.readLine()) != null)
{
diffOutput.append(s).append("\n");
}
if(!"".contentEquals(diffOutput))
{
String message = "Error: Differences were found in file [" + name + "] between the published docs and the newly generated " + fileFormat + " file for API Version [" + version + "].";
errorHeaders.add(message);
System.out.println(message);
System.out.println(diffOutput);
hadErrors = true;
hadErrorsThisVersion = true;
}
}
catch(Exception e)
{
errorHeaders.add(e.getMessage());
System.out.println(e.getMessage());
hadErrors = true;
hadErrorsThisVersion = true;
}
}
/////////////////////////////////////////////////////////////////////////////////////
// if any existing files weren't evaluated in the loop above, then that's an error //
// e.g., someone removed a thing that was previously in the API //
/////////////////////////////////////////////////////////////////////////////////////
if(!existingFileNames.isEmpty())
{
hadErrors = true;
hadErrorsThisVersion = true;
for(String existingFileName : existingFileNames)
{
String message = "Error: Previously published file [" + existingFileName + "] was not found in the current OpenAPI object for API Version [" + version + "].";
errorHeaders.add(message);
System.out.println(message);
}
}
*/
}
catch(Exception e)
{
errorHeaders.add(e.getMessage());
System.out.println(e.getMessage());
hadErrors = true;
hadErrorsThisVersion = true;
}
if(!hadErrorsThisVersion)
{
System.out.println("Success: No differences were found between the published docs and the newly generated " + fileFormat + " file for API Version [" + version + "].");
}
}
if(!errorHeaders.isEmpty())
{
System.out.println("\nError summary:");
errorHeaders.forEach(e -> System.out.println(" - " + e));
}
return (hadErrors ? 1 : 0);
}
}

View File

@ -0,0 +1,172 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.tools.codegenerators;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.apache.commons.io.FileUtils;
/*******************************************************************************
**
*******************************************************************************/
class ExecutorCodeGenerator
{
/***************************************************************************
**
***************************************************************************/
public static void main(String[] args)
{
try
{
String qqqDir = "/Users/dkelkhoff/git/kingsrook/qqq/";
new ExecutorCodeGenerator().writeAllFiles(qqqDir, "ProcessMetaData"); // don't include "Executor" on the end.
}
catch(IOException e)
{
//noinspection CallToPrintStackTrace
e.printStackTrace();
}
}
/***************************************************************************
**
***************************************************************************/
private void writeOne(String fullPath, String content) throws IOException
{
File file = new File(fullPath);
File directory = file.getParentFile();
if(!directory.exists())
{
throw (new RuntimeException("Directory for: " + fullPath + " does not exists, and I refuse to mkdir (do it yourself and/or fix your arguments)."));
}
if(file.exists())
{
throw (new RuntimeException("File at: " + fullPath + " already exists, and I refuse to overwrite files."));
}
System.out.println("Writing: " + file);
FileUtils.writeStringToFile(file, content, StandardCharsets.UTF_8);
}
/***************************************************************************
**
***************************************************************************/
void writeAllFiles(String rootPath, String baseName) throws IOException
{
if(baseName.endsWith("Executor"))
{
throw new IllegalArgumentException("Base name must not end with 'Executor'.");
}
String basePath = rootPath + "qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/";
writeOne(basePath + "executors/" + baseName + "Executor.java", makeExecutor(baseName));
writeOne(basePath + "executors/io/" + baseName + "Input.java", makeInput(baseName));
writeOne(basePath + "executors/io/" + baseName + "OutputInterface.java", makeOutputInterface(baseName));
}
/***************************************************************************
**
***************************************************************************/
private String makeExecutor(String baseName)
{
return """
package com.kingsrook.qqq.middleware.javalin.executors;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.middleware.javalin.executors.io.${baseName}Input;
import com.kingsrook.qqq.middleware.javalin.executors.io.${baseName}OutputInterface;
/*******************************************************************************
**
*******************************************************************************/
public class ${baseName}Executor extends AbstractMiddlewareExecutor<${baseName}Input, ${baseName}OutputInterface>
{
/***************************************************************************
**
***************************************************************************/
@Override
public void execute(${baseName}Input input, ${baseName}OutputInterface output) throws QException
{
}
}
""".replaceAll("\\$\\{baseName}", baseName);
}
/***************************************************************************
**
***************************************************************************/
private String makeInput(String baseName)
{
return """
package com.kingsrook.qqq.middleware.javalin.executors.io;
/*******************************************************************************
**
*******************************************************************************/
public class ${baseName}Input extends AbstractMiddlewareInput
{
}
""".replaceAll("\\$\\{baseName}", baseName);
}
/***************************************************************************
**
***************************************************************************/
private String makeOutputInterface(String baseName)
{
return """
package com.kingsrook.qqq.middleware.javalin.executors.io;
/*******************************************************************************
**
*******************************************************************************/
public interface ${baseName}OutputInterface extends AbstractMiddlewareOutputInterface
{
}
""".replaceAll("\\$\\{baseName}", baseName);
}
}

View File

@ -0,0 +1,300 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.tools.codegenerators;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.apache.commons.io.FileUtils;
/*******************************************************************************
**
*******************************************************************************/
class SpecCodeGenerator
{
/***************************************************************************
**
***************************************************************************/
public static void main(String[] args)
{
try
{
String qqqDir = "/Users/dkelkhoff/git/kingsrook/qqq/";
/////////////////
// normal case //
/////////////////
new SpecCodeGenerator().writeAllFiles(qqqDir, "v1", "ProcessMetaData");
///////////////////////////////////////////////////////////////////////////////
// if the executor isn't named the same as the spec (e.g., reused executors) //
///////////////////////////////////////////////////////////////////////////////
// new SpecCodeGenerator().writeAllFiles(qqqDir, "v1", "ProcessInsert", "ProcessInsertOrSetp");
}
catch(IOException e)
{
//noinspection CallToPrintStackTrace
e.printStackTrace();
}
}
/***************************************************************************
**
***************************************************************************/
private void writeOne(String fullPath, String content) throws IOException
{
File file = new File(fullPath);
File directory = file.getParentFile();
if(!directory.exists())
{
throw (new RuntimeException("Directory for: " + fullPath + " does not exists, and I refuse to mkdir (do it yourself and/or fix your arguments)."));
}
if(file.exists())
{
throw (new RuntimeException("File at: " + fullPath + " already exists, and I refuse to overwrite files."));
}
System.out.println("Writing: " + file);
FileUtils.writeStringToFile(file, content, StandardCharsets.UTF_8);
}
/***************************************************************************
**
***************************************************************************/
void writeAllFiles(String rootPath, String version, String baseName) throws IOException
{
writeAllFiles(rootPath, version, baseName, baseName);
}
/***************************************************************************
**
***************************************************************************/
private void writeAllFiles(String rootPath, String version, String baseName, String executorBaseName) throws IOException
{
String basePath = rootPath + "qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/";
writeOne(basePath + "specs/" + version.toLowerCase() + "/" + baseName + "Spec" + version.toUpperCase() + ".java", makeSpec(version, baseName, executorBaseName));
writeOne(basePath + "specs/" + version.toLowerCase() + "/responses/" + baseName + "Response" + version.toUpperCase() + ".java", makeResponse(version, baseName, executorBaseName));
System.out.println();
System.out.println("Hey - You probably want to add a line like:");
System.out.println(" list.add(new " + baseName + "Spec" + version.toUpperCase() + "());");
System.out.println("In:");
System.out.println(" MiddlewareVersion" + version.toUpperCase() + ".java");
System.out.println();
}
/***************************************************************************
**
***************************************************************************/
private String makeSpec(String version, String baseName, String executorBaseName)
{
return """
package com.kingsrook.qqq.middleware.javalin.specs.${version.toLowerCase};
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.middleware.javalin.executors.${executorBaseName}Executor;
import com.kingsrook.qqq.middleware.javalin.executors.io.${executorBaseName}Input;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
import com.kingsrook.qqq.middleware.javalin.specs.${version.toLowerCase}.responses.${executorBaseName}Response${version.toUpperCase};
import com.kingsrook.qqq.middleware.javalin.specs.${version.toLowerCase}.utils.Tags${version.toUpperCase};
import com.kingsrook.qqq.openapi.model.Content;
import com.kingsrook.qqq.openapi.model.Example;
import com.kingsrook.qqq.openapi.model.HttpMethod;
import com.kingsrook.qqq.openapi.model.In;
import com.kingsrook.qqq.openapi.model.Parameter;
import com.kingsrook.qqq.openapi.model.RequestBody;
import com.kingsrook.qqq.openapi.model.Schema;
import com.kingsrook.qqq.openapi.model.Type;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
/*******************************************************************************
**
*******************************************************************************/
public class ${baseName}Spec${version.toUpperCase} extends AbstractEndpointSpec<${executorBaseName}Input, ${baseName}Response${version.toUpperCase}, ${executorBaseName}Executor>
{
/***************************************************************************
**
***************************************************************************/
public BasicOperation defineBasicOperation()
{
return new BasicOperation()
.withPath(TODO)
.withHttpMethod(HttpMethod.TODO)
.withTag(Tags${version.toUpperCase}.TODO)
.withShortSummary(TODO)
.withLongDescription(""\"
TODO""\"
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public List<Parameter> defineRequestParameters()
{
return List.of(
new Parameter()
.withName(TODO)
.withDescription(TODO)
.withRequired(TODO)
.withSchema(new Schema().withType(Type.TODO))
.withExamples(Map.of("TODO", new Example().withValue(TODO)))
.withIn(In.TODO)
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public RequestBody defineRequestBody()
{
return new RequestBody()
.withContent(Map.of(
ContentType.TODO.getMimeType(), new Content()
.withSchema(new Schema()
.withType(Type.TODO)
.withProperties(Map.of(
"TODO", new Schema()
))
)
));
}
/***************************************************************************
**
***************************************************************************/
@Override
public ${executorBaseName}Input buildInput(Context context) throws Exception
{
${executorBaseName}Input input = new ${executorBaseName}Input();
input.setTODO
return (input);
}
/***************************************************************************
**
***************************************************************************/
@Override
public BasicResponse defineBasicSuccessResponse()
{
Map<String, Example> examples = Map.of(
"TODO", new Example()
.withValue(new ${baseName}Response${version.toUpperCase}()
.withTODO
)
);
return new BasicResponse(""\"
TODO""\",
new ${baseName}Response${version.toUpperCase}().toSchema(),
examples
);
}
/***************************************************************************
**
***************************************************************************/
@Override
public void handleOutput(Context context, ${baseName}Response${version.toUpperCase} output) throws Exception
{
context.result(JsonUtils.toJson(output));
}
}
"""
.replaceAll("\\$\\{version.toLowerCase}", version.toLowerCase())
.replaceAll("\\$\\{version.toUpperCase}", version.toUpperCase())
.replaceAll("\\$\\{executorBaseName}", executorBaseName)
.replaceAll("\\$\\{baseName}", baseName)
;
}
/***************************************************************************
**
***************************************************************************/
private String makeResponse(String version, String baseName, String executorBaseName)
{
return """
package com.kingsrook.qqq.middleware.javalin.specs.${version.toLowerCase}.responses;
import com.kingsrook.qqq.middleware.javalin.executors.io.${executorBaseName}OutputInterface;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.specs.ToSchema;
/*******************************************************************************
**
*******************************************************************************/
public class ${baseName}Response${version.toUpperCase} implements ${executorBaseName}OutputInterface, ToSchema
{
@OpenAPIDescription(TODO)
private String TODO;
TODO gsw
}
"""
.replaceAll("\\$\\{version.toLowerCase}", version.toLowerCase())
.replaceAll("\\$\\{version.toUpperCase}", version.toUpperCase())
.replaceAll("\\$\\{executorBaseName}", executorBaseName)
.replaceAll("\\$\\{baseName}", baseName)
;
}
}

View File

@ -0,0 +1,27 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*******************************************************************************
** These classes are meant as tools to be executed manually by a developer,
** to create other new classes (since there's a bit of boilerplate, innit?)
**
*******************************************************************************/
package com.kingsrook.qqq.middleware.javalin.tools.codegenerators;

View File

@ -0,0 +1,27 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*******************************************************************************
** This tools path - is for non-production code - rather, development and CI/CD
** tools.
**
*******************************************************************************/
package com.kingsrook.qqq.middleware.javalin.tools;

Some files were not shown because too many files have changed in this diff Show More