CE-1887 Initial (not quite finished) version 1 middleware api spec

This commit is contained in:
2024-10-17 20:26:54 -05:00
parent 8dedc98866
commit cc55b32206
89 changed files with 11930 additions and 86 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.adapters.QInstanceAdapter;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; 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.QException;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; 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.QUserFacingException;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
@ -1709,7 +1707,7 @@ public class QJavalinImplementation
} }
catch(QUserFacingException e) catch(QUserFacingException e)
{ {
handleException(HttpStatus.Code.BAD_REQUEST, context, e); QJavalinUtils.handleException(HttpStatus.Code.BAD_REQUEST, context, e);
return null; return null;
} }
return reportFormat; return reportFormat;
@ -1849,67 +1847,7 @@ public class QJavalinImplementation
*******************************************************************************/ *******************************************************************************/
public static void handleException(Context context, Exception e) public static void handleException(Context context, Exception e)
{ {
handleException(null, context, e); QJavalinUtils.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)));
} }

View File

@ -236,13 +236,13 @@ public class QJavalinProcessHandler
if(inputField.getIsRequired() && !setValue) 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; return;
} }
} }
catch(Exception e) 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; return;
} }
} }

View File

@ -22,11 +22,21 @@
package com.kingsrook.qqq.backend.javalin; package com.kingsrook.qqq.backend.javalin;
import java.util.Map;
import java.util.Objects; 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.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.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import io.javalin.http.Context; import io.javalin.http.Context;
import org.eclipse.jetty.http.HttpStatus;
/******************************************************************************* /*******************************************************************************
@ -34,6 +44,10 @@ import io.javalin.http.Context;
*******************************************************************************/ *******************************************************************************/
public class QJavalinUtils 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, ** Returns Integer if context has a valid int query parameter by the given name,
@ -178,4 +192,64 @@ public class QJavalinUtils
return value; 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,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,49 @@
/*
* 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
{
MetaDataAction metaDataAction = new MetaDataAction();
MetaDataOutput metaDataOutput = metaDataAction.execute(new com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput());
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,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 MetaDataInput extends AbstractMiddlewareInput
{
}

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

@ -34,6 +34,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.stream.Collectors; 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.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.utils.StringUtils; 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.OpenAPIDescription;
@ -64,13 +65,57 @@ public class SchemaBuilder
} }
/***************************************************************************
**
***************************************************************************/
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) private Schema classToSchema(Class<?> c, AnnotatedElement element)
{ {
Schema schema = new Schema(); SchemaFromBuilder schema = new SchemaFromBuilder();
schema.setOriginalClass(c);
if(c.isEnum()) if(c.isEnum())
{ {
@ -121,10 +166,10 @@ public class SchemaBuilder
if(openAPIMapKnownEntriesAnnotation != null) if(openAPIMapKnownEntriesAnnotation != null)
{ {
schema.withRef("#/components/schemas/" + openAPIMapKnownEntriesAnnotation.value().getSimpleName()); schema.withRef("#/components/schemas/" + openAPIMapKnownEntriesAnnotation.value().getSimpleName());
// if(openAPIMapKnownEntriesAnnotation.additionalProperties()) // if(openAPIMapKnownEntriesAnnotation.additionalProperties())
// { // {
// schema.withAdditionalProperties(true); // schema.withAdditionalProperties(true);
// } // }
} }
OpenAPIMapValueType openAPIMapValueTypeAnnotation = element.getAnnotation(OpenAPIMapValueType.class); OpenAPIMapValueType openAPIMapValueTypeAnnotation = element.getAnnotation(OpenAPIMapValueType.class);
@ -142,13 +187,23 @@ public class SchemaBuilder
} }
else else
{ {
OpenAPIOneOf openAPIOneOfAnnotation = element.getAnnotation(OpenAPIOneOf.class); OpenAPIOneOf openAPIOneOfAnnotation = element.getAnnotation(OpenAPIOneOf.class);
OpenAPIMapKnownEntries openAPIMapKnownEntriesAnnotation = element.getAnnotation(OpenAPIMapKnownEntries.class);
if(openAPIOneOfAnnotation != null) if(openAPIOneOfAnnotation != null)
{ {
String description = "[" + element + "]"; String description = "[" + element + "]";
List<Schema> oneOfList = processOneOfAnnotation(openAPIOneOfAnnotation, c, description); List<Schema> oneOfList = processOneOfAnnotation(openAPIOneOfAnnotation, c, description);
schema.withOneOf(oneOfList); schema.withOneOf(oneOfList);
} }
// todo no, lot like this else if(openAPIMapKnownEntriesAnnotation != null)
// todo no, lot like this {
// todo no, lot like this schema.withRef("#/components/schemas/" + openAPIMapKnownEntriesAnnotation.value().getSimpleName());
// todo no, lot like this // if(openAPIMapKnownEntriesAnnotation.additionalProperties())
// todo no, lot like this // {
// todo no, lot like this // schema.withAdditionalProperties(true);
// todo no, lot like this // }
// todo no, lot like this }
else else
{ {
///////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////
@ -296,5 +351,4 @@ public class SchemaBuilder
return oneOfList; return oneOfList;
} }
} }

View File

@ -31,7 +31,7 @@ import java.lang.annotation.Target;
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@Target({ ElementType.TYPE }) @Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface OpenAPIHasAdditionalProperties public @interface OpenAPIHasAdditionalProperties
{ {

View File

@ -0,0 +1,545 @@
/*
* 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);
}
}
/***************************************************************************
**
***************************************************************************/
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,360 @@
/*
* 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);
}
}
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,214 @@
/*
* 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.HashMap;
import java.util.Map;
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.Schema;
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 MetaDataInput buildInput(Context context) throws Exception
{
MetaDataInput input = new MetaDataInput();
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);
QTableMetaData exampleTable = new QTableMetaData()
.withName("person")
.withLabel("Person")
.withBackendName("example")
.withPrimaryKeyField("id")
.withIsHidden(false)
.withIcon(new QIcon().withName("person_outline"))
.withCapabilities(Capability.allReadCapabilities())
.withCapabilities(Capability.allWriteCapabilities())
.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,155 @@
/*
* 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.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;
/***************************************************************************
**
***************************************************************************/
@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));
}
}
/*******************************************************************************
** 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;
}
}

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,159 @@
/*
* 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.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("Name of an icon for the app, from the material UI icon set")
public String getIconName()
{
return (this.wrapped.getIconName());
}
/***************************************************************************
**
***************************************************************************/
@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,105 @@
/*
* 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;
/***************************************************************************
**
***************************************************************************/
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());
}
}

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,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.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("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,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.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;
/***************************************************************************
**
***************************************************************************/
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("Name of an icon for the process, from the material UI icon set")
public String getIconName()
{
return (this.wrapped.getIconName());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("Boolean to indicate if the user has permission for the process.")
public Boolean getHasPermission()
{
return (this.wrapped.getHasPermission());
}
}

View File

@ -0,0 +1,187 @@
/*
* 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;
/***************************************************************************
**
***************************************************************************/
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("Name of an icon for the table, from the material UI icon set")
public String getIconName()
{
return (this.wrapped.getIconName());
}
/***************************************************************************
**
***************************************************************************/
@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,216 @@
/*
* 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
{
ACTION_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);
}
/***************************************************************************
**
***************************************************************************/
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,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.model.dashboard.widgets.blocks.actionbutton.ActionButtonValues;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
/*******************************************************************************
**
*******************************************************************************/
@OpenAPIDescription("Values used for an ACTION_BUTTON type widget block")
public final class WidgetBlockActionButtonValues implements WidgetBlockValues
{
@OpenAPIExclude()
private ActionButtonValues wrapped;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetBlockActionButtonValues(ActionButtonValues actionButtonValues)
{
this.wrapped = actionButtonValues;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetBlockActionButtonValues()
{
}
/***************************************************************************
**
***************************************************************************/
@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());
}
}

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

