From cc55b32206b00a1c2fcbdba3a9a57b1756886964 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 17 Oct 2024 20:26:54 -0500 Subject: [PATCH] CE-1887 Initial (not quite finished) version 1 middleware api spec --- .../javalin/QJavalinImplementation.java | 66 +- .../javalin/QJavalinProcessHandler.java | 4 +- .../qqq/backend/javalin/QJavalinUtils.java | 74 + .../executors/AbstractMiddlewareExecutor.java | 41 + .../AuthenticationMetaDataExecutor.java | 46 + .../executors/ExecutorSessionUtils.java | 224 +++ .../executors/ManageSessionExecutor.java | 75 + .../javalin/executors/MetaDataExecutor.java | 49 + .../executors/ProcessInitOrStepExecutor.java | 186 ++ .../executors/ProcessMetaDataExecutor.java | 65 + .../executors/ProcessStatusExecutor.java | 121 ++ .../executors/io/AbstractMiddlewareInput.java | 30 + .../io/AbstractMiddlewareOutputInterface.java | 30 + .../io/AuthenticationMetaDataInput.java | 31 + ...AuthenticationMetaDataOutputInterface.java | 37 + .../executors/io/EmptyMiddlewareInput.java | 31 + .../io/EmptyMiddlewareOutputInterface.java | 31 + .../executors/io/ManageSessionInput.java | 63 + .../io/ManageSessionOutputInterface.java | 45 + .../javalin/executors/io/MetaDataInput.java | 31 + .../executors/io/MetaDataOutputInterface.java | 39 + .../executors/io/ProcessInitOrStepInput.java | 295 +++ ...cessInitOrStepOrStatusOutputInterface.java | 100 + .../executors/io/ProcessMetaDataInput.java | 66 + .../io/ProcessMetaDataOutputInterface.java | 37 + .../executors/io/ProcessStatusInput.java | 127 ++ .../io/ProcessStatusOutputInterface.java | 31 + .../executors/io/QueryMiddlewareInput.java | 132 ++ .../javalin/executors/io/package-info.java | 29 + .../javalin/executors/package-info.java | 40 + .../executors/utils/ProcessExecutorUtils.java | 118 ++ .../javalin/schemabuilder/SchemaBuilder.java | 68 +- .../OpenAPIHasAdditionalProperties.java | 2 +- .../javalin/specs/AbstractEndpointSpec.java | 545 ++++++ .../specs/AbstractMiddlewareVersion.java | 360 ++++ .../javalin/specs/BasicOperation.java | 194 ++ .../javalin/specs/BasicResponse.java | 79 + .../javalin/specs/CompleteOperation.java | 80 + .../javalin/specs/TagsInterface.java | 34 + .../v1/AuthenticationMetaDataSpecV1.java | 137 ++ .../javalin/specs/v1/ManageSessionSpecV1.java | 213 +++ .../javalin/specs/v1/MetaDataSpecV1.java | 214 +++ .../javalin/specs/v1/MiddlewareVersionV1.java | 70 + .../javalin/specs/v1/ProcessInitSpecV1.java | 268 +++ .../specs/v1/ProcessMetaDataSpecV1.java | 140 ++ .../javalin/specs/v1/ProcessStatusSpecV1.java | 164 ++ .../javalin/specs/v1/ProcessStepSpecV1.java | 201 ++ .../AuthenticationMetaDataResponseV1.java | 340 ++++ .../v1/responses/BasicErrorResponseV1.java | 69 + .../v1/responses/ManageSessionResponseV1.java | 107 ++ .../v1/responses/MetaDataResponseV1.java | 155 ++ .../ProcessInitOrStepOrStatusResponseV1.java | 444 +++++ .../responses/ProcessMetaDataResponseV1.java | 72 + .../v1/responses/components/AppMetaData.java | 159 ++ .../v1/responses/components/AppSection.java | 133 ++ .../v1/responses/components/AppTreeNode.java | 105 ++ .../responses/components/FieldMetaData.java | 194 ++ .../components/FrontendComponent.java | 88 + .../components/FrontendComponentValues.java | 145 ++ .../v1/responses/components/FrontendStep.java | 133 ++ .../specs/v1/responses/components/Icon.java | 92 + .../responses/components/ProcessMetaData.java | 74 + .../components/ProcessMetaDataAdjustment.java | 112 ++ .../components/ProcessMetaDataLight.java | 137 ++ .../components/TableMetaDataLight.java | 187 ++ .../v1/responses/components/WidgetBlock.java | 216 +++ .../WidgetBlockActionButtonValues.java | 82 + .../WidgetBlockInputFieldValues.java | 93 + .../components/WidgetBlockStyles.java | 61 + .../components/WidgetBlockTextStyles.java | 76 + .../components/WidgetBlockTextValues.java | 71 + .../components/WidgetBlockValues.java | 73 + .../specs/v1/utils/ProcessSpecUtilsV1.java | 205 +++ .../specs/v1/utils/QuerySpecUtils.java | 208 +++ .../javalin/specs/v1/utils/TagsV1.java | 66 + .../main/resources/openapi/v1/openapi.yaml | 1614 +++++++++++++++++ .../kingsrook/qqq/api/wip/TestContext.java | 356 ++++ .../com/kingsrook/qqq/api/wip/TestUtils.java | 92 + .../qqq/api/wip/v1/QueryGetSpecV1Test.java | 75 + .../javalin/QJavalinProcessHandlerTest.java | 7 + .../backend/javalin/QJavalinUtilsTest.java | 81 +- .../qqq/backend/javalin/TestUtils.java | 30 +- .../schemabuilder/SchemaBuilderTest.java | 101 ++ .../javalin/specs/SpecTestBase.java | 124 ++ .../v1/AuthenticationMetaDataSpecV1Test.java | 80 + .../specs/v1/ManageSessionSpecV1Test.java | 90 + .../javalin/specs/v1/MetaDataSpecV1Test.java | 79 + .../specs/v1/ProcessInitSpecV1Test.java | 219 +++ .../specs/v1/ProcessMetaDataSpecV1Test.java | 108 ++ 89 files changed, 11930 insertions(+), 86 deletions(-) create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/AbstractMiddlewareExecutor.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/AuthenticationMetaDataExecutor.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ManageSessionExecutor.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/MetaDataExecutor.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessInitOrStepExecutor.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessMetaDataExecutor.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessStatusExecutor.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AbstractMiddlewareInput.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AbstractMiddlewareOutputInterface.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AuthenticationMetaDataInput.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AuthenticationMetaDataOutputInterface.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/EmptyMiddlewareInput.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/EmptyMiddlewareOutputInterface.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionInput.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionOutputInterface.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/MetaDataInput.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/MetaDataOutputInterface.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepInput.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepOrStatusOutputInterface.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessMetaDataInput.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessMetaDataOutputInterface.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessStatusInput.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessStatusOutputInterface.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/QueryMiddlewareInput.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/package-info.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/package-info.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/utils/ProcessExecutorUtils.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/AbstractEndpointSpec.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/AbstractMiddlewareVersion.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/BasicOperation.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/BasicResponse.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/CompleteOperation.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/TagsInterface.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/AuthenticationMetaDataSpecV1.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ManageSessionSpecV1.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MiddlewareVersionV1.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessInitSpecV1.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessMetaDataSpecV1.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStatusSpecV1.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStepSpecV1.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/AuthenticationMetaDataResponseV1.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/BasicErrorResponseV1.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ManageSessionResponseV1.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/MetaDataResponseV1.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ProcessInitOrStepOrStatusResponseV1.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ProcessMetaDataResponseV1.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppMetaData.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppSection.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppTreeNode.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FieldMetaData.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FrontendComponent.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FrontendComponentValues.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FrontendStep.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/Icon.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/ProcessMetaData.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/ProcessMetaDataAdjustment.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/ProcessMetaDataLight.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableMetaDataLight.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlock.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockActionButtonValues.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockInputFieldValues.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockStyles.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockTextStyles.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockTextValues.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockValues.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/ProcessSpecUtilsV1.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/QuerySpecUtils.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/TagsV1.java create mode 100644 qqq-middleware-javalin/src/main/resources/openapi/v1/openapi.yaml create mode 100644 qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/api/wip/TestContext.java create mode 100644 qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/api/wip/TestUtils.java create mode 100644 qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/api/wip/v1/QueryGetSpecV1Test.java create mode 100644 qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/SchemaBuilderTest.java create mode 100644 qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/SpecTestBase.java create mode 100644 qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/AuthenticationMetaDataSpecV1Test.java create mode 100644 qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ManageSessionSpecV1Test.java create mode 100644 qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1Test.java create mode 100644 qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessInitSpecV1Test.java create mode 100644 qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessMetaDataSpecV1Test.java diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index ed46a8ad..efa0ea6b 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -63,12 +63,10 @@ import com.kingsrook.qqq.backend.core.actions.values.SearchPossibleValueSourceAc import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; -import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; -import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.logging.QLogger; @@ -1709,7 +1707,7 @@ public class QJavalinImplementation } catch(QUserFacingException e) { - handleException(HttpStatus.Code.BAD_REQUEST, context, e); + QJavalinUtils.handleException(HttpStatus.Code.BAD_REQUEST, context, e); return null; } return reportFormat; @@ -1849,67 +1847,7 @@ public class QJavalinImplementation *******************************************************************************/ public static void handleException(Context context, Exception e) { - handleException(null, context, e); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public static void handleException(HttpStatus.Code statusCode, Context context, Exception e) - { - QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class); - if(userFacingException != null) - { - if(userFacingException instanceof QNotFoundException) - { - statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.NOT_FOUND); // 404 - respondWithError(context, statusCode, userFacingException.getMessage()); - } - else if(userFacingException instanceof QBadRequestException) - { - statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.BAD_REQUEST); // 400 - respondWithError(context, statusCode, userFacingException.getMessage()); - } - else - { - LOG.info("User-facing exception", e); - statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR); // 500 - respondWithError(context, statusCode, userFacingException.getMessage()); - } - } - else - { - if(e instanceof QAuthenticationException) - { - respondWithError(context, HttpStatus.Code.UNAUTHORIZED, e.getMessage()); // 401 - return; - } - - if(e instanceof QPermissionDeniedException) - { - respondWithError(context, HttpStatus.Code.FORBIDDEN, e.getMessage()); // 403 - return; - } - - //////////////////////////////// - // default exception handling // - //////////////////////////////// - LOG.warn("Exception in javalin request", e); - respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, e.getClass().getSimpleName() + " (" + e.getMessage() + ")"); // 500 - } - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public static void respondWithError(Context context, HttpStatus.Code statusCode, String errorMessage) - { - context.status(statusCode.getCode()); - context.result(JsonUtils.toJson(Map.of("error", errorMessage))); + QJavalinUtils.handleException(null, context, e); } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java index 5863716c..bb00bfab 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java @@ -236,13 +236,13 @@ public class QJavalinProcessHandler if(inputField.getIsRequired() && !setValue) { - QJavalinImplementation.respondWithError(context, HttpStatus.Code.BAD_REQUEST, "Missing query param value for required input field: [" + inputField.getName() + "]"); + QJavalinUtils.respondWithError(context, HttpStatus.Code.BAD_REQUEST, "Missing query param value for required input field: [" + inputField.getName() + "]"); return; } } catch(Exception e) { - QJavalinImplementation.respondWithError(context, HttpStatus.Code.BAD_REQUEST, "Error processing query param [" + inputField.getName() + "]: " + e.getClass().getSimpleName() + " (" + e.getMessage() + ")"); + QJavalinUtils.respondWithError(context, HttpStatus.Code.BAD_REQUEST, "Error processing query param [" + inputField.getName() + "]: " + e.getClass().getSimpleName() + " (" + e.getMessage() + ")"); return; } } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinUtils.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinUtils.java index daa3fe8b..f335601d 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinUtils.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinUtils.java @@ -22,11 +22,21 @@ package com.kingsrook.qqq.backend.javalin; +import java.util.Map; import java.util.Objects; +import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; +import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; +import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException; +import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.exceptions.QValueException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import io.javalin.http.Context; +import org.eclipse.jetty.http.HttpStatus; /******************************************************************************* @@ -34,6 +44,10 @@ import io.javalin.http.Context; *******************************************************************************/ public class QJavalinUtils { + private static final QLogger LOG = QLogger.getLogger(QJavalinUtils.class); + + + /******************************************************************************* ** Returns Integer if context has a valid int query parameter by the given name, @@ -178,4 +192,64 @@ public class QJavalinUtils return value; } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void handleException(HttpStatus.Code statusCode, Context context, Exception e) + { + QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class); + if(userFacingException != null) + { + if(userFacingException instanceof QNotFoundException) + { + statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.NOT_FOUND); // 404 + respondWithError(context, statusCode, userFacingException.getMessage()); + } + else if(userFacingException instanceof QBadRequestException) + { + statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.BAD_REQUEST); // 400 + respondWithError(context, statusCode, userFacingException.getMessage()); + } + else + { + LOG.info("User-facing exception", e); + statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR); // 500 + respondWithError(context, statusCode, userFacingException.getMessage()); + } + } + else + { + if(e instanceof QAuthenticationException) + { + respondWithError(context, HttpStatus.Code.UNAUTHORIZED, e.getMessage()); // 401 + return; + } + + if(e instanceof QPermissionDeniedException) + { + respondWithError(context, HttpStatus.Code.FORBIDDEN, e.getMessage()); // 403 + return; + } + + //////////////////////////////// + // default exception handling // + //////////////////////////////// + LOG.warn("Exception in javalin request", e); + respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, e.getClass().getSimpleName() + " (" + e.getMessage() + ")"); // 500 + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void respondWithError(Context context, HttpStatus.Code statusCode, String errorMessage) + { + context.status(statusCode.getCode()); + context.result(JsonUtils.toJson(Map.of("error", errorMessage))); + } } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/AbstractMiddlewareExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/AbstractMiddlewareExecutor.java new file mode 100644 index 00000000..c83a98d5 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/AbstractMiddlewareExecutor.java @@ -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 . + */ + +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 +{ + + /*************************************************************************** + ** + ***************************************************************************/ + public abstract void execute(INPUT input, OUTPUT output) throws QException; + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/AuthenticationMetaDataExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/AuthenticationMetaDataExecutor.java new file mode 100644 index 00000000..a630feb7 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/AuthenticationMetaDataExecutor.java @@ -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 . + */ + +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 +{ + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void execute(AuthenticationMetaDataInput input, AuthenticationMetaDataOutputInterface output) throws QException + { + output.setAuthenticationMetaData(QContext.getQInstance().getAuthentication()); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java new file mode 100644 index 00000000..7a379fd5 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java @@ -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 . + */ + +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 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 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); + } + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ManageSessionExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ManageSessionExecutor.java new file mode 100644 index 00000000..64beb3cd --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ManageSessionExecutor.java @@ -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 . + */ + +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 +{ + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void execute(ManageSessionInput input, ManageSessionOutputInterface output) throws QException + { + QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); + QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(QContext.getQInstance().getAuthentication()); + + Map 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 valuesForFrontend = new LinkedHashMap<>(session.getValuesForFrontend()); + output.setValues(valuesForFrontend); + } + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/MetaDataExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/MetaDataExecutor.java new file mode 100644 index 00000000..8daeb8dd --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/MetaDataExecutor.java @@ -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 . + */ + +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 +{ + + /*************************************************************************** + ** + ***************************************************************************/ + @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); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessInitOrStepExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessInitOrStepExecutor.java new file mode 100644 index 00000000..adaededc --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessInitOrStepExecutor.java @@ -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 . + */ + +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 +{ + 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 getFieldValues(List 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(); + } + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessMetaDataExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessMetaDataExecutor.java new file mode 100644 index 00000000..2b1d4e93 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessMetaDataExecutor.java @@ -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 . + */ + +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 +{ + + /*************************************************************************** + ** + ***************************************************************************/ + @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()); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessStatusExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessStatusExecutor.java new file mode 100644 index 00000000..c0396a83 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ProcessStatusExecutor.java @@ -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 . + */ + +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 +{ + 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 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 = 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); + } + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AbstractMiddlewareInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AbstractMiddlewareInput.java new file mode 100644 index 00000000..e939781e --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AbstractMiddlewareInput.java @@ -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 . + */ + +package com.kingsrook.qqq.middleware.javalin.executors.io; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class AbstractMiddlewareInput +{ +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AbstractMiddlewareOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AbstractMiddlewareOutputInterface.java new file mode 100644 index 00000000..9bd32404 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AbstractMiddlewareOutputInterface.java @@ -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 . + */ + +package com.kingsrook.qqq.middleware.javalin.executors.io; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface AbstractMiddlewareOutputInterface +{ +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AuthenticationMetaDataInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AuthenticationMetaDataInput.java new file mode 100644 index 00000000..1d97e7bd --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AuthenticationMetaDataInput.java @@ -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 . + */ + +package com.kingsrook.qqq.middleware.javalin.executors.io; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class AuthenticationMetaDataInput extends AbstractMiddlewareInput +{ + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AuthenticationMetaDataOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AuthenticationMetaDataOutputInterface.java new file mode 100644 index 00000000..ff6d7456 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/AuthenticationMetaDataOutputInterface.java @@ -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 . + */ + +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); +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/EmptyMiddlewareInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/EmptyMiddlewareInput.java new file mode 100644 index 00000000..577f38a0 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/EmptyMiddlewareInput.java @@ -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 . + */ + +package com.kingsrook.qqq.middleware.javalin.executors.io; + + +/******************************************************************************* + ** generic middleware input that has no fields. + *******************************************************************************/ +public class EmptyMiddlewareInput extends AbstractMiddlewareInput +{ + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/EmptyMiddlewareOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/EmptyMiddlewareOutputInterface.java new file mode 100644 index 00000000..a5ddb568 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/EmptyMiddlewareOutputInterface.java @@ -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 . + */ + +package com.kingsrook.qqq.middleware.javalin.executors.io; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface EmptyMiddlewareOutputInterface extends AbstractMiddlewareOutputInterface +{ + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionInput.java new file mode 100644 index 00000000..4fbc0124 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionInput.java @@ -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 . + */ + +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); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionOutputInterface.java new file mode 100644 index 00000000..676661a5 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionOutputInterface.java @@ -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 . + */ + +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 values); + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/MetaDataInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/MetaDataInput.java new file mode 100644 index 00000000..bdec2641 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/MetaDataInput.java @@ -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 . + */ + +package com.kingsrook.qqq.middleware.javalin.executors.io; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class MetaDataInput extends AbstractMiddlewareInput +{ + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/MetaDataOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/MetaDataOutputInterface.java new file mode 100644 index 00000000..748805e1 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/MetaDataOutputInterface.java @@ -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 . + */ + +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); + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepInput.java new file mode 100644 index 00000000..c138b0bc --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepInput.java @@ -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 . + */ + +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 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 getValues() + { + return (this.values); + } + + + + /******************************************************************************* + ** Setter for values + *******************************************************************************/ + public void setValues(Map values) + { + this.values = values; + } + + + + /******************************************************************************* + ** Fluent setter for values + *******************************************************************************/ + public ProcessInitOrStepInput withValues(Map 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); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepOrStatusOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepOrStatusOutputInterface.java new file mode 100644 index 00000000..f7d0d4a5 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepOrStatusOutputInterface.java @@ -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 . + */ + +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 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); +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessMetaDataInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessMetaDataInput.java new file mode 100644 index 00000000..0d92c427 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessMetaDataInput.java @@ -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 . + */ + +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); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessMetaDataOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessMetaDataOutputInterface.java new file mode 100644 index 00000000..eb92f147 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessMetaDataOutputInterface.java @@ -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 . + */ + +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); +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessStatusInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessStatusInput.java new file mode 100644 index 00000000..2406d5a3 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessStatusInput.java @@ -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 . + */ + +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); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessStatusOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessStatusOutputInterface.java new file mode 100644 index 00000000..449e7671 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessStatusOutputInterface.java @@ -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 . + */ + +package com.kingsrook.qqq.middleware.javalin.executors.io; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface ProcessStatusOutputInterface extends AbstractMiddlewareOutputInterface +{ + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/QueryMiddlewareInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/QueryMiddlewareInput.java new file mode 100644 index 00000000..4060f149 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/QueryMiddlewareInput.java @@ -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 . + */ + +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 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 getQueryJoins() + { + return (this.queryJoins); + } + + + + /******************************************************************************* + ** Setter for queryJoins + *******************************************************************************/ + public void setQueryJoins(List queryJoins) + { + this.queryJoins = queryJoins; + } + + + + /******************************************************************************* + ** Fluent setter for queryJoins + *******************************************************************************/ + public QueryMiddlewareInput withQueryJoins(List queryJoins) + { + this.queryJoins = queryJoins; + return (this); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/package-info.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/package-info.java new file mode 100644 index 00000000..d342e723 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/package-info.java @@ -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 . + */ + +/******************************************************************************* + ** 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; \ No newline at end of file diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/package-info.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/package-info.java new file mode 100644 index 00000000..50c38d64 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/package-info.java @@ -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 . + */ + +/******************************************************************************* + ** 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; \ No newline at end of file diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/utils/ProcessExecutorUtils.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/utils/ProcessExecutorUtils.java new file mode 100644 index 00000000..9afff3b8 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/utils/ProcessExecutorUtils.java @@ -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 . + */ + +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 getValuesForCaller(String processName, RunProcessOutput runProcessOutput) + // { + // QProcessMetaData process = QContext.getQInstance().getProcess(processName); + // Map 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()); + } + } +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/SchemaBuilder.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/SchemaBuilder.java index 5ea0912b..79ac6da6 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/SchemaBuilder.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/SchemaBuilder.java @@ -34,6 +34,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.stream.Collectors; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; @@ -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) { - Schema schema = new Schema(); + SchemaFromBuilder schema = new SchemaFromBuilder(); + schema.setOriginalClass(c); if(c.isEnum()) { @@ -121,10 +166,10 @@ public class SchemaBuilder if(openAPIMapKnownEntriesAnnotation != null) { schema.withRef("#/components/schemas/" + openAPIMapKnownEntriesAnnotation.value().getSimpleName()); -// if(openAPIMapKnownEntriesAnnotation.additionalProperties()) -// { -// schema.withAdditionalProperties(true); -// } + // if(openAPIMapKnownEntriesAnnotation.additionalProperties()) + // { + // schema.withAdditionalProperties(true); + // } } OpenAPIMapValueType openAPIMapValueTypeAnnotation = element.getAnnotation(OpenAPIMapValueType.class); @@ -142,13 +187,23 @@ public class SchemaBuilder } else { - OpenAPIOneOf openAPIOneOfAnnotation = element.getAnnotation(OpenAPIOneOf.class); + OpenAPIOneOf openAPIOneOfAnnotation = element.getAnnotation(OpenAPIOneOf.class); + OpenAPIMapKnownEntries openAPIMapKnownEntriesAnnotation = element.getAnnotation(OpenAPIMapKnownEntries.class); + if(openAPIOneOfAnnotation != null) { String description = "[" + element + "]"; List oneOfList = processOneOfAnnotation(openAPIOneOfAnnotation, c, description); 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 { ///////////////////////////////////////////////////////////////////////////////////////////// @@ -296,5 +351,4 @@ public class SchemaBuilder return oneOfList; } - } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIHasAdditionalProperties.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIHasAdditionalProperties.java index a7a1fa19..497dc090 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIHasAdditionalProperties.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIHasAdditionalProperties.java @@ -31,7 +31,7 @@ import java.lang.annotation.Target; /******************************************************************************* ** *******************************************************************************/ -@Target({ ElementType.TYPE }) +@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface OpenAPIHasAdditionalProperties { diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/AbstractEndpointSpec.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/AbstractEndpointSpec.java new file mode 100644 index 00000000..c9548e94 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/AbstractEndpointSpec.java @@ -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 . + */ + +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> +{ + private static final QLogger LOG = QLogger.getLogger(AbstractEndpointSpec.class); + + protected QInstance qInstance; + + private List 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 defineAdditionalBasicResponses() + { + return (Collections.emptyList()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public Map defineResponses() + { + BasicResponse standardSuccessResponse = defineBasicSuccessResponse(); + List basicResponseList = defineAdditionalBasicResponses(); + + List allBasicResponses = new ArrayList<>(); + if(standardSuccessResponse != null) + { + allBasicResponses.add(standardSuccessResponse); + } + + if(basicResponseList != null) + { + allBasicResponses.addAll(basicResponseList); + } + + Map rs = new HashMap<>(); + for(BasicResponse basicResponse : allBasicResponses) + { + Response responseObject = rs.computeIfAbsent(basicResponse.status().getCode(), (k) -> new Response()); + responseObject.withDescription(basicResponse.description()); + Map 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 defineRequestParameters() + { + return Collections.emptyList(); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public RequestBody defineRequestBody() + { + return null; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public Map 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 getRequestParamMap(Context context, String name) + { + String requestParam = getRequestParam(context, name); + if(requestParam == null) + { + return (null); + } + + JSONObject jsonObject = new JSONObject(requestParam); + Map 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 getMemoizedRequestParameters() + { + List 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); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/AbstractMiddlewareVersion.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/AbstractMiddlewareVersion.java new file mode 100644 index 00000000..41a7fc0b --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/AbstractMiddlewareVersion.java @@ -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 . + */ + +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> 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> list = getEndpointSpecs(); + + Map paths = new LinkedHashMap<>(); + Map componentExamples = new LinkedHashMap<>(); + + Set> componentClasses = new HashSet<>(); + Map 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 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 componentSchemas, Set> componentClasses) throws QException + { + try + { + //////////////////////////////////////////////////// + // find all classes in the components sub-package // + //////////////////////////////////////////////////// + String packageName = getClass().getPackageName(); + List> 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> 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> 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."); + } + } +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/BasicOperation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/BasicOperation.java new file mode 100644 index 00000000..35b745c4 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/BasicOperation.java @@ -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 . + */ + +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); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/BasicResponse.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/BasicResponse.java new file mode 100644 index 00000000..7447ab02 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/BasicResponse.java @@ -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 . + */ + +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 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 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 examples) + { + this(ContentType.APPLICATION_JSON.getMimeType(), status, description, schemaRefName, examples); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/CompleteOperation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/CompleteOperation.java new file mode 100644 index 00000000..02c11853 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/CompleteOperation.java @@ -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 . + */ + +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); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/TagsInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/TagsInterface.java new file mode 100644 index 00000000..23d2b4a4 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/TagsInterface.java @@ -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 . + */ + +package com.kingsrook.qqq.middleware.javalin.specs; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface TagsInterface +{ + /*************************************************************************** + ** + ***************************************************************************/ + String getText(); +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/AuthenticationMetaDataSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/AuthenticationMetaDataSpecV1.java new file mode 100644 index 00000000..5dce2f06 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/AuthenticationMetaDataSpecV1.java @@ -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 . + */ + +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 +{ + + /*************************************************************************** + ** + ***************************************************************************/ + 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 defineComponentSchemas() + { + return Map.of(AuthenticationMetaDataResponseV1.class.getSimpleName(), new AuthenticationMetaDataResponseV1().toSchema()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public BasicResponse defineBasicSuccessResponse() + { + Map 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)); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ManageSessionSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ManageSessionSpecV1.java new file mode 100644 index 00000000..c15ac971 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ManageSessionSpecV1.java @@ -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 . + */ + +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 +{ + + + /*************************************************************************** + ** + ***************************************************************************/ + @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 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::new) + .with("region", "US") + .with("userCategoryId", 47) + .build() + ) + )); + + return new BasicResponse("Successful response - session has been created", + ManageSessionResponseV1.class.getSimpleName(), + examples); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public Map defineComponentSchemas() + { + return Map.of( + ManageSessionResponseV1.class.getSimpleName(), new ManageSessionResponseV1().toSchema(), + BasicErrorResponseV1.class.getSimpleName(), new BasicErrorResponseV1().toSchema() + ); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public List defineAdditionalBasicResponses() + { + Map 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)); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1.java new file mode 100644 index 00000000..2191bbfe --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1.java @@ -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 . + */ + +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 +{ + + /*************************************************************************** + ** + ***************************************************************************/ + 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 defineComponentSchemas() + { + return Map.of(MetaDataResponseV1.class.getSimpleName(), new MetaDataResponseV1().toSchema()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public BasicResponse defineBasicSuccessResponse() + { + Map 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)); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MiddlewareVersionV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MiddlewareVersionV1.java new file mode 100644 index 00000000..ec907d4f --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MiddlewareVersionV1.java @@ -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 . + */ + +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> 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> getEndpointSpecs() + { + return (list); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessInitSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessInitSpecV1.java new file mode 100644 index 00000000..62377711 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessInitSpecV1.java @@ -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 . + */ + +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 +{ + 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 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); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessMetaDataSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessMetaDataSpecV1.java new file mode 100644 index 00000000..d311dd8c --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessMetaDataSpecV1.java @@ -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 . + */ + +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 +{ + + /*************************************************************************** + ** + ***************************************************************************/ + 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 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 defineComponentSchemas() + { + return Map.of(ProcessMetaDataResponseV1.class.getSimpleName(), new ProcessMetaDataResponseV1().toSchema()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public BasicResponse defineBasicSuccessResponse() + { + Map 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())); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStatusSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStatusSpecV1.java new file mode 100644 index 00000000..d7625157 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStatusSpecV1.java @@ -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 . + */ + +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 +{ + + /*************************************************************************** + ** + ***************************************************************************/ + 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 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 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); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStepSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStepSpecV1.java new file mode 100644 index 00000000..156bf7ce --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStepSpecV1.java @@ -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 . + */ + +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 +{ + 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 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); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/AuthenticationMetaDataResponseV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/AuthenticationMetaDataResponseV1.java new file mode 100644 index 00000000..22db2725 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/AuthenticationMetaDataResponseV1.java @@ -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 . + */ + +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); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/BasicErrorResponseV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/BasicErrorResponseV1.java new file mode 100644 index 00000000..0062e105 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/BasicErrorResponseV1.java @@ -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 . + */ + +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); + } + + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ManageSessionResponseV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ManageSessionResponseV1.java new file mode 100644 index 00000000..90d6810c --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ManageSessionResponseV1.java @@ -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 . + */ + +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 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 getValues() + { + return (this.values); + } + + + + /******************************************************************************* + ** Setter for values + *******************************************************************************/ + public void setValues(Map values) + { + this.values = values; + } + + + + /******************************************************************************* + ** Fluent setter for values + *******************************************************************************/ + public ManageSessionResponseV1 withValues(Map values) + { + this.values = values; + return (this); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/MetaDataResponseV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/MetaDataResponseV1.java new file mode 100644 index 00000000..c6d91c15 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/MetaDataResponseV1.java @@ -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 . + */ + +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 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 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 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 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 getApps() + { + return apps; + } + + + + /******************************************************************************* + ** Getter for appTree + ** + *******************************************************************************/ + public List getAppTree() + { + return appTree; + } + + + + /******************************************************************************* + ** Getter for tables + ** + *******************************************************************************/ + public Map getTables() + { + return tables; + } + + + + /******************************************************************************* + ** Getter for processes + ** + *******************************************************************************/ + public Map getProcesses() + { + return processes; + } + +} \ No newline at end of file diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ProcessInitOrStepOrStatusResponseV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ProcessInitOrStepOrStatusResponseV1.java new file mode 100644 index 00000000..5723c3c2 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ProcessInitOrStepOrStatusResponseV1.java @@ -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 . + */ + +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 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 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 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 updatedFields = processMetaDataAdjustment.getUpdatedFields().entrySet() + .stream().collect(Collectors.toMap(e -> e.getKey(), f -> new FieldMetaData(f.getValue()))); + complete.processMetaDataAdjustment.setUpdatedFields(updatedFields); + + List 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; + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ProcessMetaDataResponseV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ProcessMetaDataResponseV1.java new file mode 100644 index 00000000..2a4d45b5 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/ProcessMetaDataResponseV1.java @@ -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 . + */ + +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; + } +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppMetaData.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppMetaData.java new file mode 100644 index 00000000..0dd4d914 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppMetaData.java @@ -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 . + */ + +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 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 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 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 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 getSupplementalAppMetaData() + { + return (new LinkedHashMap<>(CollectionUtils.nonNullMap(this.wrapped.getSupplementalAppMetaData()))); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppSection.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppSection.java new file mode 100644 index 00000000..236cf0df --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppSection.java @@ -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 . + */ + +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 getTables() + { + return (this.wrapped.getTables()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("List of process names for the section") + @OpenAPIListItems(value = String.class) + public List getProcesses() + { + return (this.wrapped.getProcesses()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("List of report names for the section") + @OpenAPIListItems(value = String.class) + public List getReports() + { + return (this.wrapped.getReports()); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppTreeNode.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppTreeNode.java new file mode 100644 index 00000000..85a63b4c --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppTreeNode.java @@ -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 . + */ + +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 getChildren() + { + return (CollectionUtils.nonNullList(this.wrapped.getChildren()).stream().map(a -> new AppTreeNode(a)).toList()); + } + + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FieldMetaData.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FieldMetaData.java new file mode 100644 index 00000000..5c4505ac --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FieldMetaData.java @@ -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 . + */ + +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 getAdornments() + { + return (this.wrapped.getAdornments()); + } + + // todo help content + + // todo supplemental... + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FrontendComponent.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FrontendComponent.java new file mode 100644 index 00000000..e28af1ce --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FrontendComponent.java @@ -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 . + */ + +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 getValues() + { + return (this.wrapped.getValues() == null ? null : new FrontendComponentValues(this.wrapped.getValues()).toMap()); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FrontendComponentValues.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FrontendComponentValues.java new file mode 100644 index 00000000..48bd7eac --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FrontendComponentValues.java @@ -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 . + */ + +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 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 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 includeFieldNames; + + @OpenAPIDescription(""" + Components of type=`EDIT_FORM` can specify a user-facing text label to show on screen. + """) + private String sectionLabel; + + + /*************************************************************************** + ** + ***************************************************************************/ + public FrontendComponentValues(Map values) + { + this.wrapped = values; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public FrontendComponentValues() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public Map toMap() + { + if(wrapped == null) + { + return (null); + } + + Map rs = new HashMap<>(); + for(Map.Entry entry : wrapped.entrySet()) + { + String key = entry.getKey(); + Serializable value = entry.getValue(); + + if(key.equals("blocks")) + { + ArrayList resultList = new ArrayList<>(); + + List> sourceList = (List>) value; + for(AbstractBlockWidgetData abstractBlockWidgetData : sourceList) + { + resultList.add(new WidgetBlock(abstractBlockWidgetData)); + } + + value = resultList; + } + + rs.put(key, value); + } + + return (rs); + } +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FrontendStep.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FrontendStep.java new file mode 100644 index 00000000..d8c36433 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FrontendStep.java @@ -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 . + */ + +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 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 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 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 getRecordListFields() + { + return (CollectionUtils.nonNullList(this.wrapped.getRecordListFields()).stream().map(f -> new FieldMetaData(f)).toList()); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/Icon.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/Icon.java new file mode 100644 index 00000000..d1736ea1 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/Icon.java @@ -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 . + */ + +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()); + } +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/ProcessMetaData.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/ProcessMetaData.java new file mode 100644 index 00000000..455f7705 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/ProcessMetaData.java @@ -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 . + */ + +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 getFrontendSteps() + { + return (CollectionUtils.nonNullList(this.wrapped.getFrontendSteps()).stream().map(f -> new FrontendStep(f)).toList()); + } + + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/ProcessMetaDataAdjustment.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/ProcessMetaDataAdjustment.java new file mode 100644 index 00000000..ea63437b --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/ProcessMetaDataAdjustment.java @@ -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 . + */ + +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 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 updatedFields = null; + + + + /******************************************************************************* + ** Getter for updatedFrontendStepList + ** + *******************************************************************************/ + public List getUpdatedFrontendStepList() + { + return updatedFrontendStepList; + } + + + + /******************************************************************************* + ** Setter for updatedFrontendStepList + ** + *******************************************************************************/ + public void setUpdatedFrontendStepList(List updatedFrontendStepList) + { + this.updatedFrontendStepList = updatedFrontendStepList; + } + + + + /******************************************************************************* + ** Fluent setter for updatedFrontendStepList + ** + *******************************************************************************/ + public ProcessMetaDataAdjustment withUpdatedFrontendStepList(List updatedFrontendStepList) + { + this.updatedFrontendStepList = updatedFrontendStepList; + return (this); + } + + + + /******************************************************************************* + ** Getter for updatedFields + *******************************************************************************/ + public Map getUpdatedFields() + { + return (this.updatedFields); + } + + + + /******************************************************************************* + ** Setter for updatedFields + *******************************************************************************/ + public void setUpdatedFields(Map updatedFields) + { + this.updatedFields = updatedFields; + } + + + + /******************************************************************************* + ** Fluent setter for updatedFields + *******************************************************************************/ + public ProcessMetaDataAdjustment withUpdatedFields(Map updatedFields) + { + this.updatedFields = updatedFields; + return (this); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/ProcessMetaDataLight.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/ProcessMetaDataLight.java new file mode 100644 index 00000000..6615b416 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/ProcessMetaDataLight.java @@ -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 . + */ + +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()); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableMetaDataLight.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableMetaDataLight.java new file mode 100644 index 00000000..4eb804cf --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableMetaDataLight.java @@ -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 . + */ + +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 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> getHelpContents() + { + return (this.wrapped.getHelpContents()); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlock.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlock.java new file mode 100644 index 00000000..0ed526b0 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlock.java @@ -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 . + */ + +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 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 + { + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public EnumSet 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 styleOverrides = new HashMap<>(); + private String overlayHtml; + private Map overlayStyleOverrides = new HashMap<>(); + */ + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockActionButtonValues.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockActionButtonValues.java new file mode 100644 index 00000000..52311cbb --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockActionButtonValues.java @@ -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 . + */ + +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()); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockInputFieldValues.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockInputFieldValues.java new file mode 100644 index 00000000..31186e94 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockInputFieldValues.java @@ -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 . + */ + +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()); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockStyles.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockStyles.java new file mode 100644 index 00000000..07cdcd42 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockStyles.java @@ -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 . + */ + +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); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockTextStyles.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockTextStyles.java new file mode 100644 index 00000000..3d8c5d78 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockTextStyles.java @@ -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 . + */ + +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()); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockTextValues.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockTextValues.java new file mode 100644 index 00000000..49977873 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockTextValues.java @@ -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 . + */ + +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()); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockValues.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockValues.java new file mode 100644 index 00000000..bdac09ae --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockValues.java @@ -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 . + */ + +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); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/ProcessSpecUtilsV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/ProcessSpecUtilsV1.java new file mode 100644 index 00000000..b98dffe5 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/ProcessSpecUtilsV1.java @@ -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 . + */ + +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 buildResponseExample() + { + ProcessInitOrStepOrStatusResponseV1 completeResponse = new ProcessInitOrStepOrStatusResponseV1(); + completeResponse.setType(ProcessInitOrStepOrStatusOutputInterface.Type.COMPLETE); + completeResponse.setProcessUUID(EXAMPLE_PROCESS_UUID); + Map 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()) + .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 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); + } +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/QuerySpecUtils.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/QuerySpecUtils.java new file mode 100644 index 00000000..3406c899 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/QuerySpecUtils.java @@ -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 . + */ + +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 paramMap) throws IOException + { + + QQueryFilter filter = null; + String filterParam = paramMap.get("filter"); + if(StringUtils.hasContent(filterParam)) + { + filter = JsonUtils.toObject(filterParam, QQueryFilter.class); + } + + List 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); + } +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/TagsV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/TagsV1.java new file mode 100644 index 00000000..93fbdb20 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/TagsV1.java @@ -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 . + */ + +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; + } +} diff --git a/qqq-middleware-javalin/src/main/resources/openapi/v1/openapi.yaml b/qqq-middleware-javalin/src/main/resources/openapi/v1/openapi.yaml new file mode 100644 index 00000000..55d91ab0 --- /dev/null +++ b/qqq-middleware-javalin/src/main/resources/openapi/v1/openapi.yaml @@ -0,0 +1,1614 @@ +components: + schemas: + AppMetaData: + properties: + childMap: + additionalProperties: + $ref: "#/components/schemas/AppTreeNode" + description: "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." + type: "object" + children: + description: "List of other apps, tables, process, and reports, which are\ + \ contained within this app." + items: + $ref: "#/components/schemas/AppTreeNode" + type: "array" + iconName: + description: "Name of an icon for the app, from the material UI icon set" + type: "string" + label: + description: "User-facing name for this app" + type: "string" + name: + description: "Unique name for this app within the QQQ Instance" + type: "string" + sections: + description: "List of sections - sub-divisions of the app, to further organize\ + \ its children." + items: + $ref: "#/components/schemas/AppSection" + type: "array" + supplementalAppMetaData: + description: "Additional meta-data describing the app, which may not be\ + \ known to the QQQ backend core module." + type: "object" + widgets: + description: "List of widgets names that are part of this app. These strings\ + \ should be keys to the widgets map in the QQQ Instance." + items: + type: "string" + type: "array" + type: "object" + AppSection: + properties: + icon: + $ref: "#/components/schemas/Icon" + description: "Icon to display for the section." + label: + description: "User-facing name of the section." + type: "string" + name: + description: "Unique (within the app) name for this section." + type: "string" + processes: + description: "List of process names for the section" + items: + type: "string" + type: "array" + reports: + description: "List of report names for the section" + items: + type: "string" + type: "array" + tables: + description: "List of table names for the section" + items: + type: "string" + type: "array" + type: "object" + AppTreeNode: + properties: + children: + description: "Child elements. Only applies for type='app', which contains\ + \ additional apps under it" + items: + $ref: "#/components/schemas/AppTreeNode" + type: "array" + label: + description: "User-facing name of the element." + type: "string" + name: + description: "Unique (within its type) name for this element. e.g., for\ + \ type = 'table', the table's name." + type: "string" + type: + description: "The type of node (table, process, report, app)" + type: "string" + type: "object" + AuthenticationMetaDataResponseV1: + properties: + name: + description: "Unique name for the authentication metaData object within\ + \ the QInstance.\n" + type: "string" + type: + description: "Specifier for the type of authentication module being used.\n\ + \nFrontends should use this value to determine how to prompt the user\ + \ for authentication credentials.\nIn addition, depending on this value,\ + \ additional properties will be included in this object, as\nmay be needed\ + \ to complete the authorization workflow with the provider (e.g., a baseUrl,\ + \ clientId,\nand audience for an OAuth type workflow)." + type: "string" + values: + description: "Additional values, as determined by the type of authentication\ + \ provider.\n" + oneOf: + - description: "No additional values are used for some authentication providers." + type: "object" + - description: "Additional values used by the Auth0 type authentication\ + \ provider." + properties: + audience: + description: "Audience for auth0" + type: "string" + baseUrl: + description: "BaseUrl for auth0" + type: "string" + clientId: + description: "ClientId for auth0" + type: "string" + type: "object" + type: "object" + BasicErrorResponseV1: + properties: + error: + description: "Description of the error" + type: "string" + type: "object" + FieldMetaData: + properties: + adornments: + description: "Special UI dressings to add to the field." + items: + properties: + type: + enum: + - "LINK" + - "CHIP" + - "SIZE" + - "CODE_EDITOR" + - "RENDER_HTML" + - "REVEAL" + - "FILE_DOWNLOAD" + - "ERROR" + type: "string" + values: + type: "object" + type: "object" + type: "array" + defaultValue: + description: "Default value to use in this field." + type: "string" + displayFormat: + description: "C-style format specifier for displaying values in this field." + type: "string" + isEditable: + description: "Indicate if user may edit the value in this field." + type: "boolean" + isHeavy: + description: "Indicator of 'heavy' fields, which are not loaded by default.\ + \ e.g., some blobs or long-texts" + type: "boolean" + isHidden: + description: "Indicate if this field should be hidden from users" + type: "boolean" + isRequired: + description: "Indicate if a value in this field is required." + type: "boolean" + label: + description: "User-facing name for this field" + type: "string" + maxLength: + description: "For String fields, the max length the field supports." + type: "number" + name: + description: "Unique name for this field within its container (table or\ + \ process)" + type: "string" + possibleValueSourceName: + description: "If this field's values should come from a possible value source,\ + \ then that PVS is named here." + type: "string" + type: + description: "Data-type for this field" + type: "string" + type: "object" + FrontendComponent: + properties: + type: + description: "The type of this component. e.g., what kind of UI element(s)\ + \ should be presented to the user." + enum: + - "HELP_TEXT" + - "BULK_EDIT_FORM" + - "BULK_LOAD_MAPPING" + - "VALIDATION_REVIEW_SCREEN" + - "EDIT_FORM" + - "VIEW_FORM" + - "DOWNLOAD_FORM" + - "RECORD_LIST" + - "PROCESS_SUMMARY_RESULTS" + - "GOOGLE_DRIVE_SELECT_FOLDER" + - "WIDGET" + - "HTML" + type: "string" + values: + $ref: "#/components/schemas/FrontendComponentValues" + description: "Name-value pairs specific to the type of component." + type: "object" + type: "object" + FrontendComponentValues: + additionalProperties: true + description: "These are the known values that can appear in the values map under\ + \ a FrontendComponent, to control\nhow that component should be presented\ + \ to the user.\n\nNote that additional properties may appear as well.\n\n\ + In addition, components are expected to use values from an active process's\ + \ `values` map (e.g., as included in\na `ProcessStepComplete` object), with\ + \ the following contract between component-types and expected values:\n\n\ + - For component type=`HTML`, there will be a process value with key=`${stepName}.html`\ + \ (e.g., `resultScreen.html`),\nwhose value is the HTML to display on that\ + \ screen.\n- For component type=`HELP_TEXT`: There will be a process value\ + \ with key=`text`, whose value is the text to display on that screen.\nThere\ + \ may also be a process value with key=`previewText`, which, if present, can\ + \ be shown before the full text is shown,\ne.g., with a toggle control to\ + \ hide/show the `text` value.\n" + properties: + blocks: + description: "Components of type=`WIDGET`, which are set as `isAdHocWidget=true`,\ + \ should include a list of WidgetBlocks in this value." + items: + $ref: "#/components/schemas/WidgetBlock" + type: "array" + includeFieldNames: + description: "Components of type=`EDIT_FORM` can specify a subset of field\ + \ names to include. This can be used to break a form up into\nsections,\ + \ by including multiple EDIT_FORM components, with different lists of\ + \ `includeFieldNames`.\n" + items: + type: "string" + type: "array" + isAdHocWidget: + description: "Components of type=`WIDGET`, which do not reference a widget\ + \ defined in the QQQ Instance, but instead,\nare defined as a list of\ + \ blocks within a frontend step component, will have a this value set\ + \ to true." + type: "boolean" + sectionLabel: + description: "Components of type=`EDIT_FORM` can specify a user-facing text\ + \ label to show on screen.\n" + type: "string" + widgetName: + description: "Components of type=`WIDGET`, which should render a widget\ + \ defined in the QQQ instance, this value specifies\nthe name of that\ + \ widget. Contrast with ad-hoc widgets.\n" + type: "string" + type: "object" + FrontendStep: + properties: + components: + description: "The components that make up this screen" + items: + $ref: "#/components/schemas/FrontendComponent" + type: "array" + formFields: + description: "Fields used as form fields (inputs) on this step/screen" + items: + $ref: "#/components/schemas/FieldMetaData" + type: "array" + label: + description: "The user-facing name for this step" + type: "string" + name: + description: "The unique name for this step within its process" + type: "string" + recordListFields: + description: "Fields used in record-lists shown on the step/screen." + items: + $ref: "#/components/schemas/FieldMetaData" + type: "array" + viewFields: + description: "Fields used as view-only fields on this step/screen" + items: + $ref: "#/components/schemas/FieldMetaData" + type: "array" + type: "object" + Icon: + properties: + color: + description: "A color code to use for displaying the icon" + type: "string" + name: + description: "A material UI icon name." + type: "string" + path: + description: "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." + type: "string" + type: "object" + ManageSessionResponseV1: + properties: + uuid: + description: "Unique identifier of the session. Required to be returned\ + \ on subsequent requests in the sessionUUID Cookie, to prove authentication." + type: "string" + values: + additionalProperties: true + description: "Optional object with application-defined values." + type: "object" + type: "object" + MetaDataResponseV1: + properties: + appTree: + description: "Tree of apps within the QQQ Instance, sorted and organized\ + \ hierarchically, for presentation to a user." + items: + $ref: "#/components/schemas/AppTreeNode" + type: "array" + apps: + additionalProperties: + $ref: "#/components/schemas/AppMetaData" + description: "Map of all apps within the QQQ Instance (that the user has\ + \ permission to see that they exist)." + type: "object" + processes: + additionalProperties: + $ref: "#/components/schemas/ProcessMetaDataLight" + description: "Map of all processes within the QQQ Instance (that the user\ + \ has permission to see that they exist)." + type: "object" + tables: + additionalProperties: + $ref: "#/components/schemas/TableMetaDataLight" + description: "Map of all tables within the QQQ Instance (that the user has\ + \ permission to see that they exist)." + type: "object" + type: "object" + ProcessMetaData: + properties: + frontendSteps: + description: "Frontend steps (aka, Screens) for this process." + items: + $ref: "#/components/schemas/FrontendStep" + type: "array" + hasPermission: + description: "Boolean to indicate if the user has permission for the process." + type: "boolean" + iconName: + description: "Name of an icon for the process, from the material UI icon\ + \ set" + type: "string" + isHidden: + description: "Boolean indicator of whether the process should be shown to\ + \ users or not" + type: "boolean" + label: + description: "User-facing name for this process" + type: "string" + name: + description: "Unique name for this process within the QQQ Instance" + type: "string" + stepFlow: + description: "Indicator of the Step Flow used by the process. Possible\ + \ values are: LINEAR, STATE_MACHINE." + type: "string" + tableName: + description: "If this process is associated with a table, the table name\ + \ is given here" + type: "string" + type: "object" + ProcessMetaDataAdjustment: + properties: + updatedFields: + additionalProperties: + $ref: "#/components/schemas/FieldMetaData" + description: "Fields whose meta-data has changed. e.g., changing a label,\ + \ or required status, or inline-possible-values." + type: "object" + updatedFrontendStepList: + description: "In case the backend has changed the list of frontend steps,\ + \ it will be set in this field." + items: + $ref: "#/components/schemas/FrontendStep" + type: "array" + type: "object" + ProcessMetaDataLight: + properties: + hasPermission: + description: "Boolean to indicate if the user has permission for the process." + type: "boolean" + iconName: + description: "Name of an icon for the process, from the material UI icon\ + \ set" + type: "string" + isHidden: + description: "Boolean indicator of whether the process should be shown to\ + \ users or not" + type: "boolean" + label: + description: "User-facing name for this process" + type: "string" + name: + description: "Unique name for this process within the QQQ Instance" + type: "string" + stepFlow: + description: "Indicator of the Step Flow used by the process. Possible\ + \ values are: LINEAR, STATE_MACHINE." + type: "string" + tableName: + description: "If this process is associated with a table, the table name\ + \ is given here" + type: "string" + type: "object" + ProcessMetaDataResponseV1: + properties: + frontendSteps: + description: "Frontend steps (aka, Screens) for this process." + items: + $ref: "#/components/schemas/FrontendStep" + type: "array" + hasPermission: + description: "Boolean to indicate if the user has permission for the process." + type: "boolean" + iconName: + description: "Name of an icon for the process, from the material UI icon\ + \ set" + type: "string" + isHidden: + description: "Boolean indicator of whether the process should be shown to\ + \ users or not" + type: "boolean" + label: + description: "User-facing name for this process" + type: "string" + name: + description: "Unique name for this process within the QQQ Instance" + type: "string" + stepFlow: + description: "Indicator of the Step Flow used by the process. Possible\ + \ values are: LINEAR, STATE_MACHINE." + type: "string" + tableName: + description: "If this process is associated with a table, the table name\ + \ is given here" + type: "string" + type: "object" + ProcessStepComplete: + description: "Data returned after the job is complete (whether it was synchronous,\ + \ or asynchronous)" + properties: + nextStep: + description: "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. " + type: "string" + processMetaDataAdjustment: + description: "Changes to be made to the process's metaData." + properties: + updatedFields: + additionalProperties: + $ref: "#/components/schemas/FieldMetaData" + description: "Fields whose meta-data has changed. e.g., changing a\ + \ label, or required status, or inline-possible-values." + type: "object" + updatedFrontendStepList: + description: "In case the backend has changed the list of frontend steps,\ + \ it will be set in this field." + items: + properties: + components: + description: "The components that make up this screen" + items: + $ref: "#/components/schemas/FrontendComponent" + type: "array" + formFields: + description: "Fields used as form fields (inputs) on this step/screen" + items: + $ref: "#/components/schemas/FieldMetaData" + type: "array" + label: + description: "The user-facing name for this step" + type: "string" + name: + description: "The unique name for this step within its process" + type: "string" + recordListFields: + description: "Fields used in record-lists shown on the step/screen." + items: + $ref: "#/components/schemas/FieldMetaData" + type: "array" + viewFields: + description: "Fields used as view-only fields on this step/screen" + items: + $ref: "#/components/schemas/FieldMetaData" + type: "array" + type: "object" + type: "array" + type: "object" + processUUID: + description: "Unique identifier for a running instance the process." + type: "string" + type: + description: "What kind of response has been received. Determines what\ + \ additional fields will be set." + type: "string" + values: + description: "Current values for fields used by the process.Keys are Strings,\ + \ values can be any type, as determined by the application & process." + type: "object" + type: "object" + ProcessStepError: + description: "In case an error is thrown in the backend job." + properties: + error: + description: "Exception message, in case the process step threw an error." + type: "string" + processUUID: + description: "Unique identifier for a running instance the process." + type: "string" + type: + description: "What kind of response has been received. Determines what\ + \ additional fields will be set." + type: "string" + userFacingError: + description: "Optional user-facing exception message, in case the process\ + \ step threw a user-facing error." + type: "string" + type: "object" + ProcessStepJobStarted: + description: "In case the backend needs more time, this is a UUID of the background\ + \ job that has been started." + properties: + jobUUID: + description: "Unique identifier for a running step of the process. Must\ + \ be passed into `status` check calls." + type: "string" + processUUID: + description: "Unique identifier for a running instance the process." + type: "string" + type: + description: "What kind of response has been received. Determines what\ + \ additional fields will be set." + type: "string" + type: "object" + ProcessStepResponseV1: + oneOf: + - description: "Data returned after the job is complete (whether it was synchronous,\ + \ or asynchronous)" + properties: + nextStep: + description: "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. " + type: "string" + processMetaDataAdjustment: + description: "Changes to be made to the process's metaData." + properties: + updatedFields: + additionalProperties: + $ref: "#/components/schemas/FieldMetaData" + description: "Fields whose meta-data has changed. e.g., changing\ + \ a label, or required status, or inline-possible-values." + type: "object" + updatedFrontendStepList: + description: "In case the backend has changed the list of frontend\ + \ steps, it will be set in this field." + items: + properties: + components: + description: "The components that make up this screen" + items: + $ref: "#/components/schemas/FrontendComponent" + type: "array" + formFields: + description: "Fields used as form fields (inputs) on this step/screen" + items: + $ref: "#/components/schemas/FieldMetaData" + type: "array" + label: + description: "The user-facing name for this step" + type: "string" + name: + description: "The unique name for this step within its process" + type: "string" + recordListFields: + description: "Fields used in record-lists shown on the step/screen." + items: + $ref: "#/components/schemas/FieldMetaData" + type: "array" + viewFields: + description: "Fields used as view-only fields on this step/screen" + items: + $ref: "#/components/schemas/FieldMetaData" + type: "array" + type: "object" + type: "array" + type: "object" + processUUID: + description: "Unique identifier for a running instance the process." + type: "string" + type: + description: "What kind of response has been received. Determines what\ + \ additional fields will be set." + type: "string" + values: + description: "Current values for fields used by the process.Keys are Strings,\ + \ values can be any type, as determined by the application & process." + type: "object" + type: "object" + - description: "In case the backend needs more time, this is a UUID of the background\ + \ job that has been started." + properties: + jobUUID: + description: "Unique identifier for a running step of the process. Must\ + \ be passed into `status` check calls." + type: "string" + processUUID: + description: "Unique identifier for a running instance the process." + type: "string" + type: + description: "What kind of response has been received. Determines what\ + \ additional fields will be set." + type: "string" + type: "object" + - description: "Response to a status check for a backgrounded job." + properties: + current: + description: "Optional indicator of progress (e.g., `current` of `total`,\ + \ as in (`1 of 10`)." + type: "number" + message: + description: "Status message regarding the running process step." + type: "string" + processUUID: + description: "Unique identifier for a running instance the process." + type: "string" + total: + description: "Optional indicator of progress (e.g., `current` of `total`,\ + \ as in (`1 of 10`)." + type: "number" + type: + description: "What kind of response has been received. Determines what\ + \ additional fields will be set." + type: "string" + type: "object" + - description: "In case an error is thrown in the backend job." + properties: + error: + description: "Exception message, in case the process step threw an error." + type: "string" + processUUID: + description: "Unique identifier for a running instance the process." + type: "string" + type: + description: "What kind of response has been received. Determines what\ + \ additional fields will be set." + type: "string" + userFacingError: + description: "Optional user-facing exception message, in case the process\ + \ step threw a user-facing error." + type: "string" + type: "object" + ProcessStepRunning: + description: "Response to a status check for a backgrounded job." + properties: + current: + description: "Optional indicator of progress (e.g., `current` of `total`,\ + \ as in (`1 of 10`)." + type: "number" + message: + description: "Status message regarding the running process step." + type: "string" + processUUID: + description: "Unique identifier for a running instance the process." + type: "string" + total: + description: "Optional indicator of progress (e.g., `current` of `total`,\ + \ as in (`1 of 10`)." + type: "number" + type: + description: "What kind of response has been received. Determines what\ + \ additional fields will be set." + type: "string" + type: "object" + TableMetaDataLight: + properties: + capabilities: + description: "List of strings describing actions that are supported by the\ + \ backend application for the table." + items: + type: "string" + type: "array" + deletePermission: + description: "Boolean to indicate if the user has delete permission for\ + \ the table." + type: "boolean" + editPermission: + description: "Boolean to indicate if the user has edit permission for the\ + \ table." + type: "boolean" + helpContents: + description: "Help Contents for this table." + type: "object" + iconName: + description: "Name of an icon for the table, from the material UI icon set" + type: "string" + insertPermission: + description: "Boolean to indicate if the user has insert permission for\ + \ the table." + type: "boolean" + isHidden: + description: "Boolean indicator of whether the table should be shown to\ + \ users or not" + type: "boolean" + label: + description: "User-facing name for this table" + type: "string" + name: + description: "Unique name for this table within the QQQ Instance" + type: "string" + readPermission: + description: "Boolean to indicate if the user has read permission for the\ + \ table." + type: "boolean" + variantTableLabel: + description: "If the table uses variants, this is the user-facing label\ + \ for the table that supplies variants for this table." + type: "string" + type: "object" + WidgetBlock: + properties: + blockId: + description: "Unique identifier for this block within it widget. Used as\ + \ a key for helpContents." + type: "string" + blockType: + description: "What type of block to render." + enum: + - "ACTION_BUTTON" + - "AUDIO" + - "BIG_NUMBER" + - "COMPOSITE" + - "DIVIDER" + - "IMAGE" + - "INPUT_FIELD" + - "NUMBER_ICON_BADGE" + - "PROGRESS_BAR" + - "TABLE_SUB_ROW_DETAIL_ROW" + - "TEXT" + - "UP_OR_DOWN_NUMBER" + type: "string" + conditional: + description: "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" + type: "string" + layout: + description: "For COMPOSITE type blocks, an indicator of how the sub-blocks\ + \ should be laid out" + enum: + - "FLEX_COLUMN" + - "FLEX_ROW_WRAPPED" + - "FLEX_ROW_SPACE_BETWEEN" + - "FLEX_ROW_CENTER" + - "TABLE_SUB_ROW_DETAILS" + - "BADGES_WRAPPER" + type: "string" + styles: + $ref: "#/components/schemas/WidgetBlockStyles" + description: "Styles to apply to the block. Different fields based on blockType." + oneOf: + - properties: + isAlert: + description: "Indicate if the text should be displayed as an alert\ + \ (e.g., modal popup)" + type: "boolean" + standardColor: + description: "A Standard Color to display the text in (e.g., not a\ + \ hex or RGB code)." + enum: + - "SUCCESS" + - "WARNING" + - "ERROR" + - "INFO" + - "MUTED" + type: "string" + type: "object" + subBlocks: + description: "For COMPOSITE type blocks, a list of sub-blocks." + items: + $ref: "#/components/schemas/WidgetBlock" + type: "array" + values: + $ref: "#/components/schemas/WidgetBlockValues" + description: "Values to show in the block, or otherwise control its behavior.\ + \ Different fields based on blockType." + oneOf: + - description: "Values used for an ACTION_BUTTON type widget block" + properties: + actionCode: + description: "Code used within the app as the value submitted when\ + \ the button is clicked" + type: "string" + label: + description: "User-facing label to display in the button" + type: "string" + type: "object" + - description: "Values used for a TEXT type widget block" + properties: + text: + description: "The text to display in the block" + type: "string" + type: "object" + - description: "Values used for an INPUT_FIELD type widget block" + properties: + autoFocus: + description: "Indicate whether this field should auto-focus when it\ + \ is rendered" + type: "boolean" + fieldMetaData: + description: "Metadata to define the field that this block controls" + properties: + adornments: + description: "Special UI dressings to add to the field." + items: + properties: + type: + enum: + - "LINK" + - "CHIP" + - "SIZE" + - "CODE_EDITOR" + - "RENDER_HTML" + - "REVEAL" + - "FILE_DOWNLOAD" + - "ERROR" + type: "string" + values: + type: "object" + type: "object" + type: "array" + defaultValue: + description: "Default value to use in this field." + type: "string" + displayFormat: + description: "C-style format specifier for displaying values in\ + \ this field." + type: "string" + isEditable: + description: "Indicate if user may edit the value in this field." + type: "boolean" + isHeavy: + description: "Indicator of 'heavy' fields, which are not loaded\ + \ by default. e.g., some blobs or long-texts" + type: "boolean" + isHidden: + description: "Indicate if this field should be hidden from users" + type: "boolean" + isRequired: + description: "Indicate if a value in this field is required." + type: "boolean" + label: + description: "User-facing name for this field" + type: "string" + maxLength: + description: "For String fields, the max length the field supports." + type: "number" + name: + description: "Unique name for this field within its container\ + \ (table or process)" + type: "string" + possibleValueSourceName: + description: "If this field's values should come from a possible\ + \ value source, then that PVS is named here." + type: "string" + type: + description: "Data-type for this field" + type: "string" + type: "object" + submitOnEnter: + description: "Indicate whether the form that this field is on should\ + \ be submitted when Enter is pressed" + type: "boolean" + type: "object" + type: "object" + WidgetBlockActionButtonValues: + description: "Values used for an ACTION_BUTTON type widget block" + properties: + actionCode: + description: "Code used within the app as the value submitted when the button\ + \ is clicked" + type: "string" + label: + description: "User-facing label to display in the button" + type: "string" + type: "object" + WidgetBlockInputFieldValues: + description: "Values used for an INPUT_FIELD type widget block" + properties: + autoFocus: + description: "Indicate whether this field should auto-focus when it is rendered" + type: "boolean" + fieldMetaData: + $ref: "#/components/schemas/FieldMetaData" + description: "Metadata to define the field that this block controls" + submitOnEnter: + description: "Indicate whether the form that this field is on should be\ + \ submitted when Enter is pressed" + type: "boolean" + type: "object" + WidgetBlockStyles: + type: "object" + WidgetBlockTextStyles: + properties: + isAlert: + description: "Indicate if the text should be displayed as an alert (e.g.,\ + \ modal popup)" + type: "boolean" + standardColor: + description: "A Standard Color to display the text in (e.g., not a hex or\ + \ RGB code)." + enum: + - "SUCCESS" + - "WARNING" + - "ERROR" + - "INFO" + - "MUTED" + type: "string" + type: "object" + WidgetBlockTextValues: + description: "Values used for a TEXT type widget block" + properties: + text: + description: "The text to display in the block" + type: "string" + type: "object" + WidgetBlockValues: + type: "object" + securitySchemes: + sessionUuidCookie: + in: "cookie" + name: "sessionUUID" + type: "apiKey" +info: + contact: + email: "contact@kingsrook.com" + description: "## Intro\nThis is the definition of the standard API implemented by\ + \ QQQ Middleware.\n\nDevelopers of QQQ Frontends (e.g., javascript libraries,\ + \ or native applications) use this API to access\na QQQ Backend server.\n\nAs\ + \ such, this API itself is not concerned with any of the application-level details\ + \ of any particular\napplication built using QQQ. Instead, this API is all about\ + \ the generic endpoints used for any application\nbuilt on QQQ. For example,\ + \ many endpoints work with a `${table}` path parameter - whose possible values\n\ + are defined by the application - but which are not known to this API.\n\n## Flow\n\ + The typical flow of a user (as implemented in a frontend that utilizes this API)\ + \ looks like:\n1. 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.\n2. User authenticates in frontend, as required\ + \ for the authentication provider.\n3. Frontend calls `.../manageSession`, providing\ + \ authentication details (e.g., an accessToken or other credentials) to the backend.\n\ + 4. The response from the `manageSession` call (assuming success), sets the `sessionUUID`\ + \ Cookie, which should be included in all subsequent requests for authentication.\n\ + 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).\n6. As the user interacts\ + \ with apps, tables, process, etc, the frontend utilizes the appropriate endpoints\ + \ as required.\n" + title: "QQQ Middleware API" + version: "v1" +openapi: "3.0.3" +paths: + /qqq/v1/metaData/authentication: + get: + description: "For a frontend to determine which authentication provider or mechanism\ + \ to use, it should begin its lifecycle\nby requesting this metaData object,\ + \ and inspecting the `type` property in the response.\n\nNote that this endpoint\ + \ is not secured, as its purpose is to be called as part of the workflow that\ + \ results\nin a user being authenticated." + responses: + 200: + content: + application/json: + examples: + For FULLY_ANONYMOUS type: + value: + name: "anonymous" + type: "FULLY_ANONYMOUS" + For AUTH_0 type: + value: + name: "auth0" + type: "AUTH_0" + values: + audience: "myapp.mydomain.com" + baseUrl: "https://myapp.auth0.com/" + clientId: "abcdefg1234567" + schema: + $ref: "#/components/schemas/AuthenticationMetaDataResponseV1" + description: "Successful Response" + summary: "Get authentication metaData" + tags: + - "Authentication" + /qqq/v1/manageSession: + post: + description: "After a frontend authenticates the user as per the requirements\ + \ of the authentication provider specified by the\n`type` field in the `metaData/authentication`\ + \ response, data from that authentication provider should be posted\nto this\ + \ endpoint, to create a session within the QQQ application.\n\nThe response\ + \ object will include a session identifier (`uuid`) to authenticate the user\ + \ in subsequent API calls." + requestBody: + content: + application/json: + schema: + description: "Data required to create the session. Specific needs may\ + \ vary based on the AuthenticationModule type in the QQQ Backend." + properties: + accessToken: + description: "An access token from a downstream authentication provider\ + \ (e.g., Auth0), to use as the basis for authentication and authorization." + type: "string" + type: "object" + required: true + responses: + 401: + content: + application/json: + examples: + Invalid token: + value: + error: "Unable to decode access token." + schema: + $ref: "#/components/schemas/BasicErrorResponseV1" + description: "Authentication error - session was not created" + 200: + content: + application/json: + examples: + With no custom values: + value: + uuid: "01234567-89AB-CDEF-0123-456789ABCDEF" + With custom values: + value: + uuid: "98765432-10FE-DCBA-9876-543210FEDCBA" + values: + region: "US" + userCategoryId: 47 + schema: + $ref: "#/components/schemas/ManageSessionResponseV1" + description: "Successful response - session has been created" + summary: "Create a session" + tags: + - "Authentication" + /qqq/v1/metaData: + get: + description: "Load the overall metadata, as is relevant to a frontend, for the\ + \ entire application, with permissions applied, as per the\nauthenticated\ + \ user.\n\nThis includes:\n- Apps (both as a map of name to AppMetaData (`apps`),\ + \ but also as a tree (`appTree`), for presenting\nhierarchical navigation),\n\ + - Tables (but without all details, e.g., fields),\n- Processes (also without\ + \ full details, e.g., screens),\n- Reports\n- Widgets\n- Branding\n- Help\ + \ Contents\n- Environment values\n" + responses: + 200: + content: + application/json: + examples: + Example: + value: + appTree: + - children: + - children: + - label: "Sample Person Process" + name: "samplePersonProcess" + type: "PROCESS" + label: "Child App" + name: "childApp" + type: "APP" + - label: "Person" + name: "person" + type: "TABLE" + label: "Home App" + name: "homeApp" + type: "APP" + apps: + homeApp: + childMap: + person: + label: "Person" + name: "person" + type: "TABLE" + childApp: + label: "Child App" + name: "childApp" + type: "APP" + children: + - label: "Child App" + name: "childApp" + type: "APP" + - label: "Person" + name: "person" + type: "TABLE" + iconName: "home" + label: "Home App" + name: "homeApp" + sections: + - icon: + name: "badge" + label: "Home App" + name: "homeApp" + tables: + - "person" + childApp: + childMap: + samplePersonProcess: + label: "Sample Person Process" + name: "samplePersonProcess" + type: "PROCESS" + children: + - label: "Sample Person Process" + name: "samplePersonProcess" + type: "PROCESS" + iconName: "child_friendly" + label: "Child App" + name: "childApp" + sections: + - icon: + name: "badge" + label: "Child App" + name: "childApp" + processes: + - "samplePersonProcess" + processes: + person.bulkInsert: + hasPermission: true + isHidden: true + label: "Person Bulk Insert" + name: "person.bulkInsert" + stepFlow: "LINEAR" + tableName: "person" + person.bulkEdit: + hasPermission: true + isHidden: true + label: "Person Bulk Edit" + name: "person.bulkEdit" + stepFlow: "LINEAR" + tableName: "person" + samplePersonProcess: + hasPermission: true + iconName: "person_add" + isHidden: false + label: "Sample Person Process" + name: "samplePersonProcess" + stepFlow: "LINEAR" + tableName: "person" + person.bulkDelete: + hasPermission: true + isHidden: true + label: "Person Bulk Delete" + name: "person.bulkDelete" + stepFlow: "LINEAR" + tableName: "person" + tables: + person: + capabilities: + - "TABLE_COUNT" + - "TABLE_GET" + - "TABLE_QUERY" + - "QUERY_STATS" + - "TABLE_INSERT" + - "TABLE_DELETE" + - "TABLE_UPDATE" + deletePermission: true + editPermission: true + iconName: "person_outline" + insertPermission: true + isHidden: false + label: "Person" + name: "person" + readPermission: true + schema: + $ref: "#/components/schemas/MetaDataResponseV1" + description: "Overall metadata for the application." + security: + - sessionUuidCookie: + - "N/A" + summary: "Get instance metaData" + tags: + - "General" + /qqq/v1/metaData/process/{processName}: + get: + description: "Load the full metadata for a single process, including all screens\ + \ (aka, frontend steps), which a frontend\nneeds to display to users." + parameters: + - description: "Name of the process to load." + example: "samplePersonProcess" + in: "path" + name: "processName" + required: true + schema: + type: "string" + responses: + 200: + content: + application/json: + examples: + TODO: + value: {} + schema: + $ref: "#/components/schemas/ProcessMetaDataResponseV1" + description: "The full process metadata" + security: + - sessionUuidCookie: + - "N/A" + summary: "Get process metaData" + tags: + - "Processes" + /qqq/v1/processes/{processName}/init: + post: + description: "For a user to start running a process, this endpoint should be\ + \ called, to start the process\nand run its first step(s) (any backend steps\ + \ before the first frontend step).\n\nAdditional process-specific values should\ + \ posted in a form param named `values`, as JSON object\nwith keys defined\ + \ by the process in question.\n\nFor a process which needs to operate on a\ + \ set of records that a user selected, see\n`recordsParam`, and `recordIds`\ + \ or `filterJSON`.\n\nThe response will include a `processUUID`, to be included\ + \ in all subsequent requests relevant\nto the process.\n\nNote that this request,\ + \ if it takes longer than a given threshold* to complete, will return a\n\ + a `jobUUID`, which should be sent to the `/processes/{processName}/{processUUID}/status/{jobUUID}`\n\ + endpoint, to poll for a status update.\n\n*This threshold has a default value\ + \ of 3,000 ms., but can be set per-request via the form\nparameter `stepTimeoutMillis`.\n" + parameters: + - description: "Name of the process to initialize" + example: "samplePersonProcess" + in: "path" + name: "processName" + required: true + schema: + type: "string" + requestBody: + content: + multipart/form-data: + schema: + properties: + values: + description: "Process-specific field names and values." + type: "object" + recordsParam: + description: "Specifies which other query-param will contain the\ + \ indicator of initial records to pass in to the process." + examples: + recordIds: + value: "recordIds" + filterJSON: + value: "recordIds" + type: "string" + recordIds: + description: "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." + examples: + one id: + value: "1701" + multiple ids: + value: "42,47" + type: "string" + filterJSON: + description: "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." + examples: + empty filter (all records): + value: "{}" + filter by a condition: + value: "{\"criteria\":[{\"fieldName\":\"id\",\"operator\":\"\ + LESS_THAN\",\"values\":[10]}],\"booleanOperator\":\"AND\"}" + type: "string" + stepTimeoutMillis: + description: "Optionally change the time that the server will wait\ + \ for the job before letting it go asynchronous. Default value\ + \ is 3000." + examples: + shorter timeout: + value: "500" + longer timeout: + value: "60000" + type: "integer" + file: + description: "A file upload, for processes which expect to be initialized\ + \ with an uploaded file." + format: "binary" + type: "string" + type: "object" + required: false + responses: + 200: + content: + application/json: + examples: + COMPLETE: + value: + typedResponse: + nextStep: "reviewScreen" + processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF" + type: "COMPLETE" + values: + totalAge: 32768 + firstLastName: "Aabramson" + COMPLETE with metaDataAdjustment: + value: + typedResponse: + nextStep: "inputScreen" + processMetaDataAdjustment: + updatedFields: + someField: + displayFormat: "%s" + isEditable: true + isHeavy: false + isHidden: false + isRequired: true + name: "someField" + type: "STRING" + updatedFrontendStepList: + - components: + - type: "EDIT_FORM" + formFields: + - displayFormat: "%s" + isEditable: true + isHeavy: false + isHidden: false + isRequired: false + name: "someField" + type: "STRING" + name: "inputScreen" + - components: + - type: "PROCESS_SUMMARY_RESULTS" + name: "resultScreen" + processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF" + type: "COMPLETE" + values: + totalAge: 32768 + firstLastName: "Aabramson" + JOB_STARTED: + value: + typedResponse: + jobUUID: "98765432-10FE-DCBA-9876-543210FEDCBA" + processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF" + type: "JOB_STARTED" + RUNNING: + value: + typedResponse: + current: 47 + message: "Processing person records" + processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF" + total: 1701 + type: "RUNNING" + ERROR: + value: + typedResponse: + processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF" + type: "RUNNING" + schema: + $ref: "#/components/schemas/ProcessStepResponseV1" + description: "State of the initialization of the job, with different fields\ + \ set, based on the\nstatus of the task." + security: + - sessionUuidCookie: + - "N/A" + summary: "Initialize a process" + tags: + - "Processes" + /qqq/v1/processes/{processName}/{processUUID}/step/{stepName}: + post: + description: "To run the next step in a process, this endpoint should be called,\ + \ with the `processName`\nand existing `processUUID`, as well as the step\ + \ that was just completed in the frontend,\ngiven as `stepName`.\n\nAdditional\ + \ process-specific values should posted in a form param named `values`, as\ + \ JSON object\nwith keys defined by the process in question.\n\nNote that\ + \ this request, if it takes longer than a given threshold* to complete, will\ + \ return a\na `jobUUID`, which should be sent to the `/processes/{processName}/{processUUID}/status/{jobUUID}`\n\ + endpoint, to poll for a status update.\n\n*This threshold has a default value\ + \ of 3,000 ms., but can be set per-request via the form\nparameter `stepTimeoutMillis`.\n" + parameters: + - description: "Name of the process to perform the step in." + example: "samplePersonProcess" + in: "path" + name: "processName" + required: true + schema: + type: "string" + - description: "Unique identifier for this run of the process - as was returned\ + \ by the `init` call." + example: "01234567-89AB-CDEF-0123-456789ABCDEF" + in: "path" + name: "processUUID" + required: true + schema: + type: "string" + - description: "Name of the frontend step that the user has just completed." + example: "inputForm" + in: "path" + name: "stepName" + required: true + schema: + type: "string" + requestBody: + content: + multipart/form-data: + schema: + properties: + values: + description: "Process-specific field names and values." + type: "object" + stepTimeoutMillis: + description: "Optionally change the time that the server will wait\ + \ for the job before letting it go asynchronous. Default value\ + \ is 3000." + examples: + shorter timeout: + value: "500" + longer timeout: + value: "60000" + type: "integer" + file: + description: "A file upload, for process steps which expect an uploaded\ + \ file." + format: "binary" + type: "string" + type: "object" + required: false + responses: + 200: + content: + application/json: + examples: + COMPLETE: + value: + typedResponse: + nextStep: "reviewScreen" + processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF" + type: "COMPLETE" + values: + totalAge: 32768 + firstLastName: "Aabramson" + COMPLETE with metaDataAdjustment: + value: + typedResponse: + nextStep: "inputScreen" + processMetaDataAdjustment: + updatedFields: + someField: + displayFormat: "%s" + isEditable: true + isHeavy: false + isHidden: false + isRequired: true + name: "someField" + type: "STRING" + updatedFrontendStepList: + - components: + - type: "EDIT_FORM" + formFields: + - displayFormat: "%s" + isEditable: true + isHeavy: false + isHidden: false + isRequired: false + name: "someField" + type: "STRING" + name: "inputScreen" + - components: + - type: "PROCESS_SUMMARY_RESULTS" + name: "resultScreen" + processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF" + type: "COMPLETE" + values: + totalAge: 32768 + firstLastName: "Aabramson" + JOB_STARTED: + value: + typedResponse: + jobUUID: "98765432-10FE-DCBA-9876-543210FEDCBA" + processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF" + type: "JOB_STARTED" + RUNNING: + value: + typedResponse: + current: 47 + message: "Processing person records" + processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF" + total: 1701 + type: "RUNNING" + ERROR: + value: + typedResponse: + processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF" + type: "RUNNING" + schema: + $ref: "#/components/schemas/ProcessStepResponseV1" + description: "State of the backend's running of the next step(s) of the\ + \ job, with different fields set,\nbased on the status of the job." + security: + - sessionUuidCookie: + - "N/A" + summary: "Run a step in a process" + tags: + - "Processes" + /qqq/v1/processes/{processName}/{processUUID}/status/{jobUUID}: + get: + description: "Get the status of a running job for a process.\n\nResponse is\ + \ the same format as for an init or step call that completed synchronously.\n" + parameters: + - description: "Name of the process that is being ran" + example: "samplePersonProcess" + in: "path" + name: "processName" + required: true + schema: + type: "string" + - description: "Unique identifier for this run of the process - as was returned\ + \ by the `init` call." + example: "01234567-89AB-CDEF-0123-456789ABCDEF" + in: "path" + name: "processUUID" + required: true + schema: + format: "uuid" + type: "string" + - description: "Unique identifier for the asynchronous job being executed, as\ + \ returned by an `init` or `step` call that went asynch." + example: "98765432-10FE-DCBA-9876-543210FEDCBA" + in: "path" + name: "jobUUID" + required: true + schema: + format: "uuid" + type: "string" + responses: + 200: + content: + application/json: + examples: + COMPLETE: + value: + typedResponse: + nextStep: "reviewScreen" + processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF" + type: "COMPLETE" + values: + totalAge: 32768 + firstLastName: "Aabramson" + COMPLETE with metaDataAdjustment: + value: + typedResponse: + nextStep: "inputScreen" + processMetaDataAdjustment: + updatedFields: + someField: + displayFormat: "%s" + isEditable: true + isHeavy: false + isHidden: false + isRequired: true + name: "someField" + type: "STRING" + updatedFrontendStepList: + - components: + - type: "EDIT_FORM" + formFields: + - displayFormat: "%s" + isEditable: true + isHeavy: false + isHidden: false + isRequired: false + name: "someField" + type: "STRING" + name: "inputScreen" + - components: + - type: "PROCESS_SUMMARY_RESULTS" + name: "resultScreen" + processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF" + type: "COMPLETE" + values: + totalAge: 32768 + firstLastName: "Aabramson" + JOB_STARTED: + value: + typedResponse: + jobUUID: "98765432-10FE-DCBA-9876-543210FEDCBA" + processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF" + type: "JOB_STARTED" + RUNNING: + value: + typedResponse: + current: 47 + message: "Processing person records" + processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF" + total: 1701 + type: "RUNNING" + ERROR: + value: + typedResponse: + processUUID: "01234567-89AB-CDEF-0123-456789ABCDEF" + type: "RUNNING" + schema: + $ref: "#/components/schemas/ProcessStepResponseV1" + description: "State of the backend's running of the specified job, with\ + \ different fields set,\nbased on the status of the job." + security: + - sessionUuidCookie: + - "N/A" + summary: "Get job status" + tags: + - "Processes" diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/api/wip/TestContext.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/api/wip/TestContext.java new file mode 100644 index 00000000..916c9a1a --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/api/wip/TestContext.java @@ -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 . + */ + +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 queryParams = new LinkedHashMap<>(); + private Map pathParams = new LinkedHashMap<>(); + private Map 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 appData(Key key) + { + return null; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public JsonMapper jsonMapper() + { + return null; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public T with(Class> aClass) + { + return null; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public String pathParam(String key) + { + return pathParams.get(key); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public Map 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> supplier) + { + + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void redirect(String s, HttpStatus httpStatus) + { + + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void writeJsonStream(Stream stream) + { + + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public Context skipRemainingHandlers() + { + return null; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public Set 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); + } + +} diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/api/wip/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/api/wip/TestUtils.java new file mode 100644 index 00000000..8773812e --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/api/wip/TestUtils.java @@ -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 . + */ + +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 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("\".*\""); + } + } + } + +} diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/api/wip/v1/QueryGetSpecV1Test.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/api/wip/v1/QueryGetSpecV1Test.java new file mode 100644 index 00000000..61626d9a --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/api/wip/v1/QueryGetSpecV1Test.java @@ -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 . + */ + +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); + } + +} \ No newline at end of file diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java index 74aa407c..be0483aa 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java @@ -57,6 +57,7 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase /******************************************************************************* ** test running a process ** + ** Note: ported to v1 *******************************************************************************/ @Test 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. ** + ** Note: ported to v1 *******************************************************************************/ @Test 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 ** + ** Note: ported to v1 *******************************************************************************/ @Test 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 ** + ** Note: ported to v1 *******************************************************************************/ @Test public void test_processRequiresRowsWithFilterJSON() @@ -169,6 +173,7 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase /******************************************************************************* ** test running a process with field values on the query string ** + ** Note: ported to v1 *******************************************************************************/ @Test public void test_processGreetInitWithQueryValues() @@ -185,6 +190,7 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase /******************************************************************************* ** 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 public void test_processInitGoingAsync() throws InterruptedException @@ -221,6 +227,7 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase /******************************************************************************* ** test init'ing a process that does NOT goes async ** + ** Note: not ported to v1, but feels redundant, so, not going to. *******************************************************************************/ @Test public void test_processInitNotGoingAsync() diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinUtilsTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinUtilsTest.java index c8ffe0dd..7fdf17b7 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinUtilsTest.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinUtilsTest.java @@ -24,11 +24,17 @@ package com.kingsrook.qqq.backend.javalin; import java.io.InputStream; 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; @@ -133,6 +139,14 @@ class QJavalinUtilsTest + @Override + public boolean strictContentTypes() + { + return false; + } + + + /*************************************************************************** ** ***************************************************************************/ @@ -157,17 +171,6 @@ class QJavalinUtilsTest - /*************************************************************************** - ** - ***************************************************************************/ - @Override - public T appAttribute(@NotNull String s) - { - return null; - } - - - /*************************************************************************** ** ***************************************************************************/ @@ -204,6 +207,30 @@ class QJavalinUtilsTest + @Override + public T appData(@NotNull Key key) + { + return null; + } + + + + @Override + public @NotNull JsonMapper jsonMapper() + { + return null; + } + + + + @Override + public T with(@NotNull Class> 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 routeRoles() + { + return Set.of(); + } } } \ No newline at end of file diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java index c08773fa..1367cfa5 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -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.JoinType; 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.QPossibleValueSource; 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"); } + defineApps(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", 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) ))); } } diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/SchemaBuilderTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/SchemaBuilderTest.java new file mode 100644 index 00000000..71594ce4 --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/SchemaBuilderTest.java @@ -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 . + */ + +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 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); + } + +} \ No newline at end of file diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/SpecTestBase.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/SpecTestBase.java new file mode 100644 index 00000000..072814e0 --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/SpecTestBase.java @@ -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 . + */ + +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; + } + } + +} diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/AuthenticationMetaDataSpecV1Test.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/AuthenticationMetaDataSpecV1Test.java new file mode 100644 index 00000000..d5e0ab84 --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/AuthenticationMetaDataSpecV1Test.java @@ -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 . + */ + +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 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")); + } + +} \ No newline at end of file diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ManageSessionSpecV1Test.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ManageSessionSpecV1Test.java new file mode 100644 index 00000000..6fecdc9f --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ManageSessionSpecV1Test.java @@ -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 . + */ + +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 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")); + } + +} \ No newline at end of file diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1Test.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1Test.java new file mode 100644 index 00000000..0bc86b39 --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1Test.java @@ -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 . + */ + +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 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); + } + +} \ No newline at end of file diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessInitSpecV1Test.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessInitSpecV1Test.java new file mode 100644 index 00000000..36d2fa8c --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessInitSpecV1Test.java @@ -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 . + */ + +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 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 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 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 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 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 + } + +} \ No newline at end of file diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessMetaDataSpecV1Test.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessMetaDataSpecV1Test.java new file mode 100644 index 00000000..e447482d --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessMetaDataSpecV1Test.java @@ -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 . + */ + +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 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 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"); + } + +} \ No newline at end of file