View File

@ -0,0 +1,61 @@
/*
* 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.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
WidgetBlockTextStyles
{
@OpenAPIExclude
QLogger LOG = QLogger.getLogger(WidgetBlockStyles.class);
/***************************************************************************
**
***************************************************************************/
static WidgetBlockStyles of(BlockStylesInterface blockStyles)
{
if(blockStyles == null)
{
return (null);
}
if(blockStyles instanceof TextStyles s)
{
return (new WidgetBlockTextStyles(s));
}
LOG.warn("Unrecognized block value type: " + blockStyles.getClass().getName());
return (null);
}
}

View File

@ -0,0 +1,76 @@
/*
* 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("Indicate if the text should be displayed as an alert (e.g., modal popup)")
public Boolean getIsAlert()
{
return (this.wrapped.getIsAlert());
}
/***************************************************************************
**
***************************************************************************/
@OpenAPIDescription("A Standard Color to display the text in (e.g., not a hex or RGB code).")
public TextStyles.StandardColor getStandardColor()
{
return (this.wrapped.getStandardColor());
}
}

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

View File

@ -0,0 +1,73 @@
/*
* 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.actionbutton.ActionButtonValues;
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
WidgetBlockActionButtonValues,
WidgetBlockTextValues,
WidgetBlockInputFieldValues
{
@OpenAPIExclude
QLogger LOG = QLogger.getLogger(WidgetBlockValues.class);
/***************************************************************************
**
***************************************************************************/
static WidgetBlockValues of(BlockValuesInterface blockValues)
{
if(blockValues == null)
{
return (null);
}
if(blockValues instanceof TextValues v)
{
return (new WidgetBlockTextValues(v));
}
else if(blockValues instanceof InputFieldValues v)
{
return (new WidgetBlockInputFieldValues(v));
}
else if(blockValues instanceof ActionButtonValues v)
{
return (new WidgetBlockActionButtonValues(v));
}
LOG.warn("Unrecognized block value type: " + blockValues.getClass().getName());
return (null);
}
}

View File

@ -0,0 +1,205 @@
/*
* 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.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.openapi.model.Example;
import com.kingsrook.qqq.openapi.model.Schema;
import io.javalin.http.Context;
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)
{
Map<String, Serializable> values = complete.getValues();
if(values != null)
{
String serializedValues = JsonUtils.toJson(values, mapper ->
{
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
});
outputJsonObject.put("values", new JSONObject(serializedValues));
}
}
context.result(outputJsonObject.toString());
}
/*******************************************************************************
**
*******************************************************************************/
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.middleware.javalin.executors.io.QueryMiddlewareInput;
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.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;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,356 @@
/*
* 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.api.wip;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import java.util.stream.Stream;
import io.javalin.config.Key;
import io.javalin.http.Context;
import io.javalin.http.HandlerType;
import io.javalin.http.HttpStatus;
import io.javalin.json.JsonMapper;
import io.javalin.plugin.ContextPlugin;
import io.javalin.security.RouteRole;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
/*******************************************************************************
**
*******************************************************************************/
public class TestContext implements Context
{
private Map<String, String> queryParams = new LinkedHashMap<>();
private Map<String, String> pathParams = new LinkedHashMap<>();
private Map<String, String> formParams = new LinkedHashMap<>();
private InputStream result;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public TestContext()
{
}
/***************************************************************************
**
***************************************************************************/
public TestContext withQueryParam(String key, String value)
{
queryParams.put(key, value);
return (this);
}
/***************************************************************************
**
***************************************************************************/
public TestContext withPathParam(String key, String value)
{
pathParams.put(key, value);
return (this);
}
/***************************************************************************
**
***************************************************************************/
public TestContext withFormParam(String key, String value)
{
formParams.put(key, value);
return (this);
}
/***************************************************************************
**
***************************************************************************/
@Override
public String queryParam(String key)
{
return queryParams.get(key);
}
/***************************************************************************
**
***************************************************************************/
@Override
public String formParam(String key)
{
return formParams.get(key);
}
/***************************************************************************
**
***************************************************************************/
@Override
public boolean strictContentTypes()
{
return false;
}
/***************************************************************************
**
***************************************************************************/
@Override
public HttpServletRequest req()
{
return null;
}
/***************************************************************************
**
***************************************************************************/
@Override
public HttpServletResponse res()
{
return null;
}
/***************************************************************************
**
***************************************************************************/
@Override
public HandlerType handlerType()
{
return null;
}
/***************************************************************************
**
***************************************************************************/
@Override
public String matchedPath()
{
return "";
}
/***************************************************************************
**
***************************************************************************/
@Override
public String endpointHandlerPath()
{
return "";
}
/***************************************************************************
**
***************************************************************************/
@Override
public <T> T appData(Key<T> key)
{
return null;
}
/***************************************************************************
**
***************************************************************************/
@Override
public JsonMapper jsonMapper()
{
return null;
}
/***************************************************************************
**
***************************************************************************/
@Override
public <T> T with(Class<? extends ContextPlugin<?, T>> aClass)
{
return null;
}
/***************************************************************************
**
***************************************************************************/
@Override
public String pathParam(String key)
{
return pathParams.get(key);
}
/***************************************************************************
**
***************************************************************************/
@Override
public Map<String, String> pathParamMap()
{
return pathParams;
}
/***************************************************************************
**
***************************************************************************/
@Override
public ServletOutputStream outputStream()
{
return null;
}
/***************************************************************************
**
***************************************************************************/
@Override
public Context minSizeForCompression(int i)
{
return null;
}
/***************************************************************************
**
***************************************************************************/
@Override
public Context result(InputStream inputStream)
{
this.result = inputStream;
return (this);
}
/***************************************************************************
**
***************************************************************************/
@Override
public InputStream resultInputStream()
{
return null;
}
/***************************************************************************
**
***************************************************************************/
@Override
public void future(Supplier<? extends CompletableFuture<?>> supplier)
{
}
/***************************************************************************
**
***************************************************************************/
@Override
public void redirect(String s, HttpStatus httpStatus)
{
}
/***************************************************************************
**
***************************************************************************/
@Override
public void writeJsonStream(Stream<?> stream)
{
}
/***************************************************************************
**
***************************************************************************/
@Override
public Context skipRemainingHandlers()
{
return null;
}
/***************************************************************************
**
***************************************************************************/
@Override
public Set<RouteRole> routeRoles()
{
return Set.of();
}
/*******************************************************************************
** Getter for response
**
*******************************************************************************/
public String getResultAsString() throws IOException
{
byte[] bytes = IOUtils.readFully(result, result.available());
return new String(bytes, StandardCharsets.UTF_8);
}
}

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.api.wip;
import java.util.Map;
import com.kingsrook.qqq.openapi.model.Schema;
import com.kingsrook.qqq.openapi.model.Type;
import org.assertj.core.api.AbstractStringAssert;
import org.json.JSONArray;
import org.json.JSONObject;
import static org.assertj.core.api.Assertions.assertThat;
/*******************************************************************************
**
*******************************************************************************/
public class TestUtils
{
/***************************************************************************
**
***************************************************************************/
private static void assertStringVsSchema(String string, Schema schema, String path)
{
String description = "At path " + path;
final AbstractStringAssert<?> assertion = assertThat(string).describedAs(description);
Type type = Type.valueOf(schema.getType().toUpperCase());
switch(type)
{
case OBJECT ->
{
assertion.startsWith("{");
JSONObject object = new JSONObject(string);
for(Map.Entry<String, Schema> entry : schema.getProperties().entrySet())
{
// todo deal with optional
Object subObject = object.get(entry.getKey());
assertStringVsSchema(subObject.toString(), entry.getValue(), path + "/" + entry.getKey());
}
}
case ARRAY ->
{
assertion.startsWith("[");
JSONArray array = new JSONArray(string);
for(int i = 0; i < array.length(); i++)
{
Object subObject = array.get(i);
assertStringVsSchema(subObject.toString(), schema.getItems(), path + "[" + i + "]");
}
}
case BOOLEAN ->
{
assertion.matches("(true|false)");
}
case INTEGER ->
{
assertion.matches("-?\\d+");
}
case NUMBER ->
{
assertion.matches("-?\\d+(\\.\\d+)?");
}
case STRING ->
{
assertion.matches("\".*\"");
}
}
}
}

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.api.wip.v1;
import com.kingsrook.qqq.api.wip.TestContext;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import org.junit.jupiter.api.Test;
/*******************************************************************************
** Unit test for QueryGetSpecV1
*******************************************************************************/
class QueryGetSpecV1Test
{
/***************************************************************************
**
***************************************************************************/
@Test
void testBuildInput() throws Exception
{
TestContext context = new TestContext()
.withPathParam("table", "person")
.withQueryParam("filter", "{}");
// QueryMiddlewareInput queryMiddlewareInput = new QueryGetSpecV1().buildInput(context);
// assertEquals("person", queryMiddlewareInput.getTable());
// assertNotNull(queryMiddlewareInput.getFilter());
// assertEquals(0, queryMiddlewareInput.getFilter().getCriteria().size());
// assertEquals(0, queryMiddlewareInput.getFilter().getOrderBys().size());
// assertNull(queryMiddlewareInput.getQueryJoins());
}
/***************************************************************************
**
***************************************************************************/
@Test
void testBuildOutput() throws Exception
{
TestContext context = new TestContext();
QueryOutput queryOutput = new QueryOutput(new QueryInput());
queryOutput.addRecord(new QRecord().withValue("firstName", "Darin").withDisplayValue("firstName", "Darin"));
// QueryGetSpecV1 queryGetSpecV1 = new QueryGetSpecV1();
// queryGetSpecV1.buildOutput(context, new QueryMiddlewareOutput(queryOutput));
// String resultJson = context.getResultAsString();
// TestUtils.assertResultJsonVsSpec(queryGetSpecV1.defineSimpleSuccessResponse(), resultJson);
}
}

View File

@ -57,6 +57,7 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase
/******************************************************************************* /*******************************************************************************
** test running a process ** test running a process
** **
** Note: ported to v1
*******************************************************************************/ *******************************************************************************/
@Test @Test
public void test_processGreetInit() public void test_processGreetInit()
@ -73,6 +74,7 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase
/******************************************************************************* /*******************************************************************************
** test running a process that requires rows, but we didn't tell it how to get them. ** test running a process that requires rows, but we didn't tell it how to get them.
** **
** Note: ported to v1
*******************************************************************************/ *******************************************************************************/
@Test @Test
public void test_processRequiresRowsButNotSpecified() public void test_processRequiresRowsButNotSpecified()
@ -90,6 +92,7 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase
/******************************************************************************* /*******************************************************************************
** test running a process and telling it rows to load via recordIds param ** test running a process and telling it rows to load via recordIds param
** **
** Note: ported to v1
*******************************************************************************/ *******************************************************************************/
@Test @Test
public void test_processRequiresRowsWithRecordIdParam() public void test_processRequiresRowsWithRecordIdParam()
@ -108,6 +111,7 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase
/******************************************************************************* /*******************************************************************************
** test running a process and telling it rows to load via filter JSON ** test running a process and telling it rows to load via filter JSON
** **
** Note: ported to v1
*******************************************************************************/ *******************************************************************************/
@Test @Test
public void test_processRequiresRowsWithFilterJSON() public void test_processRequiresRowsWithFilterJSON()
@ -169,6 +173,7 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase
/******************************************************************************* /*******************************************************************************
** test running a process with field values on the query string ** test running a process with field values on the query string
** **
** Note: ported to v1
*******************************************************************************/ *******************************************************************************/
@Test @Test
public void test_processGreetInitWithQueryValues() public void test_processGreetInitWithQueryValues()
@ -185,6 +190,7 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase
/******************************************************************************* /*******************************************************************************
** test init'ing a process that goes async ** test init'ing a process that goes async
** **
** Note: ported to v1, but needs todo more - the status part too in a higher-level
*******************************************************************************/ *******************************************************************************/
@Test @Test
public void test_processInitGoingAsync() throws InterruptedException public void test_processInitGoingAsync() throws InterruptedException
@ -221,6 +227,7 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase
/******************************************************************************* /*******************************************************************************
** test init'ing a process that does NOT goes async ** test init'ing a process that does NOT goes async
** **
** Note: not ported to v1, but feels redundant, so, not going to.
*******************************************************************************/ *******************************************************************************/
@Test @Test
public void test_processInitNotGoingAsync() public void test_processInitNotGoingAsync()

View File

@ -24,11 +24,17 @@ package com.kingsrook.qqq.backend.javalin;
import java.io.InputStream; import java.io.InputStream;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Stream;
import io.javalin.config.Key;
import io.javalin.http.Context; import io.javalin.http.Context;
import io.javalin.http.HandlerType; import io.javalin.http.HandlerType;
import io.javalin.http.HttpStatus; import io.javalin.http.HttpStatus;
import io.javalin.json.JsonMapper;
import io.javalin.plugin.ContextPlugin;
import io.javalin.security.RouteRole;
import jakarta.servlet.ServletOutputStream; import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@ -133,6 +139,14 @@ class QJavalinUtilsTest
@Override
public boolean strictContentTypes()
{
return false;
}
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
@ -157,17 +171,6 @@ class QJavalinUtilsTest
/***************************************************************************
**
***************************************************************************/
@Override
public <T> T appAttribute(@NotNull String s)
{
return null;
}
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
@ -204,6 +207,30 @@ class QJavalinUtilsTest
@Override
public <T> T appData(@NotNull Key<T> key)
{
return null;
}
@Override
public @NotNull JsonMapper jsonMapper()
{
return null;
}
@Override
public <T> T with(@NotNull Class<? extends ContextPlugin<?, T>> aClass)
{
return null;
}
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
@ -240,6 +267,14 @@ class QJavalinUtilsTest
@Override
public @NotNull Context minSizeForCompression(int i)
{
return null;
}
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
@ -283,5 +318,29 @@ class QJavalinUtilsTest
{ {
} }
@Override
public void writeJsonStream(@NotNull Stream<?> stream)
{
}
@Override
public @NotNull Context skipRemainingHandlers()
{
return null;
}
@Override
public @NotNull Set<RouteRole> routeRoles()
{
return Set.of();
}
} }
} }

View File

@ -62,6 +62,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
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.possiblevalues.PVSValueFormatAndFields; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
@ -189,11 +193,35 @@ public class TestUtils
throw new IllegalStateException("Error adding script tables to instance"); throw new IllegalStateException("Error adding script tables to instance");
} }
defineApps(qInstance);
return (qInstance); return (qInstance);
} }
private static void defineApps(QInstance qInstance)
{
QAppMetaData childApp = new QAppMetaData()
.withName("childApp")
.withLabel("Child App")
.withIcon(new QIcon().withName("child_friendly"))
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
.withChild(qInstance.getProcess(PROCESS_NAME_GREET_PEOPLE_INTERACTIVE));
qInstance.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(qInstance.getTable(TABLE_NAME_PERSON));
qInstance.addApp(exampleApp);
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -690,7 +718,7 @@ public class TestUtils
{ {
return (new RenderWidgetOutput(new RawHTML("title", return (new RenderWidgetOutput(new RawHTML("title",
QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE_OFFSET_MINUTES) QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE_OFFSET_MINUTES)
+ "|" + QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE) + "|" + QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE)
))); )));
} }
} }

View File

@ -0,0 +1,101 @@
/*
* 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.util.List;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.AuthenticationMetaDataResponseV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.MetaDataResponseV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.ProcessInitOrStepOrStatusResponseV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.AppTreeNode;
import com.kingsrook.qqq.openapi.model.Schema;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for SchemaBuilder
*******************************************************************************/
class SchemaBuilderTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testIncludesAOneOf()
{
Schema schema = new SchemaBuilder().classToSchema(AuthenticationMetaDataResponseV1.class);
System.out.println(schema);
Schema valuesSchema = schema.getProperties().get("values");
List<Schema> oneOf = valuesSchema.getOneOf();
assertEquals(2, oneOf.size());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUsesIncludeProperties()
{
Schema schema = new SchemaBuilder().classToSchema(ProcessInitOrStepOrStatusResponseV1.TypedResponse.class);
for(Schema oneOf : schema.getOneOf())
{
/////////////////////////////////////////////////////////////////////////////////////////
// all of the wrapped one-of schemas should contain these fields from the parent class //
/////////////////////////////////////////////////////////////////////////////////////////
assertTrue(oneOf.getProperties().containsKey("type"));
assertTrue(oneOf.getProperties().containsKey("processUUID"));
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDescriptionOnGetters()
{
Schema schema = new SchemaBuilder().classToSchema(MetaDataResponseV1.class);
assertTrue(schema.getProperties().containsKey("apps"));
assertNotNull(schema.getProperties().get("apps").getDescription());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testRecursive()
{
Schema schema = new SchemaBuilder().classToSchema(AppTreeNode.class);
Schema childrenSchema = schema.getProperties().get("children");
assertNotNull(childrenSchema.getItems());
System.out.println(schema);
}
}

View File

@ -0,0 +1,124 @@
/*
* 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.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
import com.kingsrook.qqq.backend.javalin.TestUtils;
import io.javalin.Javalin;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
/*******************************************************************************
**
*******************************************************************************/
public abstract class SpecTestBase
{
private static int PORT = 6263;
protected static Javalin service;
/***************************************************************************
**
***************************************************************************/
protected abstract AbstractEndpointSpec<?, ?, ?> getSpec();
/***************************************************************************
**
***************************************************************************/
protected abstract String getVersion();
/***************************************************************************
**
***************************************************************************/
protected String getBaseUrlAndPath()
{
return "http://localhost:" + PORT + "/qqq/" + getVersion();
}
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
@AfterEach
void beforeAndAfterEach()
{
MemoryRecordStore.fullReset();
}
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach() throws Exception
{
//////////////////////////////////////////////////////////////////////////////////////
// during initial dev here, we were having issues running multiple tests together, //
// where the second (but not first, and not any after second) would fail w/ javalin //
// not responding... so, this "works" - to constantly change our port, and stop //
// and restart aggresively... could be optimized, but it works. //
//////////////////////////////////////////////////////////////////////////////////////
PORT++;
if(service != null)
{
service.stop();
service = null;
}
if(service == null)
{
service = Javalin.create(config ->
{
config.router.apiBuilder(() -> getSpec().defineRoute(getVersion()));
}
).start(PORT);
}
TestUtils.primeTestDatabase();
}
/*******************************************************************************
**
*******************************************************************************/
@AfterAll
static void afterAll()
{
if(service != null)
{
service.stop();
service = null;
}
}
}

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.v1;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
import com.kingsrook.qqq.middleware.javalin.specs.SpecTestBase;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for AuthenticationMetaDataSpecV1
*******************************************************************************/
class AuthenticationMetaDataSpecV1Test extends SpecTestBase
{
/***************************************************************************
**
***************************************************************************/
@Override
protected AbstractEndpointSpec<?, ?, ?> getSpec()
{
return new AuthenticationMetaDataSpecV1();
}
/***************************************************************************
**
***************************************************************************/
@Override
protected String getVersion()
{
return "v1";
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test()
{
HttpResponse<String> response = Unirest.get(getBaseUrlAndPath() + "/metaData/authentication").asString();
assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertNotNull(jsonObject);
assertTrue(jsonObject.has("name"));
assertTrue(jsonObject.has("type"));
}
}

View File

@ -0,0 +1,90 @@
/*
* 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 com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
import com.kingsrook.qqq.middleware.javalin.specs.SpecTestBase;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for ManageSessionV1
*******************************************************************************/
class ManageSessionSpecV1Test extends SpecTestBase
{
/***************************************************************************
**
***************************************************************************/
@Override
protected AbstractEndpointSpec<?, ?, ?> getSpec()
{
return new ManageSessionSpecV1();
}
/***************************************************************************
**
***************************************************************************/
@Override
protected String getVersion()
{
return "v1";
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test()
{
String body = """
{
"accessToken": "abcdefg"
}
""";
HttpResponse<String> response = Unirest.post(getBaseUrlAndPath() + "/manageSession")
.header("Content-Type", "application/json")
.body(body)
.asString();
assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertNotNull(jsonObject);
assertTrue(jsonObject.has("uuid"));
assertThat(response.getHeaders().get("Set-Cookie")).anyMatch(s -> s.contains("sessionUUID"));
}
}

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;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
import com.kingsrook.qqq.middleware.javalin.specs.SpecTestBase;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for MetaDataSpecV1
*******************************************************************************/
class MetaDataSpecV1Test extends SpecTestBase
{
/***************************************************************************
**
***************************************************************************/
@Override
protected AbstractEndpointSpec<?, ?, ?> getSpec()
{
return new MetaDataSpecV1();
}
/***************************************************************************
**
***************************************************************************/
@Override
protected String getVersion()
{
return "v1";
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test()
{
HttpResponse<String> response = Unirest.get(getBaseUrlAndPath() + "/metaData").asString();
assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertThat(jsonObject.getJSONObject("tables").length()).isGreaterThanOrEqualTo(1);
assertThat(jsonObject.getJSONObject("processes").length()).isGreaterThanOrEqualTo(1);
assertThat(jsonObject.getJSONObject("apps").length()).isGreaterThanOrEqualTo(1);
assertThat(jsonObject.getJSONArray("appTree").length()).isGreaterThanOrEqualTo(1);
}
}

View File

@ -0,0 +1,219 @@
/*
* 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 com.kingsrook.qqq.backend.core.logging.QCollectingLogger;
import com.kingsrook.qqq.backend.core.logging.QLogger;
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.processes.implementations.mock.MockBackendStep;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.javalin.TestUtils;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
import com.kingsrook.qqq.middleware.javalin.specs.SpecTestBase;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
import org.json.JSONObject;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for ProcessInitSpecV1
*******************************************************************************/
class ProcessInitSpecV1Test extends SpecTestBase
{
/***************************************************************************
**
***************************************************************************/
@Override
protected AbstractEndpointSpec<?, ?, ?> getSpec()
{
return new ProcessInitSpecV1();
}
/***************************************************************************
**
***************************************************************************/
@Override
protected String getVersion()
{
return "v1";
}
/*******************************************************************************
**
*******************************************************************************/
@AfterEach
void afterEach()
{
QLogger.deactivateCollectingLoggerForClass(MockBackendStep.class);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGetInitialRecordsFromRecordIdsParam()
{
QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(MockBackendStep.class);
HttpResponse<String> response = Unirest.post(getBaseUrlAndPath() + "/processes/greet/init")
.multiPartContent()
.field("recordsParam", "recordIds")
.field("recordIds", "2,3")
.asString();
assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertNotNull(jsonObject);
assertEquals("COMPLETE", jsonObject.getString("type"));
assertEquals("null X null", jsonObject.getJSONObject("values").getString("outputMessage")); // these nulls are because we didn't pass values for some fields.
assertThat(collectingLogger.getCollectedMessages())
.filteredOn(clm -> clm.getMessage().contains("We are mocking"))
.hasSize(2);
// todo - also request records
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGetInitialRecordsFromFilterParam()
{
QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(MockBackendStep.class);
QQueryFilter queryFilter = new QQueryFilter()
.withCriteria(new QFilterCriteria()
.withFieldName("id")
.withOperator(QCriteriaOperator.IN)
.withValues(List.of(3, 4, 5)));
String filterJSON = JsonUtils.toJson(queryFilter);
HttpResponse<String> response = Unirest.post(getBaseUrlAndPath() + "/processes/greet/init")
.multiPartContent()
.field("recordsParam", "filterJSON")
.field("filterJSON", filterJSON)
.asString();
assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertNotNull(jsonObject);
assertEquals("COMPLETE", jsonObject.getString("type"));
assertEquals("null X null", jsonObject.getJSONObject("values").getString("outputMessage"));
assertThat(collectingLogger.getCollectedMessages())
.filteredOn(clm -> clm.getMessage().contains("We are mocking"))
.hasSize(3);
// todo - also request records
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testRequiresRowsButNotSpecified()
{
HttpResponse<String> response = Unirest.post(getBaseUrlAndPath() + "/processes/greet/init")
.multiPartContent()
.asString();
assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertNotNull(jsonObject);
assertEquals("ERROR", jsonObject.getString("type"));
assertTrue(jsonObject.has("error"));
assertTrue(jsonObject.getString("error").contains("Missing input records"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testFieldValues()
{
HttpResponse<String> response = Unirest.post(getBaseUrlAndPath() + "/processes/greet/init")
.multiPartContent()
.field("recordsParam", "recordIds")
.field("recordIds", "2,3")
.field("values", new JSONObject()
.put("greetingPrefix", "Hey")
.put("greetingSuffix", "Jude")
.toString()
)
.asString();
assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertNotNull(jsonObject);
assertEquals("COMPLETE", jsonObject.getString("type"));
assertEquals("Hey X Jude", jsonObject.getJSONObject("values").getString("outputMessage"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInitGoingAsync()
{
HttpResponse<String> response = Unirest.post(getBaseUrlAndPath() + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP + "/init")
.multiPartContent()
.field("stepTimeoutMillis", "50")
.field("values", new JSONObject()
.put(TestUtils.SleeperStep.FIELD_SLEEP_MILLIS, 500)
.toString()
)
.asString();
assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
String processUUID = jsonObject.getString("processUUID");
String jobUUID = jsonObject.getString("jobUUID");
assertNotNull(processUUID, "Process UUID should not be null.");
assertNotNull(jobUUID, "Job UUID should not be null");
// todo - in a higher-level test, resume test_processInitGoingAsync at the // request job status before sleep is done // line
}
}

View File

@ -0,0 +1,108 @@
/*
* 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.Map;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
import com.kingsrook.qqq.middleware.javalin.specs.SpecTestBase;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
import org.eclipse.jetty.http.HttpStatus;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for ProcessMetaDataSpecV1
*******************************************************************************/
class ProcessMetaDataSpecV1Test extends SpecTestBase
{
/***************************************************************************
**
***************************************************************************/
@Override
protected AbstractEndpointSpec<?, ?, ?> getSpec()
{
return new ProcessMetaDataSpecV1();
}
/***************************************************************************
**
***************************************************************************/
@Override
protected String getVersion()
{
return "v1";
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test()
{
HttpResponse<String> response = Unirest.get(getBaseUrlAndPath() + "/metaData/process/greetInteractive").asString();
assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertEquals("greetInteractive", jsonObject.getString("name"));
assertEquals("Greet Interactive", jsonObject.getString("label"));
assertEquals("person", jsonObject.getString("tableName"));
JSONArray frontendSteps = jsonObject.getJSONArray("frontendSteps");
JSONObject setupStep = frontendSteps.getJSONObject(0);
assertEquals("Setup", setupStep.getString("label"));
JSONArray setupFields = setupStep.getJSONArray("formFields");
assertEquals(2, setupFields.length());
assertTrue(setupFields.toList().stream().anyMatch(field -> "greetingPrefix".equals(((Map<?, ?>) field).get("name"))));
}
/*******************************************************************************
** test the process-level meta-data endpoint for a non-real name
**
*******************************************************************************/
@Test
public void testNotFound()
{
HttpResponse<String> response = Unirest.get(getBaseUrlAndPath() + "/metaData/process/notAnActualProcess").asString();
assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
String error = jsonObject.getString("error");
assertThat(error).contains("Process").contains("notAnActualProcess").contains("not found");
}
}