diff --git a/.circleci/adjust-pom-version.sh b/.circleci/adjust-pom-version.sh index 1344b019..2d139812 100755 --- a/.circleci/adjust-pom-version.sh +++ b/.circleci/adjust-pom-version.sh @@ -1,23 +1,51 @@ #!/bin/bash -if [ -z "$CIRCLE_BRANCH" ] && [ -z "$CIRCLE_TAG" ]; then - echo "Error: env vars CIRCLE_BRANCH and CIRCLE_TAG were not set." - exit 1; -fi +############################################################################ +## adjust-pom.version.sh +## During CircleCI builds - edit the qqq parent pom.xml, to set the +## value such that: +## - feature-branch builds, tagged as snapshot-*, deploy with a version +## number that includes that tag's name (minus the snapshot- part) +## - integration-branch builds deploy with a version number that includes +## the branch name slugified +## - we never deploy -SNAPSHOT versions any more - because we don't believe +## it is ever valid to not know exactly what versions you are getting +## (perhaps because we are too loose with our versioning?) +############################################################################ -if [ "$CIRCLE_BRANCH" == "dev" ] || [ "$CIRCLE_BRANCH" == "staging" ] || [ "$CIRCLE_BRANCH" == "main" ] || [ \! -z $(echo "$CIRCLE_TAG" | grep "^version-") ]; then - echo "On a primary branch or tag [${CIRCLE_BRANCH}${CIRCLE_TAG}] - will not edit the pom version."; +POM=$(dirname $0)/../pom.xml +echo "On branch: $CIRCLE_BRANCH, tag: $CIRCLE_TAG..." + +###################################################################### +## ## only do anything if the committed pom has a -SNAPSHOT version ## +###################################################################### +REVISION=$(grep '' $POM | sed 's/.*//;s/<.*//'); +echo " in pom.xml is: $REVISION" +if [ \! $(echo "$REVISION" | grep SNAPSHOT) ]; then + echo "Not on a SNAPSHOT revision, so nothing to do here." exit 0; fi -if [ -n "$CIRCLE_BRANCH" ]; then - SLUG=$(echo $CIRCLE_BRANCH | sed 's/[^a-zA-Z0-9]/-/g') -else - SLUG=$(echo $CIRCLE_TAG | sed 's/^snapshot-//g') +################################################################################## +## ## figure out if we need a SLUG: a snapshot- tag, or an integration/ branch ## +################################################################################## +SLUG="" +if [ $(echo "$CIRCLE_TAG" | grep ^snapshot-) ]; then + SLUG=$(echo "$CIRCLE_TAG" | sed "s/^snapshot-//")- + echo "Using slug [$SLUG] from tag [$CIRCLE_TAG]" + +elif [ $(echo "$CIRCLE_BRANCH" | grep ^integration/) ]; then + SLUG=$(echo "$CIRCLE_BRANCH" | sed "s,/,-,g")- + echo "Using slug [$SLUG] from branch [$CIRCLE_BRANCH]" fi -POM=$(dirname $0)/../pom.xml +################################################################ +## ## build the replcaement for -SNAPSHOT, and update the pom ## +################################################################ +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +REPLACEMENT=${SLUG}${TIMESTAMP} -echo "Updating $POM to: $SLUG-SNAPSHOT" -sed -i "s/.*/$SLUG-SNAPSHOT<\/revision>/" $POM +echo "Updating $POM -SNAPSHOT to: -$REPLACEMENT" +sed -i "s/-SNAPSHOT<\/revision>/-$REPLACEMENT<\/revision>/" $POM git diff $POM + diff --git a/.circleci/config.yml b/.circleci/config.yml index 39d8dc38..4fffc467 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -79,6 +79,19 @@ commands: - ~/.m2 key: v1-dependencies-{{ checksum "pom.xml" }} + check_middleware_api_versions: + steps: + - checkout + - restore_cache: + keys: + - v1-dependencies-{{ checksum "pom.xml" }} + - run: + name: Build and Run ValidateApiVersions + command: | + mvn -s .circleci/mvn-settings.xml -T4 install -DskipTests + mvn -s .circleci/mvn-settings.xml -pl qqq-middleware-javalin package appassembler:assemble -DskipTests + qqq-middleware-javalin/target/appassembler/bin/ValidateApiVersions -r $(pwd) + mvn_jar_deploy: steps: - checkout @@ -130,6 +143,7 @@ jobs: ## - localstack/startup - install_java17 - mvn_verify + - check_middleware_api_versions mvn_deploy: executor: localstack/default @@ -137,6 +151,7 @@ jobs: ## - localstack/startup - install_java17 - mvn_verify + - check_middleware_api_versions - mvn_jar_deploy publish_asciidoc: diff --git a/pom.xml b/pom.xml index 89f4c2b0..68110a6b 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,7 @@ qqq-backend-module-rdbms qqq-backend-module-mongodb qqq-language-support-javascript + qqq-openapi qqq-middleware-picocli qqq-middleware-javalin qqq-middleware-lambda @@ -46,7 +47,7 @@ - 0.23.0-SNAPSHOT + 0.24.0-SNAPSHOT UTF-8 UTF-8 diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ProcessAlertWidget.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/AlertWidgetRenderer.java similarity index 92% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ProcessAlertWidget.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/AlertWidgetRenderer.java index 486f192c..3fa50066 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ProcessAlertWidget.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/AlertWidgetRenderer.java @@ -23,11 +23,11 @@ package com.kingsrook.qqq.backend.core.actions.dashboard.widgets; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput; import com.kingsrook.qqq.backend.core.model.dashboard.widgets.AlertData; import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData; @@ -40,9 +40,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData; ** - alertType - name of entry in AlertType enum (ERROR, WARNING, SUCCESS) ** - alertHtml - html to display inside the alert (other than its icon) *******************************************************************************/ -public class ProcessAlertWidget extends AbstractWidgetRenderer implements MetaDataProducerInterface +public class AlertWidgetRenderer extends AbstractWidgetRenderer implements MetaDataProducerInterface { - public static final String NAME = "ProcessAlertWidget"; + public static final String NAME = "AlertWidgetRenderer"; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRenderer.java index af63d1ac..e8f654ed 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRenderer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRenderer.java @@ -301,6 +301,9 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer } } + widgetData.setAllowRecordEdit(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("allowRecordEdit")))); + widgetData.setAllowRecordDelete(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("allowRecordDelete")))); + return (new RenderWidgetOutput(widgetData)); } catch(Exception e) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/AllowAllMetaDataFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/AllowAllMetaDataFilter.java new file mode 100644 index 00000000..c64c8954 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/AllowAllMetaDataFilter.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.backend.core.actions.metadata; + + +import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; + + +/******************************************************************************* + ** a default implementation of MetaDataFilterInterface, that allows all the things + *******************************************************************************/ +public class AllowAllMetaDataFilter implements MetaDataFilterInterface +{ + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowTable(MetaDataInput input, QTableMetaData table) + { + return (true); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowProcess(MetaDataInput input, QProcessMetaData process) + { + return (true); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowReport(MetaDataInput input, QReportMetaData report) + { + return (true); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowApp(MetaDataInput input, QAppMetaData app) + { + return (true); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowWidget(MetaDataInput input, QWidgetMetaDataInterface widget) + { + return (true); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java index b1f6b52b..56c76928 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java @@ -28,13 +28,18 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult; 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.QRuntimeException; +import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode; import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData; @@ -49,6 +54,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.memoization.Memoization; /******************************************************************************* @@ -57,6 +63,12 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils; *******************************************************************************/ public class MetaDataAction { + private static final QLogger LOG = QLogger.getLogger(MetaDataAction.class); + + private static Memoization metaDataFilterMemoization = new Memoization<>(); + + + /******************************************************************************* ** *******************************************************************************/ @@ -64,10 +76,10 @@ public class MetaDataAction { ActionHelper.validateSession(metaDataInput); - // todo pre-customization - just get to modify the request? - MetaDataOutput metaDataOutput = new MetaDataOutput(); + MetaDataOutput metaDataOutput = new MetaDataOutput(); + Map treeNodes = new LinkedHashMap<>(); - Map treeNodes = new LinkedHashMap<>(); + MetaDataFilterInterface filter = getMetaDataFilter(); ///////////////////////////////////// // map tables to frontend metadata // @@ -78,6 +90,11 @@ public class MetaDataAction String tableName = entry.getKey(); QTableMetaData table = entry.getValue(); + if(!filter.allowTable(metaDataInput, table)) + { + continue; + } + PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, table); if(permissionResult.equals(PermissionCheckResult.DENY_HIDE)) { @@ -102,6 +119,11 @@ public class MetaDataAction String processName = entry.getKey(); QProcessMetaData process = entry.getValue(); + if(!filter.allowProcess(metaDataInput, process)) + { + continue; + } + PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, process); if(permissionResult.equals(PermissionCheckResult.DENY_HIDE)) { @@ -122,6 +144,11 @@ public class MetaDataAction String reportName = entry.getKey(); QReportMetaData report = entry.getValue(); + if(!filter.allowReport(metaDataInput, report)) + { + continue; + } + PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, report); if(permissionResult.equals(PermissionCheckResult.DENY_HIDE)) { @@ -142,6 +169,11 @@ public class MetaDataAction String widgetName = entry.getKey(); QWidgetMetaDataInterface widget = entry.getValue(); + if(!filter.allowWidget(metaDataInput, widget)) + { + continue; + } + PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, widget); if(permissionResult.equals(PermissionCheckResult.DENY_HIDE)) { @@ -174,9 +206,19 @@ public class MetaDataAction continue; } - apps.put(appName, new QFrontendAppMetaData(app, metaDataOutput)); - treeNodes.put(appName, new AppTreeNode(app)); + if(!filter.allowApp(metaDataInput, app)) + { + continue; + } + ////////////////////////////////////// + // build the frontend-app meta-data // + ////////////////////////////////////// + QFrontendAppMetaData frontendAppMetaData = new QFrontendAppMetaData(app, metaDataOutput); + + ///////////////////////////////////////// + // add children (if they're permitted) // + ///////////////////////////////////////// if(CollectionUtils.nullSafeHasContents(app.getChildren())) { for(QAppChildMetaData child : app.getChildren()) @@ -190,9 +232,42 @@ public class MetaDataAction } } - apps.get(appName).addChild(new AppTreeNode(child)); + ////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the child was filtered away, so it isn't in its corresponding map, then don't include it here // + ////////////////////////////////////////////////////////////////////////////////////////////////////// + if(child instanceof QTableMetaData table && !tables.containsKey(table.getName())) + { + continue; + } + if(child instanceof QProcessMetaData process && !processes.containsKey(process.getName())) + { + continue; + } + if(child instanceof QReportMetaData report && !reports.containsKey(report.getName())) + { + continue; + } + if(child instanceof QAppMetaData childApp && !apps.containsKey(childApp.getName())) + { + // continue; + } + + frontendAppMetaData.addChild(new AppTreeNode(child)); } } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the app ended up having no children, then discard it // + // todo - i think this was wrong, because it didn't take into account ... something nested maybe... // + ////////////////////////////////////////////////////////////////////////////////////////////////////// + if(CollectionUtils.nullSafeIsEmpty(frontendAppMetaData.getChildren()) && CollectionUtils.nullSafeIsEmpty(frontendAppMetaData.getWidgets())) + { + // LOG.debug("Discarding empty app", logPair("name", frontendAppMetaData.getName())); + // continue; + } + + apps.put(appName, frontendAppMetaData); + treeNodes.put(appName, new AppTreeNode(app)); } metaDataOutput.setApps(apps); @@ -228,6 +303,33 @@ public class MetaDataAction + /*************************************************************************** + ** + ***************************************************************************/ + private MetaDataFilterInterface getMetaDataFilter() + { + return metaDataFilterMemoization.getResult(QContext.getQInstance(), i -> + { + MetaDataFilterInterface filter = null; + QCodeReference metaDataFilterReference = QContext.getQInstance().getMetaDataFilter(); + if(metaDataFilterReference != null) + { + filter = QCodeLoader.getAdHoc(MetaDataFilterInterface.class, metaDataFilterReference); + LOG.debug("Using new meta-data filter of type: " + filter.getClass().getSimpleName()); + } + + if(filter == null) + { + filter = new AllowAllMetaDataFilter(); + LOG.debug("Using new default (allow-all) meta-data filter"); + } + + return (filter); + }).orElseThrow(() -> new QRuntimeException("Error getting metaDataFilter")); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataFilterInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataFilterInterface.java new file mode 100644 index 00000000..a7abb74d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataFilterInterface.java @@ -0,0 +1,64 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.metadata; + + +import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface MetaDataFilterInterface +{ + + /*************************************************************************** + ** + ***************************************************************************/ + boolean allowTable(MetaDataInput input, QTableMetaData table); + + /*************************************************************************** + ** + ***************************************************************************/ + boolean allowProcess(MetaDataInput input, QProcessMetaData process); + + /*************************************************************************** + ** + ***************************************************************************/ + boolean allowReport(MetaDataInput input, QReportMetaData report); + + /*************************************************************************** + ** + ***************************************************************************/ + boolean allowApp(MetaDataInput input, QAppMetaData app); + + /*************************************************************************** + ** + ***************************************************************************/ + boolean allowWidget(MetaDataInput input, QWidgetMetaDataInterface widget); + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java index 6f97ec1a..119df559 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java @@ -40,6 +40,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; 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.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceLambda; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; @@ -257,11 +258,20 @@ public class RunBackendStepAction { runBackendStepOutput.seedFromRequest(runBackendStepInput); - Class codeClass = Class.forName(code.getName()); - Object codeObject = codeClass.getConstructor().newInstance(); + Object codeObject; + if(code instanceof QCodeReferenceLambda qCodeReferenceLambda) + { + codeObject = qCodeReferenceLambda.getLambda(); + } + else + { + Class codeClass = Class.forName(code.getName()); + codeObject = codeClass.getConstructor().newInstance(); + } + if(!(codeObject instanceof BackendStep backendStepCodeObject)) { - throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of BackendStep")); + throw (new QException("The supplied codeReference [" + code + "] is not a reference to a BackendStep")); } backendStepCodeObject.run(runBackendStepInput, runBackendStepOutput); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java index ada00f9a..481201e3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java @@ -28,6 +28,7 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.UUID; import com.kingsrook.qqq.backend.core.actions.ActionHelper; @@ -58,6 +59,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaD 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.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QStateMachineStep; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration; @@ -134,90 +136,11 @@ public class RunProcessAction try { - String lastStepName = runProcessInput.getStartAfterStep(); - - STEP_LOOP: - while(true) + switch(Objects.requireNonNull(process.getStepFlow(), "Process [" + process.getName() + "] has a null stepFlow.")) { - /////////////////////////////////////////////////////////////////////////////////////////////////////// - // always refresh the step list - as any step that runs can modify it (in the process state). // - // this is why we don't do a loop over the step list - as we'd get ConcurrentModificationExceptions. // - /////////////////////////////////////////////////////////////////////////////////////////////////////// - List stepList = getAvailableStepList(processState, process, lastStepName); - if(stepList.isEmpty()) - { - break; - } - - QStepMetaData step = stepList.get(0); - lastStepName = step.getName(); - - if(step instanceof QFrontendStepMetaData frontendStep) - { - //////////////////////////////////////////////////////////////// - // Handle what to do with frontend steps, per request setting // - //////////////////////////////////////////////////////////////// - switch(runProcessInput.getFrontendStepBehavior()) - { - case BREAK -> - { - LOG.trace("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName()); - processFrontendStepFieldDefaultValues(processState, frontendStep); - processFrontendComponents(processState, frontendStep); - processState.setNextStepName(step.getName()); - break STEP_LOOP; - } - case SKIP -> - { - LOG.trace("Skipping frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)"); - - ////////////////////////////////////////////////////////////////////// - // much less error prone in case this code changes in the future... // - ////////////////////////////////////////////////////////////////////// - // noinspection UnnecessaryContinue - continue; - } - case FAIL -> - { - LOG.trace("Throwing error for frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)"); - throw (new QException("Failing process at step " + step.getName() + " (as requested, to fail on frontend steps)")); - } - default -> throw new IllegalStateException("Unexpected value: " + runProcessInput.getFrontendStepBehavior()); - } - } - else if(step instanceof QBackendStepMetaData backendStepMetaData) - { - /////////////////////// - // Run backend steps // - /////////////////////// - LOG.debug("Running backend step [" + step.getName() + "] in process [" + process.getName() + "]"); - RunBackendStepOutput runBackendStepOutput = runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState); - - ///////////////////////////////////////////////////////////////////////////////////////// - // if the step returned an override lastStepName, use that to determine how we proceed // - ///////////////////////////////////////////////////////////////////////////////////////// - if(runBackendStepOutput.getOverrideLastStepName() != null) - { - LOG.debug("Process step [" + lastStepName + "] returned an overrideLastStepName [" + runBackendStepOutput.getOverrideLastStepName() + "]!"); - lastStepName = runBackendStepOutput.getOverrideLastStepName(); - } - - ///////////////////////////////////////////////////////////////////////////////////////////// - // similarly, if the step produced an updatedFrontendStepList, propagate that data outward // - ///////////////////////////////////////////////////////////////////////////////////////////// - if(runBackendStepOutput.getUpdatedFrontendStepList() != null) - { - LOG.debug("Process step [" + lastStepName + "] generated an updatedFrontendStepList [" + runBackendStepOutput.getUpdatedFrontendStepList().stream().map(s -> s.getName()).toList() + "]!"); - runProcessOutput.setUpdatedFrontendStepList(runBackendStepOutput.getUpdatedFrontendStepList()); - } - } - else - { - ////////////////////////////////////////////////// - // in case we have a different step type, throw // - ////////////////////////////////////////////////// - throw (new QException("Unsure how to run a step of type: " + step.getClass().getName())); - } + case LINEAR -> runLinearStepLoop(process, processState, stateKey, runProcessInput, runProcessOutput); + case STATE_MACHINE -> runStateMachineStep(runProcessInput.getStartAfterStep(), process, processState, stateKey, runProcessInput, runProcessOutput, 0); + default -> throw (new QException("Unhandled process step flow: " + process.getStepFlow())); } /////////////////////////////////////////////////////////////////////////// @@ -259,6 +182,270 @@ public class RunProcessAction + /*************************************************************************** + ** + ***************************************************************************/ + private void runLinearStepLoop(QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws Exception + { + String lastStepName = runProcessInput.getStartAfterStep(); + + while(true) + { + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // always refresh the step list - as any step that runs can modify it (in the process state). // + // this is why we don't do a loop over the step list - as we'd get ConcurrentModificationExceptions. // + /////////////////////////////////////////////////////////////////////////////////////////////////////// + List stepList = getAvailableStepList(processState, process, lastStepName); + if(stepList.isEmpty()) + { + break; + } + + QStepMetaData step = stepList.get(0); + lastStepName = step.getName(); + + if(step instanceof QFrontendStepMetaData frontendStep) + { + LoopTodo loopTodo = prepareForFrontendStep(runProcessInput, process, frontendStep, processState); + if(loopTodo == LoopTodo.BREAK) + { + break; + } + } + else if(step instanceof QBackendStepMetaData backendStepMetaData) + { + RunBackendStepOutput runBackendStepOutput = runBackendStep(process, processState, stateKey, runProcessInput, runProcessOutput, backendStepMetaData, step); + + ///////////////////////////////////////////////////////////////////////////////////////// + // if the step returned an override lastStepName, use that to determine how we proceed // + ///////////////////////////////////////////////////////////////////////////////////////// + if(runBackendStepOutput.getOverrideLastStepName() != null) + { + LOG.debug("Process step [" + lastStepName + "] returned an overrideLastStepName [" + runBackendStepOutput.getOverrideLastStepName() + "]!"); + lastStepName = runBackendStepOutput.getOverrideLastStepName(); + } + } + else + { + ////////////////////////////////////////////////// + // in case we have a different step type, throw // + ////////////////////////////////////////////////// + throw (new QException("Unsure how to run a step of type: " + step.getClass().getName())); + } + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private enum LoopTodo + { + BREAK, + CONTINUE + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private LoopTodo prepareForFrontendStep(RunProcessInput runProcessInput, QProcessMetaData process, QFrontendStepMetaData step, ProcessState processState) throws QException + { + //////////////////////////////////////////////////////////////// + // Handle what to do with frontend steps, per request setting // + //////////////////////////////////////////////////////////////// + switch(runProcessInput.getFrontendStepBehavior()) + { + case BREAK -> + { + LOG.trace("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName()); + processFrontendStepFieldDefaultValues(processState, step); + processFrontendComponents(processState, step); + processState.setNextStepName(step.getName()); + return LoopTodo.BREAK; + } + case SKIP -> + { + LOG.trace("Skipping frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)"); + return LoopTodo.CONTINUE; + } + case FAIL -> + { + LOG.trace("Throwing error for frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)"); + throw (new QException("Failing process at step " + step.getName() + " (as requested, to fail on frontend steps)")); + } + default -> throw new IllegalStateException("Unexpected value: " + runProcessInput.getFrontendStepBehavior()); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void runStateMachineStep(String lastStepName, QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, int stackDepth) throws Exception + { + ////////////////////////////// + // check for stack-overflow // + ////////////////////////////// + Integer maxStateMachineProcessStepFlowStackDepth = Objects.requireNonNullElse(runProcessInput.getValueInteger("maxStateMachineProcessStepFlowStackDepth"), 20); + if(stackDepth > maxStateMachineProcessStepFlowStackDepth) + { + throw (new QException("StateMachine process recurred too many times (exceeded maxStateMachineProcessStepFlowStackDepth of " + maxStateMachineProcessStepFlowStackDepth + ")")); + } + + ////////////////////////////////// + // figure out what step to run: // + ////////////////////////////////// + QStepMetaData step = null; + if(!StringUtils.hasContent(lastStepName)) + { + //////////////////////////////////////////////////////////////////// + // if no lastStepName is given, start at the process's first step // + //////////////////////////////////////////////////////////////////// + if(CollectionUtils.nullSafeIsEmpty(process.getStepList())) + { + throw (new QException("Process [" + process.getName() + "] does not have a step list defined.")); + } + step = process.getStepList().get(0); + } + else + { + ///////////////////////////////////// + // else run the given lastStepName // + ///////////////////////////////////// + processState.clearNextStepName(); + step = process.getStep(lastStepName); + if(step == null) + { + throw (new QException("Could not find step by name [" + lastStepName + "]")); + } + } + + ///////////////////////////////////////////////////////////////////////// + // for the flow of: // + // we were on a frontend step (as a sub-step of a state machine step), // + // and now we're here to run that state-step's backend step - // + // find the state-machine step containing this frontend step. // + ///////////////////////////////////////////////////////////////////////// + String skipSubStepsUntil = null; + if(step instanceof QFrontendStepMetaData frontendStepMetaData) + { + QStateMachineStep stateMachineStep = getStateMachineStepContainingSubStep(process, frontendStepMetaData.getName()); + if(stateMachineStep == null) + { + throw (new QException("Could not find stateMachineStep that contains last-frontend step: " + frontendStepMetaData.getName())); + } + step = stateMachineStep; + + ////////////////////////////////////////////////////////////////////////////////// + // set this flag, to know to skip this frontend step in the sub-step loop below // + ////////////////////////////////////////////////////////////////////////////////// + skipSubStepsUntil = frontendStepMetaData.getName(); + } + + if(!(step instanceof QStateMachineStep stateMachineStep)) + { + throw (new QException("Have a non-stateMachineStep in a process using stateMachine flow... " + step.getClass().getName())); + } + + /////////////////////// + // run the sub-steps // + /////////////////////// + boolean ranAnySubSteps = false; + for(QStepMetaData subStep : stateMachineStep.getSubSteps()) + { + /////////////////////////////////////////////////////////////////////////////////////////////// + // ok, well, skip them if this flag is set (and clear the flag once we've hit this sub-step) // + /////////////////////////////////////////////////////////////////////////////////////////////// + if(skipSubStepsUntil != null) + { + if(skipSubStepsUntil.equals(subStep.getName())) + { + skipSubStepsUntil = null; + } + continue; + } + + ranAnySubSteps = true; + if(subStep instanceof QFrontendStepMetaData frontendStep) + { + LoopTodo loopTodo = prepareForFrontendStep(runProcessInput, process, frontendStep, processState); + if(loopTodo == LoopTodo.BREAK) + { + return; + } + } + else if(subStep instanceof QBackendStepMetaData backendStepMetaData) + { + RunBackendStepOutput runBackendStepOutput = runBackendStep(process, processState, stateKey, runProcessInput, runProcessOutput, backendStepMetaData, step); + Optional nextStepName = runBackendStepOutput.getProcessState().getNextStepName(); + + if(nextStepName.isEmpty() && StringUtils.hasContent(stateMachineStep.getDefaultNextStepName())) + { + nextStepName = Optional.of(stateMachineStep.getDefaultNextStepName()); + } + + if(nextStepName.isPresent()) + { + ////////////////////////////////////////////////////////////////////////////////////////////////////// + // if we've been given a next-step-name, go to that step now. // + // it might be a backend-only stateMachineStep, in which case, we should run that backend step now. // + // or it might be a frontend-then-backend step, in which case, we want to go to that frontend step. // + // if we weren't given a next-step-name, then we should stay in the same state - either to finish // + // its sub-steps, or, to fall out of the loop and end the process. // + ////////////////////////////////////////////////////////////////////////////////////////////////////// + processState.clearNextStepName(); + runStateMachineStep(nextStepName.get(), process, processState, stateKey, runProcessInput, runProcessOutput, stackDepth + 1); + return; + } + } + else + { + ////////////////////////////////////////////////// + // in case we have a different step type, throw // + ////////////////////////////////////////////////// + throw (new QException("Unsure how to run a step of type: " + step.getClass().getName())); + } + } + + if(!ranAnySubSteps) + { + if(StringUtils.hasContent(stateMachineStep.getDefaultNextStepName())) + { + runStateMachineStep(stateMachineStep.getDefaultNextStepName(), process, processState, stateKey, runProcessInput, runProcessOutput, stackDepth + 1); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QStateMachineStep getStateMachineStepContainingSubStep(QProcessMetaData process, String stepName) + { + for(QStepMetaData step : process.getAllSteps().values()) + { + if(step instanceof QStateMachineStep stateMachineStep) + { + for(QStepMetaData subStep : stateMachineStep.getSubSteps()) + { + if(subStep.getName().equals(stepName)) + { + return (stateMachineStep); + } + } + } + } + + return (null); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -336,12 +523,12 @@ public class RunProcessAction /////////////////////////////////////////////////// runProcessInput.seedFromProcessState(optionalProcessState.get()); - /////////////////////////////////////////////////////////////////////////////////////////////////// - // if we're restoring an old state, we can discard a previously stored updatedFrontendStepList - // - // it is only needed on the transitional edge from a backend-step to a frontend step, but not // - // in the other directly // - /////////////////////////////////////////////////////////////////////////////////////////////////// - optionalProcessState.get().setUpdatedFrontendStepList(null); + ///////////////////////////////////////////////////////////////////////////////////////////////////// + // if we're restoring an old state, we can discard a previously stored processMetaDataAdjustment - // + // it is only needed on the transitional edge from a backend-step to a frontend step, but not // + // in the other directly // + ///////////////////////////////////////////////////////////////////////////////////////////////////// + optionalProcessState.get().setProcessMetaDataAdjustment(null); /////////////////////////////////////////////////////////////////////////// // if there were values from the caller, put those (back) in the request // @@ -356,16 +543,40 @@ public class RunProcessAction } ProcessState processState = optionalProcessState.get(); - processState.clearNextStepName(); return processState; } + /*************************************************************************** + ** + ***************************************************************************/ + private RunBackendStepOutput runBackendStep(QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, QBackendStepMetaData backendStepMetaData, QStepMetaData step) throws Exception + { + /////////////////////// + // Run backend steps // + /////////////////////// + LOG.debug("Running backend step [" + step.getName() + "] in process [" + process.getName() + "]"); + RunBackendStepOutput runBackendStepOutput = runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState); + + ////////////////////////////////////////////////////////////////////////////////////////////// + // similarly, if the step produced a processMetaDataAdjustment, propagate that data outward // + ////////////////////////////////////////////////////////////////////////////////////////////// + if(runBackendStepOutput.getProcessMetaDataAdjustment() != null) + { + LOG.debug("Process step [" + step.getName() + "] generated a ProcessMetaDataAdjustment [" + runBackendStepOutput.getProcessMetaDataAdjustment() + "]!"); + runProcessOutput.setProcessMetaDataAdjustment(runBackendStepOutput.getProcessMetaDataAdjustment()); + } + + return runBackendStepOutput; + } + + + /******************************************************************************* ** Run a single backend step. *******************************************************************************/ - protected RunBackendStepOutput runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception + RunBackendStepOutput runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception { RunBackendStepInput runBackendStepInput = new RunBackendStepInput(processState); runBackendStepInput.setProcessName(process.getName()); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java index 0758dc44..1e824b59 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java @@ -28,7 +28,6 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -468,7 +467,8 @@ public class QValueFormatter { for(QFieldMetaData field : table.getFields().values()) { - if(field.getType().equals(QFieldType.BLOB)) + Optional fileDownloadAdornment = field.getAdornment(AdornmentType.FILE_DOWNLOAD); + if(fileDownloadAdornment.isPresent()) { ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // file name comes from: // @@ -478,20 +478,7 @@ public class QValueFormatter // - tableLabel primaryKey fieldLabel // // - and - if the FILE_DOWNLOAD adornment had a DEFAULT_EXTENSION, then it gets added (preceded by a dot) // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - Optional fileDownloadAdornment = field.getAdornment(AdornmentType.FILE_DOWNLOAD); - Map adornmentValues = Collections.emptyMap(); - - if(fileDownloadAdornment.isPresent()) - { - adornmentValues = fileDownloadAdornment.get().getValues(); - } - else - { - /////////////////////////////////////////////////////// - // don't change blobs unless they are file-downloads // - /////////////////////////////////////////////////////// - continue; - } + Map adornmentValues = fileDownloadAdornment.get().getValues(); String fileNameField = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FIELD)); String fileNameFormat = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT)); @@ -542,7 +529,13 @@ public class QValueFormatter } } - record.setValue(field.getName(), "/data/" + table.getName() + "/" + primaryKey + "/" + field.getName() + "/" + fileName); + ///////////////////////////////////////////// + // if field type is blob, update its value // + ///////////////////////////////////////////// + if(QFieldType.BLOB.equals(field.getType())) + { + record.setValue(field.getName(), "/data/" + table.getName() + "/" + primaryKey + "/" + field.getName() + "/" + fileName); + } record.setDisplayValue(field.getName(), fileName); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplication.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplication.java new file mode 100644 index 00000000..9b7fd619 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplication.java @@ -0,0 +1,55 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; + + +/******************************************************************************* + ** Version of AbstractQQQApplication that assumes all meta-data is produced + ** by MetaDataProducers in a single package. + *******************************************************************************/ +public abstract class AbstractMetaDataProducerBasedQQQApplication extends AbstractQQQApplication +{ + + /*************************************************************************** + ** + ***************************************************************************/ + public abstract String getMetaDataPackageName(); + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QInstance defineQInstance() throws QException + { + QInstance qInstance = new QInstance(); + MetaDataProducerHelper.processAllMetaDataProducersInPackage(qInstance, getMetaDataPackageName()); + return (qInstance); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractQQQApplication.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractQQQApplication.java new file mode 100644 index 00000000..03bd0b61 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractQQQApplication.java @@ -0,0 +1,78 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances; + + +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; +import com.kingsrook.qqq.backend.core.instances.validation.plugins.QInstanceValidatorPluginInterface; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** Base class to provide the definition of a QQQ-based application. + ** + ** Essentially, just how to define its meta-data - in the form of a QInstance. + ** + ** Also provides means to define the instance validation plugins to be used. + *******************************************************************************/ +public abstract class AbstractQQQApplication +{ + + /*************************************************************************** + ** + ***************************************************************************/ + public abstract QInstance defineQInstance() throws QException; + + + + /*************************************************************************** + ** + ***************************************************************************/ + public QInstance defineValidatedQInstance() throws QException, QInstanceValidationException + { + QInstance qInstance = defineQInstance(); + + QInstanceValidator.removeAllValidatorPlugins(); + for(QInstanceValidatorPluginInterface validatorPlugin : CollectionUtils.nonNullList(getValidatorPlugins())) + { + QInstanceValidator.addValidatorPlugin(validatorPlugin); + } + + QInstanceValidator qInstanceValidator = new QInstanceValidator(); + qInstanceValidator.validate(qInstance); + return (qInstance); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected List> getValidatorPlugins() + { + return new ArrayList<>(); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index 4e8d0237..7bf3c306 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -59,6 +59,7 @@ 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.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QStateMachineStep; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; @@ -415,10 +416,27 @@ public class QInstanceEnricher ** *******************************************************************************/ private void enrichStep(QStepMetaData step) + { + enrichStep(step, false); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void enrichStep(QStepMetaData step, boolean isSubStep) { if(!StringUtils.hasContent(step.getLabel())) { - step.setLabel(nameToLabel(step.getName())); + if(isSubStep && (step.getName().endsWith(".backend") || step.getName().endsWith(".frontend"))) + { + step.setLabel(nameToLabel(step.getName().replaceFirst("\\.(backend|frontend)", ""))); + } + else + { + step.setLabel(nameToLabel(step.getName())); + } } step.getInputFields().forEach(this::enrichField); @@ -439,6 +457,13 @@ public class QInstanceEnricher frontendStepMetaData.getRecordListFields().forEach(this::enrichField); } } + else if(step instanceof QStateMachineStep stateMachineStep) + { + for(QStepMetaData subStep : CollectionUtils.nonNullList(stateMachineStep.getSubSteps())) + { + enrichStep(subStep, true); + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 41f7ff2b..58618c3e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -43,6 +43,7 @@ import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer; import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph; +import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataFilterInterface; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportCustomRecordSourceInterface; import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface; @@ -74,6 +75,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; @@ -185,6 +187,7 @@ public class QInstanceValidator ////////////////////////////////////////////////////////////////////////// try { + validateInstanceAttributes(qInstance); validateBackends(qInstance); validateAuthentication(qInstance); validateAutomationProviders(qInstance); @@ -225,6 +228,19 @@ public class QInstanceValidator + /*************************************************************************** + ** + ***************************************************************************/ + private void validateInstanceAttributes(QInstance qInstance) + { + if(qInstance.getMetaDataFilter() != null) + { + validateSimpleCodeReference("Instance metaDataFilter ", qInstance.getMetaDataFilter(), MetaDataFilterInterface.class); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -928,13 +944,8 @@ public class QInstanceValidator assertCondition(Objects.equals(fieldName, field.getName()), "Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + "."); - if(field.getPossibleValueSourceName() != null) - { - assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null, - "Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + "."); - } - String prefix = "Field " + fieldName + " in table " + tableName + " "; + validateFieldPossibleValueSourceAttributes(qInstance, field, prefix); /////////////////////////////////////////////////// // validate things we know about field behaviors // @@ -1039,6 +1050,31 @@ public class QInstanceValidator + /*************************************************************************** + ** + ***************************************************************************/ + private void validateFieldPossibleValueSourceAttributes(QInstance qInstance, QFieldMetaData field, String prefix) + { + if(field.getPossibleValueSourceName() != null) + { + assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null, + prefix + "has an unrecognized possibleValueSourceName " + field.getPossibleValueSourceName()); + + assertCondition(field.getInlinePossibleValueSource() == null, prefix.trim() + " has both a possibleValueSourceName and an inlinePossibleValueSource, which is not allowed."); + } + + if(field.getInlinePossibleValueSource() != null) + { + String name = "inlinePossibleValueSource for " + prefix.trim(); + if(assertCondition(QPossibleValueSourceType.ENUM.equals(field.getInlinePossibleValueSource().getType()), name + " must have a type of ENUM.")) + { + validatePossibleValueSource(qInstance, name, field.getInlinePossibleValueSource()); + } + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -1546,6 +1582,16 @@ public class QInstanceValidator } } + for(QFieldMetaData field : process.getInputFields()) + { + validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", input field " + field.getName()); + } + + for(QFieldMetaData field : process.getOutputFields()) + { + validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", output field " + field.getName()); + } + if(process.getCancelStep() != null) { if(assertCondition(process.getCancelStep().getCode() != null, "Cancel step is missing a code reference, in process " + processName)) @@ -1948,78 +1994,88 @@ public class QInstanceValidator qInstance.getPossibleValueSources().forEach((pvsName, possibleValueSource) -> { assertCondition(Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + "."); - if(assertCondition(possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName)) + validatePossibleValueSource(qInstance, pvsName, possibleValueSource); + }); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void validatePossibleValueSource(QInstance qInstance, String name, QPossibleValueSource possibleValueSource) + { + if(assertCondition(possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + name)) + { + //////////////////////////////////////////////////////////////////////////////////////////////// + // assert about fields that should and should not be set, based on possible value source type // + // do additional type-specific validations as well // + //////////////////////////////////////////////////////////////////////////////////////////////// + switch(possibleValueSource.getType()) + { + case ENUM -> { - //////////////////////////////////////////////////////////////////////////////////////////////// - // assert about fields that should and should not be set, based on possible value source type // - // do additional type-specific validations as well // - //////////////////////////////////////////////////////////////////////////////////////////////// - switch(possibleValueSource.getType()) + assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + name + " should not have a tableName."); + assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "enum-type possibleValueSource " + name + " should not have searchFields."); + assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "enum-type possibleValueSource " + name + " should not have orderByFields."); + assertCondition(possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + name + " should not have a customCodeReference."); + + assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + name + " is missing enum values"); + } + case TABLE -> + { + assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + name + " should not have enum values."); + assertCondition(possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + name + " should not have a customCodeReference."); + + QTableMetaData tableMetaData = null; + if(assertCondition(StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + name + " is missing a tableName.")) { - case ENUM -> - { - assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + pvsName + " should not have a tableName."); - assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "enum-type possibleValueSource " + pvsName + " should not have searchFields."); - assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "enum-type possibleValueSource " + pvsName + " should not have orderByFields."); - assertCondition(possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + pvsName + " should not have a customCodeReference."); - - assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + pvsName + " is missing enum values"); - } - case TABLE -> - { - assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + pvsName + " should not have enum values."); - assertCondition(possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + pvsName + " should not have a customCodeReference."); - - QTableMetaData tableMetaData = null; - if(assertCondition(StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + pvsName + " is missing a tableName.")) - { - tableMetaData = qInstance.getTable(possibleValueSource.getTableName()); - assertCondition(tableMetaData != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + pvsName + "."); - } - - if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "table-type possibleValueSource " + pvsName + " is missing searchFields.")) - { - if(tableMetaData != null) - { - QTableMetaData finalTableMetaData = tableMetaData; - for(String searchField : possibleValueSource.getSearchFields()) - { - assertNoException(() -> finalTableMetaData.getField(searchField), "possibleValueSource " + pvsName + " has an unrecognized searchField: " + searchField); - } - } - } - - if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "table-type possibleValueSource " + pvsName + " is missing orderByFields.")) - { - if(tableMetaData != null) - { - QTableMetaData finalTableMetaData = tableMetaData; - - for(QFilterOrderBy orderByField : possibleValueSource.getOrderByFields()) - { - assertNoException(() -> finalTableMetaData.getField(orderByField.getFieldName()), "possibleValueSource " + pvsName + " has an unrecognized orderByField: " + orderByField.getFieldName()); - } - } - } - } - case CUSTOM -> - { - assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + pvsName + " should not have enum values."); - assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + pvsName + " should not have a tableName."); - assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "custom-type possibleValueSource " + pvsName + " should not have searchFields."); - assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "custom-type possibleValueSource " + pvsName + " should not have orderByFields."); - - if(assertCondition(possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference.")) - { - validateSimpleCodeReference("PossibleValueSource " + pvsName + " custom code reference: ", possibleValueSource.getCustomCodeReference(), QCustomPossibleValueProvider.class); - } - } - default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType()); + tableMetaData = qInstance.getTable(possibleValueSource.getTableName()); + assertCondition(tableMetaData != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + name + "."); } - runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance); + if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "table-type possibleValueSource " + name + " is missing searchFields.")) + { + if(tableMetaData != null) + { + QTableMetaData finalTableMetaData = tableMetaData; + for(String searchField : possibleValueSource.getSearchFields()) + { + assertNoException(() -> finalTableMetaData.getField(searchField), "possibleValueSource " + name + " has an unrecognized searchField: " + searchField); + } + } + } + + if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "table-type possibleValueSource " + name + " is missing orderByFields.")) + { + if(tableMetaData != null) + { + QTableMetaData finalTableMetaData = tableMetaData; + + for(QFilterOrderBy orderByField : possibleValueSource.getOrderByFields()) + { + assertNoException(() -> finalTableMetaData.getField(orderByField.getFieldName()), "possibleValueSource " + name + " has an unrecognized orderByField: " + orderByField.getFieldName()); + } + } + } } - }); + case CUSTOM -> + { + assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + name + " should not have enum values."); + assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + name + " should not have a tableName."); + assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "custom-type possibleValueSource " + name + " should not have searchFields."); + assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "custom-type possibleValueSource " + name + " should not have orderByFields."); + + if(assertCondition(possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + name + " is missing a customCodeReference.")) + { + validateSimpleCodeReference("PossibleValueSource " + name + " custom code reference: ", possibleValueSource.getCustomCodeReference(), QCustomPossibleValueProvider.class); + } + } + default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType()); + } + + runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataInput.java index f450493d..1ac0626b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataInput.java @@ -31,6 +31,16 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; *******************************************************************************/ public class MetaDataInput extends AbstractActionInput { + private String frontendName; + private String frontendVersion; + + private String middlewareName; + private String middlewareVersion; + + private String applicationName; + private String applicationVersion; + + /******************************************************************************* ** @@ -39,4 +49,190 @@ public class MetaDataInput extends AbstractActionInput { } + + + /******************************************************************************* + ** Getter for frontendName + *******************************************************************************/ + public String getFrontendName() + { + return (this.frontendName); + } + + + + /******************************************************************************* + ** Setter for frontendName + *******************************************************************************/ + public void setFrontendName(String frontendName) + { + this.frontendName = frontendName; + } + + + + /******************************************************************************* + ** Fluent setter for frontendName + *******************************************************************************/ + public MetaDataInput withFrontendName(String frontendName) + { + this.frontendName = frontendName; + return (this); + } + + + + /******************************************************************************* + ** Getter for frontendVersion + *******************************************************************************/ + public String getFrontendVersion() + { + return (this.frontendVersion); + } + + + + /******************************************************************************* + ** Setter for frontendVersion + *******************************************************************************/ + public void setFrontendVersion(String frontendVersion) + { + this.frontendVersion = frontendVersion; + } + + + + /******************************************************************************* + ** Fluent setter for frontendVersion + *******************************************************************************/ + public MetaDataInput withFrontendVersion(String frontendVersion) + { + this.frontendVersion = frontendVersion; + return (this); + } + + + + /******************************************************************************* + ** Getter for middlewareName + *******************************************************************************/ + public String getMiddlewareName() + { + return (this.middlewareName); + } + + + + /******************************************************************************* + ** Setter for middlewareName + *******************************************************************************/ + public void setMiddlewareName(String middlewareName) + { + this.middlewareName = middlewareName; + } + + + + /******************************************************************************* + ** Fluent setter for middlewareName + *******************************************************************************/ + public MetaDataInput withMiddlewareName(String middlewareName) + { + this.middlewareName = middlewareName; + return (this); + } + + + + /******************************************************************************* + ** Getter for middlewareVersion + *******************************************************************************/ + public String getMiddlewareVersion() + { + return (this.middlewareVersion); + } + + + + /******************************************************************************* + ** Setter for middlewareVersion + *******************************************************************************/ + public void setMiddlewareVersion(String middlewareVersion) + { + this.middlewareVersion = middlewareVersion; + } + + + + /******************************************************************************* + ** Fluent setter for middlewareVersion + *******************************************************************************/ + public MetaDataInput withMiddlewareVersion(String middlewareVersion) + { + this.middlewareVersion = middlewareVersion; + return (this); + } + + + + /******************************************************************************* + ** Getter for applicationName + *******************************************************************************/ + public String getApplicationName() + { + return (this.applicationName); + } + + + + /******************************************************************************* + ** Setter for applicationName + *******************************************************************************/ + public void setApplicationName(String applicationName) + { + this.applicationName = applicationName; + } + + + + /******************************************************************************* + ** Fluent setter for applicationName + *******************************************************************************/ + public MetaDataInput withApplicationName(String applicationName) + { + this.applicationName = applicationName; + return (this); + } + + + + /******************************************************************************* + ** Getter for applicationVersion + *******************************************************************************/ + public String getApplicationVersion() + { + return (this.applicationVersion); + } + + + + /******************************************************************************* + ** Setter for applicationVersion + *******************************************************************************/ + public void setApplicationVersion(String applicationVersion) + { + this.applicationVersion = applicationVersion; + } + + + + /******************************************************************************* + ** Fluent setter for applicationVersion + *******************************************************************************/ + public MetaDataInput withApplicationVersion(String applicationVersion) + { + this.applicationVersion = applicationVersion; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessMetaDataAdjustment.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessMetaDataAdjustment.java new file mode 100644 index 00000000..ab8c30b5 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessMetaDataAdjustment.java @@ -0,0 +1,139 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.actions.processes; + + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** Object that stores adjustments that a process wants to make, at run-time, + ** to its meta-data. + ** + ** e.g., changing the steps; updating fields (e.g., changing an inline PVS, + ** or an isRequired attribute) + *******************************************************************************/ +public class ProcessMetaDataAdjustment +{ + private static final QLogger LOG = QLogger.getLogger(ProcessMetaDataAdjustment.class); + + private List updatedFrontendStepList = null; + private Map updatedFields = null; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public ProcessMetaDataAdjustment withUpdatedField(QFieldMetaData field) + { + if(updatedFields == null) + { + updatedFields = new LinkedHashMap<>(); + } + + if(!StringUtils.hasContent(field.getName())) + { + LOG.warn("Missing name on field in withUpdatedField - no update will happen."); + } + else + { + if(updatedFields.containsKey(field.getName())) + { + LOG.info("UpdatedFields map already contained a field with this name - overwriting it.", logPair("fieldName", field.getName())); + } + + updatedFields.put(field.getName(), field); + } + return (this); + } + + + + /******************************************************************************* + ** Getter for updatedFrontendStepList + *******************************************************************************/ + public List getUpdatedFrontendStepList() + { + return (this.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-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java index c418bd07..ad7a0827 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java @@ -29,7 +29,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; /******************************************************************************* @@ -42,10 +41,7 @@ public class ProcessState implements Serializable private List stepList = new ArrayList<>(); private Optional nextStepName = Optional.empty(); - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // maybe, remove this altogether - just let the frontend compute & send if needed... but how does it know last version...? // - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private List updatedFrontendStepList = null; + private ProcessMetaDataAdjustment processMetaDataAdjustment = null; @@ -148,33 +144,36 @@ public class ProcessState implements Serializable + + /******************************************************************************* - ** Getter for updatedFrontendStepList + ** Getter for processMetaDataAdjustment *******************************************************************************/ - public List getUpdatedFrontendStepList() + public ProcessMetaDataAdjustment getProcessMetaDataAdjustment() { - return (this.updatedFrontendStepList); + return (this.processMetaDataAdjustment); } /******************************************************************************* - ** Setter for updatedFrontendStepList + ** Setter for processMetaDataAdjustment *******************************************************************************/ - public void setUpdatedFrontendStepList(List updatedFrontendStepList) + public void setProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment) { - this.updatedFrontendStepList = updatedFrontendStepList; + this.processMetaDataAdjustment = processMetaDataAdjustment; } /******************************************************************************* - ** Fluent setter for updatedFrontendStepList + ** Fluent setter for processMetaDataAdjustment *******************************************************************************/ - public ProcessState withUpdatedFrontendStepList(List updatedFrontendStepList) + public ProcessState withProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment) { - this.updatedFrontendStepList = updatedFrontendStepList; + this.processMetaDataAdjustment = processMetaDataAdjustment; return (this); } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java index 4754fcaa..7c89b98a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java @@ -374,7 +374,13 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial .map(step -> (QFrontendStepMetaData) step) .toList()); - setUpdatedFrontendStepList(updatedFrontendStepList); + ProcessMetaDataAdjustment processMetaDataAdjustment = getProcessMetaDataAdjustment(); + if(processMetaDataAdjustment == null) + { + processMetaDataAdjustment = new ProcessMetaDataAdjustment(); + } + processMetaDataAdjustment.setUpdatedFrontendStepList(updatedFrontendStepList); + setProcessMetaDataAdjustment(processMetaDataAdjustment); } @@ -411,21 +417,21 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial /******************************************************************************* - ** Getter for updatedFrontendStepList + ** Getter for ProcessMetaDataAdjustment (pass-through to processState) *******************************************************************************/ - public List getUpdatedFrontendStepList() + public ProcessMetaDataAdjustment getProcessMetaDataAdjustment() { - return (this.processState.getUpdatedFrontendStepList()); + return (this.processState.getProcessMetaDataAdjustment()); } /******************************************************************************* - ** Setter for updatedFrontendStepList + ** Setter for updatedFrontendStepList (pass-through to processState) *******************************************************************************/ - public void setUpdatedFrontendStepList(List updatedFrontendStepList) + public void setProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment) { - this.processState.setUpdatedFrontendStepList(updatedFrontendStepList); + this.processState.setProcessMetaDataAdjustment(processMetaDataAdjustment); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java index 30a5642a..f71a7137 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessOutput.java @@ -33,6 +33,7 @@ import java.util.Optional; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; +import com.kingsrook.qqq.backend.core.utils.ObjectUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -336,7 +337,12 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab *******************************************************************************/ public void setUpdatedFrontendStepList(List updatedFrontendStepList) { - this.processState.setUpdatedFrontendStepList(updatedFrontendStepList); + if(this.processState.getProcessMetaDataAdjustment() == null) + { + this.processState.setProcessMetaDataAdjustment(new ProcessMetaDataAdjustment()); + } + + this.processState.getProcessMetaDataAdjustment().setUpdatedFrontendStepList(updatedFrontendStepList); } @@ -346,7 +352,27 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab *******************************************************************************/ public List getUpdatedFrontendStepList() { - return this.processState.getUpdatedFrontendStepList(); + return ObjectUtils.tryElse(() -> this.processState.getProcessMetaDataAdjustment().getUpdatedFrontendStepList(), null); + } + + + + /******************************************************************************* + ** Getter for processMetaDataAdjustment + *******************************************************************************/ + public ProcessMetaDataAdjustment getProcessMetaDataAdjustment() + { + return (this.processState.getProcessMetaDataAdjustment()); + } + + + + /******************************************************************************* + ** Setter for processMetaDataAdjustment + *******************************************************************************/ + public void setProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment) + { + this.processState.setProcessMetaDataAdjustment(processMetaDataAdjustment); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/storage/StorageInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/storage/StorageInput.java index 19d84ce3..b0fe962e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/storage/StorageInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/storage/StorageInput.java @@ -32,6 +32,7 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; public class StorageInput extends AbstractTableActionInput implements Serializable { private String reference; + private String contentType; @@ -75,4 +76,35 @@ public class StorageInput extends AbstractTableActionInput implements Serializab return (this); } + + + /******************************************************************************* + ** Getter for contentType + *******************************************************************************/ + public String getContentType() + { + return (this.contentType); + } + + + + /******************************************************************************* + ** Setter for contentType + *******************************************************************************/ + public void setContentType(String contentType) + { + this.contentType = contentType; + } + + + + /******************************************************************************* + ** Fluent setter for contentType + *******************************************************************************/ + public StorageInput withContentType(String contentType) + { + this.contentType = contentType; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/AlertData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/AlertData.java index 5c6567fb..b5cf3538 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/AlertData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/AlertData.java @@ -22,6 +22,9 @@ package com.kingsrook.qqq.backend.core.model.dashboard.widgets; +import java.util.List; + + /******************************************************************************* ** Model containing datastructure expected by frontend alert widget ** @@ -40,8 +43,10 @@ public class AlertData extends QWidgetData - private String html; - private AlertType alertType; + private String html; + private AlertType alertType; + private Boolean hideWidget = false; + private List bulletList; @@ -139,4 +144,66 @@ public class AlertData extends QWidgetData return (this); } + + + /******************************************************************************* + ** Getter for hideWidget + *******************************************************************************/ + public boolean getHideWidget() + { + return (this.hideWidget); + } + + + + /******************************************************************************* + ** Setter for hideWidget + *******************************************************************************/ + public void setHideWidget(boolean hideWidget) + { + this.hideWidget = hideWidget; + } + + + + /******************************************************************************* + ** Fluent setter for hideWidget + *******************************************************************************/ + public AlertData withHideWidget(boolean hideWidget) + { + this.hideWidget = hideWidget; + return (this); + } + + + + /******************************************************************************* + ** Getter for bulletList + *******************************************************************************/ + public List getBulletList() + { + return (this.bulletList); + } + + + + /******************************************************************************* + ** Setter for bulletList + *******************************************************************************/ + public void setBulletList(List bulletList) + { + this.bulletList = bulletList; + } + + + + /******************************************************************************* + ** Fluent setter for bulletList + *******************************************************************************/ + public AlertData withBulletList(List bulletList) + { + this.bulletList = bulletList; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/ChildRecordListData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/ChildRecordListData.java index 1e8403ee..a6b3a6eb 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/ChildRecordListData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/ChildRecordListData.java @@ -39,9 +39,14 @@ public class ChildRecordListData extends QWidgetData private QueryOutput queryOutput; private QTableMetaData childTableMetaData; + private String tableName; private String tablePath; private String viewAllLink; private Integer totalRows; + private Boolean disableRowClick = false; + private Boolean allowRecordEdit = false; + private Boolean allowRecordDelete = false; + private Boolean isInProcess = false; private boolean canAddChildRecord = false; private Map defaultValuesForNewChildRecords; @@ -352,4 +357,173 @@ public class ChildRecordListData extends QWidgetData return (this); } + + + /******************************************************************************* + ** Getter for tableName + *******************************************************************************/ + public String getTableName() + { + return (this.tableName); + } + + + + /******************************************************************************* + ** Setter for tableName + *******************************************************************************/ + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + + + /******************************************************************************* + ** Fluent setter for tableName + *******************************************************************************/ + public ChildRecordListData withTableName(String tableName) + { + this.tableName = tableName; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for tablePath + *******************************************************************************/ + public ChildRecordListData withTablePath(String tablePath) + { + this.tablePath = tablePath; + return (this); + } + + + + /******************************************************************************* + ** Getter for disableRowClick + *******************************************************************************/ + public Boolean getDisableRowClick() + { + return (this.disableRowClick); + } + + + + /******************************************************************************* + ** Setter for disableRowClick + *******************************************************************************/ + public void setDisableRowClick(Boolean disableRowClick) + { + this.disableRowClick = disableRowClick; + } + + + + /******************************************************************************* + ** Fluent setter for disableRowClick + *******************************************************************************/ + public ChildRecordListData withDisableRowClick(Boolean disableRowClick) + { + this.disableRowClick = disableRowClick; + return (this); + } + + + + /******************************************************************************* + ** Getter for allowRecordEdit + *******************************************************************************/ + public Boolean getAllowRecordEdit() + { + return (this.allowRecordEdit); + } + + + + /******************************************************************************* + ** Setter for allowRecordEdit + *******************************************************************************/ + public void setAllowRecordEdit(Boolean allowRecordEdit) + { + this.allowRecordEdit = allowRecordEdit; + } + + + + /******************************************************************************* + ** Fluent setter for allowRecordEdit + *******************************************************************************/ + public ChildRecordListData withAllowRecordEdit(Boolean allowRecordEdit) + { + this.allowRecordEdit = allowRecordEdit; + return (this); + } + + + + /******************************************************************************* + ** Getter for allowRecordDelete + *******************************************************************************/ + public Boolean getAllowRecordDelete() + { + return (this.allowRecordDelete); + } + + + + /******************************************************************************* + ** Setter for allowRecordDelete + *******************************************************************************/ + public void setAllowRecordDelete(Boolean allowRecordDelete) + { + this.allowRecordDelete = allowRecordDelete; + } + + + + /******************************************************************************* + ** Fluent setter for allowRecordDelete + *******************************************************************************/ + public ChildRecordListData withAllowRecordDelete(Boolean allowRecordDelete) + { + this.allowRecordDelete = allowRecordDelete; + return (this); + } + + + + /******************************************************************************* + ** Getter for isInProcess + *******************************************************************************/ + public Boolean getIsInProcess() + { + return (this.isInProcess); + } + + + + /******************************************************************************* + ** Setter for isInProcess + *******************************************************************************/ + public void setIsInProcess(Boolean isInProcess) + { + this.isInProcess = isInProcess; + } + + + + /******************************************************************************* + ** Fluent setter for isInProcess + *******************************************************************************/ + public ChildRecordListData withIsInProcess(Boolean isInProcess) + { + this.isInProcess = isInProcess; + return (this); + } + } + + + diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/CompositeWidgetData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/CompositeWidgetData.java index 2cbae738..7a8756af 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/CompositeWidgetData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/CompositeWidgetData.java @@ -40,6 +40,20 @@ public class CompositeWidgetData extends AbstractBlockWidgetData> blocks = new ArrayList<>(); + private ModalMode modalMode; + + + + /*************************************************************************** + ** + ***************************************************************************/ + public enum ModalMode + { + MODAL + } + + + private Layout layout; private Map styleOverrides = new HashMap<>(); private String overlayHtml; @@ -52,12 +66,14 @@ public class CompositeWidgetData extends AbstractBlockWidgetData. + */ + +package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.audio; + + +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles; + + +/******************************************************************************* + ** block that plays an audio file + *******************************************************************************/ +public class AudioBlockData extends AbstractBlockWidgetData +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String getBlockTypeName() + { + return "AUDIO"; + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/audio/AudioValues.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/audio/AudioValues.java new file mode 100644 index 00000000..bffa3f03 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/audio/AudioValues.java @@ -0,0 +1,130 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.audio; + + +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class AudioValues implements BlockValuesInterface +{ + private String path; + private boolean showControls = false; + private boolean autoPlay = true; + + + + /******************************************************************************* + ** 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 AudioValues withPath(String path) + { + this.path = path; + return (this); + } + + + + /******************************************************************************* + ** Getter for showControls + *******************************************************************************/ + public boolean getShowControls() + { + return (this.showControls); + } + + + + /******************************************************************************* + ** Setter for showControls + *******************************************************************************/ + public void setShowControls(boolean showControls) + { + this.showControls = showControls; + } + + + + /******************************************************************************* + ** Fluent setter for showControls + *******************************************************************************/ + public AudioValues withShowControls(boolean showControls) + { + this.showControls = showControls; + return (this); + } + + + + /******************************************************************************* + ** Getter for autoPlay + *******************************************************************************/ + public boolean getAutoPlay() + { + return (this.autoPlay); + } + + + + /******************************************************************************* + ** Setter for autoPlay + *******************************************************************************/ + public void setAutoPlay(boolean autoPlay) + { + this.autoPlay = autoPlay; + } + + + + /******************************************************************************* + ** Fluent setter for autoPlay + *******************************************************************************/ + public AudioValues withAutoPlay(boolean autoPlay) + { + this.autoPlay = autoPlay; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/base/BaseStyles.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/base/BaseStyles.java index 1aab7064..c4ed7803 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/base/BaseStyles.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/base/BaseStyles.java @@ -30,4 +30,335 @@ import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStyles *******************************************************************************/ public class BaseStyles implements BlockStylesInterface { + private Directional padding; + + private String backgroundColor; + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static class Directional + { + private T top; + private T bottom; + private T left; + private T right; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public Directional() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public Directional(T top, T right, T bottom, T left) + { + this.top = top; + this.right = right; + this.bottom = bottom; + this.left = left; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static Directional of(T top, T right, T bottom, T left) + { + return (new Directional<>(top, right, bottom, left)); + } + + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static Directional of(T value) + { + return (new Directional<>(value, value, value, value)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static Directional ofTop(T top) + { + return (new Directional<>(top, null, null, null)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static Directional ofRight(T right) + { + return (new Directional<>(null, right, null, null)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static Directional ofBottom(T bottom) + { + return (new Directional<>(null, null, bottom, null)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static Directional ofLeft(T left) + { + return (new Directional<>(null, null, null, left)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static Directional ofX(T x) + { + return (new Directional<>(null, x, null, x)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static Directional ofY(T y) + { + return (new Directional<>(y, null, y, null)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static Directional ofXY(T x, T y) + { + return (new Directional<>(y, x, y, x)); + } + + + + /******************************************************************************* + ** Getter for top + ** + *******************************************************************************/ + public T getTop() + { + return top; + } + + + + /******************************************************************************* + ** Setter for top + ** + *******************************************************************************/ + public void setTop(T top) + { + this.top = top; + } + + + + /******************************************************************************* + ** Fluent setter for top + ** + *******************************************************************************/ + public Directional withTop(T top) + { + this.top = top; + return (this); + } + + + + /******************************************************************************* + ** Getter for bottom + ** + *******************************************************************************/ + public T getBottom() + { + return bottom; + } + + + + /******************************************************************************* + ** Setter for bottom + ** + *******************************************************************************/ + public void setBottom(T bottom) + { + this.bottom = bottom; + } + + + + /******************************************************************************* + ** Fluent setter for bottom + ** + *******************************************************************************/ + public Directional withBottom(T bottom) + { + this.bottom = bottom; + return (this); + } + + + + /******************************************************************************* + ** Getter for left + ** + *******************************************************************************/ + public T getLeft() + { + return left; + } + + + + /******************************************************************************* + ** Setter for left + ** + *******************************************************************************/ + public void setLeft(T left) + { + this.left = left; + } + + + + /******************************************************************************* + ** Fluent setter for left + ** + *******************************************************************************/ + public Directional withLeft(T left) + { + this.left = left; + return (this); + } + + + + /******************************************************************************* + ** Getter for right + ** + *******************************************************************************/ + public T getRight() + { + return right; + } + + + + /******************************************************************************* + ** Setter for right + ** + *******************************************************************************/ + public void setRight(T right) + { + this.right = right; + } + + + + /******************************************************************************* + ** Fluent setter for right + ** + *******************************************************************************/ + public Directional withRight(T right) + { + this.right = right; + return (this); + } + + } + + + + /******************************************************************************* + ** Getter for padding + *******************************************************************************/ + public Directional getPadding() + { + return (this.padding); + } + + + + /******************************************************************************* + ** Setter for padding + *******************************************************************************/ + public void setPadding(Directional padding) + { + this.padding = padding; + } + + + + /******************************************************************************* + ** Fluent setter for padding + *******************************************************************************/ + public BaseStyles withPadding(Directional padding) + { + this.padding = padding; + return (this); + } + + + + /******************************************************************************* + ** Getter for backgroundColor + *******************************************************************************/ + public String getBackgroundColor() + { + return (this.backgroundColor); + } + + + + /******************************************************************************* + ** Setter for backgroundColor + *******************************************************************************/ + public void setBackgroundColor(String backgroundColor) + { + this.backgroundColor = backgroundColor; + } + + + + /******************************************************************************* + ** Fluent setter for backgroundColor + *******************************************************************************/ + public BaseStyles withBackgroundColor(String backgroundColor) + { + this.backgroundColor = backgroundColor; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonBlockData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonBlockData.java new file mode 100644 index 00000000..cd043ed0 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonBlockData.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.backend.core.model.dashboard.widgets.blocks.button; + + +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots; + + +/******************************************************************************* + ** a button (for a process - not sure yet what this could do in a standalone + ** widget?) to submit the process screen to run a specific action (e.g., not just + ** 'next'), or do other control-ish things + *******************************************************************************/ +public class ButtonBlockData extends AbstractBlockWidgetData +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String getBlockTypeName() + { + return "BUTTON"; + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonStyles.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonStyles.java new file mode 100644 index 00000000..64e834a6 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonStyles.java @@ -0,0 +1,143 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.button; + + +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStylesInterface; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ButtonStyles implements BlockStylesInterface +{ + private String color; + private String format; + + + + /*************************************************************************** + ** + ***************************************************************************/ + public enum StandardColor + { + SUCCESS, + WARNING, + ERROR, + INFO, + MUTED + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public enum StandardFormat + { + OUTLINED, + FILLED, + TEXT + } + + + + /******************************************************************************* + ** Getter for color + *******************************************************************************/ + public String getColor() + { + return (this.color); + } + + + + /******************************************************************************* + ** Setter for color + *******************************************************************************/ + public void setColor(String color) + { + this.color = color; + } + + + + /******************************************************************************* + ** Fluent setter for color + *******************************************************************************/ + public ButtonStyles withColor(String color) + { + this.color = color; + return (this); + } + + + + /******************************************************************************* + ** Getter for format + *******************************************************************************/ + public String getFormat() + { + return (this.format); + } + + + + /******************************************************************************* + ** Setter for format + *******************************************************************************/ + public void setFormat(String format) + { + this.format = format; + } + + + + /******************************************************************************* + ** Fluent setter for format + *******************************************************************************/ + public ButtonStyles withFormat(String format) + { + this.format = format; + return (this); + } + + /******************************************************************************* + ** Setter for format + *******************************************************************************/ + public void setFormat(StandardFormat format) + { + this.format = (format == null ? null : format.name().toLowerCase()); + } + + + + /******************************************************************************* + ** Fluent setter for format + *******************************************************************************/ + public ButtonStyles withFormat(StandardFormat format) + { + setFormat(format); + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonValues.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonValues.java new file mode 100644 index 00000000..5faff9e4 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/button/ButtonValues.java @@ -0,0 +1,218 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.button; + + +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ButtonValues implements BlockValuesInterface +{ + private String label; + private String actionCode; + private String controlCode; + + private QIcon startIcon; + private QIcon endIcon; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ButtonValues() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ButtonValues(String label, String actionCode) + { + setLabel(label); + setActionCode(actionCode); + } + + + + /******************************************************************************* + ** Getter for label + *******************************************************************************/ + public String getLabel() + { + return (this.label); + } + + + + /******************************************************************************* + ** Setter for label + *******************************************************************************/ + public void setLabel(String label) + { + this.label = label; + } + + + + /******************************************************************************* + ** Fluent setter for label + *******************************************************************************/ + public ButtonValues withLabel(String label) + { + this.label = label; + return (this); + } + + + + /******************************************************************************* + ** Getter for actionCode + *******************************************************************************/ + public String getActionCode() + { + return (this.actionCode); + } + + + + /******************************************************************************* + ** Setter for actionCode + *******************************************************************************/ + public void setActionCode(String actionCode) + { + this.actionCode = actionCode; + } + + + + /******************************************************************************* + ** Fluent setter for actionCode + *******************************************************************************/ + public ButtonValues withActionCode(String actionCode) + { + this.actionCode = actionCode; + return (this); + } + + + + /******************************************************************************* + ** Getter for startIcon + *******************************************************************************/ + public QIcon getStartIcon() + { + return (this.startIcon); + } + + + + /******************************************************************************* + ** Setter for startIcon + *******************************************************************************/ + public void setStartIcon(QIcon startIcon) + { + this.startIcon = startIcon; + } + + + + /******************************************************************************* + ** Fluent setter for startIcon + *******************************************************************************/ + public ButtonValues withStartIcon(QIcon startIcon) + { + this.startIcon = startIcon; + return (this); + } + + + + /******************************************************************************* + ** Getter for endIcon + *******************************************************************************/ + public QIcon getEndIcon() + { + return (this.endIcon); + } + + + + /******************************************************************************* + ** Setter for endIcon + *******************************************************************************/ + public void setEndIcon(QIcon endIcon) + { + this.endIcon = endIcon; + } + + + + /******************************************************************************* + ** Fluent setter for endIcon + *******************************************************************************/ + public ButtonValues withEndIcon(QIcon endIcon) + { + this.endIcon = endIcon; + return (this); + } + + + + /******************************************************************************* + ** Getter for controlCode + *******************************************************************************/ + public String getControlCode() + { + return (this.controlCode); + } + + + + /******************************************************************************* + ** Setter for controlCode + *******************************************************************************/ + public void setControlCode(String controlCode) + { + this.controlCode = controlCode; + } + + + + /******************************************************************************* + ** Fluent setter for controlCode + *******************************************************************************/ + public ButtonValues withControlCode(String controlCode) + { + this.controlCode = controlCode; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageBlockData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageBlockData.java new file mode 100644 index 00000000..823c1fa9 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageBlockData.java @@ -0,0 +1,44 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.image; + + +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots; + + +/******************************************************************************* + ** block to display an image + *******************************************************************************/ +public class ImageBlockData extends AbstractBlockWidgetData +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String getBlockTypeName() + { + return "IMAGE"; + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageStyles.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageStyles.java new file mode 100644 index 00000000..24f252b5 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageStyles.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.backend.core.model.dashboard.widgets.blocks.image; + + +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ImageStyles extends BaseStyles +{ + private String width; + private String height; + + + /******************************************************************************* + ** Fluent setter for padding + *******************************************************************************/ + @Override + public ImageStyles withPadding(Directional padding) + { + super.setPadding(padding); + return (this); + } + + + /******************************************************************************* + ** Getter for width + *******************************************************************************/ + public String getWidth() + { + return (this.width); + } + + + + /******************************************************************************* + ** Setter for width + *******************************************************************************/ + public void setWidth(String width) + { + this.width = width; + } + + + + /******************************************************************************* + ** Fluent setter for width + *******************************************************************************/ + public ImageStyles withWidth(String width) + { + this.width = width; + return (this); + } + + + + /******************************************************************************* + ** Getter for height + *******************************************************************************/ + public String getHeight() + { + return (this.height); + } + + + + /******************************************************************************* + ** Setter for height + *******************************************************************************/ + public void setHeight(String height) + { + this.height = height; + } + + + + /******************************************************************************* + ** Fluent setter for height + *******************************************************************************/ + public ImageStyles withHeight(String height) + { + this.height = height; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageValues.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageValues.java new file mode 100644 index 00000000..8cffaacc --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/image/ImageValues.java @@ -0,0 +1,98 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.image; + + +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ImageValues implements BlockValuesInterface +{ + private String path; + private String alt; + + + + /******************************************************************************* + ** 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 ImageValues withPath(String path) + { + this.path = path; + return (this); + } + + + + /******************************************************************************* + ** Getter for alt + *******************************************************************************/ + public String getAlt() + { + return (this.alt); + } + + + + /******************************************************************************* + ** Setter for alt + *******************************************************************************/ + public void setAlt(String alt) + { + this.alt = alt; + } + + + + /******************************************************************************* + ** Fluent setter for alt + *******************************************************************************/ + public ImageValues withAlt(String alt) + { + this.alt = alt; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/inputfield/InputFieldBlockData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/inputfield/InputFieldBlockData.java new file mode 100644 index 00000000..feea9c67 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/inputfield/InputFieldBlockData.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.backend.core.model.dashboard.widgets.blocks.inputfield; + + +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles; + + +/******************************************************************************* + ** block to display an input field - initially targeted at widgets-in-processes + *******************************************************************************/ +public class InputFieldBlockData extends AbstractBlockWidgetData +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String getBlockTypeName() + { + return "INPUT_FIELD"; + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/inputfield/InputFieldValues.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/inputfield/InputFieldValues.java new file mode 100644 index 00000000..52a82f60 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/inputfield/InputFieldValues.java @@ -0,0 +1,217 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.inputfield; + + +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class InputFieldValues implements BlockValuesInterface +{ + private QFieldMetaData fieldMetaData; + + private Boolean autoFocus; + private Boolean submitOnEnter; + private Boolean hideSoftKeyboard; + private String placeholder; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public InputFieldValues() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public InputFieldValues(QFieldMetaData fieldMetaData) + { + setFieldMetaData(fieldMetaData); + } + + + + /******************************************************************************* + ** Getter for fieldMetaData + *******************************************************************************/ + public QFieldMetaData getFieldMetaData() + { + return (this.fieldMetaData); + } + + + + /******************************************************************************* + ** Setter for fieldMetaData + *******************************************************************************/ + public void setFieldMetaData(QFieldMetaData fieldMetaData) + { + this.fieldMetaData = fieldMetaData; + } + + + + /******************************************************************************* + ** Fluent setter for fieldMetaData + *******************************************************************************/ + public InputFieldValues withFieldMetaData(QFieldMetaData fieldMetaData) + { + this.fieldMetaData = fieldMetaData; + return (this); + } + + + + /******************************************************************************* + ** Getter for autoFocus + *******************************************************************************/ + public Boolean getAutoFocus() + { + return (this.autoFocus); + } + + + + /******************************************************************************* + ** Setter for autoFocus + *******************************************************************************/ + public void setAutoFocus(Boolean autoFocus) + { + this.autoFocus = autoFocus; + } + + + + /******************************************************************************* + ** Fluent setter for autoFocus + *******************************************************************************/ + public InputFieldValues withAutoFocus(Boolean autoFocus) + { + this.autoFocus = autoFocus; + return (this); + } + + + + /******************************************************************************* + ** Getter for submitOnEnter + *******************************************************************************/ + public Boolean getSubmitOnEnter() + { + return (this.submitOnEnter); + } + + + + /******************************************************************************* + ** Setter for submitOnEnter + *******************************************************************************/ + public void setSubmitOnEnter(Boolean submitOnEnter) + { + this.submitOnEnter = submitOnEnter; + } + + + + /******************************************************************************* + ** Fluent setter for submitOnEnter + *******************************************************************************/ + public InputFieldValues withSubmitOnEnter(Boolean submitOnEnter) + { + this.submitOnEnter = submitOnEnter; + return (this); + } + + + + /******************************************************************************* + ** Getter for placeholder + *******************************************************************************/ + public String getPlaceholder() + { + return (this.placeholder); + } + + + + /******************************************************************************* + ** Setter for placeholder + *******************************************************************************/ + public void setPlaceholder(String placeholder) + { + this.placeholder = placeholder; + } + + + + /******************************************************************************* + ** Fluent setter for placeholder + *******************************************************************************/ + public InputFieldValues withPlaceholder(String placeholder) + { + this.placeholder = placeholder; + return (this); + } + + + /******************************************************************************* + ** Getter for hideSoftKeyboard + *******************************************************************************/ + public Boolean getHideSoftKeyboard() + { + return (this.hideSoftKeyboard); + } + + + + /******************************************************************************* + ** Setter for hideSoftKeyboard + *******************************************************************************/ + public void setHideSoftKeyboard(Boolean hideSoftKeyboard) + { + this.hideSoftKeyboard = hideSoftKeyboard; + } + + + + /******************************************************************************* + ** Fluent setter for hideSoftKeyboard + *******************************************************************************/ + public InputFieldValues withHideSoftKeyboard(Boolean hideSoftKeyboard) + { + this.hideSoftKeyboard = hideSoftKeyboard; + return (this); + } + + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/text/TextStyles.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/text/TextStyles.java index f735798d..c72e1e25 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/text/TextStyles.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/text/TextStyles.java @@ -30,4 +30,325 @@ import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStyles *******************************************************************************/ public class TextStyles implements BlockStylesInterface { + private String color; + private String format; + private String weight; + private String size; + + + + /*************************************************************************** + ** + ***************************************************************************/ + public enum StandardColor + { + SUCCESS, + WARNING, + ERROR, + INFO, + MUTED + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public enum StandardFormat + { + DEFAULT, + ALERT, + BANNER + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public enum StandardSize + { + LARGEST, + HEADLINE, + TITLE, + BODY, + SMALLEST + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public enum StandardWeight + { + EXTRA_LIGHT("extralight"), + THIN("thin"), + MEDIUM("medium"), + SEMI_BOLD("semibold"), + BLACK("black"), + BOLD("bold"), + EXTRA_BOLD("extrabold"), + W100("100"), + W200("200"), + W300("300"), + W400("400"), + W500("500"), + W600("600"), + W700("700"), + W800("800"), + W900("900"); + + private final String value; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + StandardWeight(String value) + { + this.value = value; + } + + + + /******************************************************************************* + ** Getter for value + ** + *******************************************************************************/ + public String getValue() + { + return value; + } + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public TextStyles() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public TextStyles(StandardColor standardColor) + { + setColor(standardColor); + } + + + + /******************************************************************************* + ** Getter for format + *******************************************************************************/ + public String getFormat() + { + return (this.format); + } + + + + /******************************************************************************* + ** Setter for format + *******************************************************************************/ + public void setFormat(String format) + { + this.format = format; + } + + + + /******************************************************************************* + ** Fluent setter for format + *******************************************************************************/ + public TextStyles withFormat(String format) + { + this.format = format; + return (this); + } + + + + /******************************************************************************* + ** Setter for format + *******************************************************************************/ + public void setFormat(StandardFormat format) + { + this.format = format == null ? null : format.name().toLowerCase(); + } + + + + /******************************************************************************* + ** Fluent setter for format + *******************************************************************************/ + public TextStyles withFormat(StandardFormat format) + { + this.setFormat(format); + return (this); + } + + + + /******************************************************************************* + ** Getter for weight + *******************************************************************************/ + public String getWeight() + { + return (this.weight); + } + + + + /******************************************************************************* + ** Setter for weight + *******************************************************************************/ + public void setWeight(String weight) + { + this.weight = weight; + } + + + + /******************************************************************************* + ** Fluent setter for weight + *******************************************************************************/ + public TextStyles withWeight(String weight) + { + this.weight = weight; + return (this); + } + + + + /******************************************************************************* + ** Setter for weight + *******************************************************************************/ + public void setWeight(StandardWeight weight) + { + setWeight(weight == null ? null : weight.getValue()); + } + + + + /******************************************************************************* + ** Fluent setter for weight + *******************************************************************************/ + public TextStyles withWeight(StandardWeight weight) + { + setWeight(weight); + return (this); + } + + + + /******************************************************************************* + ** Getter for size + *******************************************************************************/ + public String getSize() + { + return (this.size); + } + + + + /******************************************************************************* + ** Setter for size + *******************************************************************************/ + public void setSize(String size) + { + this.size = size; + } + + + + /******************************************************************************* + ** Fluent setter for size + *******************************************************************************/ + public TextStyles withSize(String size) + { + this.size = size; + return (this); + } + + + /******************************************************************************* + ** Setter for size + *******************************************************************************/ + public void setSize(StandardSize size) + { + this.size = (size == null ? null : size.name().toLowerCase()); + } + + + + /******************************************************************************* + ** Fluent setter for size + *******************************************************************************/ + public TextStyles withSize(StandardSize size) + { + setSize(size); + return (this); + } + + + + /******************************************************************************* + ** Getter for color + *******************************************************************************/ + public String getColor() + { + return (this.color); + } + + + + /******************************************************************************* + ** Setter for color + *******************************************************************************/ + public void setColor(String color) + { + this.color = color; + } + + + + /******************************************************************************* + ** Fluent setter for color + *******************************************************************************/ + public TextStyles withColor(String color) + { + this.color = color; + return (this); + } + + + + /******************************************************************************* + ** Setter for color + *******************************************************************************/ + public void setColor(StandardColor color) + { + this.color = color == null ? null : color.name(); + } + + + + /******************************************************************************* + ** Fluent setter for color + *******************************************************************************/ + public TextStyles withColor(StandardColor color) + { + setColor(color); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/text/TextValues.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/text/TextValues.java index 3b82f4cb..132933b1 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/text/TextValues.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/blocks/text/TextValues.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.text; import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; /******************************************************************************* @@ -32,6 +33,9 @@ public class TextValues implements BlockValuesInterface { private String text; + private QIcon startIcon; + private QIcon endIcon; + /******************************************************************************* @@ -84,4 +88,66 @@ public class TextValues implements BlockValuesInterface return (this); } + + /******************************************************************************* + ** Getter for startIcon + *******************************************************************************/ + public QIcon getStartIcon() + { + return (this.startIcon); + } + + + + /******************************************************************************* + ** Setter for startIcon + *******************************************************************************/ + public void setStartIcon(QIcon startIcon) + { + this.startIcon = startIcon; + } + + + + /******************************************************************************* + ** Fluent setter for startIcon + *******************************************************************************/ + public TextValues withStartIcon(QIcon startIcon) + { + this.startIcon = startIcon; + return (this); + } + + + + /******************************************************************************* + ** Getter for endIcon + *******************************************************************************/ + public QIcon getEndIcon() + { + return (this.endIcon); + } + + + + /******************************************************************************* + ** Setter for endIcon + *******************************************************************************/ + public void setEndIcon(QIcon endIcon) + { + this.endIcon = endIcon; + } + + + + /******************************************************************************* + ** Fluent setter for endIcon + *******************************************************************************/ + public TextValues withEndIcon(QIcon endIcon) + { + this.endIcon = endIcon; + return (this); + } + + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducer.java index 2756715f..15ae31d7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducer.java @@ -22,9 +22,6 @@ package com.kingsrook.qqq.backend.core.model.metadata; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; - - /******************************************************************************* ** Abstract class that knows how to produce meta data objects. Useful with ** MetaDataProducerHelper, to put point at a package full of these, and populate diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java index 54c7ae4b..ac68cfd2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java @@ -22,7 +22,6 @@ package com.kingsrook.qqq.backend.core.model.metadata; -import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import java.util.ArrayList; @@ -30,14 +29,12 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; -import com.google.common.collect.ImmutableSet; -import com.google.common.reflect.ClassPath; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData; 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.utils.ClassPathUtils; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -51,8 +48,6 @@ public class MetaDataProducerHelper private static Map, Integer> comparatorValuesByType = new HashMap<>(); private static Integer defaultComparatorValue; - private static ImmutableSet topLevelClasses; - static { //////////////////////////////////////////////////////////////////////////////////////// @@ -87,7 +82,7 @@ public class MetaDataProducerHelper //////////////////////////////////////////////////////////////////////// // find all the meta data producer classes in (and under) the package // //////////////////////////////////////////////////////////////////////// - classesInPackage = getClassesInPackage(packageName); + classesInPackage = ClassPathUtils.getClassesInPackage(packageName); } catch(Exception e) { @@ -176,51 +171,4 @@ public class MetaDataProducerHelper } - - - /******************************************************************************* - ** from https://stackoverflow.com/questions/520328/can-you-find-all-classes-in-a-package-using-reflection - ** (since the original, from ChatGPT, didn't work in jars, despite GPT hallucinating that it would) - *******************************************************************************/ - private static List> getClassesInPackage(String packageName) throws IOException - { - List> classes = new ArrayList<>(); - ClassLoader loader = Thread.currentThread().getContextClassLoader(); - - for(ClassPath.ClassInfo info : getTopLevelClasses(loader)) - { - if(info.getName().startsWith(packageName)) - { - classes.add(info.load()); - } - } - - return (classes); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private static ImmutableSet getTopLevelClasses(ClassLoader loader) throws IOException - { - if(topLevelClasses == null) - { - topLevelClasses = ClassPath.from(loader).getTopLevelClasses(); - } - - return (topLevelClasses); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public static void clearTopLevelClassCache() - { - topLevelClasses = null; - } - } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/MetaDataProducerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerInterface.java similarity index 93% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/MetaDataProducerInterface.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerInterface.java index 9faf3dc4..e91073e5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/MetaDataProducerInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerInterface.java @@ -1,6 +1,6 @@ /* * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2023. Kingsrook, LLC + * Copyright (C) 2021-2024. Kingsrook, LLC * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States * contact@kingsrook.com * https://github.com/Kingsrook/ @@ -19,12 +19,10 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.backend.core.model; +package com.kingsrook.qqq.backend.core.model.metadata; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; /******************************************************************************* diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java index fea69209..a61971c8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java @@ -43,6 +43,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules; import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode; import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNodeType; @@ -113,6 +114,8 @@ public class QInstance private QPermissionRules defaultPermissionRules = QPermissionRules.defaultInstance(); private QAuditRules defaultAuditRules = QAuditRules.defaultInstanceLevelNone(); + private QCodeReference metaDataFilter = null; + ////////////////////////////////////////////////////////////////////////////////////// // todo - lock down the object (no more changes allowed) after it's been validated? // // if doing so, may need to copy all of the collections into read-only versions... // @@ -1485,4 +1488,35 @@ public class QInstance QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, listForSlot); } + + /******************************************************************************* + ** Getter for metaDataFilter + *******************************************************************************/ + public QCodeReference getMetaDataFilter() + { + return (this.metaDataFilter); + } + + + + /******************************************************************************* + ** Setter for metaDataFilter + *******************************************************************************/ + public void setMetaDataFilter(QCodeReference metaDataFilter) + { + this.metaDataFilter = metaDataFilter; + } + + + + /******************************************************************************* + ** Fluent setter for metaDataFilter + *******************************************************************************/ + public QInstance withMetaDataFilter(QCodeReference metaDataFilter) + { + this.metaDataFilter = metaDataFilter; + return (this); + } + + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java index 14a9fbb6..c300e7fa 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java @@ -177,7 +177,7 @@ public class QAuthenticationMetaData implements TopLevelMetaDataInterface /******************************************************************************* ** *******************************************************************************/ - public QAuthenticationMetaData withVales(Map values) + public QAuthenticationMetaData withValues(Map values) { this.values = values; return (this); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReferenceLambda.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReferenceLambda.java new file mode 100644 index 00000000..5cd3c050 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReferenceLambda.java @@ -0,0 +1,58 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.code; + + +/******************************************************************************* + ** Specialized type of QCodeReference that takes a lambda function object. + ** + ** Originally intended for more concise setup of backend steps in tests - but, + ** may be generally useful. + *******************************************************************************/ +public class QCodeReferenceLambda extends QCodeReference +{ + private final T lambda; + + + + /*************************************************************************** + ** + ***************************************************************************/ + public QCodeReferenceLambda(T lambda) + { + this.lambda = lambda; + this.setCodeType(QCodeType.JAVA); + this.setName("[Lambda:" + lambda.toString() + "]"); + } + + + + /******************************************************************************* + ** Getter for lambda + ** + *******************************************************************************/ + public T getLambda() + { + return lambda; + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java index d78d469d..5e5e61f0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java @@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole; import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -73,10 +74,12 @@ public class QFieldMetaData implements Cloneable // propose doing that in a secondary field, e.g., "onlyEditableOn=insert|update" // /////////////////////////////////////////////////////////////////////////////////// - private String displayFormat = "%s"; + private String displayFormat = "%s"; private Serializable defaultValue; - private String possibleValueSourceName; - private QQueryFilter possibleValueSourceFilter; + + private String possibleValueSourceName; + private QQueryFilter possibleValueSourceFilter; + private QPossibleValueSource inlinePossibleValueSource; private Integer maxLength; private Set> behaviors; @@ -1058,4 +1061,35 @@ public class QFieldMetaData implements Cloneable QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, this.helpContents); } + + /******************************************************************************* + ** Getter for inlinePossibleValueSource + *******************************************************************************/ + public QPossibleValueSource getInlinePossibleValueSource() + { + return (this.inlinePossibleValueSource); + } + + + + /******************************************************************************* + ** Setter for inlinePossibleValueSource + *******************************************************************************/ + public void setInlinePossibleValueSource(QPossibleValueSource inlinePossibleValueSource) + { + this.inlinePossibleValueSource = inlinePossibleValueSource; + } + + + + /******************************************************************************* + ** Fluent setter for inlinePossibleValueSource + *******************************************************************************/ + public QFieldMetaData withInlinePossibleValueSource(QPossibleValueSource inlinePossibleValueSource) + { + this.inlinePossibleValueSource = inlinePossibleValueSource; + return (this); + } + + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNode.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNode.java index b4ab8965..e322be68 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNode.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNode.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; 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.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -45,7 +46,7 @@ public class AppTreeNode private String label; private List children; - private String iconName; + private QIcon icon; @@ -82,7 +83,7 @@ public class AppTreeNode if(appChildMetaData.getIcon() != null) { // todo - propagate icons from parents, if they aren't set here... - this.iconName = appChildMetaData.getIcon().getName(); + this.icon = appChildMetaData.getIcon(); } } @@ -138,7 +139,18 @@ public class AppTreeNode *******************************************************************************/ public String getIconName() { - return iconName; + return (icon == null ? null : icon.getName()); + } + + + + /******************************************************************************* + ** Getter for icon + ** + *******************************************************************************/ + public QIcon getIcon() + { + return icon; } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java index ef73eac8..f549dabf 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java @@ -32,6 +32,7 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; import com.kingsrook.qqq.backend.core.model.metadata.layout.QSupplementalAppMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -45,7 +46,7 @@ public class QFrontendAppMetaData { private String name; private String label; - private String iconName; + private QIcon icon; private List widgets = new ArrayList<>(); private List children = new ArrayList<>(); @@ -56,6 +57,7 @@ public class QFrontendAppMetaData private Map supplementalAppMetaData; + /******************************************************************************* ** *******************************************************************************/ @@ -63,11 +65,7 @@ public class QFrontendAppMetaData { this.name = appMetaData.getName(); this.label = appMetaData.getLabel(); - - if(appMetaData.getIcon() != null) - { - this.iconName = appMetaData.getIcon().getName(); - } + this.icon = appMetaData.getIcon(); List filteredWidgets = CollectionUtils.nonNullList(appMetaData.getWidgets()).stream().filter(n -> metaDataOutput.getWidgets().containsKey(n)).toList(); if(CollectionUtils.nullSafeHasContents(filteredWidgets)) @@ -81,6 +79,10 @@ public class QFrontendAppMetaData List filteredTables = CollectionUtils.nonNullList(section.getTables()).stream().filter(n -> metaDataOutput.getTables().containsKey(n)).toList(); List filteredProcesses = CollectionUtils.nonNullList(section.getProcesses()).stream().filter(n -> metaDataOutput.getProcesses().containsKey(n)).toList(); List filteredReports = CollectionUtils.nonNullList(section.getReports()).stream().filter(n -> metaDataOutput.getReports().containsKey(n)).toList(); + + ////////////////////////////////////////////////////// + // only include the section if it has some contents // + ////////////////////////////////////////////////////// if(!filteredTables.isEmpty() || !filteredProcesses.isEmpty() || !filteredReports.isEmpty()) { QAppSection clonedSection = section.clone(); @@ -174,18 +176,7 @@ public class QFrontendAppMetaData *******************************************************************************/ public String getIconName() { - return iconName; - } - - - - /******************************************************************************* - ** Setter for iconName - ** - *******************************************************************************/ - public void setIconName(String iconName) - { - this.iconName = iconName; + return (icon == null ? null : icon.getName()); } @@ -235,4 +226,15 @@ public class QFrontendAppMetaData { return supplementalAppMetaData; } + + + + /******************************************************************************* + ** Getter for icon + ** + *******************************************************************************/ + public QIcon getIcon() + { + return icon; + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java index 6f424b31..653c1dcc 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java @@ -33,6 +33,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehaviorForFron 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.help.QHelpContent; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -56,6 +57,7 @@ public class QFrontendFieldMetaData implements Serializable private List adornments; private List helpContents; + private QPossibleValueSource inlinePossibleValueSource; private List behaviors; @@ -81,6 +83,7 @@ public class QFrontendFieldMetaData implements Serializable this.adornments = fieldMetaData.getAdornments(); this.defaultValue = fieldMetaData.getDefaultValue(); this.helpContents = fieldMetaData.getHelpContents(); + this.inlinePossibleValueSource = fieldMetaData.getInlinePossibleValueSource(); for(FieldBehavior behavior : CollectionUtils.nonNullCollection(fieldMetaData.getBehaviors())) { @@ -218,6 +221,17 @@ public class QFrontendFieldMetaData implements Serializable + /******************************************************************************* + ** Getter for inlinePossibleValueSource + ** + *******************************************************************************/ + public QPossibleValueSource getInlinePossibleValueSource() + { + return inlinePossibleValueSource; + } + + + /******************************************************************************* ** Getter for fieldBehaviors ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java index ba2bfa60..c435e0f4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java @@ -29,8 +29,10 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; 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.processes.QStateMachineStep; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -47,9 +49,10 @@ public class QFrontendProcessMetaData private String tableName; private boolean isHidden; - private String iconName; + private QIcon icon; private List frontendSteps; + private String stepFlow; private boolean hasPermission; @@ -68,15 +71,27 @@ public class QFrontendProcessMetaData this.label = processMetaData.getLabel(); this.tableName = processMetaData.getTableName(); this.isHidden = processMetaData.getIsHidden(); + this.stepFlow = processMetaData.getStepFlow().toString(); if(includeSteps) { if(CollectionUtils.nullSafeHasContents(processMetaData.getStepList())) { - this.frontendSteps = processMetaData.getStepList().stream() - .filter(QFrontendStepMetaData.class::isInstance) - .map(QFrontendStepMetaData.class::cast) - .collect(Collectors.toList()); + this.frontendSteps = switch(processMetaData.getStepFlow()) + { + case LINEAR -> processMetaData.getStepList().stream() + .filter(QFrontendStepMetaData.class::isInstance) + .map(QFrontendStepMetaData.class::cast) + .collect(Collectors.toList()); + + case STATE_MACHINE -> processMetaData.getAllSteps().values().stream() + .filter(QStateMachineStep.class::isInstance) + .map(QStateMachineStep.class::cast) + .flatMap(step -> step.getSubSteps().stream()) + .filter(QFrontendStepMetaData.class::isInstance) + .map(QFrontendStepMetaData.class::cast) + .collect(Collectors.toList()); + }; } else { @@ -84,10 +99,7 @@ public class QFrontendProcessMetaData } } - if(processMetaData.getIcon() != null) - { - this.iconName = processMetaData.getIcon().getName(); - } + this.icon = processMetaData.getIcon(); hasPermission = PermissionsHelper.hasProcessPermission(actionInput, name); } @@ -166,7 +178,7 @@ public class QFrontendProcessMetaData *******************************************************************************/ public String getIconName() { - return iconName; + return icon == null ? null : icon.getName(); } @@ -180,4 +192,25 @@ public class QFrontendProcessMetaData return hasPermission; } + + + /******************************************************************************* + ** Getter for stepFlow + ** + *******************************************************************************/ + public String getStepFlow() + { + return stepFlow; + } + + + + /******************************************************************************* + ** Getter for icon + ** + *******************************************************************************/ + public QIcon getIcon() + { + return icon; + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java index d98ad463..6e98fdec 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java @@ -24,7 +24,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.frontend; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -40,6 +40,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin; @@ -61,7 +62,7 @@ public class QFrontendTableMetaData private String label; private boolean isHidden; private String primaryKeyField; - private String iconName; + private QIcon icon; private Map fields; private List sections; @@ -156,10 +157,7 @@ public class QFrontendTableMetaData } } - if(tableMetaData.getIcon() != null) - { - this.iconName = tableMetaData.getIcon().getName(); - } + this.icon = tableMetaData.getIcon(); setCapabilities(backendForTable, tableMetaData); @@ -185,7 +183,7 @@ public class QFrontendTableMetaData *******************************************************************************/ private void setCapabilities(QBackendMetaData backend, QTableMetaData table) { - Set enabledCapabilities = new HashSet<>(); + Set enabledCapabilities = new LinkedHashSet<>(); for(Capability capability : Capability.values()) { if(table.isCapabilityEnabled(backend, capability)) @@ -275,7 +273,7 @@ public class QFrontendTableMetaData *******************************************************************************/ public String getIconName() { - return iconName; + return (icon == null ? null : icon.getName()); } @@ -397,4 +395,16 @@ public class QFrontendTableMetaData { return helpContents; } + + + + /******************************************************************************* + ** Getter for icon + ** + *******************************************************************************/ + public QIcon getIcon() + { + return icon; + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/ProcessStepFlow.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/ProcessStepFlow.java new file mode 100644 index 00000000..492496dd --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/ProcessStepFlow.java @@ -0,0 +1,38 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.processes; + + +/******************************************************************************* + ** Possible ways the steps of a process can flow. + ** + ** LINEAR - (the default) - the list of steps in the process are executed in-order + ** + ** STATE_MACHINE - concept of "states", each which has a backend & frontend step; + ** a backend step can (must?) set the field "stepState" (or "nextStepName") to + ** say what the next (frontend) step is. + *******************************************************************************/ +public enum ProcessStepFlow +{ + LINEAR, + STATE_MACHINE +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendStepMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendStepMetaData.java index 9de7595a..0c07043e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendStepMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendStepMetaData.java @@ -47,6 +47,8 @@ public class QFrontendStepMetaData extends QStepMetaData private List recordListFields; private Map formFieldMap; + private String format; + private List helpContents; @@ -403,4 +405,35 @@ public class QFrontendStepMetaData extends QStepMetaData QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, this.helpContents); } + + /******************************************************************************* + ** Getter for format + *******************************************************************************/ + public String getFormat() + { + return (this.format); + } + + + + /******************************************************************************* + ** Setter for format + *******************************************************************************/ + public void setFormat(String format) + { + this.format = format; + } + + + + /******************************************************************************* + ** Fluent setter for format + *******************************************************************************/ + public QFrontendStepMetaData withFormat(String format) + { + this.format = format; + return (this); + } + + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java index fc7c7687..8bb459ef 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java @@ -57,6 +57,7 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi private Integer minInputRecords = null; private Integer maxInputRecords = null; + private ProcessStepFlow stepFlow = ProcessStepFlow.LINEAR; private List stepList; // these are the steps that are ran, by-default, in the order they are ran in private Map steps; // this is the full map of possible steps @@ -213,11 +214,10 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi - /******************************************************************************* - ** add a step to the stepList and map + /*************************************************************************** ** - *******************************************************************************/ - public QProcessMetaData addStep(QStepMetaData step) + ***************************************************************************/ + public QProcessMetaData withStep(QStepMetaData step) { int index = 0; if(this.stepList != null) @@ -231,11 +231,23 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi + /******************************************************************************* + ** add a step to the stepList and map + ** + *******************************************************************************/ + @Deprecated(since = "withStep was added") + public QProcessMetaData addStep(QStepMetaData step) + { + return (withStep(step)); + } + + + /******************************************************************************* ** add a step to the stepList (at the specified index) and the step map ** *******************************************************************************/ - public QProcessMetaData addStep(int index, QStepMetaData step) + public QProcessMetaData withStep(int index, QStepMetaData step) { if(this.stepList == null) { @@ -260,11 +272,23 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi + /******************************************************************************* + ** add a step to the stepList (at the specified index) and the step map + ** + *******************************************************************************/ + @Deprecated(since = "withStep was added") + public QProcessMetaData addStep(int index, QStepMetaData step) + { + return (withStep(index, step)); + } + + + /******************************************************************************* ** add a step ONLY to the step map - NOT the list w/ default execution order. ** *******************************************************************************/ - public QProcessMetaData addOptionalStep(QStepMetaData step) + public QProcessMetaData withOptionalStep(QStepMetaData step) { if(this.steps == null) { @@ -283,6 +307,18 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi + /******************************************************************************* + ** add a step ONLY to the step map - NOT the list w/ default execution order. + ** + *******************************************************************************/ + @Deprecated(since = "withOptionalStep was added") + public QProcessMetaData addOptionalStep(QStepMetaData step) + { + return (withOptionalStep(step)); + } + + + /******************************************************************************* ** Setter for stepList ** @@ -299,7 +335,26 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi *******************************************************************************/ public QStepMetaData getStep(String stepName) { - return (steps.get(stepName)); + if(steps.containsKey(stepName)) + { + return steps.get(stepName); + } + + for(QStepMetaData step : steps.values()) + { + if(step instanceof QStateMachineStep stateMachineStep) + { + for(QStepMetaData subStep : stateMachineStep.getSubSteps()) + { + if(subStep.getName().equals(stepName)) + { + return (subStep); + } + } + } + } + + return (null); } @@ -780,4 +835,35 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi return (this); } + + + /******************************************************************************* + ** Getter for stepFlow + *******************************************************************************/ + public ProcessStepFlow getStepFlow() + { + return (this.stepFlow); + } + + + + /******************************************************************************* + ** Setter for stepFlow + *******************************************************************************/ + public void setStepFlow(ProcessStepFlow stepFlow) + { + this.stepFlow = stepFlow; + } + + + + /******************************************************************************* + ** Fluent setter for stepFlow + *******************************************************************************/ + public QProcessMetaData withStepFlow(ProcessStepFlow stepFlow) + { + this.stepFlow = stepFlow; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStateMachineStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStateMachineStep.java new file mode 100644 index 00000000..dd370142 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStateMachineStep.java @@ -0,0 +1,188 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.processes; + + +import java.util.ArrayList; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; + + +/******************************************************************************* + ** A step for a state-machine flow based Process. + ** + ** Consists of 1 or 2 sub-steps, which are frontend and/or backend. + *******************************************************************************/ +public class QStateMachineStep extends QStepMetaData +{ + private List subSteps = new ArrayList<>(); + + private String defaultNextStepName; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + private QStateMachineStep(List subSteps) + { + setStepType("stateMachine"); + this.subSteps.addAll(subSteps); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static QStateMachineStep frontendOnly(String name, QFrontendStepMetaData frontendStepMetaData) + { + if(!StringUtils.hasContent(frontendStepMetaData.getName())) + { + frontendStepMetaData.setName(name + ".frontend"); + } + + return (new QStateMachineStep(List.of(frontendStepMetaData)).withName(name)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static QStateMachineStep backendOnly(String name, QBackendStepMetaData backendStepMetaData) + { + if(!StringUtils.hasContent(backendStepMetaData.getName())) + { + backendStepMetaData.setName(name + ".backend"); + } + + return (new QStateMachineStep(List.of(backendStepMetaData)).withName(name)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static QStateMachineStep frontendThenBackend(String name, QFrontendStepMetaData frontendStepMetaData, QBackendStepMetaData backendStepMetaData) + { + if(!StringUtils.hasContent(frontendStepMetaData.getName())) + { + frontendStepMetaData.setName(name + ".frontend"); + } + + if(!StringUtils.hasContent(backendStepMetaData.getName())) + { + backendStepMetaData.setName(name + ".backend"); + } + + return (new QStateMachineStep(List.of(frontendStepMetaData, backendStepMetaData)).withName(name)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QStateMachineStep withName(String name) + { + super.withName(name); + return (this); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QStateMachineStep withLabel(String label) + { + super.withLabel(label); + return (this); + } + + + + /******************************************************************************* + ** Getter for subSteps + ** + *******************************************************************************/ + public List getSubSteps() + { + return subSteps; + } + + + + /******************************************************************************* + ** Getter for defaultNextStepName + *******************************************************************************/ + public String getDefaultNextStepName() + { + return (this.defaultNextStepName); + } + + + + /******************************************************************************* + ** Setter for defaultNextStepName + *******************************************************************************/ + public void setDefaultNextStepName(String defaultNextStepName) + { + this.defaultNextStepName = defaultNextStepName; + } + + + + /******************************************************************************* + ** Fluent setter for defaultNextStepName + *******************************************************************************/ + public QStateMachineStep withDefaultNextStepName(String defaultNextStepName) + { + this.defaultNextStepName = defaultNextStepName; + return (this); + } + + + + /******************************************************************************* + ** Get a list of all of the input fields used by this step (all of its sub-steps) + *******************************************************************************/ + @JsonIgnore + @Override + public List getInputFields() + { + List rs = new ArrayList<>(); + for(QStepMetaData subStep : subSteps) + { + rs.addAll(subStep.getInputFields()); + } + return (rs); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/sharing/ShareScopePossibleValueMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/sharing/ShareScopePossibleValueMetaDataProducer.java index 49e5bb64..33d4479a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/sharing/ShareScopePossibleValueMetaDataProducer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/sharing/ShareScopePossibleValueMetaDataProducer.java @@ -23,7 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.sharing; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.processes.implementations.sharing.ShareScope; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ScheduledReportSyncToScheduledJobProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ScheduledReportSyncToScheduledJobProcess.java index 68f0cbd3..b8cc5f4d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ScheduledReportSyncToScheduledJobProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ScheduledReportSyncToScheduledJobProcess.java @@ -26,13 +26,13 @@ import java.io.Serializable; import java.util.List; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum; 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.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; @@ -114,7 +114,7 @@ public class ScheduledReportSyncToScheduledJobProcess extends AbstractTableSyncT scheduledJob = new ScheduledJob(); scheduledJob.setLabel("Scheduled Report " + scheduledReport.getId()); scheduledJob.setDescription("Job to run Scheduled Report Id " + scheduledReport.getId() - + " (which runs Report Id " + scheduledReport.getSavedReportId() + ")"); + + " (which runs Report Id " + scheduledReport.getSavedReportId() + ")"); scheduledJob.setSchedulerName(runBackendStepInput.getValueString(SCHEDULER_NAME_FIELD_NAME)); scheduledJob.setType(ScheduledJobType.PROCESS.name()); scheduledJob.setForeignKeyType(getScheduledJobForeignKeyType()); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/HealBadRecordAutomationStatusesProcessStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/HealBadRecordAutomationStatusesProcessStep.java index 310fafe6..92b68dd6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/HealBadRecordAutomationStatusesProcessStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/HealBadRecordAutomationStatusesProcessStep.java @@ -36,7 +36,6 @@ import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; @@ -47,6 +46,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.NowWithOffset; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.HtmlWrapper; @@ -158,8 +158,8 @@ public class HealBadRecordAutomationStatusesProcessStep implements BackendStep, @Override public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException { - int recordsUpdated = 0; - boolean isReview = "preview".equals(runBackendStepInput.getStepName()); + int recordsUpdated = 0; + boolean isReview = "preview".equals(runBackendStepInput.getStepName()); //////////////////////////////////////////////////////////////////////// // if a table name is given, validate it, and run for just that table // diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStep.java index 739619dd..5451ad19 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStep.java @@ -28,9 +28,9 @@ import com.kingsrook.qqq.backend.core.actions.automation.polling.PollingAutomati import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java index 5c7cc6b0..4dc9d6d3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java @@ -176,9 +176,9 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe ////////////////////////////////////////////////////////////////////// // propagate data from inner-step state to process-level step state // ////////////////////////////////////////////////////////////////////// - if(postRunOutput.getUpdatedFrontendStepList() != null) + if(postRunOutput.getProcessMetaDataAdjustment() != null) { - runBackendStepOutput.setUpdatedFrontendStepList(postRunOutput.getUpdatedFrontendStepList()); + runBackendStepOutput.setProcessMetaDataAdjustment(postRunOutput.getProcessMetaDataAdjustment()); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -281,10 +281,10 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe ////////////////////////////////////////////////////////////////////// // propagate data from inner-step state to process-level step state // ////////////////////////////////////////////////////////////////////// - if(streamedBackendStepOutput.getUpdatedFrontendStepList() != null) + if(streamedBackendStepOutput.getProcessMetaDataAdjustment() != null) { runBackendStepOutput.getProcessState().setStepList(streamedBackendStepOutput.getProcessState().getStepList()); - runBackendStepOutput.setUpdatedFrontendStepList(streamedBackendStepOutput.getUpdatedFrontendStepList()); + runBackendStepOutput.getProcessState().setProcessMetaDataAdjustment(streamedBackendStepOutput.getProcessMetaDataAdjustment()); } //////////////////////////////////////////////// @@ -299,10 +299,10 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe ////////////////////////////////////////////////////////////////////// // propagate data from inner-step state to process-level step state // ////////////////////////////////////////////////////////////////////// - if(streamedBackendStepOutput.getUpdatedFrontendStepList() != null) + if(streamedBackendStepOutput.getProcessMetaDataAdjustment() != null) { runBackendStepOutput.getProcessState().setStepList(streamedBackendStepOutput.getProcessState().getStepList()); - runBackendStepOutput.setUpdatedFrontendStepList(streamedBackendStepOutput.getUpdatedFrontendStepList()); + runBackendStepOutput.getProcessState().setProcessMetaDataAdjustment(streamedBackendStepOutput.getProcessMetaDataAdjustment()); } /////////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java index 1111a705..aab7331b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java @@ -148,9 +148,9 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe ////////////////////////////////////////////////////////////////////// // propagate data from inner-step state to process-level step state // ////////////////////////////////////////////////////////////////////// - if(postRunOutput.getUpdatedFrontendStepList() != null) + if(postRunOutput.getProcessMetaDataAdjustment() != null) { - runBackendStepOutput.setUpdatedFrontendStepList(postRunOutput.getUpdatedFrontendStepList()); + runBackendStepOutput.setProcessMetaDataAdjustment(postRunOutput.getProcessMetaDataAdjustment()); } } @@ -219,10 +219,9 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe ////////////////////////////////////////////////////////////////////// // propagate data from inner-step state to process-level step state // ////////////////////////////////////////////////////////////////////// - if(streamedBackendStepOutput.getUpdatedFrontendStepList() != null) + if(streamedBackendStepOutput.getProcessMetaDataAdjustment() != null) { - runBackendStepOutput.getProcessState().setStepList(streamedBackendStepOutput.getProcessState().getStepList()); - runBackendStepOutput.setUpdatedFrontendStepList(streamedBackendStepOutput.getUpdatedFrontendStepList()); + runBackendStepOutput.getProcessState().setProcessMetaDataAdjustment(streamedBackendStepOutput.getProcessMetaDataAdjustment()); } //////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java index c9addb97..6dc65081 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java @@ -145,9 +145,9 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back ////////////////////////////////////////////////////////////////////// // propagate data from inner-step state to process-level step state // ////////////////////////////////////////////////////////////////////// - if(postRunOutput.getUpdatedFrontendStepList() != null) + if(postRunOutput.getProcessMetaDataAdjustment() != null) { - runBackendStepOutput.setUpdatedFrontendStepList(postRunOutput.getUpdatedFrontendStepList()); + runBackendStepOutput.setProcessMetaDataAdjustment(postRunOutput.getProcessMetaDataAdjustment()); } } @@ -183,10 +183,9 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back ////////////////////////////////////////////////////////////////////// // propagate data from inner-step state to process-level step state // ////////////////////////////////////////////////////////////////////// - if(streamedBackendStepOutput.getUpdatedFrontendStepList() != null) + if(streamedBackendStepOutput.getProcessMetaDataAdjustment() != null) { - runBackendStepOutput.getProcessState().setStepList(streamedBackendStepOutput.getProcessState().getStepList()); - runBackendStepOutput.setUpdatedFrontendStepList(streamedBackendStepOutput.getUpdatedFrontendStepList()); + runBackendStepOutput.setProcessMetaDataAdjustment(streamedBackendStepOutput.getProcessMetaDataAdjustment()); } /////////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java index 10bc4212..c8693195 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java @@ -23,10 +23,10 @@ package com.kingsrook.qqq.backend.core.processes.implementations.savedreports; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum; 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.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine; @@ -58,7 +58,7 @@ public class RenderSavedReportMetaDataProducer implements MetaDataProducerInterf public static final String FIELD_NAME_STORAGE_TABLE_NAME = "storageTableName"; public static final String FIELD_NAME_REPORT_FORMAT = "reportFormat"; public static final String FIELD_NAME_EMAIL_ADDRESS = "reportDestinationEmailAddress"; - public static final String FIELD_NAME_EMAIL_SUBJECT = "emailSubject"; + public static final String FIELD_NAME_EMAIL_SUBJECT = "emailSubject"; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RunScheduledReportMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RunScheduledReportMetaDataProducer.java index 15717f22..b23e2575 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RunScheduledReportMetaDataProducer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RunScheduledReportMetaDataProducer.java @@ -23,7 +23,7 @@ package com.kingsrook.qqq.backend.core.processes.implementations.savedreports; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/DeleteSharedRecordProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/DeleteSharedRecordProcess.java index ea73e9ff..06fd9ad2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/DeleteSharedRecordProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/DeleteSharedRecordProcess.java @@ -27,12 +27,12 @@ import java.util.Objects; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; @@ -87,9 +87,9 @@ public class DeleteSharedRecordProcess implements BackendStep, MetaDataProducerI @Override public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException { - String tableName = runBackendStepInput.getValueString("tableName"); - String recordIdString = runBackendStepInput.getValueString("recordId"); - Integer shareId = runBackendStepInput.getValueInteger("shareId"); + String tableName = runBackendStepInput.getValueString("tableName"); + String recordIdString = runBackendStepInput.getValueString("recordId"); + Integer shareId = runBackendStepInput.getValueInteger("shareId"); Objects.requireNonNull(tableName, "Missing required input: tableName"); Objects.requireNonNull(recordIdString, "Missing required input: recordId"); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/EditSharedRecordProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/EditSharedRecordProcess.java index 406bea55..5158c841 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/EditSharedRecordProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/EditSharedRecordProcess.java @@ -28,12 +28,12 @@ import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/GetSharedRecordsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/GetSharedRecordsProcess.java index 5bf42978..7dda49a5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/GetSharedRecordsProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/GetSharedRecordsProcess.java @@ -37,7 +37,6 @@ import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; @@ -47,6 +46,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; 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 com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; @@ -134,7 +134,7 @@ public class GetSharedRecordsProcess implements BackendStep, MetaDataProducerInt ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // iterate results, building QRecords to output - note - we'll need to collect ids, then look them up in audience-source tables // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - ArrayList resultList = new ArrayList<>(); + ArrayList resultList = new ArrayList<>(); ListingHash audienceIds = new ListingHash<>(); for(QRecord record : queryOutput.getRecords()) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/InsertSharedRecordProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/InsertSharedRecordProcess.java index c3ba8688..1705b460 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/InsertSharedRecordProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/sharing/InsertSharedRecordProcess.java @@ -32,13 +32,13 @@ import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockMetaDataProducer.java index b7382f24..4e9b214a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockMetaDataProducer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockMetaDataProducer.java @@ -24,7 +24,7 @@ package com.kingsrook.qqq.backend.core.processes.locks; import java.util.List; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/RescheduleAllJobsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/RescheduleAllJobsProcess.java index 9c3840ca..efae5bab 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/RescheduleAllJobsProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/RescheduleAllJobsProcess.java @@ -25,9 +25,9 @@ package com.kingsrook.qqq.backend.core.scheduler.processes; import java.util.List; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/ScheduleAllNewJobsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/ScheduleAllNewJobsProcess.java index 408ec1b8..2bade6b4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/ScheduleAllNewJobsProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/ScheduleAllNewJobsProcess.java @@ -25,9 +25,9 @@ package com.kingsrook.qqq.backend.core.scheduler.processes; import java.util.List; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/UnscheduleAllJobsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/UnscheduleAllJobsProcess.java index 11492b06..8b495550 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/UnscheduleAllJobsProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/processes/UnscheduleAllJobsProcess.java @@ -25,9 +25,9 @@ package com.kingsrook.qqq.backend.core.scheduler.processes; import java.util.List; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseAllQuartzJobsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseAllQuartzJobsProcess.java index 123b48c9..841c4cd7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseAllQuartzJobsProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseAllQuartzJobsProcess.java @@ -25,9 +25,9 @@ package com.kingsrook.qqq.backend.core.scheduler.quartz.processes; import java.util.List; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseQuartzJobsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseQuartzJobsProcess.java index 5d141d18..276d62f5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseQuartzJobsProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseQuartzJobsProcess.java @@ -25,10 +25,10 @@ package com.kingsrook.qqq.backend.core.scheduler.quartz.processes; import java.util.List; import java.util.function.BiFunction; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeAllQuartzJobsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeAllQuartzJobsProcess.java index 7d81e31f..2c9ff778 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeAllQuartzJobsProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeAllQuartzJobsProcess.java @@ -25,9 +25,9 @@ package com.kingsrook.qqq.backend.core.scheduler.quartz.processes; import java.util.List; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeQuartzJobsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeQuartzJobsProcess.java index 772e1bd0..33b2caea 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeQuartzJobsProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeQuartzJobsProcess.java @@ -25,10 +25,10 @@ package com.kingsrook.qqq.backend.core.scheduler.quartz.processes; import java.util.List; import java.util.function.BiFunction; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ClassPathUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ClassPathUtils.java new file mode 100644 index 00000000..4b90666f --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ClassPathUtils.java @@ -0,0 +1,87 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.utils; + + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import com.google.common.collect.ImmutableSet; +import com.google.common.reflect.ClassPath; + + +/******************************************************************************* + ** Utilities for reading classes - e.g., finding all in a package + *******************************************************************************/ +@SuppressWarnings("ALL") // the api we're using here, from google, is marked Beta +public class ClassPathUtils +{ + private static ImmutableSet topLevelClasses; + + + + /******************************************************************************* + ** from https://stackoverflow.com/questions/520328/can-you-find-all-classes-in-a-package-using-reflection + ** + *******************************************************************************/ + public static List> getClassesInPackage(String packageName) throws IOException + { + List> classes = new ArrayList<>(); + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + + for(ClassPath.ClassInfo info : getTopLevelClasses(loader)) + { + if(info.getName().startsWith(packageName)) + { + classes.add(info.load()); + } + } + + return (classes); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static ImmutableSet getTopLevelClasses(ClassLoader loader) throws IOException + { + if(topLevelClasses == null) + { + topLevelClasses = ClassPath.from(loader).getTopLevelClasses(); + } + + return (topLevelClasses); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void clearTopLevelClassCache() + { + topLevelClasses = null; + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CountingHash.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CountingHash.java index 8db0e364..e3abc974 100755 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CountingHash.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CountingHash.java @@ -94,6 +94,29 @@ public class CountingHash extends AbstractMap clm.getMessage().contains("Using new default")).hasSize(1); + + ///////////////////////////////////////////////////////////// + // set up new instance to use a custom filter, to deny all // + ///////////////////////////////////////////////////////////// + QInstance instance = TestUtils.defineInstance(); + instance.setMetaDataFilter(new QCodeReference(DenyAllFilter.class)); + reInitInstanceInContext(instance); + + ///////////////////////////////////////////////////// + // re-run, and assert all tables are filtered away // + ///////////////////////////////////////////////////// + result = new MetaDataAction().execute(new MetaDataInput()); + assertTrue(result.getTables().isEmpty(), "should be no tables"); + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // run again (with the same instance as before) to assert about memoization of the filter based on the QInstance // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + new MetaDataAction().execute(new MetaDataInput()); + assertThat(collectingLogger.getCollectedMessages()).filteredOn(clm -> clm.getMessage().contains("filter of type: DenyAllFilter")).hasSize(1); + + QLogger.deactivateCollectingLoggerForClass(MetaDataAction.class); + + //////////////////////////////////////////////////////////// + // run now with the AllowAllFilter, confirm we get tables // + //////////////////////////////////////////////////////////// + instance = TestUtils.defineInstance(); + instance.setMetaDataFilter(new QCodeReference(AllowAllMetaDataFilter.class)); + reInitInstanceInContext(instance); + result = new MetaDataAction().execute(new MetaDataInput()); + assertFalse(result.getTables().isEmpty(), "should be some tables"); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static class DenyAllFilter implements MetaDataFilterInterface + { + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowTable(MetaDataInput input, QTableMetaData table) + { + return false; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowProcess(MetaDataInput input, QProcessMetaData process) + { + return false; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowReport(MetaDataInput input, QReportMetaData report) + { + return false; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowApp(MetaDataInput input, QAppMetaData app) + { + return false; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean allowWidget(MetaDataInput input, QWidgetMetaDataInterface widget) + { + return false; + } + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessActionTest.java new file mode 100644 index 00000000..1d989f96 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessActionTest.java @@ -0,0 +1,327 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.processes; + + +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.metadata.code.QCodeReferenceLambda; +import com.kingsrook.qqq.backend.core.model.metadata.processes.ProcessStepFlow; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +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.processes.QStateMachineStep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for RunProcessAction + *******************************************************************************/ +class RunProcessActionTest extends BaseTest +{ + private static List log = new ArrayList<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() + { + log.clear(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testStateMachineTwoBackendSteps() throws QException + { + QProcessMetaData process = new QProcessMetaData().withName("test") + + ///////////////////////////////////////////////////////////////// + // two-steps - a, points at b; b has no next-step, so it exits // + ///////////////////////////////////////////////////////////////// + .addStep(QStateMachineStep.backendOnly("a", new QBackendStepMetaData().withName("aBackend") + .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> + { + log.add("in StepA"); + runBackendStepOutput.getProcessState().setNextStepName("b"); + })))) + + .addStep(QStateMachineStep.backendOnly("b", new QBackendStepMetaData().withName("bBackend") + .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> + { + log.add("in StepB"); + })))) + + .withStepFlow(ProcessStepFlow.STATE_MACHINE); + + QContext.getQInstance().addProcess(process); + + RunProcessInput input = new RunProcessInput(); + input.setProcessName("test"); + input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK); + + RunProcessOutput runProcessOutput = new RunProcessAction().execute(input); + assertEquals(List.of("in StepA", "in StepB"), log); + assertThat(runProcessOutput.getProcessState().getNextStepName()).isEmpty(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testStateMachineTwoFrontendOnlySteps() throws QException + { + QProcessMetaData process = new QProcessMetaData().withName("test") + + .addStep(QStateMachineStep.frontendOnly("a", new QFrontendStepMetaData().withName("aFrontend")).withDefaultNextStepName("b")) + .addStep(QStateMachineStep.frontendOnly("b", new QFrontendStepMetaData().withName("bFrontend"))) + + .withStepFlow(ProcessStepFlow.STATE_MACHINE); + + QContext.getQInstance().addProcess(process); + + RunProcessInput input = new RunProcessInput(); + input.setProcessName("test"); + input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK); + + RunProcessOutput runProcessOutput = new RunProcessAction().execute(input); + assertThat(runProcessOutput.getProcessState().getNextStepName()) + .isPresent().get() + .isEqualTo("aFrontend"); + + ///////////////////////////// + // resume after a, go to b // + ///////////////////////////// + input.setStartAfterStep("aFrontend"); + runProcessOutput = new RunProcessAction().execute(input); + assertThat(runProcessOutput.getProcessState().getNextStepName()) + .isPresent().get() + .isEqualTo("bFrontend"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testStateMachineOneBackendStepReferencingItselfDoesNotInfiniteLoop() throws QException + { + QProcessMetaData process = new QProcessMetaData().withName("test") + + /////////////////////////////////////////////////////////////// + // set up step that always points back at itself. // + // since it never goes to the frontend, it'll stack overflow // + // (though we'll catch it ourselves before JVM does) // + /////////////////////////////////////////////////////////////// + .addStep(QStateMachineStep.backendOnly("a", new QBackendStepMetaData().withName("aBackend") + .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> + { + log.add("in StepA"); + runBackendStepOutput.getProcessState().setNextStepName("a"); + })))) + + .withStepFlow(ProcessStepFlow.STATE_MACHINE); + + QContext.getQInstance().addProcess(process); + + RunProcessInput input = new RunProcessInput(); + input.setProcessName("test"); + input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK); + + assertThatThrownBy(() -> new RunProcessAction().execute(input)) + .isInstanceOf(QException.class) + .hasMessageContaining("maxStateMachineProcessStepFlowStackDepth of 20"); + + /////////////////////////////////////////////////// + // make sure we can set a custom max-stack-depth // + /////////////////////////////////////////////////// + input.addValue("maxStateMachineProcessStepFlowStackDepth", 5); + assertThatThrownBy(() -> new RunProcessAction().execute(input)) + .isInstanceOf(QException.class) + .hasMessageContaining("maxStateMachineProcessStepFlowStackDepth of 5"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testStateMachineTwoBackendStepsReferencingEachOtherDoesNotInfiniteLoop() throws QException + { + QProcessMetaData process = new QProcessMetaData().withName("test") + + /////////////////////////////////////////////////////////////// + // set up two steps that always points back at each other. // + // since it never goes to the frontend, it'll stack overflow // + // (though we'll catch it ourselves before JVM does) // + /////////////////////////////////////////////////////////////// + .addStep(QStateMachineStep.backendOnly("a", new QBackendStepMetaData().withName("aBackend") + .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> + { + log.add("in StepA"); + runBackendStepOutput.getProcessState().setNextStepName("b"); + })))) + + .addStep(QStateMachineStep.backendOnly("b", new QBackendStepMetaData().withName("bBackend") + .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> + { + log.add("in StepB"); + runBackendStepOutput.getProcessState().setNextStepName("a"); + })))) + + .withStepFlow(ProcessStepFlow.STATE_MACHINE); + + QContext.getQInstance().addProcess(process); + + RunProcessInput input = new RunProcessInput(); + input.setProcessName("test"); + input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK); + + assertThatThrownBy(() -> new RunProcessAction().execute(input)) + .isInstanceOf(QException.class) + .hasMessageContaining("maxStateMachineProcessStepFlowStackDepth of 20"); + + /////////////////////////////////////////////////// + // make sure we can set a custom max-stack-depth // + /////////////////////////////////////////////////// + input.addValue("maxStateMachineProcessStepFlowStackDepth", 5); + assertThatThrownBy(() -> new RunProcessAction().execute(input)) + .isInstanceOf(QException.class) + .hasMessageContaining("maxStateMachineProcessStepFlowStackDepth of 5"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testStateSequenceOfFrontendAndBackendSteps() throws QException + { + QProcessMetaData process = new QProcessMetaData().withName("test") + + .addStep(QStateMachineStep.frontendThenBackend("a", + new QFrontendStepMetaData().withName("aFrontend"), + new QBackendStepMetaData().withName("aBackend") + .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> + { + log.add("in StepA"); + runBackendStepOutput.getProcessState().setNextStepName("b"); + })))) + + .addStep(QStateMachineStep.frontendThenBackend("b", + new QFrontendStepMetaData().withName("bFrontend"), + new QBackendStepMetaData().withName("bBackend") + .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> + { + log.add("in StepB"); + runBackendStepOutput.getProcessState().setNextStepName("c"); + })))) + + .addStep(QStateMachineStep.frontendThenBackend("c", + new QFrontendStepMetaData().withName("cFrontend"), + new QBackendStepMetaData().withName("cBackend") + .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> + { + log.add("in StepC"); + runBackendStepOutput.getProcessState().setNextStepName("d"); + })))) + + .addStep(QStateMachineStep.frontendOnly("d", + new QFrontendStepMetaData().withName("dFrontend"))) + + .withStepFlow(ProcessStepFlow.STATE_MACHINE); + + QContext.getQInstance().addProcess(process); + + RunProcessInput input = new RunProcessInput(); + input.setProcessName("test"); + input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK); + + //////////////////////////////////////////////////////// + // start the process - we should be sent to aFrontend // + //////////////////////////////////////////////////////// + RunProcessOutput runProcessOutput = new RunProcessAction().execute(input); + assertThat(runProcessOutput.getProcessState().getNextStepName()) + .isPresent().get() + .isEqualTo("aFrontend"); + + /////////////////////////////////////////////////////////////////////////////////////////// + // resume after aFrontend - we should run StepA (backend), and then be sent to bFrontend // + /////////////////////////////////////////////////////////////////////////////////////////// + input.setStartAfterStep("aFrontend"); + runProcessOutput = new RunProcessAction().execute(input); + assertEquals(List.of("in StepA"), log); + assertThat(runProcessOutput.getProcessState().getNextStepName()) + .isPresent().get() + .isEqualTo("bFrontend"); + + /////////////////////////////////////////////////////////////////////////////////////////// + // resume after bFrontend - we should run StepB (backend), and then be sent to cFrontend // + /////////////////////////////////////////////////////////////////////////////////////////// + input.setStartAfterStep("bFrontend"); + runProcessOutput = new RunProcessAction().execute(input); + assertEquals(List.of("in StepA", "in StepB"), log); + assertThat(runProcessOutput.getProcessState().getNextStepName()) + .isPresent().get() + .isEqualTo("cFrontend"); + + /////////////////////////////////////////////////////////////////////////////////////////// + // resume after cFrontend - we should run StepC (backend), and then be sent to dFrontend // + /////////////////////////////////////////////////////////////////////////////////////////// + input.setStartAfterStep("cFrontend"); + runProcessOutput = new RunProcessAction().execute(input); + assertEquals(List.of("in StepA", "in StepB", "in StepC"), log); + assertThat(runProcessOutput.getProcessState().getNextStepName()) + .isPresent().get() + .isEqualTo("dFrontend"); + + //////////////////////////////////////////////////////////////////////////////////// + // if we resume again here, we'll be past the end of the process, so no next-step // + //////////////////////////////////////////////////////////////////////////////////// + input.setStartAfterStep("dFrontend"); + runProcessOutput = new RunProcessAction().execute(input); + assertEquals(List.of("in StepA", "in StepB", "in StepC"), log); + assertThat(runProcessOutput.getProcessState().getNextStepName()).isEmpty(); + + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/DistinctFilteringRecordPipeTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/DistinctFilteringRecordPipeTest.java new file mode 100644 index 00000000..9d651f2a --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/DistinctFilteringRecordPipeTest.java @@ -0,0 +1,81 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.reporting; + + +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for DistinctFilteringRecordPipe + *******************************************************************************/ +class DistinctFilteringRecordPipeTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSingleFieldKey() throws QException + { + DistinctFilteringRecordPipe pipe = new DistinctFilteringRecordPipe(new UniqueKey("id")); + pipe.addRecord(new QRecord().withValue("id", 1)); + pipe.addRecord(new QRecord().withValue("id", 1)); + assertEquals(1, pipe.consumeAvailableRecords().size()); + + pipe.addRecord(new QRecord().withValue("id", 1)); + assertEquals(0, pipe.consumeAvailableRecords().size()); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMultiFieldKey() throws QException + { + DistinctFilteringRecordPipe pipe = new DistinctFilteringRecordPipe(new UniqueKey("type", "name")); + + //////////////////////////// + // add 3 distinct records // + //////////////////////////// + pipe.addRecord(new QRecord().withValue("type", 1).withValue("name", "A")); + pipe.addRecord(new QRecord().withValue("type", 1).withValue("name", "B")); + pipe.addRecord(new QRecord().withValue("type", 2).withValue("name", "B")); + assertEquals(3, pipe.consumeAvailableRecords().size()); + + /////////////////////////////////////////////////////////////////// + // now re-add those 3 (should all be discarded) plus one new one // + /////////////////////////////////////////////////////////////////// + pipe.addRecord(new QRecord().withValue("type", 1).withValue("name", "A")); + pipe.addRecord(new QRecord().withValue("type", 1).withValue("name", "B")); + pipe.addRecord(new QRecord().withValue("type", 2).withValue("name", "B")); + pipe.addRecord(new QRecord().withValue("type", 2).withValue("name", "A")); + assertEquals(1, pipe.consumeAvailableRecords().size()); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java index 0f84cc78..c2594727 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java @@ -32,6 +32,7 @@ import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.Month; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -80,6 +81,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -340,6 +342,16 @@ public class GenerateReportActionTest extends BaseTest ** *******************************************************************************/ private String runToString(ReportFormat reportFormat, String reportName) throws Exception + { + return (runToString(reportFormat, reportName, Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now()))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String runToString(ReportFormat reportFormat, String reportName, Map inputValues) throws Exception { String name = "/tmp/report." + reportFormat.getExtension(); try(FileOutputStream fileOutputStream = new FileOutputStream(name)) @@ -347,7 +359,7 @@ public class GenerateReportActionTest extends BaseTest ReportInput reportInput = new ReportInput(); reportInput.setReportName(reportName); reportInput.setReportDestination(new ReportDestination().withReportFormat(reportFormat).withReportOutputStream(fileOutputStream)); - reportInput.setInputValues(Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now())); + reportInput.setInputValues(inputValues); new GenerateReportAction().execute(reportInput); System.out.println("Wrote File: " + name); return (FileUtils.readFileToString(new File(name), StandardCharsets.UTF_8)); @@ -978,4 +990,55 @@ public class GenerateReportActionTest extends BaseTest assertThat(row.get("Home State Name")).isEqualTo("IL"); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFilterMissingValue() throws Exception + { + ////////////////////////////////////////////////////////////////////////////////////////////////////// + // define a report in the instance, with an input field - then we'll run it w/o supplying the value // + ////////////////////////////////////////////////////////////////////////////////////////////////////// + QReportMetaData report = new QReportMetaData() + .withName(REPORT_NAME) + .withDataSources(List.of( + new QReportDataSource() + .withName("persons") + .withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withQueryFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, List.of("${input.startDate}")))))) + .withViews(List.of( + new QReportView() + .withName("table1") + .withLabel("Table 1") + .withDataSourceName("persons") + .withType(ReportType.TABLE) + .withColumns(List.of(new QReportField().withName("id"))) + )); + + QInstance qInstance = QContext.getQInstance(); + qInstance.addReport(report); + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // the report should run, but with the filter removed, so it should find all (6) person records // + ////////////////////////////////////////////////////////////////////////////////////////////////// + insertPersonRecords(qInstance); + String json = runToString(ReportFormat.JSON, report.getName(), Collections.emptyMap()); + JSONArray reportJsonArray = new JSONArray(json); + assertEquals(6, reportJsonArray.length()); + + ///////////////////////////////////////////////////////////////////////////////////////////// + // re-run now, pretending to be a report that wasn't defined in meta-data, but instead was // + // ah-hoc-style, in which case, a missing input is defined as, should throw exception. // + ///////////////////////////////////////////////////////////////////////////////////////////// + ReportInput reportInput = new ReportInput(); + report.setName(null); + reportInput.setReportMetaData(report); + reportInput.setReportDestination(new ReportDestination().withReportFormat(ReportFormat.JSON).withReportOutputStream(new ByteArrayOutputStream())); + assertThatThrownBy(() -> new GenerateReportAction().execute(reportInput)) + .hasMessageContaining("Missing value for criteria on field: birthDate"); + } + } \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/BoldHeaderAndFooterFastExcelStylerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/BoldHeaderAndFooterFastExcelStylerTest.java new file mode 100644 index 00000000..13b37fa9 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/BoldHeaderAndFooterFastExcelStylerTest.java @@ -0,0 +1,52 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel; + + +import java.io.ByteArrayOutputStream; +import com.kingsrook.qqq.backend.core.BaseTest; +import org.dhatim.fastexcel.StyleSetter; +import org.dhatim.fastexcel.Workbook; +import org.dhatim.fastexcel.Worksheet; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** Unit test for BoldHeaderAndFooterFastExcelStyler + *******************************************************************************/ +class BoldHeaderAndFooterFastExcelStylerTest extends BaseTest +{ + + /******************************************************************************* + ** ... kinda just here to add test coverage to the class. I suppose, it + ** makes sure there's not an NPE inside that method at least...? + *******************************************************************************/ + @Test + void test() + { + Workbook workbook = new Workbook(new ByteArrayOutputStream(), "Test", null); + Worksheet worksheet = workbook.newWorksheet("Sheet 1"); + StyleSetter headerStyle = worksheet.range(0, 0, 1, 1).style(); + new BoldHeaderAndFooterFastExcelStyler().styleHeaderRow(headerStyle); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorTest.java new file mode 100644 index 00000000..3434052b --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorTest.java @@ -0,0 +1,60 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.scripts; + + +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.exceptions.QCodeException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertSame; + + +/******************************************************************************* + ** Unit test for QCodeExecutor + *******************************************************************************/ +class QCodeExecutorTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testConvertJavaObject() throws QCodeException + { + Object input = new Object(); + Object converted = ((QCodeExecutor) (codeReference, inputContext, executionLogger) -> null).convertJavaObject(input, null); + assertSame(input, converted); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testConvertObjectToJava() throws QCodeException + { + Object input = new Object(); + Object converted = ((QCodeExecutor) (codeReference, inputContext, executionLogger) -> null).convertObjectToJava(input); + assertSame(input, converted); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplicationTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplicationTest.java new file mode 100644 index 00000000..d5321bcc --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplicationTest.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.backend.core.instances; + + +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for AbstractMetaDataProducerBasedQQQApplication + *******************************************************************************/ +class AbstractMetaDataProducerBasedQQQApplicationTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + QInstance qInstance = new TestApplication().defineQInstance(); + assertEquals(1, qInstance.getTables().size()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static class TestApplication extends AbstractMetaDataProducerBasedQQQApplication + { + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public String getMetaDataPackageName() + { + return getClass().getPackage().getName() + ".producers"; + } + } +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractQQQApplicationTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractQQQApplicationTest.java new file mode 100644 index 00000000..59288f64 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractQQQApplicationTest.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.backend.core.instances; + + +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for AbstractQQQApplication + *******************************************************************************/ +class AbstractQQQApplicationTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + TestApplication testApplication = new TestApplication(); + QInstance qInstance = testApplication.defineQInstance(); + assertEquals(1, qInstance.getTables().size()); + assertTrue(qInstance.getTables().containsKey("testTable")); + + assertThatThrownBy(() -> testApplication.defineValidatedQInstance()) + .hasMessageContaining("validation"); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static class TestApplication extends AbstractQQQApplication + { + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QInstance defineQInstance() throws QException + { + QInstance qInstance = new QInstance(); + qInstance.addTable(new QTableMetaData().withName("testTable")); + return (qInstance); + } + } +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index e6651f3a..89c69734 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -38,6 +38,7 @@ import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.dashboard.PersonsByCreateDateBarChart; import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer; import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ParentWidgetRenderer; +import com.kingsrook.qqq.backend.core.actions.metadata.AllowAllMetaDataFilter; import com.kingsrook.qqq.backend.core.actions.processes.CancelProcessActionTest; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportCustomRecordSourceInterface; @@ -139,6 +140,21 @@ public class QInstanceValidatorTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMetaDataFilter() + { + assertValidationFailureReasons((qInstance) -> qInstance.setMetaDataFilter(new QCodeReference(QInstanceValidator.class)), + "Instance metaDataFilter CodeReference is not of the expected type"); + + assertValidationSuccess((qInstance) -> qInstance.setMetaDataFilter(new QCodeReference(AllowAllMetaDataFilter.class))); + assertValidationSuccess((qInstance) -> qInstance.setMetaDataFilter(null)); + } + + + /******************************************************************************* ** Test an instance with null backends - should throw. ** @@ -265,6 +281,38 @@ public class QInstanceValidatorTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableFieldInlinePossibleValueSource() + { + //////////////////////////////////////////////////// + // make sure can't have both named and inline PVS // + //////////////////////////////////////////////////// + assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId") + .withInlinePossibleValueSource(new QPossibleValueSource().withType(QPossibleValueSourceType.TABLE).withTableName("person")), + "both a possibleValueSourceName and an inlinePossibleValueSource"); + + ///////////////////////////////////////////// + // make require inline PVS to be enum type // + ///////////////////////////////////////////// + assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId") + .withPossibleValueSourceName(null) + .withInlinePossibleValueSource(new QPossibleValueSource().withType(QPossibleValueSourceType.TABLE)), + "must have a type of ENUM"); + + //////////////////////////////////////////////////// + // make sure validation on the inline PVS happens // + //////////////////////////////////////////////////// + assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId") + .withPossibleValueSourceName(null) + .withInlinePossibleValueSource(new QPossibleValueSource().withType(QPossibleValueSourceType.ENUM)), + "missing enum values"); + } + + + /******************************************************************************* ** Test that if a process specifies a table that doesn't exist, that it fails. ** @@ -717,8 +765,8 @@ public class QInstanceValidatorTest extends BaseTest @Test public void test_validateFieldWithMissingPossibleValueSource() { - assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId").setPossibleValueSourceName("not a real possible value source"), - "Unrecognized possibleValueSourceName"); + assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> qInstance.getTable("person").getField("homeStateId").setPossibleValueSourceName("not a real possible value source"), + "unrecognized possibleValueSourceName"); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/producers/TestMetaDataProducer.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/producers/TestMetaDataProducer.java new file mode 100644 index 00000000..66e57b9b --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/producers/TestMetaDataProducer.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.backend.core.instances.producers; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class TestMetaDataProducer implements MetaDataProducerInterface +{ + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QTableMetaData produce(QInstance qInstance) throws QException + { + return new QTableMetaData().withName("fromProducer"); + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/messaging/email/EmailMessagingProviderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/messaging/email/EmailMessagingProviderTest.java index 3f008fd7..20d9ac84 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/messaging/email/EmailMessagingProviderTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/messaging/email/EmailMessagingProviderTest.java @@ -35,6 +35,7 @@ import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.OS; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /******************************************************************************* @@ -64,4 +65,19 @@ class EmailMessagingProviderTest extends BaseTest ); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMissingInputs() + { + assertThatThrownBy(() -> new SendMessageAction().execute(new SendMessageInput())) + .hasMessageContaining("provider name was not given"); + + assertThatThrownBy(() -> new SendMessageAction().execute(new SendMessageInput().withMessagingProviderName("notFound"))) + .hasMessageContaining("was not found"); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/producers/TestDisabledMetaDataProducer.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/producers/TestDisabledMetaDataProducer.java index 5d2c3be7..ae137970 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/producers/TestDisabledMetaDataProducer.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/producers/TestDisabledMetaDataProducer.java @@ -23,7 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.producers; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/producers/TestImplementsMetaDataProducer.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/producers/TestImplementsMetaDataProducer.java index 14ce9359..9e66213c 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/producers/TestImplementsMetaDataProducer.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/producers/TestImplementsMetaDataProducer.java @@ -23,7 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.producers; import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportProcessTest.java index 3e6efae7..3a5523d8 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportProcessTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportProcessTest.java @@ -161,9 +161,19 @@ class RenderSavedReportProcessTest extends BaseTest *******************************************************************************/ private static InputStream getInputStream(RunProcessOutput runProcessOutput) throws QException { - String storageTableName = runProcessOutput.getValueString("storageTableName"); - String storageReference = runProcessOutput.getValueString("storageReference"); - InputStream inputStream = new MemoryStorageAction().getInputStream(new StorageInput(storageTableName).withReference(storageReference)); + String storageTableName = runProcessOutput.getValueString("storageTableName"); + String storageReference = runProcessOutput.getValueString("storageReference"); + + MemoryStorageAction memoryStorageAction = new MemoryStorageAction(); + StorageInput storageInput = new StorageInput(storageTableName).withReference(storageReference); + InputStream inputStream = memoryStorageAction.getInputStream(storageInput); + + ///////////////////////////////////////////////////////////////////////////////////////////////////// + // to help add a little bit of class-level code coverage, for the QStorageInterface, call a method // + // that has a defualt implementation in there // + ///////////////////////////////////////////////////////////////////////////////////////////////////// + memoryStorageAction.getDownloadURL(storageInput); + return inputStream; } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/CountingHashTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/CountingHashTest.java index 7033b170..08a2ddb7 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/CountingHashTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/CountingHashTest.java @@ -30,7 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; /******************************************************************************* - ** Unit test for CountingHash + ** Unit test for CountingHash *******************************************************************************/ class CountingHashTest extends BaseTest { @@ -73,4 +73,19 @@ class CountingHashTest extends BaseTest assertEquals(1, alwaysMutable.get("B")); } -} \ No newline at end of file + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPut() + { + CountingHash alwaysMutable = new CountingHash<>(Map.of("A", 5)); + alwaysMutable.put("A", 25); + assertEquals(25, alwaysMutable.get("A")); + alwaysMutable.put("A"); + assertEquals(26, alwaysMutable.get("A")); + } + +} diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3StorageAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3StorageAction.java index 96c72bde..bbffb63d 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3StorageAction.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3StorageAction.java @@ -58,7 +58,7 @@ public class S3StorageAction extends AbstractS3Action implements QStorageInterfa AmazonS3 amazonS3 = getS3Utils().getAmazonS3(); String fullPath = getFullPath(storageInput); - S3UploadOutputStream s3UploadOutputStream = new S3UploadOutputStream(amazonS3, backend.getBucketName(), fullPath); + S3UploadOutputStream s3UploadOutputStream = new S3UploadOutputStream(amazonS3, backend.getBucketName(), fullPath, storageInput.getContentType()); return (s3UploadOutputStream); } catch(Exception e) diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/utils/S3UploadOutputStream.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/utils/S3UploadOutputStream.java index f67493ee..2edfbbc3 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/utils/S3UploadOutputStream.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/utils/S3UploadOutputStream.java @@ -53,6 +53,7 @@ public class S3UploadOutputStream extends OutputStream private final AmazonS3 amazonS3; private final String bucketName; private final String key; + private final String contentType; private byte[] buffer = new byte[5 * 1024 * 1024]; private int offset = 0; @@ -68,11 +69,12 @@ public class S3UploadOutputStream extends OutputStream ** Constructor ** *******************************************************************************/ - public S3UploadOutputStream(AmazonS3 amazonS3, String bucketName, String key) + public S3UploadOutputStream(AmazonS3 amazonS3, String bucketName, String key, String contentType) { this.amazonS3 = amazonS3; this.bucketName = bucketName; this.key = key; + this.contentType = contentType; } @@ -96,6 +98,13 @@ public class S3UploadOutputStream extends OutputStream *******************************************************************************/ private void uploadIfNeeded() { + ObjectMetadata objectMetadata = null; + if(this.contentType != null) + { + objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType(this.contentType); + } + if(offset == buffer.length) { ////////////////////////////////////////// @@ -104,7 +113,8 @@ public class S3UploadOutputStream extends OutputStream if(initiateMultipartUploadResult == null) { LOG.info("Initiating a multipart upload", logPair("key", key)); - initiateMultipartUploadResult = amazonS3.initiateMultipartUpload(new InitiateMultipartUploadRequest(bucketName, key)); + initiateMultipartUploadResult = amazonS3.initiateMultipartUpload(new InitiateMultipartUploadRequest(bucketName, key, objectMetadata)); + uploadPartResultList = new ArrayList<>(); } @@ -115,7 +125,8 @@ public class S3UploadOutputStream extends OutputStream .withInputStream(new ByteArrayInputStream(buffer)) .withBucketName(bucketName) .withKey(key) - .withPartSize(buffer.length); + .withPartSize(buffer.length) + .withObjectMetadata(objectMetadata); uploadPartResultList.add(amazonS3.uploadPart(uploadPartRequest)); @@ -166,6 +177,13 @@ public class S3UploadOutputStream extends OutputStream return; } + ObjectMetadata objectMetadata = null; + if(this.contentType != null) + { + objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType(this.contentType); + } + if(initiateMultipartUploadResult != null) { if(offset > 0) @@ -180,7 +198,8 @@ public class S3UploadOutputStream extends OutputStream .withInputStream(new ByteArrayInputStream(buffer, 0, offset)) .withBucketName(bucketName) .withKey(key) - .withPartSize(offset); + .withPartSize(offset) + .withObjectMetadata(objectMetadata); uploadPartResultList.add(amazonS3.uploadPart(uploadPartRequest)); } @@ -193,8 +212,12 @@ public class S3UploadOutputStream extends OutputStream } else { + if(objectMetadata == null) + { + objectMetadata = new ObjectMetadata(); + } + LOG.info("Putting object (non-multipart)", logPair("key", key), logPair("length", offset)); - ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentLength(offset); PutObjectResult putObjectResult = amazonS3.putObject(bucketName, key, new ByteArrayInputStream(buffer, 0, offset), objectMetadata); } diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/utils/S3UploadOutputStreamTest.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/utils/S3UploadOutputStreamTest.java index 96d43bce..dc0a15b0 100644 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/utils/S3UploadOutputStreamTest.java +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/s3/utils/S3UploadOutputStreamTest.java @@ -31,7 +31,7 @@ import org.junit.jupiter.api.Test; /******************************************************************************* - ** Unit test for S3UploadOutputStream + ** Unit test for S3UploadOutputStream *******************************************************************************/ class S3UploadOutputStreamTest extends BaseS3Test { @@ -57,11 +57,11 @@ class S3UploadOutputStreamTest extends BaseS3Test outputStream.write("\n]\n".getBytes(StandardCharsets.UTF_8)); outputStream.close(); - S3UploadOutputStream s3UploadOutputStream = new S3UploadOutputStream(getS3Utils().getAmazonS3(), bucketName, key); + S3UploadOutputStream s3UploadOutputStream = new S3UploadOutputStream(getS3Utils().getAmazonS3(), bucketName, key, null); s3UploadOutputStream.write(outputStream.toByteArray(), 0, 5 * 1024 * 1024); s3UploadOutputStream.write(outputStream.toByteArray(), 0, 3 * 1024 * 1024); s3UploadOutputStream.write(outputStream.toByteArray(), 0, 3 * 1024 * 1024); s3UploadOutputStream.close(); } -} \ No newline at end of file +} diff --git a/qqq-bom/pom.xml b/qqq-bom/pom.xml index 7e3b3ac2..4b6321ce 100644 --- a/qqq-bom/pom.xml +++ b/qqq-bom/pom.xml @@ -74,6 +74,11 @@ qqq-middleware-api ${revision} + + com.kingsrook.qqq + qqq-openapi + ${revision} + com.kingsrook.qqq qqq-middleware-picocli diff --git a/qqq-dev-tools/CURRENT-SNAPSHOT-VERSION b/qqq-dev-tools/CURRENT-SNAPSHOT-VERSION index ca222b7c..2094a100 100644 --- a/qqq-dev-tools/CURRENT-SNAPSHOT-VERSION +++ b/qqq-dev-tools/CURRENT-SNAPSHOT-VERSION @@ -1 +1 @@ -0.23.0 +0.24.0 diff --git a/qqq-middleware-api/pom.xml b/qqq-middleware-api/pom.xml index 1d9812ee..5a2d3c6a 100644 --- a/qqq-middleware-api/pom.xml +++ b/qqq-middleware-api/pom.xml @@ -48,13 +48,13 @@ qqq-middleware-javalin ${revision} + + com.kingsrook.qqq + qqq-openapi + ${revision} + - - io.javalin - javalin - 5.1.4 - com.konghq unirest-java diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java index 7b42895b..247242b1 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java @@ -51,22 +51,6 @@ import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessUtils; import com.kingsrook.qqq.api.model.metadata.tables.ApiAssociationMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; -import com.kingsrook.qqq.api.model.openapi.Components; -import com.kingsrook.qqq.api.model.openapi.Contact; -import com.kingsrook.qqq.api.model.openapi.Content; -import com.kingsrook.qqq.api.model.openapi.Example; -import com.kingsrook.qqq.api.model.openapi.ExampleWithListValue; -import com.kingsrook.qqq.api.model.openapi.ExampleWithSingleValue; -import com.kingsrook.qqq.api.model.openapi.Info; -import com.kingsrook.qqq.api.model.openapi.Method; -import com.kingsrook.qqq.api.model.openapi.OpenAPI; -import com.kingsrook.qqq.api.model.openapi.Parameter; -import com.kingsrook.qqq.api.model.openapi.Path; -import com.kingsrook.qqq.api.model.openapi.RequestBody; -import com.kingsrook.qqq.api.model.openapi.Response; -import com.kingsrook.qqq.api.model.openapi.Schema; -import com.kingsrook.qqq.api.model.openapi.SecurityScheme; -import com.kingsrook.qqq.api.model.openapi.Tag; import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; @@ -93,6 +77,22 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.YamlUtils; import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; +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.ExampleWithListValue; +import com.kingsrook.qqq.openapi.model.ExampleWithSingleValue; +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.Parameter; +import com.kingsrook.qqq.openapi.model.Path; +import com.kingsrook.qqq.openapi.model.RequestBody; +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.Tag; import io.javalin.http.ContentType; import io.javalin.http.HttpStatus; import org.apache.commons.lang.BooleanUtils; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiMetaDataEnricher.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiMetaDataEnricher.java index ae7b2757..bc562d4b 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiMetaDataEnricher.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiMetaDataEnricher.java @@ -31,14 +31,14 @@ import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInput; import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessInputFieldsContainer; import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData; import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaDataContainer; -import com.kingsrook.qqq.api.model.openapi.ExampleWithListValue; -import com.kingsrook.qqq.api.model.openapi.ExampleWithSingleValue; -import com.kingsrook.qqq.api.model.openapi.HttpMethod; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; 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.QProcessMetaData; +import com.kingsrook.qqq.openapi.model.ExampleWithListValue; +import com.kingsrook.qqq.openapi.model.ExampleWithSingleValue; +import com.kingsrook.qqq.openapi.model.HttpMethod; /******************************************************************************* diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiProcessOutput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiProcessOutput.java index f2429917..41b11e7e 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiProcessOutput.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/implementations/savedreports/RenderSavedReportProcessApiProcessOutput.java @@ -27,15 +27,15 @@ import java.util.LinkedHashMap; import java.util.Map; import com.kingsrook.qqq.api.model.actions.HttpApiResponse; import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessOutputInterface; -import com.kingsrook.qqq.api.model.openapi.Content; -import com.kingsrook.qqq.api.model.openapi.Response; -import com.kingsrook.qqq.api.model.openapi.Schema; import com.kingsrook.qqq.backend.core.actions.tables.StorageAction; import com.kingsrook.qqq.backend.core.exceptions.QException; 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.reporting.ReportFormat; import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput; +import com.kingsrook.qqq.openapi.model.Content; +import com.kingsrook.qqq.openapi.model.Response; +import com.kingsrook.qqq.openapi.model.Schema; import org.eclipse.jetty.http.HttpStatus; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java index 8c541218..e1412297 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java @@ -53,7 +53,6 @@ import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaDataContaine import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessUtils; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; -import com.kingsrook.qqq.api.model.openapi.HttpMethod; import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.context.QContext; @@ -87,6 +86,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; import com.kingsrook.qqq.backend.javalin.QJavalinAccessLogger; import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; +import com.kingsrook.qqq.openapi.model.HttpMethod; import io.javalin.apibuilder.ApiBuilder; import io.javalin.apibuilder.EndpointGroup; import io.javalin.http.ContentType; @@ -135,8 +135,17 @@ public class QJavalinApiHandler /////////////////////////////////////////////// // static endpoints to support rapidoc pages // /////////////////////////////////////////////// - ApiBuilder.get("/api/docs/js/rapidoc.min.js", (context) -> QJavalinApiHandler.serveResource(context, "rapidoc/rapidoc-9.3.4.min.js", MapBuilder.of("Content-Type", ContentType.JAVASCRIPT))); - ApiBuilder.get("/api/docs/css/qqq-api-styles.css", (context) -> QJavalinApiHandler.serveResource(context, "rapidoc/rapidoc-overrides.css", MapBuilder.of("Content-Type", ContentType.CSS))); + try + { + ApiBuilder.get("/api/docs/js/rapidoc.min.js", (context) -> QJavalinApiHandler.serveResource(context, "rapidoc/rapidoc-9.3.8.min.js", MapBuilder.of("Content-Type", ContentType.JAVASCRIPT))); + ApiBuilder.get("/api/docs/css/qqq-api-styles.css", (context) -> QJavalinApiHandler.serveResource(context, "rapidoc/rapidoc-overrides.css", MapBuilder.of("Content-Type", ContentType.CSS))); + } + catch(IllegalArgumentException iae) + { + ////////////////////////////////////////////////////////////// + // assume a different module already registered these paths // + ////////////////////////////////////////////////////////////// + } ApiBuilder.get("/apis.json", QJavalinApiHandler::doGetApisJson); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GenerateOpenApiSpecOutput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GenerateOpenApiSpecOutput.java index c9dc8be9..68db04b0 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GenerateOpenApiSpecOutput.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GenerateOpenApiSpecOutput.java @@ -22,8 +22,8 @@ package com.kingsrook.qqq.api.model.actions; -import com.kingsrook.qqq.api.model.openapi.OpenAPI; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; +import com.kingsrook.qqq.openapi.model.OpenAPI; /******************************************************************************* diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java index c472d976..03069285 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java @@ -32,13 +32,13 @@ import java.util.Set; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; -import com.kingsrook.qqq.api.model.openapi.SecurityScheme; -import com.kingsrook.qqq.api.model.openapi.Server; import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.openapi.model.SecurityScheme; +import com.kingsrook.qqq.openapi.model.Server; import org.apache.commons.lang.BooleanUtils; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java index 78415898..32db55b1 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java @@ -23,10 +23,10 @@ package com.kingsrook.qqq.api.model.metadata.fields; import java.util.Map; -import com.kingsrook.qqq.api.model.openapi.Example; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.openapi.model.Example; /******************************************************************************* diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java index 5da8c160..622d9cc7 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java @@ -29,7 +29,6 @@ import com.kingsrook.qqq.api.ApiSupplementType; import com.kingsrook.qqq.api.model.APIVersionRange; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; -import com.kingsrook.qqq.api.model.openapi.HttpMethod; import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -39,6 +38,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; +import com.kingsrook.qqq.openapi.model.HttpMethod; import org.apache.commons.lang.BooleanUtils; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessObjectOutput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessObjectOutput.java index 20cf7453..5530aa2c 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessObjectOutput.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessObjectOutput.java @@ -31,11 +31,6 @@ import java.util.Objects; import com.kingsrook.qqq.api.actions.GenerateOpenApiSpecAction; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; -import com.kingsrook.qqq.api.model.openapi.Content; -import com.kingsrook.qqq.api.model.openapi.ExampleWithListValue; -import com.kingsrook.qqq.api.model.openapi.ExampleWithSingleValue; -import com.kingsrook.qqq.api.model.openapi.Response; -import com.kingsrook.qqq.api.model.openapi.Schema; 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.metadata.fields.QFieldMetaData; @@ -43,6 +38,11 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ObjectUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; +import com.kingsrook.qqq.openapi.model.Content; +import com.kingsrook.qqq.openapi.model.ExampleWithListValue; +import com.kingsrook.qqq.openapi.model.ExampleWithSingleValue; +import com.kingsrook.qqq.openapi.model.Response; +import com.kingsrook.qqq.openapi.model.Schema; import io.javalin.http.ContentType; import org.eclipse.jetty.http.HttpStatus; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java index 1ee3e70d..2f6eae4e 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java @@ -25,11 +25,11 @@ package com.kingsrook.qqq.api.model.metadata.processes; import java.io.Serializable; import java.util.Map; import com.kingsrook.qqq.api.model.actions.HttpApiResponse; -import com.kingsrook.qqq.api.model.openapi.Response; import com.kingsrook.qqq.backend.core.exceptions.QException; 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.utils.collections.MapBuilder; +import com.kingsrook.qqq.openapi.model.Response; import org.eclipse.jetty.http.HttpStatus; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java index 0fcb94a2..97d405f0 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java @@ -28,9 +28,6 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import com.kingsrook.qqq.api.model.openapi.Content; -import com.kingsrook.qqq.api.model.openapi.Response; -import com.kingsrook.qqq.api.model.openapi.Schema; 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.ProcessSummaryFilterLink; @@ -42,6 +39,9 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; +import com.kingsrook.qqq.openapi.model.Content; +import com.kingsrook.qqq.openapi.model.Response; +import com.kingsrook.qqq.openapi.model.Schema; import io.javalin.http.ContentType; import org.apache.commons.lang.NotImplementedException; import org.eclipse.jetty.http.HttpStatus; diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java index 66cb753c..9c198ec0 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TestUtils.java @@ -38,7 +38,6 @@ import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessObjectOutput; import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessSummaryListOutput; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; -import com.kingsrook.qqq.api.model.openapi.HttpMethod; import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCustomizer; import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer; import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreUpdateCustomizer; @@ -86,6 +85,7 @@ import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwith import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; import com.kingsrook.qqq.backend.core.processes.implementations.savedreports.RenderSavedReportMetaDataProducer; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.openapi.model.HttpMethod; /******************************************************************************* diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerPermissionsTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerPermissionsTest.java index c884d47c..d5601688 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerPermissionsTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerPermissionsTest.java @@ -78,9 +78,10 @@ class QJavalinApiHandlerPermissionsTest extends BaseTest } qJavalinImplementation = new QJavalinImplementation(qInstance); - qJavalinImplementation.startJavalinServer(PORT); + qJavalinImplementation.clearJavalinRoutes(); EndpointGroup routes = new QJavalinApiHandler(qInstance).getRoutes(); - qJavalinImplementation.getJavalinService().routes(routes); + qJavalinImplementation.addJavalinRoutes(routes); + qJavalinImplementation.startJavalinServer(PORT); } diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java index 20fc10b1..ecfb1073 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java @@ -112,9 +112,10 @@ class QJavalinApiHandlerTest extends BaseTest .withInitialVersion(TestUtils.V2022_Q4)))); qJavalinImplementation = new QJavalinImplementation(qInstance); - qJavalinImplementation.startJavalinServer(PORT); + qJavalinImplementation.clearJavalinRoutes(); EndpointGroup routes = new QJavalinApiHandler(qInstance).getRoutes(); - qJavalinImplementation.getJavalinService().routes(routes); + qJavalinImplementation.addJavalinRoutes(routes); + qJavalinImplementation.startJavalinServer(PORT); } diff --git a/qqq-middleware-javalin/pom.xml b/qqq-middleware-javalin/pom.xml index 309acce7..dc94f1ae 100644 --- a/qqq-middleware-javalin/pom.xml +++ b/qqq-middleware-javalin/pom.xml @@ -33,7 +33,10 @@ - + + + 1.9.10 + 6.3.0 @@ -43,6 +46,12 @@ qqq-backend-core ${revision} + + com.kingsrook.qqq + qqq-openapi + ${revision} + + com.kingsrook.qqq qqq-backend-module-rdbms @@ -60,7 +69,17 @@ io.javalin javalin - 5.6.1 + ${javalin.version} + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.slf4j + slf4j-simple + 1.7.36 com.konghq @@ -74,11 +93,6 @@ 2.2.220 test - - org.slf4j - slf4j-simple - 1.7.36 - @@ -105,4 +119,74 @@ + + src/main/java + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.1 + + 11 + 11 + + + io.javalin.community.openapi + openapi-annotation-processor + ${javalin.version} + + + + + + org.jacoco + jacoco-maven-plugin + + + com/kingsrook/qqq/middleware/javalin/executors/io/*.class + com/kingsrook/qqq/middleware/javalin/tools/**/*.class + com/kingsrook/qqq/middleware/javalin/specs/**/*.class + + + + + org.codehaus.mojo + appassembler-maven-plugin + 1.10 + + + + com.kingsrook.qqq.middleware.javalin.tools.ValidateAPIVersions + ValidateApiVersions + + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.4.3 + + false + + + *:* + + META-INF/* + + + + + + + ${plugin.shade.phase} + + shade + + + + + + + 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 cca918ab..1b2094be 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; @@ -106,7 +104,6 @@ import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSo import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; @@ -123,6 +120,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.QStatusMessage; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; import com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule; +import com.kingsrook.qqq.backend.core.utils.ClassPathUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; @@ -178,7 +176,8 @@ public class QJavalinImplementation private static int DEFAULT_PORT = 8001; - private static Javalin service; + private static Javalin service; + private static List endpointGroups; private static long startTime = 0; @@ -241,8 +240,18 @@ public class QJavalinImplementation { // todo port from arg // todo base path from arg? - and then potentially multiple instances too (chosen based on the root path??) - service = Javalin.create().start(port); - service.routes(getRoutes()); + + service = Javalin.create(config -> + { + config.router.apiBuilder(getRoutes()); + + for(EndpointGroup endpointGroup : CollectionUtils.nonNullList(endpointGroups)) + { + config.router.apiBuilder(endpointGroup); + } + } + ).start(port); + service.before(QJavalinImplementation::hotSwapQInstance); service.before((Context context) -> context.header("Content-Type", "application/json")); service.after(QJavalinImplementation::clearQContext); @@ -292,7 +301,7 @@ public class QJavalinImplementation //////////////////////////////////////////////////////////////////////////////// // clear the cache of classes in this class, so that new classes can be found // //////////////////////////////////////////////////////////////////////////////// - MetaDataProducerHelper.clearTopLevelClassCache(); + ClassPathUtils.clearTopLevelClassCache(); ///////////////////////////////////////////////// // try to get a new instance from the supplier // @@ -1014,8 +1023,7 @@ public class QJavalinImplementation QRecord record = getOutput.getRecord(); if(record == null) { - throw (new QNotFoundException("Could not find " + table.getLabel() + " with " - + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); + throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); } QValueFormatter.setBlobValuesToDownloadUrls(table, List.of(record)); @@ -1078,8 +1086,7 @@ public class QJavalinImplementation /////////////////////////////////////////////////////// if(getOutput.getRecord() == null) { - throw (new QNotFoundException("Could not find " + table.getLabel() + " with " - + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); + throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); } String mimeType = null; @@ -1727,7 +1734,7 @@ public class QJavalinImplementation } catch(QUserFacingException e) { - handleException(HttpStatus.Code.BAD_REQUEST, context, e); + QJavalinUtils.handleException(HttpStatus.Code.BAD_REQUEST, context, e); return null; } return reportFormat; @@ -1816,7 +1823,7 @@ public class QJavalinImplementation if(CollectionUtils.nullSafeHasContents(valuesParamList)) { String valuesParam = valuesParamList.get(0); - values = JsonUtils.toObject(valuesParam, new TypeReference<>(){}); + values = JsonUtils.toObject(valuesParam, new TypeReference<>() {}); } } @@ -1867,67 +1874,7 @@ public class QJavalinImplementation *******************************************************************************/ public static void handleException(Context context, Exception e) { - handleException(null, context, e); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public static void handleException(HttpStatus.Code statusCode, Context context, Exception e) - { - QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class); - if(userFacingException != null) - { - if(userFacingException instanceof QNotFoundException) - { - statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.NOT_FOUND); // 404 - respondWithError(context, statusCode, userFacingException.getMessage()); - } - else if(userFacingException instanceof QBadRequestException) - { - statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.BAD_REQUEST); // 400 - respondWithError(context, statusCode, userFacingException.getMessage()); - } - else - { - LOG.info("User-facing exception", e); - statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR); // 500 - respondWithError(context, statusCode, userFacingException.getMessage()); - } - } - else - { - if(e instanceof QAuthenticationException) - { - respondWithError(context, HttpStatus.Code.UNAUTHORIZED, e.getMessage()); // 401 - return; - } - - if(e instanceof QPermissionDeniedException) - { - respondWithError(context, HttpStatus.Code.FORBIDDEN, e.getMessage()); // 403 - return; - } - - //////////////////////////////// - // default exception handling // - //////////////////////////////// - LOG.warn("Exception in javalin request", e); - respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, e.getClass().getSimpleName() + " (" + e.getMessage() + ")"); // 500 - } - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public static void respondWithError(Context context, HttpStatus.Code statusCode, String errorMessage) - { - context.status(statusCode.getCode()); - context.result(JsonUtils.toJson(Map.of("error", errorMessage))); + QJavalinUtils.handleException(null, context, e); } @@ -1946,7 +1893,7 @@ public class QJavalinImplementation ** Getter for javalinMetaData ** *******************************************************************************/ - public QJavalinMetaData getJavalinMetaData() + public static QJavalinMetaData getJavalinMetaData() { return javalinMetaData; } @@ -1975,7 +1922,7 @@ public class QJavalinImplementation /******************************************************************************* - ** Getter for qInstanceHotSwapSupplier + ** Getter for qInstance *******************************************************************************/ public static QInstance getQInstance() { @@ -1984,6 +1931,16 @@ public class QJavalinImplementation + /******************************************************************************* + ** Setter for qInstance + *******************************************************************************/ + public static void setQInstance(QInstance qInstance) + { + QJavalinImplementation.qInstance = qInstance; + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -2011,4 +1968,30 @@ public class QJavalinImplementation { return (startTime); } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public void addJavalinRoutes(EndpointGroup routes) + { + if(endpointGroups == null) + { + endpointGroups = new ArrayList<>(); + } + endpointGroups.add(routes); + } + + + + /*************************************************************************** + ** if restarting this class, and you want to re-run addJavalinRoutes, but + ** not create duplicates, well, you might want to call this method! + ***************************************************************************/ + public void clearJavalinRoutes() + { + endpointGroups = null; + } + } 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 cb9ee53e..ec1c5bbf 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 @@ -41,6 +41,7 @@ import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; +import com.fasterxml.jackson.annotation.JsonInclude; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus; @@ -231,13 +232,13 @@ public class QJavalinProcessHandler if(inputField.getIsRequired() && !setValue) { - QJavalinImplementation.respondWithError(context, HttpStatus.Code.BAD_REQUEST, "Missing query param value for required input field: [" + inputField.getName() + "]"); + QJavalinUtils.respondWithError(context, HttpStatus.Code.BAD_REQUEST, "Missing query param value for required input field: [" + inputField.getName() + "]"); return; } } catch(Exception e) { - QJavalinImplementation.respondWithError(context, HttpStatus.Code.BAD_REQUEST, "Error processing query param [" + inputField.getName() + "]: " + e.getClass().getSimpleName() + " (" + e.getMessage() + ")"); + QJavalinUtils.respondWithError(context, HttpStatus.Code.BAD_REQUEST, "Error processing query param [" + inputField.getName() + "]: " + e.getClass().getSimpleName() + " (" + e.getMessage() + ")"); return; } } @@ -428,7 +429,18 @@ public class QJavalinProcessHandler QJavalinAccessLogger.logEndSuccess(); } - context.result(JsonUtils.toJson(resultForCaller)); + /////////////////////////////////////////////////////////////////////////////////// + // Note: originally we did not have this serializationInclusion:ALWAYS here - // + // which meant that null and empty values from backend would not go to frontend, // + // which made things like clearing out a value in a field not happen. // + // So, this is added to get that beneficial effect. Unclear if there are any // + // negative side-effects - but be aware. // + // One could imagine that we'd need this to be configurable in the future? // + /////////////////////////////////////////////////////////////////////////////////// + context.result(JsonUtils.toJson(resultForCaller, mapper -> + { + mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS); + })); } @@ -449,11 +461,19 @@ public class QJavalinProcessHandler resultForCaller.put("values", runProcessOutput.getValues()); runProcessOutput.getProcessState().getNextStepName().ifPresent(nextStep -> resultForCaller.put("nextStep", nextStep)); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + // todo - delete after all frontends look for processMetaDataAdjustment instead of updatedFrontendStepList // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// List updatedFrontendStepList = runProcessOutput.getUpdatedFrontendStepList(); if(updatedFrontendStepList != null) { resultForCaller.put("updatedFrontendStepList", updatedFrontendStepList); } + + if(runProcessOutput.getProcessMetaDataAdjustment() != null) + { + resultForCaller.put("processMetaDataAdjustment", runProcessOutput.getProcessMetaDataAdjustment()); + } } 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/QApplicationJavalinServer.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java new file mode 100644 index 00000000..d18a6a03 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java @@ -0,0 +1,529 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin; + + +import java.util.List; +import java.util.function.Consumer; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; +import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.utils.ClassPathUtils; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; +import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion; +import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1; +import io.javalin.Javalin; +import io.javalin.http.Context; +import org.apache.commons.lang.BooleanUtils; + + +/******************************************************************************* + ** Second-generation qqq javalin server. + ** + ** An evolution over the original QJavalinImplementation, which both managed + ** the javalin instance itself, but also provided all of the endpoint handlers... + ** This class instead just configures & starts the server. + ** + ** Makes several setters available, to let application-developer choose what + ** standard qqq endpoints are served (e.g., frontend-material-dashboard, the + ** legacy-unversioned middleware, newer versioned-middleware, and additional qqq + ** modules or application-defined services (both provided as instances of + ** QJavalinRouteProviderInterface). + ** + ** System property `qqq.javalin.hotSwapInstance` (defaults to false), causes the + ** QInstance to be re-loaded every X millis, to avoid some server restarts while + ** doing dev. + *******************************************************************************/ +public class QApplicationJavalinServer +{ + private static final QLogger LOG = QLogger.getLogger(QApplicationJavalinServer.class); + + private final AbstractQQQApplication application; + + private Integer port = 8000; + private boolean serveFrontendMaterialDashboard = true; + private boolean serveLegacyUnversionedMiddlewareAPI = true; + private List middlewareVersionList = List.of(new MiddlewareVersionV1()); + private List additionalRouteProviders = null; + private Consumer javalinConfigurationCustomizer = null; + + private long lastQInstanceHotSwapMillis; + private long millisBetweenHotSwaps = 2500; + private Consumer hotSwapCustomizer = null; + + private Javalin service; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public QApplicationJavalinServer(AbstractQQQApplication application) + { + this.application = application; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public void start() throws QException + { + QInstance qInstance = application.defineValidatedQInstance(); + + service = Javalin.create(config -> + { + if(serveFrontendMaterialDashboard) + { + //////////////////////////////////////////////////////////////////////////////////////// + // If you have any assets to add to the web server (e.g., logos, icons) place them at // + // src/main/resources/material-dashboard-overlay (or a directory of your choice // + // under src/main/resources) and use this line of code to tell javalin about it. // + // Make sure to add your app-specific directory to the javalin config before the core // + // material-dashboard directory, so in case the same file exists in both (e.g., // + // favicon.png), the app-specific one will be used. // + //////////////////////////////////////////////////////////////////////////////////////// + config.staticFiles.add("/material-dashboard-overlay"); + + ///////////////////////////////////////////////////////////////////// + // tell javalin where to find material-dashboard static web assets // + ///////////////////////////////////////////////////////////////////// + config.staticFiles.add("/material-dashboard"); + + //////////////////////////////////////////////////////////// + // set the index page for the SPA from material dashboard // + //////////////////////////////////////////////////////////// + config.spaRoot.addFile("/", "material-dashboard/index.html"); + } + + /////////////////////////////////////////// + // add qqq routes to the javalin service // + /////////////////////////////////////////// + if(serveLegacyUnversionedMiddlewareAPI) + { + try + { + QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(qInstance); + config.router.apiBuilder(qJavalinImplementation.getRoutes()); + } + catch(QInstanceValidationException e) + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // we should be pretty comfortable that this won't happen, because we've pre-validated the instance above... // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + throw new RuntimeException(e); + } + } + + ///////////////////////////////////// + // versioned qqq middleware routes // + ///////////////////////////////////// + if(CollectionUtils.nullSafeHasContents(middlewareVersionList)) + { + config.router.apiBuilder(new QMiddlewareApiSpecHandler(middlewareVersionList).defineJavalinEndpointGroup()); + for(AbstractMiddlewareVersion version : middlewareVersionList) + { + version.setQInstance(qInstance); + config.router.apiBuilder(version.getJavalinEndpointGroup(qInstance)); + } + } + + //////////////////////////////////////////////////////////////////////////// + // additional route providers (e.g., application-apis, other middlewares) // + //////////////////////////////////////////////////////////////////////////// + for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders)) + { + routeProvider.setQInstance(qInstance); + config.router.apiBuilder(routeProvider.getJavalinEndpointGroup()); + } + }); + + ////////////////////////////////////////////////////////////////////////////////////// + // per system property, set the server to hot-swap the q instance before all routes // + ////////////////////////////////////////////////////////////////////////////////////// + String hotSwapPropertyValue = System.getProperty("qqq.javalin.hotSwapInstance", "false"); + if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(hotSwapPropertyValue))) + { + LOG.info("Server will hotSwap QInstance before requests every [" + millisBetweenHotSwaps + "] millis."); + service.before(context -> hotSwapQInstance()); + } + + service.before((Context context) -> context.header("Content-Type", "application/json")); + service.after(QJavalinImplementation::clearQContext); + + //////////////////////////////////////////////// + // allow a configuration-customizer to be run // + //////////////////////////////////////////////// + if(javalinConfigurationCustomizer != null) + { + javalinConfigurationCustomizer.accept(service); + } + + service.start(port); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public void stop() + { + if(this.service == null) + { + LOG.info("Stop called, but there is no javalin service, so noop."); + return; + } + + this.service.stop(); + } + + + + /******************************************************************************* + ** If there's a qInstanceHotSwapSupplier, and its been a little while, replace + ** the qInstance with a new one from the supplier. Meant to be used while doing + ** development. + *******************************************************************************/ + public void hotSwapQInstance() + { + long now = System.currentTimeMillis(); + if(now - lastQInstanceHotSwapMillis < millisBetweenHotSwaps) + { + return; + } + + lastQInstanceHotSwapMillis = now; + + try + { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // clear the cache of classes in this class, so that new classes can be found if a meta-data-producer is being used // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ClassPathUtils.clearTopLevelClassCache(); + + /////////////////////////////////////////////////////////////// + // try to get a new, validated instance from the application // + /////////////////////////////////////////////////////////////// + QInstance newQInstance = application.defineValidatedQInstance(); + if(newQInstance == null) + { + LOG.warn("Got a null qInstance from the application.defineQInstance(). Not hot-swapping."); + return; + } + + //////////////////////////////////////// + // allow a hot-swap customizer to run // + //////////////////////////////////////// + if(hotSwapCustomizer != null) + { + hotSwapCustomizer.accept(newQInstance); + } + + /////////////////////////////////////////////////////////////////////// + // pass the new qInstance into all of the objects serving qqq routes // + /////////////////////////////////////////////////////////////////////// + if(serveLegacyUnversionedMiddlewareAPI) + { + QJavalinImplementation.setQInstance(newQInstance); + } + + if(CollectionUtils.nullSafeHasContents(middlewareVersionList)) + { + for(AbstractMiddlewareVersion spec : CollectionUtils.nonNullList(middlewareVersionList)) + { + spec.setQInstance(newQInstance); + } + } + + for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders)) + { + routeProvider.setQInstance(newQInstance); + } + + LOG.info("Swapped qInstance"); + } + catch(QInstanceValidationException e) + { + LOG.error("Validation Error while hot-swapping QInstance", e); + } + catch(Exception e) + { + LOG.error("Error hot-swapping QInstance", e); + } + } + + + + /******************************************************************************* + ** Getter for port + *******************************************************************************/ + public Integer getPort() + { + return (this.port); + } + + + + /******************************************************************************* + ** Setter for port + *******************************************************************************/ + public void setPort(Integer port) + { + this.port = port; + } + + + + /******************************************************************************* + ** Fluent setter for port + *******************************************************************************/ + public QApplicationJavalinServer withPort(Integer port) + { + this.port = port; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void setMillisBetweenHotSwaps(long millisBetweenHotSwaps) + { + this.millisBetweenHotSwaps = millisBetweenHotSwaps; + } + + + + /******************************************************************************* + ** Getter for serveFrontendMaterialDashboard + *******************************************************************************/ + public boolean getServeFrontendMaterialDashboard() + { + return (this.serveFrontendMaterialDashboard); + } + + + + /******************************************************************************* + ** Setter for serveFrontendMaterialDashboard + *******************************************************************************/ + public void setServeFrontendMaterialDashboard(boolean serveFrontendMaterialDashboard) + { + this.serveFrontendMaterialDashboard = serveFrontendMaterialDashboard; + } + + + + /******************************************************************************* + ** Fluent setter for serveFrontendMaterialDashboard + *******************************************************************************/ + public QApplicationJavalinServer withServeFrontendMaterialDashboard(boolean serveFrontendMaterialDashboard) + { + this.serveFrontendMaterialDashboard = serveFrontendMaterialDashboard; + return (this); + } + + + + /******************************************************************************* + ** Getter for serveLegacyUnversionedMiddlewareAPI + *******************************************************************************/ + public boolean getServeLegacyUnversionedMiddlewareAPI() + { + return (this.serveLegacyUnversionedMiddlewareAPI); + } + + + + /******************************************************************************* + ** Setter for serveLegacyUnversionedMiddlewareAPI + *******************************************************************************/ + public void setServeLegacyUnversionedMiddlewareAPI(boolean serveLegacyUnversionedMiddlewareAPI) + { + this.serveLegacyUnversionedMiddlewareAPI = serveLegacyUnversionedMiddlewareAPI; + } + + + + /******************************************************************************* + ** Fluent setter for serveLegacyUnversionedMiddlewareAPI + *******************************************************************************/ + public QApplicationJavalinServer withServeLegacyUnversionedMiddlewareAPI(boolean serveLegacyUnversionedMiddlewareAPI) + { + this.serveLegacyUnversionedMiddlewareAPI = serveLegacyUnversionedMiddlewareAPI; + return (this); + } + + + + /******************************************************************************* + ** Getter for middlewareVersionList + *******************************************************************************/ + public List getMiddlewareVersionList() + { + return (this.middlewareVersionList); + } + + + + /******************************************************************************* + ** Setter for middlewareVersionList + *******************************************************************************/ + public void setMiddlewareVersionList(List middlewareVersionList) + { + this.middlewareVersionList = middlewareVersionList; + } + + + + /******************************************************************************* + ** Fluent setter for middlewareVersionList + *******************************************************************************/ + public QApplicationJavalinServer withMiddlewareVersionList(List middlewareVersionList) + { + this.middlewareVersionList = middlewareVersionList; + return (this); + } + + + + /******************************************************************************* + ** Getter for additionalRouteProviders + *******************************************************************************/ + public List getAdditionalRouteProviders() + { + return (this.additionalRouteProviders); + } + + + + /******************************************************************************* + ** Setter for additionalRouteProviders + *******************************************************************************/ + public void setAdditionalRouteProviders(List additionalRouteProviders) + { + this.additionalRouteProviders = additionalRouteProviders; + } + + + + /******************************************************************************* + ** Fluent setter for additionalRouteProviders + *******************************************************************************/ + public QApplicationJavalinServer withAdditionalRouteProviders(List additionalRouteProviders) + { + this.additionalRouteProviders = additionalRouteProviders; + return (this); + } + + + + /******************************************************************************* + ** Getter for MILLIS_BETWEEN_HOT_SWAPS + *******************************************************************************/ + public long getMillisBetweenHotSwaps() + { + return (millisBetweenHotSwaps); + } + + + + /******************************************************************************* + ** Fluent setter for MILLIS_BETWEEN_HOT_SWAPS + *******************************************************************************/ + public QApplicationJavalinServer withMillisBetweenHotSwaps(long millisBetweenHotSwaps) + { + this.millisBetweenHotSwaps = millisBetweenHotSwaps; + return (this); + } + + + + /******************************************************************************* + ** Getter for hotSwapCustomizer + *******************************************************************************/ + public Consumer getHotSwapCustomizer() + { + return (this.hotSwapCustomizer); + } + + + + /******************************************************************************* + ** Setter for hotSwapCustomizer + *******************************************************************************/ + public void setHotSwapCustomizer(Consumer hotSwapCustomizer) + { + this.hotSwapCustomizer = hotSwapCustomizer; + } + + + + /******************************************************************************* + ** Fluent setter for hotSwapCustomizer + *******************************************************************************/ + public QApplicationJavalinServer withHotSwapCustomizer(Consumer hotSwapCustomizer) + { + this.hotSwapCustomizer = hotSwapCustomizer; + return (this); + } + + + + /******************************************************************************* + ** Getter for javalinConfigurationCustomizer + *******************************************************************************/ + public Consumer getJavalinConfigurationCustomizer() + { + return (this.javalinConfigurationCustomizer); + } + + + + /******************************************************************************* + ** Setter for javalinConfigurationCustomizer + *******************************************************************************/ + public void setJavalinConfigurationCustomizer(Consumer javalinConfigurationCustomizer) + { + this.javalinConfigurationCustomizer = javalinConfigurationCustomizer; + } + + + + /******************************************************************************* + ** Fluent setter for javalinConfigurationCustomizer + *******************************************************************************/ + public QApplicationJavalinServer withJavalinConfigurationCustomizer(Consumer javalinConfigurationCustomizer) + { + this.javalinConfigurationCustomizer = javalinConfigurationCustomizer; + return (this); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java new file mode 100644 index 00000000..dabe521c --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java @@ -0,0 +1,47 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin; + + +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import io.javalin.apibuilder.EndpointGroup; + + +/******************************************************************************* + ** Interface for classes that can provide a list of endpoints to a javalin + ** server. + *******************************************************************************/ +public interface QJavalinRouteProviderInterface +{ + + /*************************************************************************** + ** For initial setup when server boots, set the qInstance - but also, + ** e.g., for development, to do a hot-swap. + ***************************************************************************/ + void setQInstance(QInstance qInstance); + + /*************************************************************************** + ** + ***************************************************************************/ + EndpointGroup getJavalinEndpointGroup(); + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QMiddlewareApiSpecHandler.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QMiddlewareApiSpecHandler.java new file mode 100644 index 00000000..7988435c --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QMiddlewareApiSpecHandler.java @@ -0,0 +1,269 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin; + + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.YamlUtils; +import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; +import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; +import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion; +import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1; +import com.kingsrook.qqq.openapi.model.OpenAPI; +import io.javalin.apibuilder.ApiBuilder; +import io.javalin.apibuilder.EndpointGroup; +import io.javalin.http.ContentType; +import io.javalin.http.Context; +import org.apache.commons.io.IOUtils; + + +/******************************************************************************* + ** javalin-handler that serves both rapidoc static html/css/js files, and + ** dynamically generated openapi json/yaml, for a given list of qqq middleware + ** versions + *******************************************************************************/ +public class QMiddlewareApiSpecHandler +{ + private final List middlewareVersionList; + private final String basePath; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public QMiddlewareApiSpecHandler(List middlewareVersionList) + { + this(middlewareVersionList, "qqq"); + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public QMiddlewareApiSpecHandler(List middlewareVersionList, String basePath) + { + this.middlewareVersionList = middlewareVersionList; + this.basePath = basePath.replaceFirst("^/+", "").replaceFirst("/+$", "");; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public EndpointGroup defineJavalinEndpointGroup() + { + return (() -> + { + ApiBuilder.get("/api/docs/js/rapidoc.min.js", (context) -> serveResource(context, "rapidoc/rapidoc-9.3.8.min.js", MapBuilder.of("Content-Type", ContentType.JAVASCRIPT))); + ApiBuilder.get("/api/docs/css/qqq-api-styles.css", (context) -> serveResource(context, "rapidoc/rapidoc-overrides.css", MapBuilder.of("Content-Type", ContentType.CSS))); + ApiBuilder.get("/images/qqq-api-logo.png", (context) -> serveResource(context, "images/qqq-on-crown-trans-160x80.png", MapBuilder.of("Content-Type", ContentType.IMAGE_PNG.getMimeType()))); + + ////////////////////////////////////////////// + // default page is the current version spec // + ////////////////////////////////////////////// + ApiBuilder.get("/" + basePath + "/", context -> doSpecHtml(context)); + ApiBuilder.get("/" + basePath + "/versions.json", context -> doVersions(context)); + + //////////////////////////////////////////// + // default page for a version is its spec // + //////////////////////////////////////////// + for(AbstractMiddlewareVersion middlewareSpec : middlewareVersionList) + { + String version = middlewareSpec.getVersion(); + String versionPath = "/" + basePath + "/" + version; + ApiBuilder.get(versionPath + "/", context -> doSpecHtml(context, version)); + + /////////////////////////////////////////// + // add known paths for specs & docs page // + /////////////////////////////////////////// + ApiBuilder.get(versionPath + "/openapi.yaml", context -> doSpecYaml(context, version)); + ApiBuilder.get(versionPath + "/openapi.json", context -> doSpecJson(context, version)); + ApiBuilder.get(versionPath + "/openapi.html", context -> doSpecHtml(context, version)); + } + }); + } + + + + /******************************************************************************* + ** list the versions in this api + *******************************************************************************/ + private void doVersions(Context context) + { + Map rs = new HashMap<>(); + + List supportedVersions = middlewareVersionList.stream().map(msi -> msi.getVersion()).toList(); + String currentVersion = supportedVersions.get(supportedVersions.size() - 1); + + rs.put("supportedVersions", supportedVersions); + rs.put("currentVersion", currentVersion); + + context.contentType(ContentType.APPLICATION_JSON); + context.result(JsonUtils.toJson(rs)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void serveResource(Context context, String resourcePath, Map headers) + { + InputStream resourceAsStream = QJavalinImplementation.class.getClassLoader().getResourceAsStream(resourcePath); + for(Map.Entry entry : CollectionUtils.nonNullMap(headers).entrySet()) + { + context.header(entry.getKey(), entry.getValue()); + } + context.result(resourceAsStream); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void doSpecYaml(Context context, String version) + { + try + { + OpenAPI openAPI = new MiddlewareVersionV1().generateOpenAPIModel(basePath); + context.contentType(ContentType.APPLICATION_YAML); + context.result(YamlUtils.toYaml(openAPI)); + } + catch(Exception e) + { + QJavalinImplementation.handleException(context, e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void doSpecJson(Context context, String version) + { + try + { + OpenAPI openAPI = new MiddlewareVersionV1().generateOpenAPIModel(basePath); + context.contentType(ContentType.APPLICATION_JSON); + context.result(JsonUtils.toJson(openAPI)); + } + catch(Exception e) + { + QJavalinImplementation.handleException(context, e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void doSpecHtml(Context context) + { + String version = null; + + try + { + version = context.pathParam("version"); + } + catch(Exception e) + { + //////////////// + // leave null // + //////////////// + } + + if(!StringUtils.hasContent(version)) + { + List supportedVersions = middlewareVersionList.stream().map(msi -> msi.getVersion()).toList(); + version = supportedVersions.get(supportedVersions.size() - 1); + } + + doSpecHtml(context, version); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void doSpecHtml(Context context, String version) + { + try + { + ////////////////////////////////// + // read html from resource file // + ////////////////////////////////// + InputStream resourceAsStream = QMiddlewareApiSpecHandler.class.getClassLoader().getResourceAsStream("rapidoc/rapidoc-container.html"); + String html = IOUtils.toString(resourceAsStream, StandardCharsets.UTF_8); + + ///////////////////////////////// + // do replacements in the html // + ///////////////////////////////// + html = html.replace("{spec-url}", "/" + basePath + "/" + version + "/openapi.json"); + html = html.replace("{version}", version); + html = html.replace("{primaryColor}", "#444444"); + html = html.replace("{navLogoImg}", ""); + + Optional middlewareSpec = middlewareVersionList.stream().filter(msi -> msi.getVersion().equals(version)).findFirst(); + if(middlewareSpec.isEmpty()) + { + throw (new QUserFacingException("Unrecognized version: " + version)); + } + + OpenAPI openAPI = middlewareSpec.get().generateOpenAPIModel(basePath); + html = html.replace("{title}", openAPI.getInfo().getTitle() + " - " + version); + + StringBuilder otherVersionOptions = new StringBuilder(); + for(AbstractMiddlewareVersion otherVersionSpec : middlewareVersionList) + { + otherVersionOptions.append(""); + } + + html = html.replace("{otherVersionOptions}", otherVersionOptions.toString()); + + context.contentType(ContentType.HTML); + context.result(html); + } + catch(Exception e) + { + QJavalinImplementation.handleException(context, e); + } + } + +} 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..b16d8496 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/MetaDataExecutor.java @@ -0,0 +1,58 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +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 + { + com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput actionInput = new com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput(); + + actionInput.setMiddlewareName(input.getMiddlewareName()); + actionInput.setMiddlewareVersion(input.getMiddlewareVersion()); + actionInput.setFrontendName(input.getFrontendName()); + actionInput.setFrontendVersion(input.getFrontendVersion()); + actionInput.setApplicationName(input.getApplicationName()); + actionInput.setApplicationVersion(input.getApplicationVersion()); + + MetaDataAction metaDataAction = new MetaDataAction(); + MetaDataOutput metaDataOutput = metaDataAction.execute(actionInput); + output.setMetaDataOutput(metaDataOutput); + } + +} 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-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Content.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionInput.java similarity index 74% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Content.java rename to qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionInput.java index c62795ed..4fbc0124 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Content.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ManageSessionInput.java @@ -1,6 +1,6 @@ /* * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2023. Kingsrook, LLC + * Copyright (C) 2021-2024. Kingsrook, LLC * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States * contact@kingsrook.com * https://github.com/Kingsrook/ @@ -19,44 +19,44 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.middleware.javalin.executors.io; /******************************************************************************* ** *******************************************************************************/ -public class Content +public class ManageSessionInput extends AbstractMiddlewareInput { - private Schema schema; + private String accessToken; /******************************************************************************* - ** Getter for schema + ** Getter for accessToken *******************************************************************************/ - public Schema getSchema() + public String getAccessToken() { - return (this.schema); + return (this.accessToken); } /******************************************************************************* - ** Setter for schema + ** Setter for accessToken *******************************************************************************/ - public void setSchema(Schema schema) + public void setAccessToken(String accessToken) { - this.schema = schema; + this.accessToken = accessToken; } /******************************************************************************* - ** Fluent setter for schema + ** Fluent setter for accessToken *******************************************************************************/ - public Content withSchema(Schema schema) + public ManageSessionInput withAccessToken(String accessToken) { - this.schema = schema; + 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..2ec771bd --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/MetaDataInput.java @@ -0,0 +1,225 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.executors.io; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class MetaDataInput extends AbstractMiddlewareInput +{ + private String frontendName; + private String frontendVersion; + + private String middlewareName; + private String middlewareVersion; + + private String applicationName; + private String applicationVersion; + + + + /******************************************************************************* + ** Getter for frontendName + *******************************************************************************/ + public String getFrontendName() + { + return (this.frontendName); + } + + + + /******************************************************************************* + ** Setter for frontendName + *******************************************************************************/ + public void setFrontendName(String frontendName) + { + this.frontendName = frontendName; + } + + + + /******************************************************************************* + ** Fluent setter for frontendName + *******************************************************************************/ + public MetaDataInput withFrontendName(String frontendName) + { + this.frontendName = frontendName; + return (this); + } + + + + /******************************************************************************* + ** Getter for frontendVersion + *******************************************************************************/ + public String getFrontendVersion() + { + return (this.frontendVersion); + } + + + + /******************************************************************************* + ** Setter for frontendVersion + *******************************************************************************/ + public void setFrontendVersion(String frontendVersion) + { + this.frontendVersion = frontendVersion; + } + + + + /******************************************************************************* + ** Fluent setter for frontendVersion + *******************************************************************************/ + public MetaDataInput withFrontendVersion(String frontendVersion) + { + this.frontendVersion = frontendVersion; + return (this); + } + + + + /******************************************************************************* + ** Getter for middlewareName + *******************************************************************************/ + public String getMiddlewareName() + { + return (this.middlewareName); + } + + + + /******************************************************************************* + ** Setter for middlewareName + *******************************************************************************/ + public void setMiddlewareName(String middlewareName) + { + this.middlewareName = middlewareName; + } + + + + /******************************************************************************* + ** Fluent setter for middlewareName + *******************************************************************************/ + public MetaDataInput withMiddlewareName(String middlewareName) + { + this.middlewareName = middlewareName; + return (this); + } + + + + /******************************************************************************* + ** Getter for middlewareVersion + *******************************************************************************/ + public String getMiddlewareVersion() + { + return (this.middlewareVersion); + } + + + + /******************************************************************************* + ** Setter for middlewareVersion + *******************************************************************************/ + public void setMiddlewareVersion(String middlewareVersion) + { + this.middlewareVersion = middlewareVersion; + } + + + + /******************************************************************************* + ** Fluent setter for middlewareVersion + *******************************************************************************/ + public MetaDataInput withMiddlewareVersion(String middlewareVersion) + { + this.middlewareVersion = middlewareVersion; + return (this); + } + + + + /******************************************************************************* + ** Getter for applicationName + *******************************************************************************/ + public String getApplicationName() + { + return (this.applicationName); + } + + + + /******************************************************************************* + ** Setter for applicationName + *******************************************************************************/ + public void setApplicationName(String applicationName) + { + this.applicationName = applicationName; + } + + + + /******************************************************************************* + ** Fluent setter for applicationName + *******************************************************************************/ + public MetaDataInput withApplicationName(String applicationName) + { + this.applicationName = applicationName; + return (this); + } + + + + /******************************************************************************* + ** Getter for applicationVersion + *******************************************************************************/ + public String getApplicationVersion() + { + return (this.applicationVersion); + } + + + + /******************************************************************************* + ** Setter for applicationVersion + *******************************************************************************/ + public void setApplicationVersion(String applicationVersion) + { + this.applicationVersion = applicationVersion; + } + + + + /******************************************************************************* + ** Fluent setter for applicationVersion + *******************************************************************************/ + public MetaDataInput withApplicationVersion(String applicationVersion) + { + this.applicationVersion = applicationVersion; + return (this); + } + +} 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 new file mode 100644 index 00000000..9b0de98c --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/SchemaBuilder.java @@ -0,0 +1,347 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.schemabuilder; + + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIHasAdditionalProperties; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIIncludeProperties; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapKnownEntries; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapValueType; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIOneOf; +import com.kingsrook.qqq.openapi.model.Schema; +import com.kingsrook.qqq.openapi.model.Type; + + +/******************************************************************************* + ** This class facilitates generating OpenAPI Schema objects based on reflectively + ** reading classes and annotations + *******************************************************************************/ +public class SchemaBuilder +{ + + /*************************************************************************** + ** + ***************************************************************************/ + public Schema classToSchema(Class c) + { + return classToSchema(c, c); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static class SchemaFromBuilder extends Schema + { + private Class originalClass; + + + + /******************************************************************************* + ** Getter for originalClass + ** + *******************************************************************************/ + @JsonIgnore + public Class getOriginalClass() + { + return originalClass; + } + + + + /******************************************************************************* + ** Setter for originalClass + ** + *******************************************************************************/ + public void setOriginalClass(Class originalClass) + { + this.originalClass = originalClass; + } + + + + /******************************************************************************* + ** Fluent setter for originalClass + ** + *******************************************************************************/ + public SchemaFromBuilder withOriginalClass(Class originalClass) + { + this.originalClass = originalClass; + return (this); + } + + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private Schema classToSchema(Class c, AnnotatedElement element) + { + SchemaFromBuilder schema = new SchemaFromBuilder(); + schema.setOriginalClass(c); + + if(c.isEnum()) + { + schema.withType(Type.STRING); + schema.withEnumValues(Arrays.stream(c.getEnumConstants()).map(e -> String.valueOf(e)).collect(Collectors.toList())); + } + else if(c.equals(String.class)) + { + schema.withType(Type.STRING); + } + else if(c.equals(Integer.class) || c.equals(BigDecimal.class)) + { + schema.withType(Type.NUMBER); + } + else if(c.equals(Boolean.class)) + { + schema.withType(Type.BOOLEAN); + } + else if(c.equals(List.class)) + { + schema.withType(Type.ARRAY); + // Class itemType = field.getType().getTypeParameters()[0].getBounds().getClass(); + + OpenAPIListItems openAPIListItemsAnnotation = element.getAnnotation(OpenAPIListItems.class); + if(openAPIListItemsAnnotation == null) + { + // todo - can this be allowed, to make a generic list? maybe. + // throw (new QRuntimeException("A List field [" + field.getName() + "] was missing its @OpenAPIItems annotation")); + } + else + { + if(openAPIListItemsAnnotation.useRef()) + { + schema.withItems(new Schema().withRef("#/components/schemas/" + openAPIListItemsAnnotation.value().getSimpleName())); + } + else + { + Class itemType = openAPIListItemsAnnotation.value(); + schema.withItems(classToSchema(itemType)); + } + } + } + else if(c.equals(Map.class)) + { + schema.withType(Type.OBJECT); + + OpenAPIMapKnownEntries openAPIMapKnownEntriesAnnotation = element.getAnnotation(OpenAPIMapKnownEntries.class); + if(openAPIMapKnownEntriesAnnotation != null) + { + schema.withRef("#/components/schemas/" + openAPIMapKnownEntriesAnnotation.value().getSimpleName()); + // if(openAPIMapKnownEntriesAnnotation.additionalProperties()) + // { + // schema.withAdditionalProperties(true); + // } + } + + OpenAPIMapValueType openAPIMapValueTypeAnnotation = element.getAnnotation(OpenAPIMapValueType.class); + if(openAPIMapValueTypeAnnotation != null) + { + if(openAPIMapValueTypeAnnotation.useRef()) + { + schema.withAdditionalProperties(new Schema().withRef("#/components/schemas/" + openAPIMapValueTypeAnnotation.value().getSimpleName())); + } + else + { + schema.withAdditionalProperties(classToSchema(openAPIMapValueTypeAnnotation.value())); + } + } + } + else + { + OpenAPIOneOf openAPIOneOfAnnotation = element.getAnnotation(OpenAPIOneOf.class); + if(openAPIOneOfAnnotation != null) + { + String description = "[" + element + "]"; + List oneOfList = processOneOfAnnotation(openAPIOneOfAnnotation, c, description); + schema.withOneOf(oneOfList); + } + else + { + ///////////////////////////////////////////////////////////////////////////////////////////// + // else, if not a one-of then assume the schema is an object, and build out its properties // + ///////////////////////////////////////////////////////////////////////////////////////////// + schema.withType(Type.OBJECT); + + Map properties = new TreeMap<>(); + schema.withProperties(properties); + + ////////////////////////////////////////////////////////////////////////////////////////// + // if we're told to includeProperties (e.g., from ancestor classes), then go find those // + ////////////////////////////////////////////////////////////////////////////////////////// + OpenAPIIncludeProperties openAPIIncludePropertiesAnnotation = c.getAnnotation(OpenAPIIncludeProperties.class); + if(openAPIIncludePropertiesAnnotation != null) + { + Set> ancestorClasses = Arrays.stream(openAPIIncludePropertiesAnnotation.ancestorClasses()).collect(Collectors.toSet()); + Class superClass = c.getSuperclass(); + do + { + if(ancestorClasses.contains(superClass)) + { + addDeclaredFieldsToProperties(superClass, properties); + addDeclaredMethodsToProperties(superClass, properties); + } + superClass = superClass.getSuperclass(); + } + while(superClass != null); + } + + /////////////////////////////////////////////////////////////////////// + // make all declared-fields and getters in the class into properties // + /////////////////////////////////////////////////////////////////////// + addDeclaredFieldsToProperties(c, properties); + addDeclaredMethodsToProperties(c, properties); + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // now (after schema may have been replaced, e.g., in a recursive call), add more details to it // + ////////////////////////////////////////////////////////////////////////////////////////////////// + OpenAPIDescription openAPIDescriptionAnnotation = element.getAnnotation(OpenAPIDescription.class); + if(openAPIDescriptionAnnotation != null) + { + schema.setDescription(openAPIDescriptionAnnotation.value()); + } + + if(element.isAnnotationPresent(OpenAPIHasAdditionalProperties.class)) + { + schema.withAdditionalProperties(true); + } + + return (schema); + } + + + + /*************************************************************************** + ** Getter methods with an annotation + ***************************************************************************/ + private void addDeclaredMethodsToProperties(Class c, Map properties) + { + for(Method method : c.getDeclaredMethods()) + { + OpenAPIDescription methodDescription = method.getAnnotation(OpenAPIDescription.class); + OpenAPIExclude openAPIExclude = method.getAnnotation(OpenAPIExclude.class); + if(method.getName().startsWith("get") && method.getParameterCount() == 0 && methodDescription != null && openAPIExclude == null) + { + String name = StringUtils.lcFirst(method.getName().substring(3)); + properties.put(name, getMemberSchema(method)); + } + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void addDeclaredFieldsToProperties(Class c, Map properties) + { + for(Field declaredField : c.getDeclaredFields()) + { + OpenAPIExclude openAPIExclude = declaredField.getAnnotation(OpenAPIExclude.class); + if(openAPIExclude == null) + { + properties.put(declaredField.getName(), getMemberSchema(declaredField)); + } + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private Schema getMemberSchema(AccessibleObject member) + { + Class type; + if(member instanceof Field field) + { + type = field.getType(); + } + else if(member instanceof Method method) + { + type = method.getReturnType(); + } + else + { + throw (new IllegalArgumentException("Unsupported AccessibleObject: " + member)); + } + + return (classToSchema(type, member)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private List processOneOfAnnotation(OpenAPIOneOf openAPIOneOfAnnotation, Class type, String description) + { + List oneOfList = new ArrayList<>(); + + if(openAPIOneOfAnnotation.mode().equals(OpenAPIOneOf.Mode.PERMITTED_SUBCLASSES)) + { + Class[] permittedSubclasses = type.getPermittedSubclasses(); + for(Class permittedSubclass : permittedSubclasses) + { + oneOfList.add(classToSchema(permittedSubclass)); + } + } + else if(openAPIOneOfAnnotation.mode().equals(OpenAPIOneOf.Mode.SPECIFIED_LIST)) + { + for(Class oneOfClass : openAPIOneOfAnnotation.options()) + { + oneOfList.add(classToSchema(oneOfClass)); + } + } + + if(oneOfList.isEmpty()) + { + throw (new QRuntimeException("Could not find any options to use for an @OpenAPIOneOf annotation on " + description)); + } + return oneOfList; + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/ToSchema.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/ToSchema.java new file mode 100644 index 00000000..792032ad --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/ToSchema.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.schemabuilder; + + +import com.kingsrook.qqq.openapi.model.Schema; + + +/******************************************************************************* + ** Mark a class as eligible for running through the SchemaBuilder. + ** + ** Actually not really necessary, as schemaBuilder can run on any class - but + ** does provide a method that a class might use to customize how it gets + ** schemafied. + *******************************************************************************/ +public interface ToSchema +{ + + /*************************************************************************** + ** + ***************************************************************************/ + default Schema toSchema() + { + return new SchemaBuilder().classToSchema(getClass()); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIDescription.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIDescription.java new file mode 100644 index 00000000..563b5cfe --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIDescription.java @@ -0,0 +1,42 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/*************************************************************************** + ** + ***************************************************************************/ +@Target({ ElementType.FIELD, ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface OpenAPIDescription +{ + /*************************************************************************** + ** + ***************************************************************************/ + String value(); +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIEnumSubSet.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIEnumSubSet.java new file mode 100644 index 00000000..0c38baa5 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIEnumSubSet.java @@ -0,0 +1,55 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.EnumSet; + + +/******************************************************************************* + ** + *******************************************************************************/ +@Target({ ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface OpenAPIEnumSubSet +{ + /*************************************************************************** + ** + ***************************************************************************/ + Class> value(); + + + /*************************************************************************** + ** + ***************************************************************************/ + interface EnumSubSet> + { + /*************************************************************************** + ** + ***************************************************************************/ + EnumSet getSubSet(); + } +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIExclude.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIExclude.java new file mode 100644 index 00000000..de6d6039 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIExclude.java @@ -0,0 +1,38 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/******************************************************************************* + ** + *******************************************************************************/ +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface OpenAPIExclude +{ +} 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 new file mode 100644 index 00000000..497dc090 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIHasAdditionalProperties.java @@ -0,0 +1,38 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/******************************************************************************* + ** + *******************************************************************************/ +@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface OpenAPIHasAdditionalProperties +{ +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIIncludeProperties.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIIncludeProperties.java new file mode 100644 index 00000000..aeb380de --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIIncludeProperties.java @@ -0,0 +1,42 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/*************************************************************************** + ** + ***************************************************************************/ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface OpenAPIIncludeProperties +{ + /*************************************************************************** + ** + ***************************************************************************/ + Class[] ancestorClasses() default { }; +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIListItems.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIListItems.java new file mode 100644 index 00000000..fc04792e --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIListItems.java @@ -0,0 +1,47 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/*************************************************************************** + ** + ***************************************************************************/ +@Target({ ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface OpenAPIListItems +{ + /*************************************************************************** + ** + ***************************************************************************/ + Class value(); + + /*************************************************************************** + ** + ***************************************************************************/ + boolean useRef() default false; +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIMapKnownEntries.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIMapKnownEntries.java new file mode 100644 index 00000000..ffbc354a --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIMapKnownEntries.java @@ -0,0 +1,47 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/*************************************************************************** + ** + ***************************************************************************/ +@Target({ ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface OpenAPIMapKnownEntries +{ + /*************************************************************************** + ** + ***************************************************************************/ + Class value(); + + /*************************************************************************** + ** + ***************************************************************************/ + boolean useRef() default false; +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIMapValueType.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIMapValueType.java new file mode 100644 index 00000000..e82603ce --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIMapValueType.java @@ -0,0 +1,47 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/*************************************************************************** + ** + ***************************************************************************/ +@Target({ ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface OpenAPIMapValueType +{ + /*************************************************************************** + ** + ***************************************************************************/ + Class value(); + + /*************************************************************************** + ** + ***************************************************************************/ + boolean useRef() default false; +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIOneOf.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIOneOf.java new file mode 100644 index 00000000..b7e8861c --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/annotations/OpenAPIOneOf.java @@ -0,0 +1,57 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/*************************************************************************** + ** + ***************************************************************************/ +@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface OpenAPIOneOf +{ + /*************************************************************************** + ** + ***************************************************************************/ + Mode mode() default Mode.PERMITTED_SUBCLASSES; + + /*************************************************************************** + ** + ***************************************************************************/ + Class[] options() default { }; + + + /*************************************************************************** + ** + ***************************************************************************/ + enum Mode + { + PERMITTED_SUBCLASSES, + SPECIFIED_LIST + } +} 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..e27c5e43 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/AbstractEndpointSpec.java @@ -0,0 +1,546 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +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); + default -> throw new IllegalStateException("Unexpected value: " + completeOperation.getHttpMethod()); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public OUTPUT serveRequest(Context context) throws Exception + { + try + { + if(isSecured()) + { + ExecutorSessionUtils.setupSession(context, qInstance); + } + else + { + QContext.setQInstance(qInstance); + } + + INPUT input = buildInput(context); + EXECUTOR executor = newExecutor(); + OUTPUT output = newOutput(); + executor.execute(input, output); + handleOutput(context, output); + return (output); + } + catch(Exception e) + { + handleException(context, e); + return (null); + } + finally + { + QContext.clear(); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected void handleException(Context context, Exception e) + { + QJavalinUtils.handleException(null, context, e); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public Method defineMethod() + { + BasicOperation basicOperation = defineBasicOperation(); + + Method method = new Method() + .withTag(basicOperation.getTag().getText()) + .withSummary(basicOperation.getShortSummary()) + .withDescription(basicOperation.getLongDescription()) + .withParameters(defineRequestParameters()) + .withRequestBody(defineRequestBody()) + .withResponses(defineResponses()); + + customizeMethod(method); + + return (method); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected void customizeMethod(Method method) + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public BasicOperation defineBasicOperation() + { + throw new NotImplementedException(getClass().getSimpleName() + " did not implement defineBasicOperation or defineMethod"); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public CompleteOperation defineCompleteOperation() + { + CompleteOperation completeOperation = new CompleteOperation(defineBasicOperation()); + completeOperation.setMethod(defineMethod()); + return completeOperation; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public boolean isSecured() + { + return (true); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public BasicResponse defineBasicSuccessResponse() + { + return (null); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public List 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..d922ab33 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/AbstractMiddlewareVersion.java @@ -0,0 +1,361 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +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); + } + default -> throw new IllegalStateException("Unexpected value: " + completeOperation.getHttpMethod()); + } + + 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..cfb4c22e --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1.java @@ -0,0 +1,282 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.specs.v1; + + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction; +import com.kingsrook.qqq.backend.core.context.CapturedContext; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; +import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.middleware.javalin.executors.MetaDataExecutor; +import com.kingsrook.qqq.middleware.javalin.executors.io.MetaDataInput; +import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec; +import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation; +import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse; +import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.MetaDataResponseV1; +import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1; +import com.kingsrook.qqq.openapi.model.Example; +import com.kingsrook.qqq.openapi.model.HttpMethod; +import com.kingsrook.qqq.openapi.model.In; +import com.kingsrook.qqq.openapi.model.Parameter; +import com.kingsrook.qqq.openapi.model.Schema; +import com.kingsrook.qqq.openapi.model.Type; +import io.javalin.http.Context; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class MetaDataSpecV1 extends AbstractEndpointSpec +{ + + /*************************************************************************** + ** + ***************************************************************************/ + public BasicOperation defineBasicOperation() + { + return new BasicOperation() + .withPath("/metaData") + .withHttpMethod(HttpMethod.GET) + .withTag(TagsV1.GENERAL) + .withShortSummary("Get instance metaData") + .withLongDescription(""" + Load the overall metadata, as is relevant to a frontend, for the entire application, with permissions applied, as per the + authenticated user. + + This includes: + - Apps (both as a map of name to AppMetaData (`apps`), but also as a tree (`appTree`), for presenting + hierarchical navigation), + - Tables (but without all details, e.g., fields), + - Processes (also without full details, e.g., screens), + - Reports + - Widgets + - Branding + - Help Contents + - Environment values + """ + ); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public List defineRequestParameters() + { + return List.of( + + new Parameter() + .withName("frontendName") + .withDescription(""" + Name of the frontend requesting the meta-data. + Generally a QQQ frontend library, unless a custom application frontend has been built.""") + .withIn(In.QUERY) + .withSchema(new Schema().withType(Type.STRING)) + .withExample("qqq-frontend-material-dashboard"), + + new Parameter() + .withName("frontendVersion") + .withDescription("Version of the frontend requesting the meta-data.") + .withIn(In.QUERY) + .withSchema(new Schema().withType(Type.STRING)) + .withExample("0.23.0"), + + new Parameter() + .withName("applicationName") + .withDescription(""" + Name of the application requesting the meta-data. e.g., an instance of a specific frontend + (i.e., an application might be deployed with 2 different qqq-frontend-material-dashboard frontends, + in which case this attribute allows differentiation between them).""") + .withIn(In.QUERY) + .withSchema(new Schema().withType(Type.STRING)) + .withExample("my-admin-web-app"), + + new Parameter() + .withName("applicationVersion") + .withDescription("Version of the application requesting the meta-data.") + .withIn(In.QUERY) + .withSchema(new Schema().withType(Type.STRING)) + .withExample("20241021") + + ); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public MetaDataInput buildInput(Context context) throws Exception + { + MetaDataInput input = new MetaDataInput(); + + input.setMiddlewareName("qqq-middleware-javalin"); + input.setMiddlewareVersion("v1"); + + input.setFrontendName(getRequestParam(context, "frontendName")); + input.setFrontendVersion(getRequestParam(context, "frontendVersion")); + + input.setApplicationName(getRequestParam(context, "applicationName")); + input.setApplicationVersion(getRequestParam(context, "applicationVersion")); + + return (input); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public Map 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); + + ////////////////////////////////////// + // create stable sorting of entries // + ////////////////////////////////////// + TreeSet capabilities = new TreeSet<>(Comparator.comparing((Capability c) -> c.name())); + capabilities.addAll(Capability.allReadCapabilities()); + capabilities.addAll(Capability.allWriteCapabilities()); + + QTableMetaData exampleTable = new QTableMetaData() + .withName("person") + .withLabel("Person") + .withBackendName("example") + .withPrimaryKeyField("id") + .withIsHidden(false) + .withIcon(new QIcon().withName("person_outline")) + .withEnabledCapabilities(capabilities) + .withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED)) + .withField(new QFieldMetaData("id", QFieldType.INTEGER)); + exampleInstance.addTable(exampleTable); + + QProcessMetaData exampleProcess = new QProcessMetaData() + .withName("samplePersonProcess") + .withLabel("Sample Person Process") + .withTableName("person") + .withIsHidden(false) + .withIcon(new QIcon().withName("person_add")) + .withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED)) + .withStep(new QFrontendStepMetaData().withName("example")); + exampleInstance.addProcess(exampleProcess); + + QAppMetaData childApp = new QAppMetaData() + .withName("childApp") + .withLabel("Child App") + .withIcon(new QIcon().withName("child_friendly")) + .withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED)) + .withChild(exampleProcess); + exampleInstance.addApp(childApp); + + QAppMetaData exampleApp = new QAppMetaData() + .withName("homeApp") + .withLabel("Home App") + .withIcon(new QIcon().withName("home")) + .withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED)) + .withChild(childApp) + .withChild(exampleTable); + exampleInstance.addApp(exampleApp); + + QContext.withTemporaryContext(new CapturedContext(exampleInstance, new QSystemUserSession()), () -> + { + try + { + MetaDataAction metaDataAction = new MetaDataAction(); + MetaDataOutput output = metaDataAction.execute(new com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput()); + examples.put("Example", new Example() + .withValue(new MetaDataResponseV1() + .withMetaDataOutput(output) + ) + ); + } + catch(Exception e) + { + examples.put("Example", new Example().withValue("Error building example: " + e.getMessage()) + ); + } + }); + + return new BasicResponse(""" + Overall metadata for the application.""", + MetaDataResponseV1.class.getSimpleName(), + examples + ); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleOutput(Context context, MetaDataResponseV1 output) throws Exception + { + context.result(JsonUtils.toJson(output)); + } + +} 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..7b7f51c1 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/MetaDataResponseV1.java @@ -0,0 +1,177 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.specs.v1.responses; + + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendWidgetMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.middleware.javalin.executors.io.MetaDataOutputInterface; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapValueType; +import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.AppMetaData; +import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.AppTreeNode; +import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.ProcessMetaDataLight; +import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.TableMetaDataLight; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class MetaDataResponseV1 implements MetaDataOutputInterface, ToSchema +{ + @OpenAPIDescription("Map of all apps within the QQQ Instance (that the user has permission to see that they exist).") + @OpenAPIMapValueType(value = AppMetaData.class, useRef = true) + private Map 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; + + @OpenAPIDescription("Map of all widgets within the QQQ Instance (that the user has permission to see that they exist).") + @OpenAPIMapValueType(value = ProcessMetaDataLight.class, useRef = true) + private Map widgets; + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void setMetaDataOutput(MetaDataOutput metaDataOutput) + { + apps = new HashMap<>(); + for(QFrontendAppMetaData app : CollectionUtils.nonNullMap(metaDataOutput.getApps()).values()) + { + apps.put(app.getName(), new AppMetaData(app)); + } + + appTree = new ArrayList<>(); + for(com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode app : CollectionUtils.nonNullList(metaDataOutput.getAppTree())) + { + appTree.add(new AppTreeNode(app)); + } + + tables = new HashMap<>(); + for(QFrontendTableMetaData table : CollectionUtils.nonNullMap(metaDataOutput.getTables()).values()) + { + tables.put(table.getName(), new TableMetaDataLight(table)); + } + + processes = new HashMap<>(); + for(QFrontendProcessMetaData process : CollectionUtils.nonNullMap(metaDataOutput.getProcesses()).values()) + { + processes.put(process.getName(), new ProcessMetaDataLight(process)); + } + + widgets = new HashMap<>(); + for(QFrontendWidgetMetaData widget : CollectionUtils.nonNullMap(metaDataOutput.getWidgets()).values()) + { + widgets.put(widget.getName(), new WidgetMetaData(widget)); + } + + } + + + + /******************************************************************************* + ** Fluent setter for MetaDataOutput + ** + *******************************************************************************/ + public MetaDataResponseV1 withMetaDataOutput(MetaDataOutput metaDataOutput) + { + setMetaDataOutput(metaDataOutput); + return (this); + } + + + + /******************************************************************************* + ** Getter for apps + ** + *******************************************************************************/ + public Map 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; + } + + + + /******************************************************************************* + ** Getter for widgets + ** + *******************************************************************************/ + public Map getWidgets() + { + return widgets; + } +} \ 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/WidgetMetaData.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/WidgetMetaData.java new file mode 100644 index 00000000..d23cf2a8 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/WidgetMetaData.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; + + +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendWidgetMetaData; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class WidgetMetaData +{ + @OpenAPIExclude() + private QFrontendWidgetMetaData wrapped; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetMetaData(QFrontendWidgetMetaData wrapped) + { + this.wrapped = wrapped; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetMetaData() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Unique name for this widget within the QQQ Instance") + public String getName() + { + return (this.wrapped.getName()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("User-facing name for this widget") + public String getLabel() + { + return (this.wrapped.getLabel()); + } + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("The type of this widget.") + // todo enum of the NAMES of the widget types?? or, can we just f'ing change to return the enum.name's? + public String getType() + { + return this.wrapped.getType(); + } + +} 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..a31855d7 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppMetaData.java @@ -0,0 +1,161 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components; + + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapKnownEntries; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapValueType; + + +/*************************************************************************** + ** + ***************************************************************************/ +public class AppMetaData implements ToSchema +{ + @OpenAPIExclude() + private QFrontendAppMetaData wrapped; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public AppMetaData(QFrontendAppMetaData wrapped) + { + this.wrapped = wrapped; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public AppMetaData() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Unique name for this app within the QQQ Instance") + public String getName() + { + return (this.wrapped.getName()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("User-facing name for this app") + public String getLabel() + { + return (this.wrapped.getLabel()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Icon to display for the app.") + @OpenAPIMapKnownEntries(value = Icon.class, useRef = true) + public Icon getIcon() + { + return (this.wrapped.getIcon() == null ? null : new Icon(this.wrapped.getIcon())); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("List of widgets names that are part of this app. These strings should be keys to the widgets map in the QQQ Instance.") + @OpenAPIListItems(value = String.class) + public List 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..62d6c7ae --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/AppTreeNode.java @@ -0,0 +1,120 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapKnownEntries; + + +/*************************************************************************** + ** + ***************************************************************************/ +public class AppTreeNode implements ToSchema +{ + @OpenAPIExclude() + private com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode wrapped; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public AppTreeNode(com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode wrapped) + { + this.wrapped = wrapped; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public AppTreeNode() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("The type of node (table, process, report, app)") + public String getType() + { + return (this.wrapped.getType() == null ? null : this.wrapped.getType().name()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Unique (within its type) name for this element. e.g., for type = 'table', the table's name.") + public String getName() + { + return (this.wrapped.getName()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("User-facing name of the element.") + public String getLabel() + { + return (this.wrapped.getLabel()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Child elements. Only applies for type='app', which contains additional apps under it") + @OpenAPIListItems(value = AppTreeNode.class, useRef = true) + public List getChildren() + { + return (CollectionUtils.nonNullList(this.wrapped.getChildren()).stream().map(a -> new AppTreeNode(a)).toList()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Icon to display for the item.") + @OpenAPIMapKnownEntries(value = Icon.class, useRef = true) + public Icon getIcon() + { + return (this.wrapped.getIcon() == null ? null : new Icon(this.wrapped.getIcon())); + } + +} 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..33a204e4 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FrontendStep.java @@ -0,0 +1,144 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class FrontendStep implements ToSchema +{ + @OpenAPIExclude() + private QFrontendStepMetaData wrapped; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public FrontendStep(QFrontendStepMetaData wrapped) + { + this.wrapped = wrapped; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public FrontendStep() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("The unique name for this step within its process") + public String getName() + { + return (this.wrapped.getName()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("The user-facing name for this step") + public String getLabel() + { + return (this.wrapped.getLabel()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("An optional indicator of the screen format preferred by the application to be used for this screen. Different frontends may support different formats, and implement them differently.") + public String getFormat() + { + return (this.wrapped.getFormat()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("The components that make up this screen") + @OpenAPIListItems(value = FrontendComponent.class, useRef = true) + public List 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..acd2a3c7 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/ProcessMetaDataLight.java @@ -0,0 +1,139 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components; + + +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapKnownEntries; + + +/*************************************************************************** + ** + ***************************************************************************/ +public class ProcessMetaDataLight implements ToSchema +{ + @OpenAPIExclude() + protected QFrontendProcessMetaData wrapped; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ProcessMetaDataLight(QFrontendProcessMetaData wrapped) + { + this.wrapped = wrapped; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ProcessMetaDataLight() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Unique name for this process within the QQQ Instance") + public String getName() + { + return (this.wrapped.getName()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("User-facing name for this process") + public String getLabel() + { + return (this.wrapped.getLabel()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("If this process is associated with a table, the table name is given here") + public String getTableName() + { + return (this.wrapped.getTableName()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Boolean indicator of whether the process should be shown to users or not") + public Boolean getIsHidden() + { + return (this.wrapped.getIsHidden()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Indicator of the Step Flow used by the process. Possible values are: LINEAR, STATE_MACHINE.") + public String getStepFlow() + { + return (this.wrapped.getStepFlow()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Icon to display for the process.") + @OpenAPIMapKnownEntries(value = Icon.class, useRef = true) + public Icon getIcon() + { + return (this.wrapped.getIcon() == null ? null : new Icon(this.wrapped.getIcon())); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Boolean to indicate if the user has permission for the process.") + public Boolean getHasPermission() + { + return (this.wrapped.getHasPermission()); + } + +} 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..c9ea3a8a --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableMetaDataLight.java @@ -0,0 +1,189 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapKnownEntries; + + +/*************************************************************************** + ** + ***************************************************************************/ +public class TableMetaDataLight implements ToSchema +{ + @OpenAPIExclude() + private QFrontendTableMetaData wrapped; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public TableMetaDataLight(QFrontendTableMetaData wrapped) + { + this.wrapped = wrapped; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public TableMetaDataLight() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Unique name for this table within the QQQ Instance") + public String getName() + { + return (this.wrapped.getName()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("User-facing name for this table") + public String getLabel() + { + return (this.wrapped.getLabel()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Boolean indicator of whether the table should be shown to users or not") + public Boolean getIsHidden() + { + return (this.wrapped.getIsHidden()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Icon to display for the table") + @OpenAPIMapKnownEntries(value = Icon.class, useRef = true) + public Icon getIcon() + { + return (this.wrapped.getIcon() == null ? null : new Icon(this.wrapped.getIcon())); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("List of strings describing actions that are supported by the backend application for the table.") + @OpenAPIListItems(value = String.class) // todo - better, enum + public List 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..92a70bb6 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlock.java @@ -0,0 +1,232 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components; + + +import java.io.Serializable; +import java.util.EnumSet; +import java.util.List; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.CompositeWidgetData; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIEnumSubSet; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIOneOf; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class WidgetBlock implements Serializable, ToSchema +{ + @OpenAPIExclude() + private static final QLogger LOG = QLogger.getLogger(WidgetBlock.class); + + @OpenAPIExclude() + private final AbstractBlockWidgetData wrapped; + + + + /*************************************************************************** + ** + ***************************************************************************/ + public WidgetBlock(AbstractBlockWidgetData abstractBlockWidgetData) + { + this.wrapped = abstractBlockWidgetData; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Unique identifier for this block within it widget. Used as a key for helpContents.") + public String getBlockId() + { + return (this.wrapped.getBlockId()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public enum BlockType + { + BUTTON, + AUDIO, + BIG_NUMBER, + COMPOSITE, + DIVIDER, + IMAGE, + INPUT_FIELD, + NUMBER_ICON_BADGE, + PROGRESS_BAR, + // todo? REVEAL, + TABLE_SUB_ROW_DETAIL_ROW, + TEXT, + UP_OR_DOWN_NUMBER + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("What type of block to render.") + public BlockType getBlockType() + { + return (BlockType.valueOf(this.wrapped.getBlockTypeName())); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Values to show in the block, or otherwise control its behavior. Different fields based on blockType.") + @OpenAPIOneOf() + public WidgetBlockValues getValues() + { + return (WidgetBlockValues.of(this.wrapped.getValues())); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Styles to apply to the block. Different fields based on blockType.") + @OpenAPIOneOf() + public WidgetBlockStyles getStyles() + { + return (WidgetBlockStyles.of(this.wrapped.getStyles())); + } + + // todo link, links + + // todo tooltip, tooltips + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Optional field name (e.g,. from a process's set of fields) to act as a 'guard' for the block - e.g., only include it in the UI if the value for this field is true") + public String getConditional() + { + return (this.wrapped.getConditional()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("For COMPOSITE type blocks, a list of sub-blocks.") + @OpenAPIListItems(value = WidgetBlock.class, useRef = true) + public List getSubBlocks() + { + if(this.wrapped instanceof CompositeWidgetData compositeWidgetData) + { + return (compositeWidgetData.getBlocks() == null ? null : compositeWidgetData.getBlocks().stream().map(b -> new WidgetBlock(b)).toList()); + } + + return (null); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("For COMPOSITE type blocks, optional control to make the widget appear modally") + public CompositeWidgetData.ModalMode getModalMode() + { + if(this.wrapped instanceof CompositeWidgetData compositeWidgetData) + { + return (compositeWidgetData.getModalMode()); + } + + return (null); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static class LayoutSubSet implements OpenAPIEnumSubSet.EnumSubSet + { + /*************************************************************************** + ** + ***************************************************************************/ + @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/WidgetBlockAudioValues.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockAudioValues.java new file mode 100644 index 00000000..c88e771a --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockAudioValues.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.dashboard.widgets.blocks.audio.AudioValues; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; + + +/******************************************************************************* + ** + *******************************************************************************/ +@OpenAPIDescription("Values used for an AUDIO type widget block") +public final class WidgetBlockAudioValues implements WidgetBlockValues +{ + @OpenAPIExclude() + private AudioValues wrapped; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetBlockAudioValues(AudioValues textValues) + { + this.wrapped = textValues; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetBlockAudioValues() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("The path to the audio file on the server") + public String getPath() + { + return (this.wrapped.getPath()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Control if the file should automatically play when the block is rendered") + public Boolean getAutoPlay() + { + return (this.wrapped.getAutoPlay()); + } + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Control if on-screen controls should be shown to allow the user to control playback") + public Boolean getShowControls() + { + return (this.wrapped.getShowControls()); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockBaseStyles.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockBaseStyles.java new file mode 100644 index 00000000..66541a85 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockBaseStyles.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.responses.components; + + +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; + + +/******************************************************************************* + ** + *******************************************************************************/ +public final class WidgetBlockBaseStyles implements WidgetBlockStyles +{ + @OpenAPIExclude() + private BaseStyles wrapped; + + + + /*************************************************************************** + ** + ***************************************************************************/ + public WidgetBlockBaseStyles(BaseStyles baseStyles) + { + this.wrapped = baseStyles; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetBlockBaseStyles() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Optional padding to apply to the block") + public BaseStyles.Directional getPadding() + { + return (this.wrapped.getPadding()); + } + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("A background color to use for the block") + public String getBackgroundColor() + { + return (this.wrapped.getBackgroundColor()); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockButtonStyles.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockButtonStyles.java new file mode 100644 index 00000000..5f06ea61 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockButtonStyles.java @@ -0,0 +1,84 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components; + + +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.button.ButtonStyles; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; + + +/******************************************************************************* + ** + *******************************************************************************/ +public final class WidgetBlockButtonStyles implements WidgetBlockStyles +{ + + @OpenAPIExclude() + private ButtonStyles wrapped; + + + + /*************************************************************************** + ** + ***************************************************************************/ + public WidgetBlockButtonStyles(ButtonStyles buttonStyles) + { + this.wrapped = buttonStyles; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetBlockButtonStyles() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("A Color to use for the button. May be specified as a StandardColor (one of: " + + "SUCCESS, WARNING, ERROR, INFO, MUTED) or an RGB code.") + public String getColor() + { + return (this.wrapped.getColor()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("An optional indicator of the screen format preferred by the application to be used for this block, " + + "such as OUTLINED, FILLED, or TEXT. Different frontends may support different formats, and implement them differently.") + public String getFormat() + { + return (this.wrapped.getFormat()); + } + + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockButtonValues.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockButtonValues.java new file mode 100644 index 00000000..73ff0022 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockButtonValues.java @@ -0,0 +1,123 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components; + + +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.button.ButtonValues; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; + + +/******************************************************************************* + ** + *******************************************************************************/ +@OpenAPIDescription("Values used for a BUTTON type widget block") +public final class WidgetBlockButtonValues implements WidgetBlockValues +{ + @OpenAPIExclude() + private ButtonValues wrapped; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetBlockButtonValues(ButtonValues buttonValues) + { + this.wrapped = buttonValues; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetBlockButtonValues() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("User-facing label to display in the button") + public String getLabel() + { + return (this.wrapped.getLabel()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Code used within the app as the value submitted when the button is clicked") + public String getActionCode() + { + return (this.wrapped.getActionCode()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription(""" + Instructions for what should happen in the frontend (e.g., within a screen), when the button is clicked. + + To show a modal composite block, use format: `showModal:${blockId}` (e.g., `showModal:myBlock`) + + To hide a modal composite block, use format: `hideModal:${blockId}` (e.g., `hideModal:myBlock`) + + To toggle visibility of a modal composite block, use format: `toggleModal:${blockId}` (e.g., `toggleModal:myBlock`) + """) + public String getControlCode() + { + return (this.wrapped.getControlCode()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("An optional icon to display before the text in the button") + public Icon getStartIcon() + { + return (this.wrapped.getStartIcon() == null ? null : new Icon(this.wrapped.getStartIcon())); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("An optional icon to display after the text in the button") + public Icon getEndIcon() + { + return (this.wrapped.getEndIcon() == null ? null : new Icon(this.wrapped.getEndIcon())); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockImageStyles.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockImageStyles.java new file mode 100644 index 00000000..c2fc96f2 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockImageStyles.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.dashboard.widgets.blocks.base.BaseStyles; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.image.ImageStyles; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; + + +/******************************************************************************* + ** + *******************************************************************************/ +public final class WidgetBlockImageStyles implements WidgetBlockStyles +{ + @OpenAPIExclude() + private ImageStyles wrapped; + + + + /*************************************************************************** + ** + ***************************************************************************/ + public WidgetBlockImageStyles(ImageStyles imageStyles) + { + this.wrapped = imageStyles; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetBlockImageStyles() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("A request to render the image at a specified width.") + public String getWidth() + { + return (this.wrapped.getWidth()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("A request to render the image at a specified height.") + public String getHeight() + { + return (this.wrapped.getHeight()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Optional padding to apply to the image") + public BaseStyles.Directional getPadding() + { + return (this.wrapped.getPadding()); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockImageValues.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockImageValues.java new file mode 100644 index 00000000..1665c375 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockImageValues.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.image.ImageValues; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; + + +/******************************************************************************* + ** + *******************************************************************************/ +@OpenAPIDescription("Values used for an IMAGE type widget block") +public final class WidgetBlockImageValues implements WidgetBlockValues +{ + @OpenAPIExclude() + private ImageValues wrapped; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetBlockImageValues(ImageValues textValues) + { + this.wrapped = textValues; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetBlockImageValues() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("The path to the image on the server") + public String getPath() + { + return (this.wrapped.getPath()); + } + +} 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..76e50df1 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockInputFieldValues.java @@ -0,0 +1,115 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components; + + +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.inputfield.InputFieldValues; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; + + +/******************************************************************************* + ** + *******************************************************************************/ +@OpenAPIDescription("Values used for an INPUT_FIELD type widget block") +public final class WidgetBlockInputFieldValues implements WidgetBlockValues +{ + @OpenAPIExclude() + private InputFieldValues wrapped; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetBlockInputFieldValues(InputFieldValues textValues) + { + this.wrapped = textValues; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetBlockInputFieldValues() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Metadata to define the field that this block controls") + public FieldMetaData getFieldMetaData() + { + return (new FieldMetaData(this.wrapped.getFieldMetaData())); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Indicate whether this field should auto-focus when it is rendered") + public Boolean getAutoFocus() + { + return (this.wrapped.getAutoFocus()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Indicate whether the form that this field is on should be submitted when Enter is pressed") + public Boolean getSubmitOnEnter() + { + return (this.wrapped.getSubmitOnEnter()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Indicate if the frontend uses a software/on-screen keyboard, if the application should try to hide it (e.g., upon auto-focus).") + public Boolean getHideSoftKeyboard() + { + return (this.wrapped.getHideSoftKeyboard()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("Optional placeholder text to display in the input box.") + public String getPlaceholder() + { + return (this.wrapped.getPlaceholder()); + } + +} 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..ac7c0da8 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockStyles.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.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStylesInterface; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.button.ButtonStyles; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.image.ImageStyles; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.text.TextStyles; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; + + +/******************************************************************************* + ** + *******************************************************************************/ +public sealed interface WidgetBlockStyles extends ToSchema permits + WidgetBlockBaseStyles, + WidgetBlockButtonStyles, + WidgetBlockImageStyles, + WidgetBlockTextStyles +{ + @OpenAPIExclude + QLogger LOG = QLogger.getLogger(WidgetBlockStyles.class); + + + /*************************************************************************** + ** + ***************************************************************************/ + static WidgetBlockStyles of(BlockStylesInterface blockStyles) + { + if(blockStyles == null) + { + return (null); + } + + if(blockStyles instanceof ButtonStyles s) + { + return (new WidgetBlockButtonStyles(s)); + } + else if(blockStyles instanceof ImageStyles s) + { + return (new WidgetBlockImageStyles(s)); + } + else if(blockStyles instanceof TextStyles s) + { + return (new WidgetBlockTextStyles(s)); + } + ////////////////////////////////////////////////////////////////////////////////////////////// + // note - important for this one to be last, since it's a base class to some of the above!! // + ////////////////////////////////////////////////////////////////////////////////////////////// + else if(blockStyles instanceof BaseStyles s) + { + return (new WidgetBlockBaseStyles(s)); + } + + LOG.warn("Unrecognized block value type: " + blockStyles.getClass().getName()); + return (null); + } + +} 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..d8355517 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockTextStyles.java @@ -0,0 +1,106 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components; + + +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.text.TextStyles; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; + + +/******************************************************************************* + ** + *******************************************************************************/ +public final class WidgetBlockTextStyles implements WidgetBlockStyles +{ + @OpenAPIExclude() + private TextStyles wrapped; + + + + /*************************************************************************** + ** + ***************************************************************************/ + public WidgetBlockTextStyles(TextStyles textStyles) + { + this.wrapped = textStyles; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetBlockTextStyles() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("A Color to display the text in. May be specified as a StandardColor (one of: " + + "SUCCESS, WARNING, ERROR, INFO, MUTED) or an RGB code.") + public String getColor() + { + return (this.wrapped.getColor()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("An optional indicator of the screen format preferred by the application to be used for this block. " + + "Different frontends may support different formats, and implement them differently.") + public String getFormat() + { + return (this.wrapped.getFormat()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("An optional indicator of the weight at which the text should be rendered. May be a named value (one of" + + "extralight, thin, medium, black, semibold, bold, extrabold) or a numeric, e.g., 100, 200, ..., 900") + public String getWeight() + { + return (this.wrapped.getWeight()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("An optional indicator of the size at which the text should be rendered. May be a named value (one of" + + "largest, headline, title, body, smallest) or a numeric size - both are up to the frontend to interpret.") + public String getSize() + { + return (this.wrapped.getSize()); + } + +} 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..eef1c051 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockTextValues.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.text.TextValues; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; + + +/******************************************************************************* + ** + *******************************************************************************/ +@OpenAPIDescription("Values used for a TEXT type widget block") +public final class WidgetBlockTextValues implements WidgetBlockValues +{ + @OpenAPIExclude() + private TextValues wrapped; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetBlockTextValues(TextValues textValues) + { + this.wrapped = textValues; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetBlockTextValues() + { + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("The text to display in the block") + public String getText() + { + return (this.wrapped.getText()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("An optional icon to display before the text") + public Icon getStartIcon() + { + return (this.wrapped.getStartIcon() == null ? null : new Icon(this.wrapped.getStartIcon())); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @OpenAPIDescription("An optional icon to display after the text") + public Icon getEndIcon() + { + return (this.wrapped.getEndIcon() == null ? null : new Icon(this.wrapped.getEndIcon())); + } + +} 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..2f17949e --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/WidgetBlockValues.java @@ -0,0 +1,85 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components; + + +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.audio.AudioValues; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.button.ButtonValues; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.image.ImageValues; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.inputfield.InputFieldValues; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.text.TextValues; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema; +import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude; + + +/******************************************************************************* + ** + *******************************************************************************/ +public sealed interface WidgetBlockValues extends ToSchema permits + WidgetBlockAudioValues, + WidgetBlockButtonValues, + WidgetBlockImageValues, + WidgetBlockInputFieldValues, + WidgetBlockTextValues +{ + @OpenAPIExclude + QLogger LOG = QLogger.getLogger(WidgetBlockValues.class); + + + /*************************************************************************** + ** + ***************************************************************************/ + static WidgetBlockValues of(BlockValuesInterface blockValues) + { + if(blockValues == null) + { + return (null); + } + + if(blockValues instanceof AudioValues v) + { + return (new WidgetBlockAudioValues(v)); + } + else if(blockValues instanceof ButtonValues v) + { + return (new WidgetBlockButtonValues(v)); + } + else if(blockValues instanceof ImageValues v) + { + return (new WidgetBlockImageValues(v)); + } + else if(blockValues instanceof InputFieldValues v) + { + return (new WidgetBlockInputFieldValues(v)); + } + else if(blockValues instanceof TextValues v) + { + return (new WidgetBlockTextValues(v)); + } + + LOG.warn("Unrecognized block value type: " + blockValues.getClass().getName()); + return (null); + } + +} 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..9b1ffca7 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/ProcessSpecUtilsV1.java @@ -0,0 +1,281 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.specs.v1.utils; + + +import java.io.File; +import java.io.Serializable; +import java.time.LocalDate; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessMetaDataAdjustment; +import com.kingsrook.qqq.backend.core.model.actions.processes.QUploadedFile; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; +import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; +import com.kingsrook.qqq.middleware.javalin.executors.io.ProcessInitOrStepOrStatusOutputInterface; +import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.ProcessInitOrStepOrStatusResponseV1; +import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.WidgetBlock; +import com.kingsrook.qqq.openapi.model.Example; +import com.kingsrook.qqq.openapi.model.Schema; +import io.javalin.http.Context; +import org.json.JSONArray; +import org.json.JSONObject; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ProcessSpecUtilsV1 +{ + private static final QLogger LOG = QLogger.getLogger(ProcessSpecUtilsV1.class); + + public static final String EXAMPLE_PROCESS_UUID = "01234567-89AB-CDEF-0123-456789ABCDEF"; + public static final String EXAMPLE_JOB_UUID = "98765432-10FE-DCBA-9876-543210FEDCBA"; + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static String getResponseSchemaRefName() + { + return ("ProcessStepResponseV1"); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static Schema buildResponseSchema() + { + return new Schema().withOneOf(List.of( + new Schema().withRef("#/components/schemas/ProcessStepComplete"), + new Schema().withRef("#/components/schemas/ProcessStepJobStarted"), + new Schema().withRef("#/components/schemas/ProcessStepRunning"), + new Schema().withRef("#/components/schemas/ProcessStepError") + )); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static LinkedHashMap 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) + { + //////////////////////////////////////////////////////////////////////////////////// + // here's where we'll handle the values map specially - note - that we'll also // + // be mapping some specific object types into their API-versioned responses types // + //////////////////////////////////////////////////////////////////////////////////// + Map values = complete.getValues(); + if(values != null) + { + JSONObject valuesAsJsonObject = new JSONObject(); + for(Map.Entry valueEntry : values.entrySet()) + { + String name = valueEntry.getKey(); + Serializable value = valueEntry.getValue(); + + Serializable valueToMakeIntoJson = value; + if(value instanceof String s) + { + valuesAsJsonObject.put(name, s); + continue; + } + else if(value instanceof Boolean b) + { + valuesAsJsonObject.put(name, b); + continue; + } + else if(value instanceof Number n) + { + valuesAsJsonObject.put(name, n); + continue; + } + else if(value == null) + { + valuesAsJsonObject.put(name, (Object) null); + continue; + } + ////////////////////////////////////////////////////////////////////////////////// + // if there are any types that we want to make sure we send back using this API // + // version's mapped objects, then add cases for them here, and wrap them. // + ////////////////////////////////////////////////////////////////////////////////// + else if(value instanceof AbstractBlockWidgetData abstractBlockWidgetData) + { + valueToMakeIntoJson = new WidgetBlock(abstractBlockWidgetData); + } + + String valueAsJsonString = JsonUtils.toJson(valueToMakeIntoJson, mapper -> + { + mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS); + }); + + if(valueAsJsonString.startsWith("[")) + { + valuesAsJsonObject.put(name, new JSONArray(valueAsJsonString)); + } + else if(valueAsJsonString.startsWith("{")) + { + valuesAsJsonObject.put(name, new JSONObject(valueAsJsonString)); + } + else + { + /////////////////////////////////////////////////////////////////////////////////// + // curious, if/when this ever happens, since we should get all "primitive" types // + // above, and everything else, I think, would be an object or an array, right? // + /////////////////////////////////////////////////////////////////////////////////// + valuesAsJsonObject.put(name, valueAsJsonString); + } + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // ... this might be a concept for us at some point in time - but might be better to not do as a value itself? // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Serializable backendOnlyValues = values.get("_qqqBackendOnlyValues"); + // if(backendOnlyValues instanceof String backendOnlyValuesString) + // { + // for(String key : backendOnlyValuesString.split(",")) + // { + // jsonObject.remove(key); + // } + // jsonObject.remove("_qqqBackendOnlyValues"); + // } + + outputJsonObject.put("values", valuesAsJsonObject); + } + } + + String json = outputJsonObject.toString(3); + System.out.println(json); + context.result(json); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void archiveUploadedFile(String processName, QUploadedFile qUploadedFile) + { + String fileName = QValueFormatter.formatDate(LocalDate.now()) + + File.separator + processName + + File.separator + qUploadedFile.getFilename(); + + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(QJavalinImplementation.getJavalinMetaData().getUploadedFileArchiveTableName()); + insertInput.setRecords(List.of(new QRecord() + .withValue("fileName", fileName) + .withValue("contents", qUploadedFile.getBytes()) + )); + + new InsertAction().executeAsync(insertInput); + } +} 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..d648ba54 --- /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.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; +import com.kingsrook.qqq.middleware.javalin.executors.io.QueryMiddlewareInput; +import com.kingsrook.qqq.openapi.model.Schema; +import com.kingsrook.qqq.openapi.model.Type; +import org.json.JSONArray; +import org.json.JSONObject; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QuerySpecUtils +{ + + /*************************************************************************** + ** + ***************************************************************************/ + public static Schema defineQueryJoinsSchema() + { + Schema queryJoinsSchema = new Schema() + .withType(Type.ARRAY) + .withItems(new Schema() + .withProperties(MapBuilder.of( + "joinTable", new Schema() + .withType(Type.STRING), + "select", new Schema() + .withType(Type.BOOLEAN), + "type", new Schema() + .withType(Type.STRING) + .withEnumValues(Arrays.stream(QueryJoin.Type.values()).map(o -> o.name()).toList()), + "alias", new Schema() + .withType(Type.STRING), + "baseTableOrAlias", new Schema() + .withType(Type.STRING) + )) + ); + return queryJoinsSchema; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static Schema defineQQueryFilterSchema() + { + Schema qQueryFilterSchema = new Schema() + .withType(Type.OBJECT) + .withExample(List.of( + JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.LESS_THAN, 5))) + )) + .withProperties(MapBuilder.of( + "criteria", new Schema() + .withType(Type.ARRAY) + .withItems(new Schema() + .withProperties(MapBuilder.of( + "fieldName", new Schema() + .withType(Type.STRING), + "operator", new Schema() + .withType(Type.STRING) + .withEnumValues(Arrays.stream(QCriteriaOperator.values()).map(o -> o.name()).toList()), + "values", new Schema() + .withType(Type.ARRAY) + .withItems(new Schema().withOneOf(List.of( + new Schema().withType(Type.INTEGER), + new Schema().withType(Type.STRING) + ))) + )) + ), + "orderBys", new Schema() + .withType(Type.ARRAY) + .withItems(new Schema() + .withProperties(MapBuilder.of( + "fieldName", new Schema() + .withType(Type.STRING), + "isAscending", new Schema() + .withType(Type.BOOLEAN))) + ), + "booleanOperator", new Schema().withType(Type.STRING).withEnumValues(Arrays.stream(QQueryFilter.BooleanOperator.values()).map(o -> o.name()).toList()), + "skip", new Schema().withType(Type.INTEGER), + "limit", new Schema().withType(Type.INTEGER) + // todo - subfilters?? + )); + return qQueryFilterSchema; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static Schema getQueryResponseSchema() + { + Schema schema = new Schema() + .withDescription("Records found by the query. May be empty.") + .withType(Type.OBJECT) + .withProperties(MapBuilder.of( + "records", new Schema() + .withType(Type.ARRAY) + .withItems(new Schema() + .withType(Type.OBJECT) + .withProperties(MapBuilder.of( + "recordLabel", new Schema().withType(Type.STRING), + "tableName", new Schema().withType(Type.STRING), + "values", new Schema().withType(Type.OBJECT).withDescription("Keys for each field in the table"), + "displayValues", new Schema().withType(Type.OBJECT) + )) + ) + )); + return schema; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static QueryMiddlewareInput buildInput(Map 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/java/com/kingsrook/qqq/middleware/javalin/tools/APIUtils.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/APIUtils.java new file mode 100644 index 00000000..72c85d7c --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/APIUtils.java @@ -0,0 +1,160 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.tools; + + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import org.yaml.snakeyaml.LoaderOptions; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class APIUtils +{ + public static final String PUBLISHED_API_LOCATION = "qqq-middleware-javalin/src/main/resources/openapi/"; + public static final List FILE_FORMATS = List.of("json", "yaml"); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String getPublishedAPIFile(String apiPath, String name, String fileFormat) throws Exception + { + String fileLocation = apiPath + "/" + name + "." + fileFormat; + File file = new File(fileLocation); + if(!file.exists()) + { + throw (new Exception("Error: The file [" + file.getPath() + "] could not be found.")); + } + + Path path = Paths.get(fileLocation); + return (StringUtils.join("\n", Files.readAllLines(path))); + } + + + + /******************************************************************************* + ** get a map representation of the yaml. + ** we'll remove things from that map, writing out specific sub-files that we know we want (e.g., per-table & process). + ** also, there are some objects we just don't care about (e.g., tags, they'll always be in lock-step with the tables). + ** then we'll write out everything else left in the map at the end. + *******************************************************************************/ + @SuppressWarnings("unchecked") + static Map> splitUpYamlForPublishing(String openApiYaml) throws JsonProcessingException + { + Map apiYaml = parseYaml(openApiYaml); + Map components = (Map) apiYaml.get("components"); + Map schemas = (Map) components.get("schemas"); + Map paths = (Map) apiYaml.remove("paths"); + apiYaml.remove("tags"); + + Map> groupedPaths = new HashMap<>(); + for(Map.Entry entry : paths.entrySet()) + { + /////////////////////////////////////////////////////////////////////////////// + // keys here look like: apiName/apiVersion/table-or-process/ // + // let's discard the apiName & version, and group under the table-or-process // + /////////////////////////////////////////////////////////////////////////////// + String key = entry.getKey(); + String[] parts = key.split("/"); + String uniquePart = parts[3]; + groupedPaths.computeIfAbsent(uniquePart, k -> new TreeMap<>()); + groupedPaths.get(uniquePart).put(entry.getKey(), entry.getValue()); + } + + for(Map.Entry> entry : groupedPaths.entrySet()) + { + String name = entry.getKey(); + Map subMap = entry.getValue(); + if(schemas.containsKey(name)) + { + subMap.put("schema", schemas.remove(name)); + } + + name += "SearchResult"; + if(schemas.containsKey(name)) + { + subMap.put("searchResultSchema", schemas.remove(name)); + } + } + + //////////////////////////////////////////////////////// + // put the left-over yaml as a final entry in the map // + //////////////////////////////////////////////////////// + groupedPaths.put("openapi", apiYaml); + + return groupedPaths; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + private static Map parseYaml(String yaml) throws JsonProcessingException + { + //////////////////////////////////////////////////////////////////////////////////////////////////////// + // need a larger limit than you get by default (and qqq's yamlUtils doens't let us customize this...) // + //////////////////////////////////////////////////////////////////////////////////////////////////////// + LoaderOptions loaderOptions = new LoaderOptions(); + loaderOptions.setCodePointLimit(100 * 1024 * 1024); // 100 MB + YAMLFactory yamlFactory = YAMLFactory.builder() + .loaderOptions(loaderOptions) + .build(); + YAMLMapper mapper = new YAMLMapper(yamlFactory); + + mapper.findAndRegisterModules(); + return (mapper.readValue(yaml, Map.class)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static File[] listPublishedAPIFiles(String path) throws Exception + { + File file = new File(path); + if(!file.exists()) + { + throw (new Exception("Error: API Directory [" + file.getPath() + "] could not be found.")); + } + + File[] files = file.listFiles(); + return (files); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/PublishAPI.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/PublishAPI.java new file mode 100644 index 00000000..a411c403 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/PublishAPI.java @@ -0,0 +1,150 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.tools; + + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.Callable; +import com.fasterxml.jackson.databind.MapperFeature; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.YamlUtils; +import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion; +import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1; +import com.kingsrook.qqq.openapi.model.OpenAPI; +import picocli.CommandLine; + + +/******************************************************************************* + ** + *******************************************************************************/ +@CommandLine.Command(name = "publishAPI") +public class PublishAPI implements Callable +{ + @CommandLine.Option(names = { "-r", "--repoRoot" }) + private String repoRoot; + + @CommandLine.Option(names = { "--sortFileContentsForHuman" }, description = "By default, properties in the yaml are sorted alphabetically, to help with stability (for diffing). This option preserves the 'natural' order of the file, so may look a little bette for human consumption") + private boolean sortFileContentsForHuman = false; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void main(String[] args) throws Exception + { + // for a run from the IDE, to override args... args = new String[] { "-r", "/Users/dkelkhoff/git/kingsrook/qqq/" }; + int exitCode = new CommandLine(new PublishAPI()).execute(args); + System.exit(exitCode); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Integer call() throws Exception + { + AbstractMiddlewareVersion middlewareVersion = new MiddlewareVersionV1(); + + if(!StringUtils.hasContent(repoRoot)) + { + throw (new QException("Repo root argument was not given.")); + } + + if(!new File(repoRoot).exists()) + { + throw (new QException("Repo root directory [" + repoRoot + "] was not found.")); + } + + String allApisPath = repoRoot + "/" + APIUtils.PUBLISHED_API_LOCATION + "/"; + if(!new File(allApisPath).exists()) + { + throw (new QException("APIs directory [" + allApisPath + "] was not found.")); + } + + File versionDirectory = new File(allApisPath + middlewareVersion.getVersion() + "/"); + if(!versionDirectory.exists()) + { + if(!versionDirectory.mkdirs()) + { + // CTEngCliUtils.printError("Error: An error occurred creating directory [" + apiDirectory.getPath() + "]."); + System.err.println("Error: An error occurred creating directory [" + versionDirectory.getPath() + "]."); + return (1); + } + } + + ///////////////////////////////////////////////////////////////////////////////////////////////// + // build the openapi spec - then run it through a "grouping" function, which will make several // + // subsets of it (e.g., grouped by table mostly) - then we'll write out each such file // + ///////////////////////////////////////////////////////////////////////////////////////////////// + OpenAPI openAPI = middlewareVersion.generateOpenAPIModel("qqq"); + String yaml = YamlUtils.toYaml(openAPI, mapper -> + { + if(sortFileContentsForHuman) + { + //////////////////////////////////////////////// + // this is actually the default mapper config // + //////////////////////////////////////////////// + } + else + { + mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); + } + }); + + writeFile(yaml, versionDirectory, "openapi.yaml"); + + ////////////////////////////////////////////////////////////////////////////////////// + // if we want to split up by some paths, components, we could use a version of this // + ////////////////////////////////////////////////////////////////////////////////////// + // Map> groupedPaths = APIUtils.splitUpYamlForPublishing(yaml); + // for(String name : groupedPaths.keySet()) + // { + // writeFile(groupedPaths.get(name), versionDirectory, name + ".yaml"); + // } + // CTEngCliUtils.printSuccess("Files for [" + apiInstanceMetaData.getName() + "] [" + apiVersion + "] have been successfully published."); + // System.out.println("Files for [" + middlewareVersion.getClass().getSimpleName() + "] have been successfully published."); + + return (0); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void writeFile(String yaml, File directory, String fileBaseName) throws IOException + { + String yamlFileName = directory.getAbsolutePath() + "/" + fileBaseName; + Path yamlPath = Paths.get(yamlFileName); + Files.write(yamlPath, yaml.getBytes()); + System.out.println("Wrote [" + yamlPath + "]"); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/ValidateAPIVersions.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/ValidateAPIVersions.java new file mode 100644 index 00000000..fdc918ae --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/ValidateAPIVersions.java @@ -0,0 +1,253 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.tools; + + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import com.fasterxml.jackson.databind.MapperFeature; +import com.kingsrook.qqq.backend.core.utils.YamlUtils; +import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion; +import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1; +import com.kingsrook.qqq.openapi.model.OpenAPI; +import picocli.CommandLine; + + +/******************************************************************************* + ** + *******************************************************************************/ +@CommandLine.Command(name = "validateApiVersions") +public class ValidateAPIVersions implements Callable +{ + @CommandLine.Option(names = { "-r", "--repoRoot" }) + String repoRoot; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void main(String[] args) throws Exception + { + // for a run from the IDE, to override args... args = new String[] { "-r", "/Users/dkelkhoff/git/kingsrook/qqq/" }; + int exitCode = new CommandLine(new ValidateAPIVersions()).execute(args); + System.out.println("Exiting with code: " + exitCode); + System.exit(exitCode); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Integer call() throws Exception + { + String fileFormat = "yaml"; + boolean hadErrors = false; + List errorHeaders = new ArrayList<>(); + + List specList = List.of(new MiddlewareVersionV1()); + + for(AbstractMiddlewareVersion middlewareVersion : specList) + { + String version = middlewareVersion.getVersion(); + boolean hadErrorsThisVersion = false; + + ////////// + // todo // + ////////// + // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // // if this api version is in the list of "future" versions, consider it a "beta" and don't do any validation // + // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // for(APIVersion futureAPIVersion : apiInstanceMetaData.getFutureVersions()) + // { + // if(apiVersion.equals(futureAPIVersion)) + // { + // continue versionLoop; + // } + // } + + try + { + //////////////////////////////////////////////////////////// + // list current files - so we can tell if all get diff'ed // + //////////////////////////////////////////////////////////// + Set existingFileNames = new HashSet<>(); + String versionPath = repoRoot + "/" + APIUtils.PUBLISHED_API_LOCATION + "/" + version + "/"; + versionPath = versionPath.replaceAll("/+", "/"); + for(File file : APIUtils.listPublishedAPIFiles(versionPath)) + { + existingFileNames.add(file.getPath().replaceFirst(versionPath, "")); + } + + /////////////////////////////////////////////////////////// + // generate a new spec based on current code in codebase // + /////////////////////////////////////////////////////////// + OpenAPI openAPI = middlewareVersion.generateOpenAPIModel("qqq"); + String yaml = YamlUtils.toYaml(openAPI, mapper -> + { + mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); + }); + + ///////////////////////////////////////////////////////////////////// + // get the published API file - then diff it to what we just wrote // + ///////////////////////////////////////////////////////////////////// + String publishedAPI = APIUtils.getPublishedAPIFile(versionPath, "openapi", fileFormat); + + String newFileName = "/tmp/" + version + "-new." + fileFormat; + String publishedFileName = "/tmp/" + version + "-published." + fileFormat; + Files.write(Path.of(newFileName), yaml.getBytes()); + Files.write(Path.of(publishedFileName), publishedAPI.getBytes()); + + Runtime rt = Runtime.getRuntime(); + String[] commands = { "diff", "-w", publishedFileName, newFileName }; + Process proc = rt.exec(commands); + + BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream())); + + StringBuilder diffOutput = new StringBuilder(); + String s; + while((s = stdInput.readLine()) != null) + { + diffOutput.append(s).append("\n"); + } + + if(!"".contentEquals(diffOutput)) + { + String message = "Error: Differences were found in openapi.yaml file between the published docs and the newly generated file for API Version [" + version + "]."; + errorHeaders.add(message); + System.out.println(message); + System.out.println(diffOutput); + hadErrors = true; + hadErrorsThisVersion = true; + } + + + ////////////////////////////////////////////////////////////////////////////////////// + // if we want to split up by some paths, components, we could use a version of this // + ////////////////////////////////////////////////////////////////////////////////////// + /* + Map> groupedPaths = APIUtils.splitUpYamlForPublishing(yaml); + + /////////////////////////////////////////////////////////////////////////////////////// + // for each of the groupings (e.g., files), compare to what was previously published // + /////////////////////////////////////////////////////////////////////////////////////// + for(Map.Entry> entry : groupedPaths.entrySet()) + { + try + { + String name = entry.getKey(); + String newFileToDiff = YamlUtils.toYaml(entry.getValue()); + + ///////////////////////////////////////////////////////////////////// + // get the published API file - then diff it to what we just wrote // + ///////////////////////////////////////////////////////////////////// + String publishedAPI = APIUtils.getPublishedAPIFile(versionPath, name, fileFormat); + existingFileNames.remove(name + "." + fileFormat); + + String newFileName = "/tmp/" + version + "-new." + fileFormat; + String publishedFileName = "/tmp/" + version + "-published." + fileFormat; + Files.write(Path.of(newFileName), newFileToDiff.getBytes()); + Files.write(Path.of(publishedFileName), publishedAPI.getBytes()); + + Runtime rt = Runtime.getRuntime(); + String[] commands = { "diff", "-w", publishedFileName, newFileName }; + Process proc = rt.exec(commands); + + BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream())); + + StringBuilder diffOutput = new StringBuilder(); + String s; + while((s = stdInput.readLine()) != null) + { + diffOutput.append(s).append("\n"); + } + + if(!"".contentEquals(diffOutput)) + { + String message = "Error: Differences were found in file [" + name + "] between the published docs and the newly generated " + fileFormat + " file for API Version [" + version + "]."; + errorHeaders.add(message); + System.out.println(message); + System.out.println(diffOutput); + hadErrors = true; + hadErrorsThisVersion = true; + } + } + catch(Exception e) + { + errorHeaders.add(e.getMessage()); + System.out.println(e.getMessage()); + hadErrors = true; + hadErrorsThisVersion = true; + } + } + + ///////////////////////////////////////////////////////////////////////////////////// + // if any existing files weren't evaluated in the loop above, then that's an error // + // e.g., someone removed a thing that was previously in the API // + ///////////////////////////////////////////////////////////////////////////////////// + if(!existingFileNames.isEmpty()) + { + hadErrors = true; + hadErrorsThisVersion = true; + for(String existingFileName : existingFileNames) + { + String message = "Error: Previously published file [" + existingFileName + "] was not found in the current OpenAPI object for API Version [" + version + "]."; + errorHeaders.add(message); + System.out.println(message); + } + } + */ + } + catch(Exception e) + { + errorHeaders.add(e.getMessage()); + System.out.println(e.getMessage()); + hadErrors = true; + hadErrorsThisVersion = true; + } + + if(!hadErrorsThisVersion) + { + System.out.println("Success: No differences were found between the published docs and the newly generated " + fileFormat + " file for API Version [" + version + "]."); + } + } + + if(!errorHeaders.isEmpty()) + { + System.out.println("\nError summary:"); + errorHeaders.forEach(e -> System.out.println(" - " + e)); + } + + return (hadErrors ? 1 : 0); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/ExecutorCodeGenerator.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/ExecutorCodeGenerator.java new file mode 100644 index 00000000..1ab8a0eb --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/ExecutorCodeGenerator.java @@ -0,0 +1,172 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.tools.codegenerators; + + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.apache.commons.io.FileUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +class ExecutorCodeGenerator +{ + + /*************************************************************************** + ** + ***************************************************************************/ + public static void main(String[] args) + { + try + { + String qqqDir = "/Users/dkelkhoff/git/kingsrook/qqq/"; + new ExecutorCodeGenerator().writeAllFiles(qqqDir, "ProcessMetaData"); // don't include "Executor" on the end. + } + catch(IOException e) + { + //noinspection CallToPrintStackTrace + e.printStackTrace(); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void writeOne(String fullPath, String content) throws IOException + { + File file = new File(fullPath); + File directory = file.getParentFile(); + + if(!directory.exists()) + { + throw (new RuntimeException("Directory for: " + fullPath + " does not exists, and I refuse to mkdir (do it yourself and/or fix your arguments).")); + } + + if(file.exists()) + { + throw (new RuntimeException("File at: " + fullPath + " already exists, and I refuse to overwrite files.")); + } + + System.out.println("Writing: " + file); + FileUtils.writeStringToFile(file, content, StandardCharsets.UTF_8); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + void writeAllFiles(String rootPath, String baseName) throws IOException + { + if(baseName.endsWith("Executor")) + { + throw new IllegalArgumentException("Base name must not end with 'Executor'."); + } + + String basePath = rootPath + "qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/"; + writeOne(basePath + "executors/" + baseName + "Executor.java", makeExecutor(baseName)); + writeOne(basePath + "executors/io/" + baseName + "Input.java", makeInput(baseName)); + writeOne(basePath + "executors/io/" + baseName + "OutputInterface.java", makeOutputInterface(baseName)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private String makeExecutor(String baseName) + { + return """ + package com.kingsrook.qqq.middleware.javalin.executors; + + + import com.kingsrook.qqq.backend.core.exceptions.QException; + import com.kingsrook.qqq.middleware.javalin.executors.io.${baseName}Input; + import com.kingsrook.qqq.middleware.javalin.executors.io.${baseName}OutputInterface; + + + /******************************************************************************* + ** + *******************************************************************************/ + public class ${baseName}Executor extends AbstractMiddlewareExecutor<${baseName}Input, ${baseName}OutputInterface> + { + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void execute(${baseName}Input input, ${baseName}OutputInterface output) throws QException + { + } + + } + """.replaceAll("\\$\\{baseName}", baseName); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private String makeInput(String baseName) + { + return """ + package com.kingsrook.qqq.middleware.javalin.executors.io; + + + /******************************************************************************* + ** + *******************************************************************************/ + public class ${baseName}Input extends AbstractMiddlewareInput + { + + } + """.replaceAll("\\$\\{baseName}", baseName); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private String makeOutputInterface(String baseName) + { + return """ + package com.kingsrook.qqq.middleware.javalin.executors.io; + + + /******************************************************************************* + ** + *******************************************************************************/ + public interface ${baseName}OutputInterface extends AbstractMiddlewareOutputInterface + { + + } + """.replaceAll("\\$\\{baseName}", baseName); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/SpecCodeGenerator.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/SpecCodeGenerator.java new file mode 100644 index 00000000..5a5e09c2 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/SpecCodeGenerator.java @@ -0,0 +1,300 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.tools.codegenerators; + + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.apache.commons.io.FileUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +class SpecCodeGenerator +{ + + /*************************************************************************** + ** + ***************************************************************************/ + public static void main(String[] args) + { + try + { + String qqqDir = "/Users/dkelkhoff/git/kingsrook/qqq/"; + + ///////////////// + // normal case // + ///////////////// + new SpecCodeGenerator().writeAllFiles(qqqDir, "v1", "ProcessMetaData"); + + /////////////////////////////////////////////////////////////////////////////// + // if the executor isn't named the same as the spec (e.g., reused executors) // + /////////////////////////////////////////////////////////////////////////////// + // new SpecCodeGenerator().writeAllFiles(qqqDir, "v1", "ProcessInsert", "ProcessInsertOrSetp"); + } + catch(IOException e) + { + //noinspection CallToPrintStackTrace + e.printStackTrace(); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void writeOne(String fullPath, String content) throws IOException + { + File file = new File(fullPath); + File directory = file.getParentFile(); + + if(!directory.exists()) + { + throw (new RuntimeException("Directory for: " + fullPath + " does not exists, and I refuse to mkdir (do it yourself and/or fix your arguments).")); + } + + if(file.exists()) + { + throw (new RuntimeException("File at: " + fullPath + " already exists, and I refuse to overwrite files.")); + } + + System.out.println("Writing: " + file); + FileUtils.writeStringToFile(file, content, StandardCharsets.UTF_8); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + void writeAllFiles(String rootPath, String version, String baseName) throws IOException + { + writeAllFiles(rootPath, version, baseName, baseName); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void writeAllFiles(String rootPath, String version, String baseName, String executorBaseName) throws IOException + { + String basePath = rootPath + "qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/"; + writeOne(basePath + "specs/" + version.toLowerCase() + "/" + baseName + "Spec" + version.toUpperCase() + ".java", makeSpec(version, baseName, executorBaseName)); + writeOne(basePath + "specs/" + version.toLowerCase() + "/responses/" + baseName + "Response" + version.toUpperCase() + ".java", makeResponse(version, baseName, executorBaseName)); + + System.out.println(); + System.out.println("Hey - You probably want to add a line like:"); + System.out.println(" list.add(new " + baseName + "Spec" + version.toUpperCase() + "());"); + System.out.println("In:"); + System.out.println(" MiddlewareVersion" + version.toUpperCase() + ".java"); + System.out.println(); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private String makeSpec(String version, String baseName, String executorBaseName) + { + return """ + package com.kingsrook.qqq.middleware.javalin.specs.${version.toLowerCase}; + + + import java.util.List; + import java.util.Map; + import com.kingsrook.qqq.backend.core.utils.JsonUtils; + import com.kingsrook.qqq.middleware.javalin.executors.${executorBaseName}Executor; + import com.kingsrook.qqq.middleware.javalin.executors.io.${executorBaseName}Input; + import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec; + import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation; + import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse; + import com.kingsrook.qqq.middleware.javalin.specs.${version.toLowerCase}.responses.${executorBaseName}Response${version.toUpperCase}; + import com.kingsrook.qqq.middleware.javalin.specs.${version.toLowerCase}.utils.Tags${version.toUpperCase}; + import com.kingsrook.qqq.openapi.model.Content; + import com.kingsrook.qqq.openapi.model.Example; + import com.kingsrook.qqq.openapi.model.HttpMethod; + import com.kingsrook.qqq.openapi.model.In; + import com.kingsrook.qqq.openapi.model.Parameter; + import com.kingsrook.qqq.openapi.model.RequestBody; + import com.kingsrook.qqq.openapi.model.Schema; + import com.kingsrook.qqq.openapi.model.Type; + import io.javalin.http.ContentType; + import io.javalin.http.Context; + + + /******************************************************************************* + ** + *******************************************************************************/ + public class ${baseName}Spec${version.toUpperCase} extends AbstractEndpointSpec<${executorBaseName}Input, ${baseName}Response${version.toUpperCase}, ${executorBaseName}Executor> + { + + /*************************************************************************** + ** + ***************************************************************************/ + public BasicOperation defineBasicOperation() + { + return new BasicOperation() + .withPath(TODO) + .withHttpMethod(HttpMethod.TODO) + .withTag(Tags${version.toUpperCase}.TODO) + .withShortSummary(TODO) + .withLongDescription(""\" + TODO""\" + ); + } + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public List defineRequestParameters() + { + return List.of( + new Parameter() + .withName(TODO) + .withDescription(TODO) + .withRequired(TODO) + .withSchema(new Schema().withType(Type.TODO)) + .withExamples(Map.of("TODO", new Example().withValue(TODO))) + .withIn(In.TODO) + ); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public RequestBody defineRequestBody() + { + return new RequestBody() + .withContent(Map.of( + ContentType.TODO.getMimeType(), new Content() + .withSchema(new Schema() + .withType(Type.TODO) + .withProperties(Map.of( + "TODO", new Schema() + )) + ) + )); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public ${executorBaseName}Input buildInput(Context context) throws Exception + { + ${executorBaseName}Input input = new ${executorBaseName}Input(); + input.setTODO + return (input); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public BasicResponse defineBasicSuccessResponse() + { + Map examples = Map.of( + + "TODO", new Example() + .withValue(new ${baseName}Response${version.toUpperCase}() + .withTODO + ) + + ); + + return new BasicResponse(""\" + TODO""\", + + new ${baseName}Response${version.toUpperCase}().toSchema(), + examples + ); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void handleOutput(Context context, ${baseName}Response${version.toUpperCase} output) throws Exception + { + context.result(JsonUtils.toJson(output)); + } + + } + """ + .replaceAll("\\$\\{version.toLowerCase}", version.toLowerCase()) + .replaceAll("\\$\\{version.toUpperCase}", version.toUpperCase()) + .replaceAll("\\$\\{executorBaseName}", executorBaseName) + .replaceAll("\\$\\{baseName}", baseName) + ; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private String makeResponse(String version, String baseName, String executorBaseName) + { + return """ + package com.kingsrook.qqq.middleware.javalin.specs.${version.toLowerCase}.responses; + + + import com.kingsrook.qqq.middleware.javalin.executors.io.${executorBaseName}OutputInterface; + import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; + import com.kingsrook.qqq.middleware.javalin.specs.ToSchema; + + + /******************************************************************************* + ** + *******************************************************************************/ + public class ${baseName}Response${version.toUpperCase} implements ${executorBaseName}OutputInterface, ToSchema + { + @OpenAPIDescription(TODO) + private String TODO; + + TODO gsw + } + """ + .replaceAll("\\$\\{version.toLowerCase}", version.toLowerCase()) + .replaceAll("\\$\\{version.toUpperCase}", version.toUpperCase()) + .replaceAll("\\$\\{executorBaseName}", executorBaseName) + .replaceAll("\\$\\{baseName}", baseName) + ; + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/package-info.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/package-info.java new file mode 100644 index 00000000..4f4cfe11 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/package-info.java @@ -0,0 +1,27 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/******************************************************************************* + ** These classes are meant as tools to be executed manually by a developer, + ** to create other new classes (since there's a bit of boilerplate, innit?) + ** + *******************************************************************************/ +package com.kingsrook.qqq.middleware.javalin.tools.codegenerators; \ No newline at end of file diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/package-info.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/package-info.java new file mode 100644 index 00000000..2e2a4b8f --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/package-info.java @@ -0,0 +1,27 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/******************************************************************************* + ** This tools path - is for non-production code - rather, development and CI/CD + ** tools. + ** + *******************************************************************************/ +package com.kingsrook.qqq.middleware.javalin.tools; \ No newline at end of file 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..f89ba784 --- /dev/null +++ b/qqq-middleware-javalin/src/main/resources/openapi/v1/openapi.yaml @@ -0,0 +1,1967 @@ +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" + icon: + $ref: "#/components/schemas/Icon" + description: "Icon to display for the app." + 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" + icon: + $ref: "#/components/schemas/Icon" + description: "Icon to display for the item." + 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" + - "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" + format: + description: "An optional indicator of the screen format preferred by the\ + \ application to be used for this screen. Different frontends may support\ + \ different formats, and implement them differently." + type: "string" + 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" + widgets: + additionalProperties: + $ref: "#/components/schemas/ProcessMetaDataLight" + description: "Map of all widgets 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" + icon: + $ref: "#/components/schemas/Icon" + description: "Icon to display for the process." + 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" + icon: + $ref: "#/components/schemas/Icon" + description: "Icon to display for the process." + 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" + icon: + description: "Icon to display for the process." + 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" + 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" + format: + description: "An optional indicator of the screen format preferred\ + \ by the application to be used for this screen. Different\ + \ frontends may support different formats, and implement them\ + \ differently." + type: "string" + 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" + format: + description: "An optional indicator of the screen format preferred\ + \ by the application to be used for this screen. Different\ + \ frontends may support different formats, and implement them\ + \ differently." + type: "string" + 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" + icon: + $ref: "#/components/schemas/Icon" + description: "Icon to display for the table" + 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: + - "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" + modalMode: + description: "For COMPOSITE type blocks, optional control to make the widget\ + \ appear modally" + enum: + - "MODAL" + type: "string" + styles: + $ref: "#/components/schemas/WidgetBlockStyles" + description: "Styles to apply to the block. Different fields based on blockType." + oneOf: + - properties: + backgroundColor: + description: "A background color to use for the block" + type: "string" + padding: + description: "Optional padding to apply to the block" + properties: + bottom: + type: "object" + left: + type: "object" + right: + type: "object" + top: + type: "object" + type: "object" + type: "object" + - properties: + color: + description: "A Color to use for the button. May be specified as\ + \ a StandardColor (one of: SUCCESS, WARNING, ERROR, INFO, MUTED)\ + \ or an RGB code." + type: "string" + format: + description: "An optional indicator of the screen format preferred\ + \ by the application to be used for this block, such as OUTLINED,\ + \ FILLED, or TEXT. Different frontends may support different formats,\ + \ and implement them differently." + type: "string" + type: "object" + - properties: + height: + description: "A request to render the image at a specified height." + type: "string" + padding: + description: "Optional padding to apply to the image" + properties: + bottom: + type: "object" + left: + type: "object" + right: + type: "object" + top: + type: "object" + type: "object" + width: + description: "A request to render the image at a specified width." + type: "string" + type: "object" + - properties: + color: + description: "A Color to display the text in. May be specified as\ + \ a StandardColor (one of: SUCCESS, WARNING, ERROR, INFO, MUTED)\ + \ or an RGB code." + type: "string" + format: + description: "An optional indicator of the screen format preferred\ + \ by the application to be used for this block. Different frontends\ + \ may support different formats, and implement them differently." + type: "string" + size: + description: "An optional indicator of the size at which the text\ + \ should be rendered. May be a named value (one oflargest, headline,\ + \ title, body, smallest) or a numeric size - both are up to the\ + \ frontend to interpret." + type: "string" + weight: + description: "An optional indicator of the weight at which the text\ + \ should be rendered. May be a named value (one ofextralight, thin,\ + \ medium, black, semibold, bold, extrabold) or a numeric, e.g.,\ + \ 100, 200, ..., 900" + 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 AUDIO type widget block" + properties: + autoPlay: + description: "Control if the file should automatically play when the\ + \ block is rendered" + type: "boolean" + path: + description: "The path to the audio file on the server" + type: "string" + showControls: + description: "Control if on-screen controls should be shown to allow\ + \ the user to control playback" + type: "boolean" + type: "object" + - description: "Values used for a BUTTON type widget block" + properties: + actionCode: + description: "Code used within the app as the value submitted when\ + \ the button is clicked" + type: "string" + controlCode: + description: "Instructions for what should happen in the frontend\ + \ (e.g., within a screen), when the button is clicked.\n\nTo show\ + \ a modal composite block, use format: `showModal:${blockId}` (e.g.,\ + \ `showModal:myBlock`)\n\nTo hide a modal composite block, use format:\ + \ `hideModal:${blockId}` (e.g., `hideModal:myBlock`)\n\nTo toggle\ + \ visibility of a modal composite block, use format: `toggleModal:${blockId}`\ + \ (e.g., `toggleModal:myBlock`)\n" + type: "string" + endIcon: + description: "An optional icon to display after the text in the button" + 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" + label: + description: "User-facing label to display in the button" + type: "string" + startIcon: + description: "An optional icon to display before the text in the button" + 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" + type: "object" + - description: "Values used for an IMAGE type widget block" + properties: + path: + description: "The path to the image on the server" + 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" + hideSoftKeyboard: + description: "Indicate if the frontend uses a software/on-screen keyboard,\ + \ if the application should try to hide it (e.g., upon auto-focus)." + type: "boolean" + placeholder: + description: "Optional placeholder text to display in the input box." + type: "string" + submitOnEnter: + description: "Indicate whether the form that this field is on should\ + \ be submitted when Enter is pressed" + type: "boolean" + type: "object" + - description: "Values used for a TEXT type widget block" + properties: + endIcon: + description: "An optional icon to display after the text" + 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" + startIcon: + description: "An optional icon to display before the text" + 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" + text: + description: "The text to display in the block" + type: "string" + type: "object" + type: "object" + WidgetBlockAudioValues: + description: "Values used for an AUDIO type widget block" + properties: + autoPlay: + description: "Control if the file should automatically play when the block\ + \ is rendered" + type: "boolean" + path: + description: "The path to the audio file on the server" + type: "string" + showControls: + description: "Control if on-screen controls should be shown to allow the\ + \ user to control playback" + type: "boolean" + type: "object" + WidgetBlockBaseStyles: + properties: + backgroundColor: + description: "A background color to use for the block" + type: "string" + padding: + description: "Optional padding to apply to the block" + properties: + bottom: + type: "object" + left: + type: "object" + right: + type: "object" + top: + type: "object" + type: "object" + type: "object" + WidgetBlockButtonStyles: + properties: + color: + description: "A Color to use for the button. May be specified as a StandardColor\ + \ (one of: SUCCESS, WARNING, ERROR, INFO, MUTED) or an RGB code." + type: "string" + format: + description: "An optional indicator of the screen format preferred by the\ + \ application to be used for this block, such as OUTLINED, FILLED, or\ + \ TEXT. Different frontends may support different formats, and implement\ + \ them differently." + type: "string" + type: "object" + WidgetBlockButtonValues: + description: "Values used for a BUTTON type widget block" + properties: + actionCode: + description: "Code used within the app as the value submitted when the button\ + \ is clicked" + type: "string" + controlCode: + description: "Instructions for what should happen in the frontend (e.g.,\ + \ within a screen), when the button is clicked.\n\nTo show a modal composite\ + \ block, use format: `showModal:${blockId}` (e.g., `showModal:myBlock`)\n\ + \nTo hide a modal composite block, use format: `hideModal:${blockId}`\ + \ (e.g., `hideModal:myBlock`)\n\nTo toggle visibility of a modal composite\ + \ block, use format: `toggleModal:${blockId}` (e.g., `toggleModal:myBlock`)\n" + type: "string" + endIcon: + $ref: "#/components/schemas/Icon" + description: "An optional icon to display after the text in the button" + label: + description: "User-facing label to display in the button" + type: "string" + startIcon: + $ref: "#/components/schemas/Icon" + description: "An optional icon to display before the text in the button" + type: "object" + WidgetBlockImageStyles: + properties: + height: + description: "A request to render the image at a specified height." + type: "string" + padding: + description: "Optional padding to apply to the image" + properties: + bottom: + type: "object" + left: + type: "object" + right: + type: "object" + top: + type: "object" + type: "object" + width: + description: "A request to render the image at a specified width." + type: "string" + type: "object" + WidgetBlockImageValues: + description: "Values used for an IMAGE type widget block" + properties: + path: + description: "The path to the image on the server" + 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" + hideSoftKeyboard: + description: "Indicate if the frontend uses a software/on-screen keyboard,\ + \ if the application should try to hide it (e.g., upon auto-focus)." + type: "boolean" + placeholder: + description: "Optional placeholder text to display in the input box." + type: "string" + 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: + color: + description: "A Color to display the text in. May be specified as a StandardColor\ + \ (one of: SUCCESS, WARNING, ERROR, INFO, MUTED) or an RGB code." + type: "string" + format: + description: "An optional indicator of the screen format preferred by the\ + \ application to be used for this block. Different frontends may support\ + \ different formats, and implement them differently." + type: "string" + size: + description: "An optional indicator of the size at which the text should\ + \ be rendered. May be a named value (one oflargest, headline, title,\ + \ body, smallest) or a numeric size - both are up to the frontend to interpret." + type: "string" + weight: + description: "An optional indicator of the weight at which the text should\ + \ be rendered. May be a named value (one ofextralight, thin, medium,\ + \ black, semibold, bold, extrabold) or a numeric, e.g., 100, 200, ...,\ + \ 900" + type: "string" + type: "object" + WidgetBlockTextValues: + description: "Values used for a TEXT type widget block" + properties: + endIcon: + $ref: "#/components/schemas/Icon" + description: "An optional icon to display after the text" + startIcon: + $ref: "#/components/schemas/Icon" + description: "An optional icon to display before the text" + 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" + parameters: + - description: "Name of the frontend requesting the meta-data.\nGenerally a\ + \ QQQ frontend library, unless a custom application frontend has been built." + example: "qqq-frontend-material-dashboard" + in: "query" + name: "frontendName" + schema: + type: "string" + - description: "Version of the frontend requesting the meta-data." + example: "0.23.0" + in: "query" + name: "frontendVersion" + schema: + type: "string" + - description: "Name of the application requesting the meta-data. e.g., an\ + \ instance of a specific frontend\n(i.e., an application might be deployed\ + \ with 2 different qqq-frontend-material-dashboard frontends,\nin which\ + \ case this attribute allows differentiation between them)." + example: "my-admin-web-app" + in: "query" + name: "applicationName" + schema: + type: "string" + - description: "Version of the application requesting the meta-data." + example: "20241021" + in: "query" + name: "applicationVersion" + schema: + type: "string" + responses: + 200: + content: + application/json: + examples: + Example: + value: + appTree: + - children: + - children: + - icon: + name: "person_add" + label: "Sample Person Process" + name: "samplePersonProcess" + type: "PROCESS" + icon: + name: "child_friendly" + label: "Child App" + name: "childApp" + type: "APP" + - icon: + name: "person_outline" + label: "Person" + name: "person" + type: "TABLE" + icon: + name: "home" + label: "Home App" + name: "homeApp" + type: "APP" + apps: + homeApp: + childMap: + person: + icon: + name: "person_outline" + label: "Person" + name: "person" + type: "TABLE" + childApp: + icon: + name: "child_friendly" + label: "Child App" + name: "childApp" + type: "APP" + children: + - icon: + name: "child_friendly" + label: "Child App" + name: "childApp" + type: "APP" + - icon: + name: "person_outline" + label: "Person" + name: "person" + type: "TABLE" + icon: + name: "home" + label: "Home App" + name: "homeApp" + sections: + - icon: + name: "badge" + label: "Home App" + name: "homeApp" + tables: + - "person" + childApp: + childMap: + samplePersonProcess: + icon: + name: "person_add" + label: "Sample Person Process" + name: "samplePersonProcess" + type: "PROCESS" + children: + - icon: + name: "person_add" + label: "Sample Person Process" + name: "samplePersonProcess" + type: "PROCESS" + icon: + name: "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 + icon: + name: "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_UPDATE" + - "TABLE_DELETE" + deletePermission: true + editPermission: true + icon: + name: "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/backend/javalin/QJavalinProcessHandlerTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java index 74aa407c..563ad04a 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 *******************************************************************************/ @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() @@ -235,6 +242,7 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase /******************************************************************************* ** test running a step a process that goes async ** + ** Note: ported to v1 *******************************************************************************/ @Test public void test_processStepGoingAsync() throws InterruptedException @@ -341,6 +349,7 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase /******************************************************************************* ** test init'ing a process that goes async and then throws ** + ** Note: ported to v1 *******************************************************************************/ @Test public void test_processInitGoingAsyncThenThrowing() throws InterruptedException 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..b1e41f11 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,38 @@ 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); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -412,7 +443,7 @@ public class TestUtils return new QProcessMetaData() .withName("greet") .withTableName(TABLE_NAME_PERSON) - .addStep(new QBackendStepMetaData() + .withStep(new QBackendStepMetaData() .withName("prepare") .withCode(new QCodeReference() .withName(MockBackendStep.class.getName()) @@ -444,13 +475,13 @@ public class TestUtils .withName(PROCESS_NAME_GREET_PEOPLE_INTERACTIVE) .withTableName(TABLE_NAME_PERSON) - .addStep(new QFrontendStepMetaData() + .withStep(new QFrontendStepMetaData() .withName("setup") .withFormField(new QFieldMetaData("greetingPrefix", QFieldType.STRING)) .withFormField(new QFieldMetaData("greetingSuffix", QFieldType.STRING)) ) - .addStep(new QBackendStepMetaData() + .withStep(new QBackendStepMetaData() .withName("doWork") .withCode(new QCodeReference() .withName(MockBackendStep.class.getName()) @@ -469,7 +500,7 @@ public class TestUtils .withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING)))) ) - .addStep(new QFrontendStepMetaData() + .withStep(new QFrontendStepMetaData() .withName("results") .withFormField(new QFieldMetaData("outputMessage", QFieldType.STRING)) ); @@ -500,7 +531,7 @@ public class TestUtils return new QProcessMetaData() .withName(PROCESS_NAME_SIMPLE_SLEEP) .withIsHidden(true) - .addStep(SleeperStep.getMetaData()); + .withStep(SleeperStep.getMetaData()); } @@ -512,11 +543,11 @@ public class TestUtils { return new QProcessMetaData() .withName(PROCESS_NAME_SLEEP_INTERACTIVE) - .addStep(new QFrontendStepMetaData() + .withStep(new QFrontendStepMetaData() .withName(SCREEN_0) .withFormField(new QFieldMetaData("outputMessage", QFieldType.STRING))) - .addStep(SleeperStep.getMetaData()) - .addStep(new QFrontendStepMetaData() + .withStep(SleeperStep.getMetaData()) + .withStep(new QFrontendStepMetaData() .withName(SCREEN_1) .withFormField(new QFieldMetaData("outputMessage", QFieldType.STRING))); } @@ -530,7 +561,7 @@ public class TestUtils { return new QProcessMetaData() .withName(PROCESS_NAME_SIMPLE_THROW) - .addStep(ThrowerStep.getMetaData()); + .withStep(ThrowerStep.getMetaData()); } @@ -690,7 +721,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/QApplicationJavalinServerTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServerTest.java new file mode 100644 index 00000000..a1cdf6b2 --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServerTest.java @@ -0,0 +1,210 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public 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; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.javalin.TestUtils; +import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1; +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; + + +/******************************************************************************* + ** Unit test for QApplicationJavalinServer + *******************************************************************************/ +class QApplicationJavalinServerTest +{ + private static final int PORT = 6265; + + private QApplicationJavalinServer javalinServer; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @AfterEach + void afterEach() + { + javalinServer.stop(); + TestApplication.callCount = 0; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testWithLegacyImplementation() throws QException + { + javalinServer = new QApplicationJavalinServer(getQqqApplication()) + .withPort(PORT) + .withServeFrontendMaterialDashboard(false); + javalinServer.start(); + + HttpResponse response = Unirest.get("http://localhost:" + PORT + "/metaData").asString(); + assertEquals(200, response.getStatus()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testWithoutLegacyImplementation() throws QException + { + javalinServer = new QApplicationJavalinServer(getQqqApplication()) + .withPort(PORT) + .withServeLegacyUnversionedMiddlewareAPI(false) + .withServeFrontendMaterialDashboard(false); + javalinServer.start(); + + HttpResponse response = Unirest.get("http://localhost:" + PORT + "/metaData").asString(); + assertEquals(404, response.getStatus()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testWithVersionedImplementation() throws QException + { + javalinServer = new QApplicationJavalinServer(getQqqApplication()) + .withPort(PORT) + .withMiddlewareVersionList(List.of(new MiddlewareVersionV1())) + .withServeFrontendMaterialDashboard(false); + javalinServer.start(); + + HttpResponse response = Unirest.get("http://localhost:" + PORT + "/qqq/v1/metaData").asString(); + assertEquals(200, response.getStatus()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testWithoutHotSwap() throws QException + { + testWithOrWithoutHotSwap(false); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testWithHotSwap() throws QException + { + testWithOrWithoutHotSwap(true); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void testWithOrWithoutHotSwap(boolean withHotSwap) throws QException + { + System.setProperty("qqq.javalin.hotSwapInstance", String.valueOf(withHotSwap)); + javalinServer = new QApplicationJavalinServer(getQqqApplication()) + .withPort(PORT) + .withMiddlewareVersionList(List.of(new MiddlewareVersionV1())) + .withMillisBetweenHotSwaps(0) + .withServeFrontendMaterialDashboard(false); + javalinServer.start(); + System.clearProperty("qqq.javalin.hotSwapInstance"); + assertThat(TestApplication.callCount).isEqualTo(1); + + HttpResponse response = Unirest.get("http://localhost:" + PORT + "/qqq/v1/metaData").asString(); + assertEquals(200, response.getStatus()); + + response = Unirest.get("http://localhost:" + PORT + "/qqq/v1/metaData").asString(); + assertEquals(200, response.getStatus()); + JSONObject metaData = new JSONObject(response.getBody()); + JSONObject tables = metaData.getJSONObject("tables"); + String aTableName = tables.keySet().iterator().next(); + JSONObject aTable = tables.getJSONObject(aTableName); + + if(withHotSwap) + { + assertThat(aTable.getString("label")).doesNotEndWith("1"); + assertThat(TestApplication.callCount).isGreaterThanOrEqualTo(1); + } + else + { + assertThat(aTable.getString("label")).endsWith("1"); + assertThat(TestApplication.callCount).isEqualTo(1); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private static AbstractQQQApplication getQqqApplication() + { + return new TestApplication(); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static class TestApplication extends AbstractQQQApplication + { + static int callCount = 0; + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QInstance defineQInstance() throws QException + { + callCount++; + QInstance qInstance = TestUtils.defineInstance(); + + qInstance.getTables().values().forEach(t -> t.setLabel(t.getLabel() + callCount)); + + return (qInstance); + } + } + +} \ No newline at end of file diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QMiddlewareApiSpecHandlerTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QMiddlewareApiSpecHandlerTest.java new file mode 100644 index 00000000..c4e66445 --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QMiddlewareApiSpecHandlerTest.java @@ -0,0 +1,152 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public 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; + + +import java.util.List; +import java.util.Map; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.YamlUtils; +import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion; +import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1; +import io.javalin.Javalin; +import kong.unirest.HttpResponse; +import kong.unirest.Unirest; +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeAll; +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 QMiddlewareApiSpecHandler + *******************************************************************************/ +class QMiddlewareApiSpecHandlerTest +{ + private static int PORT = 6264; + + protected static Javalin service; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeAll + static void beforeAll() + { + service = Javalin.create(config -> + { + List middlewareVersionList = List.of(new MiddlewareVersionV1()); + config.router.apiBuilder(new QMiddlewareApiSpecHandler(middlewareVersionList).defineJavalinEndpointGroup()); + } + ).start(PORT); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private String getBaseUrlAndPath() + { + return "http://localhost:" + PORT + "/qqq"; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testIndex() + { + HttpResponse response = Unirest.get(getBaseUrlAndPath()).asString(); + assertEquals(200, response.getStatus()); + assertThat(response.getBody()).contains(" response = Unirest.get(getBaseUrlAndPath() + "/versions.json").asString(); + assertEquals(200, response.getStatus()); + JSONObject object = new JSONObject(response.getBody()); + object.getJSONArray("supportedVersions"); + assertEquals("v1", object.getString("currentVersion")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSpecYaml() throws JsonProcessingException + { + HttpResponse response = Unirest.get(getBaseUrlAndPath() + "/v1/openapi.yaml").asString(); + assertEquals(200, response.getStatus()); + Map map = YamlUtils.toMap(response.getBody()); + assertTrue(map.containsKey("openapi")); + assertTrue(map.containsKey("info")); + assertTrue(map.containsKey("paths")); + assertTrue(map.containsKey("components")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSpecJson() throws JsonProcessingException + { + HttpResponse response = Unirest.get(getBaseUrlAndPath() + "/v1/openapi.json").asString(); + assertEquals(200, response.getStatus()); + JSONObject map = JsonUtils.toJSONObject(response.getBody()); + assertTrue(map.has("openapi")); + assertTrue(map.has("info")); + assertTrue(map.has("paths")); + assertTrue(map.has("components")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testServeResources() + { + HttpResponse response = Unirest.get("http://localhost:" + PORT + "/api/docs/js/rapidoc.min.js").asString(); + assertEquals(200, response.getStatus()); + } + +} 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/schemabuilder/ToSchemaTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/ToSchemaTest.java new file mode 100644 index 00000000..e9ec9a57 --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/schemabuilder/ToSchemaTest.java @@ -0,0 +1,59 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public 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 com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription; +import com.kingsrook.qqq.openapi.model.Schema; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for ToSchema + *******************************************************************************/ +class ToSchemaTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() + { + Schema schema = new TestsToSchema().toSchema(); + Schema myFieldSchema = schema.getProperties().get("myField"); + assertEquals("This is a field", myFieldSchema.getDescription()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static class TestsToSchema implements ToSchema + { + @OpenAPIDescription("This is a field") + private String myField; + } + +} \ 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..a51e6b90 --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/SpecTestBase.java @@ -0,0 +1,149 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public 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.Collections; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +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 = 6273; + + protected static Javalin service; + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected abstract AbstractEndpointSpec getSpec(); + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected List> getAdditionalSpecs() + { + return (Collections.emptyList()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + 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 -> + { + QInstance qInstance = TestUtils.defineInstance(); + + AbstractEndpointSpec spec = getSpec(); + spec.setQInstance(qInstance); + config.router.apiBuilder(() -> spec.defineRoute(getVersion())); + + for(AbstractEndpointSpec additionalSpec : getAdditionalSpecs()) + { + additionalSpec.setQInstance(qInstance); + config.router.apiBuilder(() -> additionalSpec.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 diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStatusSpecV1Test.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStatusSpecV1Test.java new file mode 100644 index 00000000..ed045652 --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/ProcessStatusSpecV1Test.java @@ -0,0 +1,323 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public 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.concurrent.TimeUnit; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; +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.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for ProcessStatusSpecV1 + *******************************************************************************/ +class ProcessStatusSpecV1Test extends SpecTestBase +{ + private static final int MORE_THAN_TIMEOUT = 500; + private static final int TIMEOUT = 300; + private static final int LESS_THAN_TIMEOUT = 50; + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + protected AbstractEndpointSpec getSpec() + { + return new ProcessStatusSpecV1(); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + protected List> getAdditionalSpecs() + { + return List.of(new ProcessInitSpecV1(), new ProcessStepSpecV1()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + protected String getVersion() + { + return "v1"; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @AfterEach + void afterEach() + { + QLogger.deactivateCollectingLoggerForClass(MockBackendStep.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInitWentAsync() + { + ///////////////////////////////////////// + // init process, which should go async // + ///////////////////////////////////////// + String processBasePath = getBaseUrlAndPath() + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP; + HttpResponse response = Unirest.post(processBasePath + "/init") + .multiPartContent() + .field("stepTimeoutMillis", String.valueOf(TIMEOUT)) + .field("values", new JSONObject() + .put(TestUtils.SleeperStep.FIELD_SLEEP_MILLIS, MORE_THAN_TIMEOUT) + .toString()) + .asString(); + + /////////////////////////////////// + // assert we got back job-status // + /////////////////////////////////// + assertEquals(200, response.getStatus()); + JSONObject jsonObject = assertProcessStepWentAsyncResponse(response); + 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"); + + ///////////////////////////////////////////// + // request job status before sleep is done // + ///////////////////////////////////////////// + response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); + jsonObject = assertProcessStepRunningResponse(response); + + /////////////////////////////////// + // sleep, to let that job finish // + /////////////////////////////////// + SleepUtils.sleep(MORE_THAN_TIMEOUT, TimeUnit.MILLISECONDS); + + //////////////////////////////////////////////////////// + // request job status again, get back results instead // + //////////////////////////////////////////////////////// + response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); + jsonObject = assertProcessStepCompleteResponse(response); + } + + + + /******************************************************************************* + ** test running a step a process that goes async + ** + *******************************************************************************/ + @Test + public void test_processStepGoingAsync() throws InterruptedException + { + /////////////////////////////////////////////////////////// + // first init the process, to get its UUID // + // note this process doesn't sleep until its second step // + /////////////////////////////////////////////////////////// + String processBasePath = getBaseUrlAndPath() + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE; + HttpResponse response = Unirest.post(processBasePath + "/init") + .multiPartContent() + .field("values", new JSONObject() + .put(TestUtils.SleeperStep.FIELD_SLEEP_MILLIS, MORE_THAN_TIMEOUT) + .toString()) + .asString(); + + JSONObject jsonObject = assertProcessStepCompleteResponse(response); + String processUUID = jsonObject.getString("processUUID"); + String nextStep = jsonObject.getString("nextStep"); + assertNotNull(processUUID, "Process UUID should not be null."); + assertNotNull(nextStep, "There should be a next step"); + assertFalse(jsonObject.getJSONObject("values").has("didSleep"), "There should not (yet) be a value from the backend step"); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // second, run the 'nextStep' (the backend step, that sleeps). run it with a long enough sleep so that it'll go async // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + response = Unirest.post(processBasePath + "/" + processUUID + "/step/" + nextStep) + .multiPartContent() + .field("stepTimeoutMillis", String.valueOf(TIMEOUT)) + .asString(); + + jsonObject = assertProcessStepWentAsyncResponse(response); + String jobUUID = jsonObject.getString("jobUUID"); + + /////////////////////////////////// + // sleep, to let that job finish // + /////////////////////////////////// + Thread.sleep(MORE_THAN_TIMEOUT); + + /////////////////////////////// + // third, request job status // + /////////////////////////////// + response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); + + jsonObject = assertProcessStepCompleteResponse(response); + String nextStep2 = jsonObject.getString("nextStep"); + assertNotNull(nextStep2, "There be one more next step"); + assertNotEquals(nextStep, nextStep2, "The next step should be different this time."); + assertTrue(jsonObject.getJSONObject("values").has("didSleep"), "There should be a value from the backend step"); + } + + + + /******************************************************************************* + ** test init'ing a process that goes async and then throws + ** + *******************************************************************************/ + @Test + public void test_processInitGoingAsyncThenThrowing() throws InterruptedException + { + String processBasePath = getBaseUrlAndPath() + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_THROW; + HttpResponse response = Unirest.post(processBasePath + "/init") + .multiPartContent() + .field("stepTimeoutMillis", String.valueOf(TIMEOUT)) + .field("values", new JSONObject() + .put(TestUtils.SleeperStep.FIELD_SLEEP_MILLIS, MORE_THAN_TIMEOUT) + .toString()) + .asString(); + + JSONObject jsonObject = assertProcessStepWentAsyncResponse(response); + String processUUID = jsonObject.getString("processUUID"); + String jobUUID = jsonObject.getString("jobUUID"); + + ///////////////////////////////////////////// + // request job status before sleep is done // + ///////////////////////////////////////////// + response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); + jsonObject = assertProcessStepRunningResponse(response); + + /////////////////////////////////// + // sleep, to let that job finish // + /////////////////////////////////// + Thread.sleep(MORE_THAN_TIMEOUT); + + ///////////////////////////////////////////////////////////// + // request job status again, get back error status instead // + ///////////////////////////////////////////////////////////// + response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString(); + jsonObject = assertProcessStepErrorResponse(response); + } + + + + /******************************************************************************* + ** every time a process step (or init) has gone async, expect what the + ** response should look like + *******************************************************************************/ + private JSONObject assertProcessStepWentAsyncResponse(HttpResponse response) + { + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + + assertEquals("JOB_STARTED", jsonObject.getString("type")); + + assertTrue(jsonObject.has("processUUID"), "Async-started response should have a processUUID"); + assertTrue(jsonObject.has("jobUUID"), "Async-started response should have a jobUUID"); + + assertFalse(jsonObject.has("values"), "Async-started response should NOT have values"); + assertFalse(jsonObject.has("error"), "Async-started response should NOT have error"); + + return (jsonObject); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private JSONObject assertProcessStepRunningResponse(HttpResponse response) + { + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + + assertEquals("RUNNING", jsonObject.getString("type"), "Step Running response should have type=RUNNING"); + + assertFalse(jsonObject.has("values"), "Step Running response should NOT have values"); + assertFalse(jsonObject.has("error"), "Step Running response should NOT have error"); + + return (jsonObject); + } + + + + /******************************************************************************* + ** every time a process step (sync or async) completes, expect certain things + ** to be (and not to be) in the json response. + *******************************************************************************/ + private JSONObject assertProcessStepCompleteResponse(HttpResponse response) + { + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + + assertEquals("COMPLETE", jsonObject.getString("type"), "Step Running response should have type=COMPLETE"); + assertTrue(jsonObject.has("values"), "Step Complete response should have values"); + + assertFalse(jsonObject.has("jobUUID"), "Step Complete response should not have a jobUUID"); + assertFalse(jsonObject.has("error"), "Step Complete response should not have an error"); + + return (jsonObject); + } + + + + /******************************************************************************* + ** every time a process step (sync or async) has an error, expect certain things + ** to be (and not to be) in the json response. + *******************************************************************************/ + private JSONObject assertProcessStepErrorResponse(HttpResponse response) + { + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + + assertEquals("ERROR", jsonObject.getString("type"), "Step Running response should have type=ERROR"); + assertTrue(jsonObject.has("error"), "Step Error response should have an error"); + + assertFalse(jsonObject.has("jobUUID"), "Step Error response should not have a jobUUID"); + assertFalse(jsonObject.has("values"), "Step Error response should not have values"); + + return (jsonObject); + } + +} \ No newline at end of file diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/ExecutorCodeGeneratorTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/ExecutorCodeGeneratorTest.java new file mode 100644 index 00000000..bded2d84 --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/ExecutorCodeGeneratorTest.java @@ -0,0 +1,56 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public 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.tools.codegenerators; + + +import java.io.File; +import java.io.IOException; +import java.util.UUID; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for ExecutorCodeGenerator + *******************************************************************************/ +class ExecutorCodeGeneratorTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws IOException + { + String rootPath = "/tmp/" + UUID.randomUUID() + "/"; + File dir = new File(rootPath + "/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io"); + assertTrue(dir.mkdirs()); + new ExecutorCodeGenerator().writeAllFiles(rootPath, "SomeTest"); + + File anExpectedFile = new File(dir.getAbsolutePath() + "/SomeTestOutputInterface.java"); + assertTrue(anExpectedFile.exists()); + + FileUtils.deleteDirectory(new File(rootPath)); + } + +} \ No newline at end of file diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/SpecCodeGeneratorTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/SpecCodeGeneratorTest.java new file mode 100644 index 00000000..38023197 --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/SpecCodeGeneratorTest.java @@ -0,0 +1,56 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public 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.tools.codegenerators; + + +import java.io.File; +import java.io.IOException; +import java.util.UUID; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for SpecCodeGenerator + *******************************************************************************/ +class SpecCodeGeneratorTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws IOException + { + String rootPath = "/tmp/" + UUID.randomUUID() + "/"; + File dir = new File(rootPath + "/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses"); + assertTrue(dir.mkdirs()); + new SpecCodeGenerator().writeAllFiles(rootPath, "v1", "SomeTest"); + + File anExpectedFile = new File(dir.getAbsolutePath() + "/SomeTestResponseV1.java"); + assertTrue(anExpectedFile.exists()); + + FileUtils.deleteDirectory(new File(rootPath)); + } + +} \ No newline at end of file diff --git a/qqq-openapi/pom.xml b/qqq-openapi/pom.xml new file mode 100644 index 00000000..c223f288 --- /dev/null +++ b/qqq-openapi/pom.xml @@ -0,0 +1,81 @@ + + + + + 4.0.0 + + qqq-openapi + + + com.kingsrook.qqq + qqq-parent-project + ${revision} + + + + + + + + + + + + com.kingsrook.qqq + qqq-backend-core + ${revision} + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-core + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + + + + + + + + + + diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Components.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Components.java similarity index 99% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Components.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Components.java index 678f7bdc..51c5f322 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Components.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Components.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; import java.util.Map; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Contact.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Contact.java similarity index 97% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Contact.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Contact.java index 3ce5d6c4..7a5c9602 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Contact.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Contact.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; /******************************************************************************* diff --git a/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Content.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Content.java new file mode 100644 index 00000000..1f39bd13 --- /dev/null +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Content.java @@ -0,0 +1,98 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public 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.openapi.model; + + +import java.util.Map; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class Content +{ + private Schema schema; + private Map examples; + + + + /******************************************************************************* + ** Getter for schema + *******************************************************************************/ + public Schema getSchema() + { + return (this.schema); + } + + + + /******************************************************************************* + ** Setter for schema + *******************************************************************************/ + public void setSchema(Schema schema) + { + this.schema = schema; + } + + + + /******************************************************************************* + ** Fluent setter for schema + *******************************************************************************/ + public Content withSchema(Schema schema) + { + this.schema = schema; + return (this); + } + + + + /******************************************************************************* + ** Getter for examples + *******************************************************************************/ + public Map getExamples() + { + return (this.examples); + } + + + + /******************************************************************************* + ** Setter for examples + *******************************************************************************/ + public void setExamples(Map examples) + { + this.examples = examples; + } + + + + /******************************************************************************* + ** Fluent setter for examples + *******************************************************************************/ + public Content withExamples(Map examples) + { + this.examples = examples; + return (this); + } + +} diff --git a/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Discriminator.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Discriminator.java new file mode 100644 index 00000000..694a2260 --- /dev/null +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Discriminator.java @@ -0,0 +1,98 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public 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.openapi.model; + + +import java.util.Map; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class Discriminator +{ + private String propertyName; + private Map mapping; + + + + /******************************************************************************* + ** Getter for propertyName + *******************************************************************************/ + public String getPropertyName() + { + return (this.propertyName); + } + + + + /******************************************************************************* + ** Setter for propertyName + *******************************************************************************/ + public void setPropertyName(String propertyName) + { + this.propertyName = propertyName; + } + + + + /******************************************************************************* + ** Fluent setter for propertyName + *******************************************************************************/ + public Discriminator withPropertyName(String propertyName) + { + this.propertyName = propertyName; + return (this); + } + + + /******************************************************************************* + ** Getter for mapping + *******************************************************************************/ + public Map getMapping() + { + return (this.mapping); + } + + + + /******************************************************************************* + ** Setter for mapping + *******************************************************************************/ + public void setMapping(Map mapping) + { + this.mapping = mapping; + } + + + + /******************************************************************************* + ** Fluent setter for mapping + *******************************************************************************/ + public Discriminator withMapping(Map mapping) + { + this.mapping = mapping; + return (this); + } + + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Example.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Example.java similarity index 76% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Example.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Example.java index ea3162de..3c0dc357 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Example.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Example.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; import com.fasterxml.jackson.annotation.JsonGetter; @@ -32,6 +32,7 @@ public class Example { private String summary; private String ref; + private Object value; @@ -96,4 +97,35 @@ public class Example return (this); } + + /******************************************************************************* + ** Getter for value + *******************************************************************************/ + public Object getValue() + { + return (this.value); + } + + + + /******************************************************************************* + ** Setter for value + *******************************************************************************/ + public void setValue(Object value) + { + this.value = value; + } + + + + /******************************************************************************* + ** Fluent setter for value + *******************************************************************************/ + public Example withValue(Object value) + { + this.value = value; + return (this); + } + + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithListValue.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/ExampleWithListValue.java similarity index 98% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithListValue.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/ExampleWithListValue.java index 12601327..3094fd2d 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithListValue.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/ExampleWithListValue.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; import java.util.List; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithSingleValue.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/ExampleWithSingleValue.java similarity index 98% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithSingleValue.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/ExampleWithSingleValue.java index 79ebab6c..137f06b8 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithSingleValue.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/ExampleWithSingleValue.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; import java.io.Serializable; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExternalDocs.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/ExternalDocs.java similarity index 98% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExternalDocs.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/ExternalDocs.java index 3167c308..61f567cb 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExternalDocs.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/ExternalDocs.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; /******************************************************************************* diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/HttpMethod.java similarity index 96% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/HttpMethod.java index 6d6a8dad..1258619b 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/HttpMethod.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; /******************************************************************************* diff --git a/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/In.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/In.java new file mode 100644 index 00000000..7b27eebc --- /dev/null +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/In.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.openapi.model; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum In +{ + PATH, + QUERY, + HEADER, + COOKIE +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Info.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Info.java similarity index 99% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Info.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Info.java index 963d408b..2e1042bc 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Info.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Info.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; /******************************************************************************* diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Method.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Method.java similarity index 95% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Method.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Method.java index bbea8fe4..0f65295a 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Method.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Method.java @@ -19,9 +19,10 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -40,7 +41,7 @@ public class Method private RequestBody requestBody; private List parameters; private Map responses; - + private List>> security; @@ -200,6 +201,21 @@ public class Method + /******************************************************************************* + ** Fluent setter for tags + *******************************************************************************/ + public Method withTag(String tag) + { + if(this.tags == null) + { + this.tags = new ArrayList<>(); + } + this.tags.add(tag); + return (this); + } + + + /******************************************************************************* ** Getter for requestBody *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/OAuth2.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/OAuth2.java similarity index 98% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/OAuth2.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/OAuth2.java index d98978e9..5fcf8b78 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/OAuth2.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/OAuth2.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; import java.util.Map; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/OAuth2Flow.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/OAuth2Flow.java similarity index 98% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/OAuth2Flow.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/OAuth2Flow.java index 7f8ecac4..64c12856 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/OAuth2Flow.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/OAuth2Flow.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; import java.util.Map; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/OpenAPI.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/OpenAPI.java similarity index 98% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/OpenAPI.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/OpenAPI.java index daf33408..1cfd80ba 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/OpenAPI.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/OpenAPI.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; import java.util.List; @@ -32,7 +32,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; *******************************************************************************/ public class OpenAPI { - private String openapi = "3.0.3"; // todo not version + private String openapi = "3.0.3"; private Info info; private ExternalDocs externalDocs; private List servers; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Parameter.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Parameter.java similarity index 84% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Parameter.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Parameter.java index 9d43451f..7db6c9f8 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Parameter.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Parameter.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; import java.util.Map; @@ -37,7 +37,7 @@ public class Parameter private Schema schema; private Boolean explode; private Map examples; - private Example example; + private Object example; @@ -116,9 +116,10 @@ public class Parameter /******************************************************************************* ** Setter for in *******************************************************************************/ + @Deprecated(since = "Use version that takes enum") public void setIn(String in) { - this.in = in; + this.in = In.valueOf(in.toUpperCase()).toString().toLowerCase(); } @@ -126,9 +127,31 @@ public class Parameter /******************************************************************************* ** Fluent setter for in *******************************************************************************/ + @Deprecated(since = "Use version that takes enum") public Parameter withIn(String in) { - this.in = in; + setIn(in); + return (this); + } + + + + /******************************************************************************* + ** Setter for in + *******************************************************************************/ + public void setIn(In in) + { + this.in = in.toString().toLowerCase(); + } + + + + /******************************************************************************* + ** Fluent setter for in + *******************************************************************************/ + public Parameter withIn(In in) + { + setIn(in); return (this); } @@ -261,7 +284,7 @@ public class Parameter /******************************************************************************* ** Getter for example *******************************************************************************/ - public Example getExample() + public Object getExample() { return (this.example); } @@ -269,7 +292,7 @@ public class Parameter /******************************************************************************* - ** Setter for examplee + ** Setter for example *******************************************************************************/ public void setExample(Example example) { @@ -279,7 +302,7 @@ public class Parameter /******************************************************************************* - ** Fluent setter for examplee + ** Fluent setter for example *******************************************************************************/ public Parameter withExample(Example example) { @@ -287,4 +310,24 @@ public class Parameter return (this); } + + /******************************************************************************* + ** Setter for example + *******************************************************************************/ + public void setExample(String example) + { + this.example = example; + } + + + + /******************************************************************************* + ** Fluent setter for example + *******************************************************************************/ + public Parameter withExample(String example) + { + this.example = example; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Path.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Path.java similarity index 99% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Path.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Path.java index 3c24db9c..a2376dae 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Path.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Path.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; /******************************************************************************* diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Property.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Property.java similarity index 96% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Property.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Property.java index 9e6df242..a3072572 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Property.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Property.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; /******************************************************************************* diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/RequestBody.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/RequestBody.java similarity index 88% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/RequestBody.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/RequestBody.java index 318f49b0..d9be1435 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/RequestBody.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/RequestBody.java @@ -19,9 +19,10 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; +import java.util.LinkedHashMap; import java.util.Map; @@ -127,4 +128,19 @@ public class RequestBody return (this); } + + + /******************************************************************************* + ** Fluent setter for content + *******************************************************************************/ + public RequestBody withContent(String key, Content content) + { + if(this.content == null) + { + this.content = new LinkedHashMap<>(); + } + this.content.put(key, content); + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Response.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Response.java similarity index 98% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Response.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Response.java index 602d58c6..01219333 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Response.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Response.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; import java.util.Map; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Schema.java similarity index 61% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Schema.java index 3f858881..ed08a513 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Schema.java @@ -19,10 +19,11 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; import java.math.BigDecimal; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.fasterxml.jackson.annotation.JsonGetter; @@ -33,18 +34,23 @@ import com.fasterxml.jackson.annotation.JsonGetter; *******************************************************************************/ public class Schema { - private String type; - private String format; - private String description; - private List enumValues; - private Schema items; - private Map properties; - private Object example; - private String ref; - private List allOf; - private Boolean readOnly; - private Boolean nullable; - private Integer maxLength; + private String type; + private String format; + private String description; + private List enumValues; + private Schema items; + private Map properties; + private Object example; + private Map examples; + private String ref; + private List allOf; + private List anyOf; + private List oneOf; + private Boolean readOnly; + private Boolean nullable; + private Integer maxLength; + private Discriminator discriminator; + private Object additionalProperties; @@ -61,9 +67,10 @@ public class Schema /******************************************************************************* ** Setter for type *******************************************************************************/ + @Deprecated(since = "Use version that takes enum") public void setType(String type) { - this.type = type; + this.type = Type.valueOf(type.toUpperCase()).toString().toLowerCase(); } @@ -71,9 +78,31 @@ public class Schema /******************************************************************************* ** Fluent setter for type *******************************************************************************/ + @Deprecated(since = "Use version that takes enum") public Schema withType(String type) { - this.type = type; + setType(type); + return (this); + } + + + + /******************************************************************************* + ** Setter for type + *******************************************************************************/ + public void setType(Type type) + { + this.type = type == null ? null : type.toString().toLowerCase(); + } + + + + /******************************************************************************* + ** Fluent setter for type + *******************************************************************************/ + public Schema withType(Type type) + { + setType(type); return (this); } @@ -172,6 +201,21 @@ public class Schema + /******************************************************************************* + ** Fluent setter for properties + *******************************************************************************/ + public Schema withProperty(String key, Schema schema) + { + if(this.properties == null) + { + this.properties = new LinkedHashMap<>(); + } + this.properties.put(key, schema); + return (this); + } + + + /******************************************************************************* ** Getter for example *******************************************************************************/ @@ -462,4 +506,205 @@ public class Schema return (this); } + + + /******************************************************************************* + ** Getter for discriminator + *******************************************************************************/ + public Discriminator getDiscriminator() + { + return (this.discriminator); + } + + + + /******************************************************************************* + ** Setter for discriminator + *******************************************************************************/ + public void setDiscriminator(Discriminator discriminator) + { + this.discriminator = discriminator; + } + + + + /******************************************************************************* + ** Fluent setter for discriminator + *******************************************************************************/ + public Schema withDiscriminator(Discriminator discriminator) + { + this.discriminator = discriminator; + return (this); + } + + + + /******************************************************************************* + ** Getter for anyOf + *******************************************************************************/ + public List getAnyOf() + { + return (this.anyOf); + } + + + + /******************************************************************************* + ** Setter for anyOf + *******************************************************************************/ + public void setAnyOf(List anyOf) + { + this.anyOf = anyOf; + } + + + + /******************************************************************************* + ** Fluent setter for anyOf + *******************************************************************************/ + public Schema withAnyOf(List anyOf) + { + this.anyOf = anyOf; + return (this); + } + + + + /******************************************************************************* + ** Getter for oneOf + *******************************************************************************/ + public List getOneOf() + { + return (this.oneOf); + } + + + + /******************************************************************************* + ** Setter for oneOf + *******************************************************************************/ + public void setOneOf(List oneOf) + { + this.oneOf = oneOf; + } + + + + /******************************************************************************* + ** Fluent setter for oneOf + *******************************************************************************/ + public Schema withOneOf(List oneOf) + { + this.oneOf = oneOf; + return (this); + } + + + + /******************************************************************************* + ** Getter for examples + *******************************************************************************/ + public Map getExamples() + { + return (this.examples); + } + + + + /******************************************************************************* + ** Setter for examples + *******************************************************************************/ + public void setExamples(Map examples) + { + this.examples = examples; + } + + + + /******************************************************************************* + ** Fluent setter for examples + *******************************************************************************/ + public Schema withExamples(Map examples) + { + this.examples = examples; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for examples + *******************************************************************************/ + public Schema withExample(String name, Example example) + { + if(this.examples == null) + { + this.examples = new LinkedHashMap<>(); + } + this.examples.put(name, example); + return (this); + } + + + + /******************************************************************************* + ** Getter for additionalProperties + *******************************************************************************/ + public Object getAdditionalProperties() + { + return (this.additionalProperties); + } + + + + /******************************************************************************* + ** Setter for additionalProperties + *******************************************************************************/ + public void setAdditionalProperties(Schema additionalProperties) + { + this.additionalProperties = additionalProperties; + } + + + + /******************************************************************************* + ** Fluent setter for additionalProperties + *******************************************************************************/ + public Schema withAdditionalProperties(Schema additionalProperties) + { + this.additionalProperties = additionalProperties; + return (this); + } + + + + /******************************************************************************* + ** Setter for additionalProperties + *******************************************************************************/ + public void setAdditionalProperties(Boolean additionalProperties) + { + this.additionalProperties = additionalProperties; + } + + + + /******************************************************************************* + ** Fluent setter for additionalProperties + *******************************************************************************/ + public Schema withAdditionalProperties(Boolean additionalProperties) + { + this.additionalProperties = additionalProperties; + return (this); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public Schema withRefToSchema(String componentName) + { + return withRef("#/components/schemas/" + componentName); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/SecurityScheme.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/SecurityScheme.java similarity index 99% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/SecurityScheme.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/SecurityScheme.java index dfbd56b9..8c5d8750 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/SecurityScheme.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/SecurityScheme.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/SecuritySchemeType.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/SecuritySchemeType.java similarity index 97% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/SecuritySchemeType.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/SecuritySchemeType.java index 06eb002a..6b04b954 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/SecuritySchemeType.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/SecuritySchemeType.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; /******************************************************************************* diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Server.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Server.java similarity index 98% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Server.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Server.java index 0d5f7a83..0fd70e33 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Server.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Server.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; /******************************************************************************* diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Tag.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Tag.java similarity index 98% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Tag.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Tag.java index e9a43da8..1712f0c2 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Tag.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Tag.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.api.model.openapi; +package com.kingsrook.qqq.openapi.model; /******************************************************************************* diff --git a/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Type.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Type.java new file mode 100644 index 00000000..7646083e --- /dev/null +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/Type.java @@ -0,0 +1,36 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public 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.openapi.model; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum Type +{ + ARRAY, + BOOLEAN, + INTEGER, + NUMBER, + OBJECT, + STRING; +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/package-info.java b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/package-info.java similarity index 96% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/package-info.java rename to qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/package-info.java index b717d881..6af8cb94 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/package-info.java +++ b/qqq-openapi/src/main/java/com/kingsrook/qqq/openapi/model/package-info.java @@ -22,4 +22,4 @@ /******************************************************************************* ** The POJOs in this package represent a model of an OpenAPI spec file. *******************************************************************************/ -package com.kingsrook.qqq.api.model.openapi; \ No newline at end of file +package com.kingsrook.qqq.openapi.model; \ No newline at end of file diff --git a/qqq-openapi/src/main/resources/images/qqq-on-crown-trans-160x80.png b/qqq-openapi/src/main/resources/images/qqq-on-crown-trans-160x80.png new file mode 100644 index 00000000..33027394 Binary files /dev/null and b/qqq-openapi/src/main/resources/images/qqq-on-crown-trans-160x80.png differ diff --git a/qqq-openapi/src/main/resources/rapidoc/rapidoc-9.3.8.min.js b/qqq-openapi/src/main/resources/rapidoc/rapidoc-9.3.8.min.js new file mode 100644 index 00000000..f6ba6e3c --- /dev/null +++ b/qqq-openapi/src/main/resources/rapidoc/rapidoc-9.3.8.min.js @@ -0,0 +1,3915 @@ +/*! RapiDoc 9.3.8 | Author - Mrinmoy Majumdar | License information can be found in rapidoc-min.js.LICENSE.txt */ +(()=>{var e,t,r={557:(e,t,r)=>{"use strict";const s=globalThis,n=s.ShadowRoot&&(void 0===s.ShadyCSS||s.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,i=Symbol(),o=new WeakMap;class a{constructor(e,t,r){if(this._$cssResult$=!0,r!==i)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const t=this.t;if(n&&void 0===e){const r=void 0!==t&&1===t.length;r&&(e=o.get(t)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),r&&o.set(t,e))}return e}toString(){return this.cssText}}const l=e=>new a("string"==typeof e?e:e+"",void 0,i),c=(e,...t)=>{const r=1===e.length?e[0]:t.reduce(((t,r,s)=>t+(e=>{if(!0===e._$cssResult$)return e.cssText;if("number"==typeof e)return e;throw Error("Value passed to 'css' function must be a 'css' function result: "+e+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(r)+e[s+1]),e[0]);return new a(r,e,i)},p=(e,t)=>{if(n)e.adoptedStyleSheets=t.map((e=>e instanceof CSSStyleSheet?e:e.styleSheet));else for(const r of t){const t=document.createElement("style"),n=s.litNonce;void 0!==n&&t.setAttribute("nonce",n),t.textContent=r.cssText,e.appendChild(t)}},u=n?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const r of e.cssRules)t+=r.cssText;return l(t)})(e):e,{is:d,defineProperty:h,getOwnPropertyDescriptor:m,getOwnPropertyNames:f,getOwnPropertySymbols:g,getPrototypeOf:y}=Object,v=globalThis,b=v.trustedTypes,x=b?b.emptyScript:"",w=v.reactiveElementPolyfillSupport,$=(e,t)=>e,S={toAttribute(e,t){switch(t){case Boolean:e=e?x:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let r=e;switch(t){case Boolean:r=null!==e;break;case Number:r=null===e?null:Number(e);break;case Object:case Array:try{r=JSON.parse(e)}catch(e){r=null}}return r}},E=(e,t)=>!d(e,t),k={attribute:!0,type:String,converter:S,reflect:!1,hasChanged:E};Symbol.metadata??=Symbol("metadata"),v.litPropertyMetadata??=new WeakMap;class A extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=k){if(t.state&&(t.attribute=!1),this._$Ei(),this.elementProperties.set(e,t),!t.noAccessor){const r=Symbol(),s=this.getPropertyDescriptor(e,r,t);void 0!==s&&h(this.prototype,e,s)}}static getPropertyDescriptor(e,t,r){const{get:s,set:n}=m(this.prototype,e)??{get(){return this[t]},set(e){this[t]=e}};return{get(){return s?.call(this)},set(t){const i=s?.call(this);n.call(this,t),this.requestUpdate(e,i,r)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??k}static _$Ei(){if(this.hasOwnProperty($("elementProperties")))return;const e=y(this);e.finalize(),void 0!==e.l&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty($("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty($("properties"))){const e=this.properties,t=[...f(e),...g(e)];for(const r of t)this.createProperty(r,e[r])}const e=this[Symbol.metadata];if(null!==e){const t=litPropertyMetadata.get(e);if(void 0!==t)for(const[e,r]of t)this.elementProperties.set(e,r)}this._$Eh=new Map;for(const[e,t]of this.elementProperties){const r=this._$Eu(e,t);void 0!==r&&this._$Eh.set(r,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const r=new Set(e.flat(1/0).reverse());for(const e of r)t.unshift(u(e))}else void 0!==e&&t.push(u(e));return t}static _$Eu(e,t){const r=t.attribute;return!1===r?void 0:"string"==typeof r?r:"string"==typeof e?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise((e=>this.enableUpdating=e)),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach((e=>e(this)))}addController(e){(this._$EO??=new Set).add(e),void 0!==this.renderRoot&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){const e=new Map,t=this.constructor.elementProperties;for(const r of t.keys())this.hasOwnProperty(r)&&(e.set(r,this[r]),delete this[r]);e.size>0&&(this._$Ep=e)}createRenderRoot(){const e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return p(e,this.constructor.elementStyles),e}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach((e=>e.hostConnected?.()))}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach((e=>e.hostDisconnected?.()))}attributeChangedCallback(e,t,r){this._$AK(e,r)}_$EC(e,t){const r=this.constructor.elementProperties.get(e),s=this.constructor._$Eu(e,r);if(void 0!==s&&!0===r.reflect){const n=(void 0!==r.converter?.toAttribute?r.converter:S).toAttribute(t,r.type);this._$Em=e,null==n?this.removeAttribute(s):this.setAttribute(s,n),this._$Em=null}}_$AK(e,t){const r=this.constructor,s=r._$Eh.get(e);if(void 0!==s&&this._$Em!==s){const e=r.getPropertyOptions(s),n="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==e.converter?.fromAttribute?e.converter:S;this._$Em=s,this[s]=n.fromAttribute(t,e.type),this._$Em=null}}requestUpdate(e,t,r){if(void 0!==e){if(r??=this.constructor.getPropertyOptions(e),!(r.hasChanged??E)(this[e],t))return;this.P(e,t,r)}!1===this.isUpdatePending&&(this._$ES=this._$ET())}P(e,t,r){this._$AL.has(e)||this._$AL.set(e,t),!0===r.reflect&&this._$Em!==e&&(this._$Ej??=new Set).add(e)}async _$ET(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}const e=this.scheduleUpdate();return null!=e&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[e,t]of this._$Ep)this[e]=t;this._$Ep=void 0}const e=this.constructor.elementProperties;if(e.size>0)for(const[t,r]of e)!0!==r.wrapped||this._$AL.has(t)||void 0===this[t]||this.P(t,this[t],r)}let e=!1;const t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach((e=>e.hostUpdate?.())),this.update(t)):this._$EU()}catch(t){throw e=!1,this._$EU(),t}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach((e=>e.hostUpdated?.())),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EU(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Ej&&=this._$Ej.forEach((e=>this._$EC(e,this[e]))),this._$EU()}updated(e){}firstUpdated(e){}}A.elementStyles=[],A.shadowRootOptions={mode:"open"},A[$("elementProperties")]=new Map,A[$("finalized")]=new Map,w?.({ReactiveElement:A}),(v.reactiveElementVersions??=[]).push("2.0.4");const O=globalThis,j=O.trustedTypes,T=j?j.createPolicy("lit-html",{createHTML:e=>e}):void 0,P="$lit$",C=`lit$${Math.random().toFixed(9).slice(2)}$`,I="?"+C,_=`<${I}>`,R=document,F=()=>R.createComment(""),M=e=>null===e||"object"!=typeof e&&"function"!=typeof e,L=Array.isArray,D=e=>L(e)||"function"==typeof e?.[Symbol.iterator],B="[ \t\n\f\r]",q=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,N=/-->/g,U=/>/g,z=RegExp(`>|${B}(?:([^\\s"'>=/]+)(${B}*=${B}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),H=/'/g,V=/"/g,W=/^(?:script|style|textarea|title)$/i,G=e=>(t,...r)=>({_$litType$:e,strings:t,values:r}),J=G(1),K=(G(2),G(3),Symbol.for("lit-noChange")),Y=Symbol.for("lit-nothing"),X=new WeakMap,Z=R.createTreeWalker(R,129);function Q(e,t){if(!L(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==T?T.createHTML(t):t}const ee=(e,t)=>{const r=e.length-1,s=[];let n,i=2===t?"":3===t?"":"",o=q;for(let t=0;t"===l[0]?(o=n??q,c=-1):void 0===l[1]?c=-2:(c=o.lastIndex-l[2].length,a=l[1],o=void 0===l[3]?z:'"'===l[3]?V:H):o===V||o===H?o=z:o===N||o===U?o=q:(o=z,n=void 0);const u=o===z&&e[t+1].startsWith("/>")?" ":"";i+=o===q?r+_:c>=0?(s.push(a),r.slice(0,c)+P+r.slice(c)+C+u):r+C+(-2===c?t:u)}return[Q(e,i+(e[r]||"")+(2===t?"":3===t?"":"")),s]};class te{constructor({strings:e,_$litType$:t},r){let s;this.parts=[];let n=0,i=0;const o=e.length-1,a=this.parts,[l,c]=ee(e,t);if(this.el=te.createElement(l,r),Z.currentNode=this.el.content,2===t||3===t){const e=this.el.content.firstChild;e.replaceWith(...e.childNodes)}for(;null!==(s=Z.nextNode())&&a.length0){s.textContent=j?j.emptyScript:"";for(let r=0;r2||""!==r[0]||""!==r[1]?(this._$AH=Array(r.length-1).fill(new String),this.strings=r):this._$AH=Y}_$AI(e,t=this,r,s){const n=this.strings;let i=!1;if(void 0===n)e=re(this,e,t,0),i=!M(e)||e!==this._$AH&&e!==K,i&&(this._$AH=e);else{const s=e;let o,a;for(e=n[0],o=0;o{const s=r?.renderBefore??t;let n=s._$litPart$;if(void 0===n){const e=r?.renderBefore??null;s._$litPart$=n=new ne(t.insertBefore(F(),e),e,void 0,r??{})}return n._$AI(e),n})(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this.o?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this.o?.setConnected(!1)}render(){return K}}de._$litElement$=!0,de.finalized=!0,globalThis.litElementHydrateSupport?.({LitElement:de});const he=globalThis.litElementPolyfillSupport;he?.({LitElement:de});function me(){return{async:!1,baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,hooks:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}(globalThis.litElementVersions??=[]).push("4.1.0");let fe={async:!1,baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,hooks:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1};const ge=/[&<>"']/,ye=new RegExp(ge.source,"g"),ve=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,be=new RegExp(ve.source,"g"),xe={"&":"&","<":"<",">":">",'"':""","'":"'"},we=e=>xe[e];function $e(e,t){if(t){if(ge.test(e))return e.replace(ye,we)}else if(ve.test(e))return e.replace(be,we);return e}const Se=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function Ee(e){return e.replace(Se,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const ke=/(^|[^\[])\^/g;function Ae(e,t){e="string"==typeof e?e:e.source,t=t||"";const r={replace:(t,s)=>(s=(s=s.source||s).replace(ke,"$1"),e=e.replace(t,s),r),getRegex:()=>new RegExp(e,t)};return r}const Oe=/[^\w:]/g,je=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function Te(e,t,r){if(e){let e;try{e=decodeURIComponent(Ee(r)).replace(Oe,"").toLowerCase()}catch(e){return null}if(0===e.indexOf("javascript:")||0===e.indexOf("vbscript:")||0===e.indexOf("data:"))return null}t&&!je.test(r)&&(r=function(e,t){Pe[" "+e]||(Ce.test(e)?Pe[" "+e]=e+"/":Pe[" "+e]=Me(e,"/",!0));e=Pe[" "+e];const r=-1===e.indexOf(":");return"//"===t.substring(0,2)?r?t:e.replace(Ie,"$1")+t:"/"===t.charAt(0)?r?t:e.replace(_e,"$1")+t:e+t}(t,r));try{r=encodeURI(r).replace(/%25/g,"%")}catch(e){return null}return r}const Pe={},Ce=/^[^:]+:\/*[^/]*$/,Ie=/^([^:]+:)[\s\S]*$/,_e=/^([^:]+:\/*[^/]*)[\s\S]*$/;const Re={exec:function(){}};function Fe(e,t){const r=e.replace(/\|/g,((e,t,r)=>{let s=!1,n=t;for(;--n>=0&&"\\"===r[n];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(r[0].trim()||r.shift(),r.length>0&&!r[r.length-1].trim()&&r.pop(),r.length>t)r.splice(t);else for(;r.length1;)1&t&&(r+=e),t>>=1,e+=e;return r+e}function De(e,t,r,s){const n=t.href,i=t.title?$e(t.title):null,o=e[1].replace(/\\([\[\]])/g,"$1");if("!"!==e[0].charAt(0)){s.state.inLink=!0;const e={type:"link",raw:r,href:n,title:i,text:o,tokens:s.inlineTokens(o)};return s.state.inLink=!1,e}return{type:"image",raw:r,href:n,title:i,text:$e(o)}}class Be{constructor(e){this.options=e||fe}space(e){const t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:Me(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],r=function(e,t){const r=e.match(/^(\s+)(?:```)/);if(null===r)return t;const s=r[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[r]=t;return r.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline._escapes,"$1"):t[2],text:r}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=Me(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){const e=t[0].replace(/^ *>[ \t]?/gm,""),r=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=r,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let r,s,n,i,o,a,l,c,p,u,d,h,m=t[1].trim();const f=m.length>1,g={type:"list",raw:"",ordered:f,start:f?+m.slice(0,-1):"",loose:!1,items:[]};m=f?`\\d{1,9}\\${m.slice(-1)}`:`\\${m}`,this.options.pedantic&&(m=f?m:"[*+-]");const y=new RegExp(`^( {0,3}${m})((?:[\t ][^\\n]*)?(?:\\n|$))`);for(;e&&(h=!1,t=y.exec(e))&&!this.rules.block.hr.test(e);){if(r=t[0],e=e.substring(r.length),c=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),p=e.split("\n",1)[0],this.options.pedantic?(i=2,d=c.trimLeft()):(i=t[2].search(/[^ ]/),i=i>4?1:i,d=c.slice(i),i+=t[1].length),a=!1,!c&&/^ *$/.test(p)&&(r+=p+"\n",e=e.substring(p.length+1),h=!0),!h){const t=new RegExp(`^ {0,${Math.min(3,i-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),s=new RegExp(`^ {0,${Math.min(3,i-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),n=new RegExp(`^ {0,${Math.min(3,i-1)}}(?:\`\`\`|~~~)`),o=new RegExp(`^ {0,${Math.min(3,i-1)}}#`);for(;e&&(u=e.split("\n",1)[0],p=u,this.options.pedantic&&(p=p.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),!n.test(p))&&!o.test(p)&&!t.test(p)&&!s.test(e);){if(p.search(/[^ ]/)>=i||!p.trim())d+="\n"+p.slice(i);else{if(a)break;if(c.search(/[^ ]/)>=4)break;if(n.test(c))break;if(o.test(c))break;if(s.test(c))break;d+="\n"+p}a||p.trim()||(a=!0),r+=u+"\n",e=e.substring(u.length+1),c=p.slice(i)}}g.loose||(l?g.loose=!0:/\n *\n *$/.test(r)&&(l=!0)),this.options.gfm&&(s=/^\[[ xX]\] /.exec(d),s&&(n="[ ] "!==s[0],d=d.replace(/^\[[ xX]\] +/,""))),g.items.push({type:"list_item",raw:r,task:!!s,checked:n,loose:!1,text:d}),g.raw+=r}g.items[g.items.length-1].raw=r.trimRight(),g.items[g.items.length-1].text=d.trimRight(),g.raw=g.raw.trimRight();const v=g.items.length;for(o=0;o"space"===e.type)),t=e.length>0&&e.some((e=>/\n.*\n/.test(e.raw)));g.loose=t}if(g.loose)for(o=0;o$/,"$1").replace(this.rules.inline._escapes,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline._escapes,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:r,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(t){const e={type:"table",header:Fe(t[1]).map((e=>({text:e}))),align:t[2].replace(/^ *|\| *$/g,"").split(/ *\| */),rows:t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[]};if(e.header.length===e.align.length){e.raw=t[0];let r,s,n,i,o=e.align.length;for(r=0;r({text:e})));for(o=e.header.length,s=0;s/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:this.options.sanitize?"text":"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(t[0]):$e(t[0]):t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=Me(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;const r=e.length;let s=0,n=0;for(;n-1){const r=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,r).trim(),t[3]=""}}let r=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(r);e&&(r=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return r=r.trim(),/^$/.test(e)?r.slice(1):r.slice(1,-1)),De(t,{href:r?r.replace(this.rules.inline._escapes,"$1"):r,title:s?s.replace(this.rules.inline._escapes,"$1"):s},t[0],this.lexer)}}reflink(e,t){let r;if((r=this.rules.inline.reflink.exec(e))||(r=this.rules.inline.nolink.exec(e))){let e=(r[2]||r[1]).replace(/\s+/g," ");if(e=t[e.toLowerCase()],!e){const e=r[0].charAt(0);return{type:"text",raw:e,text:e}}return De(r,e,r[0],this.lexer)}}emStrong(e,t,r=""){let s=this.rules.inline.emStrong.lDelim.exec(e);if(!s)return;if(s[3]&&r.match(/[\p{L}\p{N}]/u))return;const n=s[1]||s[2]||"";if(!n||n&&(""===r||this.rules.inline.punctuation.exec(r))){const r=s[0].length-1;let n,i,o=r,a=0;const l="*"===s[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(l.lastIndex=0,t=t.slice(-1*e.length+r);null!=(s=l.exec(t));){if(n=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!n)continue;if(i=n.length,s[3]||s[4]){o+=i;continue}if((s[5]||s[6])&&r%3&&!((r+i)%3)){a+=i;continue}if(o-=i,o>0)continue;i=Math.min(i,i+o+a);const t=e.slice(0,r+s.index+(s[0].length-n.length)+i);if(Math.min(r,i)%2){const e=t.slice(1,-1);return{type:"em",raw:t,text:e,tokens:this.lexer.inlineTokens(e)}}const l=t.slice(2,-2);return{type:"strong",raw:t,text:l,tokens:this.lexer.inlineTokens(l)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const r=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return r&&s&&(e=e.substring(1,e.length-1)),e=$e(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e,t){const r=this.rules.inline.autolink.exec(e);if(r){let e,s;return"@"===r[2]?(e=$e(this.options.mangle?t(r[1]):r[1]),s="mailto:"+e):(e=$e(r[1]),s=e),{type:"link",raw:r[0],text:e,href:s,tokens:[{type:"text",raw:e,text:e}]}}}url(e,t){let r;if(r=this.rules.inline.url.exec(e)){let e,s;if("@"===r[2])e=$e(this.options.mangle?t(r[0]):r[0]),s="mailto:"+e;else{let t;do{t=r[0],r[0]=this.rules.inline._backpedal.exec(r[0])[0]}while(t!==r[0]);e=$e(r[0]),s="www."===r[1]?"http://"+r[0]:r[0]}return{type:"link",raw:r[0],text:e,href:s,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e,t){const r=this.rules.inline.text.exec(e);if(r){let e;return e=this.lexer.state.inRawBlock?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(r[0]):$e(r[0]):r[0]:$e(this.options.smartypants?t(r[0]):r[0]),{type:"text",raw:r[0],text:e}}}}const qe={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,hr:/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/,html:"^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/,table:Re,lheading:/^((?:.|\n(?!\n))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\.|[^\[\]\\])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/};qe.def=Ae(qe.def).replace("label",qe._label).replace("title",qe._title).getRegex(),qe.bullet=/(?:[*+-]|\d{1,9}[.)])/,qe.listItemStart=Ae(/^( *)(bull) */).replace("bull",qe.bullet).getRegex(),qe.list=Ae(qe.list).replace(/bull/g,qe.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+qe.def.source+")").getRegex(),qe._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",qe._comment=/|$)/,qe.html=Ae(qe.html,"i").replace("comment",qe._comment).replace("tag",qe._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),qe.paragraph=Ae(qe._paragraph).replace("hr",qe.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",qe._tag).getRegex(),qe.blockquote=Ae(qe.blockquote).replace("paragraph",qe.paragraph).getRegex(),qe.normal={...qe},qe.gfm={...qe.normal,table:"^ *([^\\n ].*\\|.*)\\n {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"},qe.gfm.table=Ae(qe.gfm.table).replace("hr",qe.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",qe._tag).getRegex(),qe.gfm.paragraph=Ae(qe._paragraph).replace("hr",qe.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("table",qe.gfm.table).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",qe._tag).getRegex(),qe.pedantic={...qe.normal,html:Ae("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",qe._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:Re,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:Ae(qe.normal._paragraph).replace("hr",qe.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",qe.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()};const Ne={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:Re,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(ref)\]/,nolink:/^!?\[(ref)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/^(?:[^_*\\]|\\.)*?\_\_(?:[^_*\\]|\\.)*?\*(?:[^_*\\]|\\.)*?(?=\_\_)|(?:[^*\\]|\\.)+(?=[^*])|[punct_](\*+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|(?:[^punct*_\s\\]|\\.)(\*+)(?=[^punct*_\s])/,rDelimUnd:/^(?:[^_*\\]|\\.)*?\*\*(?:[^_*\\]|\\.)*?\_(?:[^_*\\]|\\.)*?(?=\*\*)|(?:[^_\\]|\\.)+(?=[^_])|[punct*](\_+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:Re,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\.5&&(r="x"+r.toString(16)),s+="&#"+r+";";return s}Ne._punctuation="!\"#$%&'()+\\-.,/:;<=>?@\\[\\]`^{|}~",Ne.punctuation=Ae(Ne.punctuation).replace(/punctuation/g,Ne._punctuation).getRegex(),Ne.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,Ne.escapedEmSt=/(?:^|[^\\])(?:\\\\)*\\[*_]/g,Ne._comment=Ae(qe._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),Ne.emStrong.lDelim=Ae(Ne.emStrong.lDelim).replace(/punct/g,Ne._punctuation).getRegex(),Ne.emStrong.rDelimAst=Ae(Ne.emStrong.rDelimAst,"g").replace(/punct/g,Ne._punctuation).getRegex(),Ne.emStrong.rDelimUnd=Ae(Ne.emStrong.rDelimUnd,"g").replace(/punct/g,Ne._punctuation).getRegex(),Ne._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,Ne._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,Ne._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,Ne.autolink=Ae(Ne.autolink).replace("scheme",Ne._scheme).replace("email",Ne._email).getRegex(),Ne._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,Ne.tag=Ae(Ne.tag).replace("comment",Ne._comment).replace("attribute",Ne._attribute).getRegex(),Ne._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,Ne._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,Ne._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,Ne.link=Ae(Ne.link).replace("label",Ne._label).replace("href",Ne._href).replace("title",Ne._title).getRegex(),Ne.reflink=Ae(Ne.reflink).replace("label",Ne._label).replace("ref",qe._label).getRegex(),Ne.nolink=Ae(Ne.nolink).replace("ref",qe._label).getRegex(),Ne.reflinkSearch=Ae(Ne.reflinkSearch,"g").replace("reflink",Ne.reflink).replace("nolink",Ne.nolink).getRegex(),Ne.normal={...Ne},Ne.pedantic={...Ne.normal,strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:Ae(/^!?\[(label)\]\((.*?)\)/).replace("label",Ne._label).getRegex(),reflink:Ae(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",Ne._label).getRegex()},Ne.gfm={...Ne.normal,escape:Ae(Ne.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\t+" ".repeat(r.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((s=>!!(r=s.call({lexer:this},e,t))&&(e=e.substring(r.raw.length),t.push(r),!0)))))if(r=this.tokenizer.space(e))e=e.substring(r.raw.length),1===r.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(r);else if(r=this.tokenizer.code(e))e=e.substring(r.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?t.push(r):(s.raw+="\n"+r.raw,s.text+="\n"+r.text,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(r=this.tokenizer.fences(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.heading(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.hr(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.blockquote(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.list(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.html(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.def(e))e=e.substring(r.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title}):(s.raw+="\n"+r.raw,s.text+="\n"+r.raw,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(r=this.tokenizer.table(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.lheading(e))e=e.substring(r.raw.length),t.push(r);else{if(n=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const r=e.slice(1);let s;this.options.extensions.startBlock.forEach((function(e){s=e.call({lexer:this},r),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(n=e.substring(0,t+1))}if(this.state.top&&(r=this.tokenizer.paragraph(n)))s=t[t.length-1],i&&"paragraph"===s.type?(s.raw+="\n"+r.raw,s.text+="\n"+r.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(r),i=n.length!==e.length,e=e.substring(r.raw.length);else if(r=this.tokenizer.text(e))e=e.substring(r.raw.length),s=t[t.length-1],s&&"text"===s.type?(s.raw+="\n"+r.raw,s.text+="\n"+r.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(r);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let r,s,n,i,o,a,l=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(l));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(l=l.slice(0,i.index)+"["+Le("a",i[0].length-2)+"]"+l.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(l));)l=l.slice(0,i.index)+"["+Le("a",i[0].length-2)+"]"+l.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.escapedEmSt.exec(l));)l=l.slice(0,i.index+i[0].length-2)+"++"+l.slice(this.tokenizer.rules.inline.escapedEmSt.lastIndex),this.tokenizer.rules.inline.escapedEmSt.lastIndex--;for(;e;)if(o||(a=""),o=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(r=s.call({lexer:this},e,t))&&(e=e.substring(r.raw.length),t.push(r),!0)))))if(r=this.tokenizer.escape(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.tag(e))e=e.substring(r.raw.length),s=t[t.length-1],s&&"text"===r.type&&"text"===s.type?(s.raw+=r.raw,s.text+=r.text):t.push(r);else if(r=this.tokenizer.link(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(r.raw.length),s=t[t.length-1],s&&"text"===r.type&&"text"===s.type?(s.raw+=r.raw,s.text+=r.text):t.push(r);else if(r=this.tokenizer.emStrong(e,l,a))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.codespan(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.br(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.del(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.autolink(e,ze))e=e.substring(r.raw.length),t.push(r);else if(this.state.inLink||!(r=this.tokenizer.url(e,ze))){if(n=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const r=e.slice(1);let s;this.options.extensions.startInline.forEach((function(e){s=e.call({lexer:this},r),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(n=e.substring(0,t+1))}if(r=this.tokenizer.inlineText(n,Ue))e=e.substring(r.raw.length),"_"!==r.raw.slice(-1)&&(a=r.raw.slice(-1)),o=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=r.raw,s.text+=r.text):t.push(r);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(r.raw.length),t.push(r);return t}}class Ve{constructor(e){this.options=e||fe}code(e,t,r){const s=(t||"").match(/\S*/)[0];if(this.options.highlight){const t=this.options.highlight(e,s);null!=t&&t!==e&&(r=!0,e=t)}return e=e.replace(/\n$/,"")+"\n",s?'
'+(r?e:$e(e,!0))+"
\n":"
"+(r?e:$e(e,!0))+"
\n"}blockquote(e){return`
\n${e}
\n`}html(e){return e}heading(e,t,r,s){if(this.options.headerIds){return`${e}\n`}return`${e}\n`}hr(){return this.options.xhtml?"
\n":"
\n"}list(e,t,r){const s=t?"ol":"ul";return"<"+s+(t&&1!==r?' start="'+r+'"':"")+">\n"+e+"\n"}listitem(e){return`
  • ${e}
  • \n`}checkbox(e){return" "}paragraph(e){return`

    ${e}

    \n`}table(e,t){return t&&(t=`${t}`),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return`\n${e}\n`}tablecell(e,t){const r=t.header?"th":"td";return(t.align?`<${r} align="${t.align}">`:`<${r}>`)+e+`\n`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return this.options.xhtml?"
    ":"
    "}del(e){return`${e}`}link(e,t,r){if(null===(e=Te(this.options.sanitize,this.options.baseUrl,e)))return r;let s='",s}image(e,t,r){if(null===(e=Te(this.options.sanitize,this.options.baseUrl,e)))return r;let s=`${r}":">",s}text(e){return e}}class We{strong(e){return e}em(e){return e}codespan(e){return e}del(e){return e}html(e){return e}text(e){return e}link(e,t,r){return""+r}image(e,t,r){return""+r}br(){return""}}class Ge{constructor(){this.seen={}}serialize(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")}getNextSafeSlug(e,t){let r=e,s=0;if(this.seen.hasOwnProperty(r)){s=this.seen[e];do{s++,r=e+"-"+s}while(this.seen.hasOwnProperty(r))}return t||(this.seen[e]=s,this.seen[r]=0),r}slug(e,t={}){const r=this.serialize(e);return this.getNextSafeSlug(r,t.dryrun)}}class Je{constructor(e){this.options=e||fe,this.options.renderer=this.options.renderer||new Ve,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new We,this.slugger=new Ge}static parse(e,t){return new Je(t).parse(e)}static parseInline(e,t){return new Je(t).parseInline(e)}parse(e,t=!0){let r,s,n,i,o,a,l,c,p,u,d,h,m,f,g,y,v,b,x,w="";const $=e.length;for(r=0;r<$;r++)if(u=e[r],this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[u.type]&&(x=this.options.extensions.renderers[u.type].call({parser:this},u),!1!==x||!["space","hr","heading","code","table","blockquote","list","html","paragraph","text"].includes(u.type)))w+=x||"";else switch(u.type){case"space":continue;case"hr":w+=this.renderer.hr();continue;case"heading":w+=this.renderer.heading(this.parseInline(u.tokens),u.depth,Ee(this.parseInline(u.tokens,this.textRenderer)),this.slugger);continue;case"code":w+=this.renderer.code(u.text,u.lang,u.escaped);continue;case"table":for(c="",l="",i=u.header.length,s=0;s0&&"paragraph"===g.tokens[0].type?(g.tokens[0].text=b+" "+g.tokens[0].text,g.tokens[0].tokens&&g.tokens[0].tokens.length>0&&"text"===g.tokens[0].tokens[0].type&&(g.tokens[0].tokens[0].text=b+" "+g.tokens[0].tokens[0].text)):g.tokens.unshift({type:"text",text:b}):f+=b),f+=this.parse(g.tokens,m),p+=this.renderer.listitem(f,v,y);w+=this.renderer.list(p,d,h);continue;case"html":w+=this.renderer.html(u.text);continue;case"paragraph":w+=this.renderer.paragraph(this.parseInline(u.tokens));continue;case"text":for(p=u.tokens?this.parseInline(u.tokens):u.text;r+1<$&&"text"===e[r+1].type;)u=e[++r],p+="\n"+(u.tokens?this.parseInline(u.tokens):u.text);w+=t?this.renderer.paragraph(p):p;continue;default:{const e='Token with "'+u.type+'" type was not found.';if(this.options.silent)return void console.error(e);throw new Error(e)}}return w}parseInline(e,t){t=t||this.renderer;let r,s,n,i="";const o=e.length;for(r=0;r{"function"==typeof s&&(n=s,s=null);const i={...s},o=function(e,t,r){return s=>{if(s.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+$e(s.message+"",!0)+"
    ";return t?Promise.resolve(e):r?void r(null,e):e}if(t)return Promise.reject(s);if(!r)throw s;r(s)}}((s={...Xe.defaults,...i}).silent,s.async,n);if(null==r)return o(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof r)return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(r)+", string expected"));if(function(e){e&&e.sanitize&&!e.silent&&console.warn("marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options")}(s),s.hooks&&(s.hooks.options=s),n){const i=s.highlight;let a;try{s.hooks&&(r=s.hooks.preprocess(r)),a=e(r,s)}catch(e){return o(e)}const l=function(e){let r;if(!e)try{s.walkTokens&&Xe.walkTokens(a,s.walkTokens),r=t(a,s),s.hooks&&(r=s.hooks.postprocess(r))}catch(t){e=t}return s.highlight=i,e?o(e):n(null,r)};if(!i||i.length<3)return l();if(delete s.highlight,!a.length)return l();let c=0;return Xe.walkTokens(a,(function(e){"code"===e.type&&(c++,setTimeout((()=>{i(e.text,e.lang,(function(t,r){if(t)return l(t);null!=r&&r!==e.text&&(e.text=r,e.escaped=!0),c--,0===c&&l()}))}),0))})),void(0===c&&l())}if(s.async)return Promise.resolve(s.hooks?s.hooks.preprocess(r):r).then((t=>e(t,s))).then((e=>s.walkTokens?Promise.all(Xe.walkTokens(e,s.walkTokens)).then((()=>e)):e)).then((e=>t(e,s))).then((e=>s.hooks?s.hooks.postprocess(e):e)).catch(o);try{s.hooks&&(r=s.hooks.preprocess(r));const n=e(r,s);s.walkTokens&&Xe.walkTokens(n,s.walkTokens);let i=t(n,s);return s.hooks&&(i=s.hooks.postprocess(i)),i}catch(e){return o(e)}}}function Xe(e,t,r){return Ye(He.lex,Je.parse)(e,t,r)}Xe.options=Xe.setOptions=function(e){var t;return Xe.defaults={...Xe.defaults,...e},t=Xe.defaults,fe=t,Xe},Xe.getDefaults=me,Xe.defaults=fe,Xe.use=function(...e){const t=Xe.defaults.extensions||{renderers:{},childTokens:{}};e.forEach((e=>{const r={...e};if(r.async=Xe.defaults.async||r.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if(e.renderer){const r=t.renderers[e.name];t.renderers[e.name]=r?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=r.apply(this,t)),s}:e.renderer}if(e.tokenizer){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");t[e.level]?t[e.level].unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),r.extensions=t),e.renderer){const t=Xe.defaults.renderer||new Ve;for(const r in e.renderer){const s=t[r];t[r]=(...n)=>{let i=e.renderer[r].apply(t,n);return!1===i&&(i=s.apply(t,n)),i}}r.renderer=t}if(e.tokenizer){const t=Xe.defaults.tokenizer||new Be;for(const r in e.tokenizer){const s=t[r];t[r]=(...n)=>{let i=e.tokenizer[r].apply(t,n);return!1===i&&(i=s.apply(t,n)),i}}r.tokenizer=t}if(e.hooks){const t=Xe.defaults.hooks||new Ke;for(const r in e.hooks){const s=t[r];Ke.passThroughHooks.has(r)?t[r]=n=>{if(Xe.defaults.async)return Promise.resolve(e.hooks[r].call(t,n)).then((e=>s.call(t,e)));const i=e.hooks[r].call(t,n);return s.call(t,i)}:t[r]=(...n)=>{let i=e.hooks[r].apply(t,n);return!1===i&&(i=s.apply(t,n)),i}}r.hooks=t}if(e.walkTokens){const t=Xe.defaults.walkTokens;r.walkTokens=function(r){let s=[];return s.push(e.walkTokens.call(this,r)),t&&(s=s.concat(t.call(this,r))),s}}Xe.setOptions(r)}))},Xe.walkTokens=function(e,t){let r=[];for(const s of e)switch(r=r.concat(t.call(Xe,s)),s.type){case"table":for(const e of s.header)r=r.concat(Xe.walkTokens(e.tokens,t));for(const e of s.rows)for(const s of e)r=r.concat(Xe.walkTokens(s.tokens,t));break;case"list":r=r.concat(Xe.walkTokens(s.items,t));break;default:Xe.defaults.extensions&&Xe.defaults.extensions.childTokens&&Xe.defaults.extensions.childTokens[s.type]?Xe.defaults.extensions.childTokens[s.type].forEach((function(e){r=r.concat(Xe.walkTokens(s[e],t))})):s.tokens&&(r=r.concat(Xe.walkTokens(s.tokens,t)))}return r},Xe.parseInline=Ye(He.lexInline,Je.parseInline),Xe.Parser=Je,Xe.parser=Je.parse,Xe.Renderer=Ve,Xe.TextRenderer=We,Xe.Lexer=He,Xe.lexer=He.lex,Xe.Tokenizer=Be,Xe.Slugger=Ge,Xe.Hooks=Ke,Xe.parse=Xe;Xe.options,Xe.setOptions,Xe.use,Xe.walkTokens,Xe.parseInline,Je.parse,He.lex;var Ze=r(848),Qe=r.n(Ze);r(113),r(83),r(378),r(976),r(514),r(22),r(342),r(784),r(651);const et=c` + .hover-bg:hover { + background: var(--bg3); + } + ::selection { + background: var(--selection-bg); + color: var(--selection-fg); + } + .regular-font { + font-family:var(--font-regular); + } + .mono-font { + font-family:var(--font-mono); + } + .title { + font-size: calc(var(--font-size-small) + 18px); + font-weight: normal + } + .sub-title{ font-size: 20px; } + .req-res-title { + font-family: var(--font-regular); + font-size: calc(var(--font-size-small) + 4px); + font-weight:bold; + margin-bottom:8px; + text-align:left; + } + .tiny-title { + font-size:calc(var(--font-size-small) + 1px); + font-weight:bold; + } + .regular-font-size { font-size: var(--font-size-regular); } + .small-font-size { font-size: var(--font-size-small); } + .upper { text-transform: uppercase; } + .primary-text { color: var(--primary-color); } + .bold-text { font-weight:bold; } + .gray-text { color: var(--light-fg); } + .red-text { color: var(--red) } + .blue-text { color: var(--blue) } + .multiline { + overflow: scroll; + max-height: var(--resp-area-height, 400px); + color: var(--fg3); + } + .method-fg.put { color: var(--orange); } + .method-fg.post { color: var(--green); } + .method-fg.get { color: var(--blue); } + .method-fg.delete { color: var(--red); } + .method-fg.options, + .method-fg.head, + .method-fg.patch { + color: var(--yellow); + } + + h1 { font-family:var(--font-regular); font-size:28px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h2 { font-family:var(--font-regular); font-size:24px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h3 { font-family:var(--font-regular); font-size:18px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h4 { font-family:var(--font-regular); font-size:16px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h5 { font-family:var(--font-regular); font-size:14px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h6 { font-family:var(--font-regular); font-size:14px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + + h1,h2,h3,h4,h5,h5 { + margin-block-end: 0.2em; + } + p { margin-block-start: 0.5em; } + a { color: var(--blue); cursor:pointer; } + a.inactive-link { + color:var(--fg); + text-decoration: none; + cursor:text; + } + + code, + pre { + margin: 0px; + font-family: var(--font-mono); + font-size: calc(var(--font-size-mono) - 1px); + } + + .m-markdown, + .m-markdown-small { + display:block; + } + + .m-markdown p, + .m-markdown span { + font-size: var(--font-size-regular); + line-height:calc(var(--font-size-regular) + 8px); + } + .m-markdown li { + font-size: var(--font-size-regular); + line-height:calc(var(--font-size-regular) + 10px); + } + + .m-markdown-small p, + .m-markdown-small span, + .m-markdown-small li { + font-size: var(--font-size-small); + line-height: calc(var(--font-size-small) + 6px); + } + .m-markdown-small li { + line-height: calc(var(--font-size-small) + 8px); + } + + .m-markdown p:not(:first-child) { + margin-block-start: 24px; + } + + .m-markdown-small p:not(:first-child) { + margin-block-start: 12px; + } + .m-markdown-small p:first-child { + margin-block-start: 0; + } + + .m-markdown p, + .m-markdown-small p { + margin-block-end: 0 + } + + .m-markdown code span { + font-size:var(--font-size-mono); + } + + .m-markdown-small code, + .m-markdown code { + padding: 1px 6px; + border-radius: 2px; + color: var(--inline-code-fg); + background-color: var(--bg3); + font-size: calc(var(--font-size-mono)); + line-height: 1.2; + } + + .m-markdown-small code { + font-size: calc(var(--font-size-mono) - 1px); + } + + .m-markdown-small pre, + .m-markdown pre { + white-space: pre-wrap; + overflow-x: auto; + line-height: normal; + border-radius: 2px; + border: 1px solid var(--code-border-color); + } + + .m-markdown pre { + padding: 12px; + background-color: var(--code-bg); + color:var(--code-fg); + } + + .m-markdown-small pre { + margin-top: 4px; + padding: 2px 4px; + background-color: var(--bg3); + color: var(--fg2); + } + + .m-markdown-small pre code, + .m-markdown pre code { + border:none; + padding:0; + } + + .m-markdown pre code { + color: var(--code-fg); + background-color: var(--code-bg); + background-color: transparent; + } + + .m-markdown-small pre code { + color: var(--fg2); + background-color: var(--bg3); + } + + .m-markdown ul, + .m-markdown ol { + padding-inline-start: 30px; + } + + .m-markdown-small ul, + .m-markdown-small ol { + padding-inline-start: 20px; + } + + .m-markdown-small a, + .m-markdown a { + color:var(--blue); + } + + .m-markdown-small img, + .m-markdown img { + max-width: 100%; + } + + /* Markdown table */ + + .m-markdown-small table, + .m-markdown table { + border-spacing: 0; + margin: 10px 0; + border-collapse: separate; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + font-size: calc(var(--font-size-small) + 1px); + line-height: calc(var(--font-size-small) + 4px); + max-width: 100%; + } + + .m-markdown-small table { + font-size: var(--font-size-small); + line-height: calc(var(--font-size-small) + 2px); + margin: 8px 0; + } + + .m-markdown-small td, + .m-markdown-small th, + .m-markdown td, + .m-markdown th { + vertical-align: top; + border-top: 1px solid var(--border-color); + line-height: calc(var(--font-size-small) + 4px); + } + + .m-markdown-small tr:first-child th, + .m-markdown tr:first-child th { + border-top: 0 none; + } + + .m-markdown th, + .m-markdown td { + padding: 10px 12px; + } + + .m-markdown-small th, + .m-markdown-small td { + padding: 8px 8px; + } + + .m-markdown th, + .m-markdown-small th { + font-weight: 600; + background-color: var(--bg2); + vertical-align: middle; + } + + .m-markdown-small table code { + font-size: calc(var(--font-size-mono) - 2px); + } + + .m-markdown table code { + font-size: calc(var(--font-size-mono) - 1px); + } + + .m-markdown blockquote, + .m-markdown-small blockquote { + margin-inline-start: 0; + margin-inline-end: 0; + border-left: 3px solid var(--border-color); + padding: 6px 0 6px 6px; + } + .m-markdown hr{ + border: 1px solid var(--border-color); + } +`,tt=c` +/* Button */ +.m-btn { + border-radius: var(--border-radius); + font-weight: 600; + display: inline-block; + padding: 6px 16px; + font-size: var(--font-size-small); + outline: 0; + line-height: 1; + text-align: center; + white-space: nowrap; + border: 2px solid var(--primary-color); + background-color:transparent; + user-select: none; + cursor: pointer; + box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); + transition-duration: 0.75s; +} +.m-btn.primary { + background-color: var(--primary-color); + color: var(--primary-color-invert); +} +.m-btn.thin-border { border-width: 1px; } +.m-btn.large { padding:8px 14px; } +.m-btn.small { padding:5px 12px; } +.m-btn.tiny { padding:5px 6px; } +.m-btn.circle { border-radius: 50%; } +.m-btn:hover { + background-color: var(--primary-color); + color: var(--primary-color-invert); +} +.m-btn.nav { border: 2px solid var(--nav-accent-color); } +.m-btn.nav:hover { + background-color: var(--nav-accent-color); +} +.m-btn:disabled { + background-color: var(--bg3); + color: var(--fg3); + border-color: var(--fg3); + cursor: not-allowed; + opacity: 0.4; +} +.m-btn:active { + filter: brightness(75%); + transform: scale(0.95); + transition:scale 0s; +} +.toolbar-btn { + cursor: pointer; + padding: 4px; + margin:0 2px; + font-size: var(--font-size-small); + min-width: 50px; + color: var(--primary-color-invert); + border-radius: 2px; + border: none; + background-color: var(--primary-color); +} + +input, textarea, select, button, pre { + color:var(--fg); + outline: none; + background-color: var(--input-bg); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); +} +button { + font-family: var(--font-regular); +} + +/* Form Inputs */ +pre, +select, +textarea, +input[type="file"], +input[type="text"], +input[type="password"] { + font-family: var(--font-mono); + font-weight: 400; + font-size: var(--font-size-small); + transition: border .2s; + padding: 6px 5px; +} + +select { + font-family: var(--font-regular); + padding: 5px 30px 5px 5px; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%3E%3Cpath%20d%3D%22M10.3%203.3L6%207.6%201.7%203.3A1%201%200%2000.3%204.7l5%205a1%201%200%20001.4%200l5-5a1%201%200%2010-1.4-1.4z%22%20fill%3D%22%23777777%22%2F%3E%3C%2Fsvg%3E"); + background-position: calc(100% - 5px) center; + background-repeat: no-repeat; + background-size: 10px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + cursor: pointer; +} + +select:hover { + border-color: var(--primary-color); +} + +textarea::placeholder, +input[type="text"]::placeholder, +input[type="password"]::placeholder { + color: var(--placeholder-color); + opacity:1; +} + + +input[type="file"]{ + font-family: var(--font-regular); + padding:2px; + cursor:pointer; + border: 1px solid var(--primary-color); + min-height: calc(var(--font-size-small) + 18px); +} + +input[type="file"]::-webkit-file-upload-button { + font-family: var(--font-regular); + font-size: var(--font-size-small); + outline: none; + cursor:pointer; + padding: 3px 8px; + border: 1px solid var(--primary-color); + background-color: var(--primary-color); + color: var(--primary-color-invert); + border-radius: var(--border-radius);; + -webkit-appearance: none; +} + +pre, +textarea { + scrollbar-width: thin; + scrollbar-color: var(--border-color) var(--input-bg); +} + +pre::-webkit-scrollbar, +textarea::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +pre::-webkit-scrollbar-track, +textarea::-webkit-scrollbar-track { + background:var(--input-bg); +} + +pre::-webkit-scrollbar-thumb, +textarea::-webkit-scrollbar-thumb { + border-radius: 2px; + background-color: var(--border-color); +} + +.link { + font-size:var(--font-size-small); + text-decoration: underline; + color:var(--blue); + font-family:var(--font-mono); + margin-bottom:2px; +} + +/* Toggle Body */ +input[type="checkbox"] { + appearance: none; + display: inline-block; + background-color: var(--light-bg); + border: 1px solid var(--light-bg); + border-radius: 9px; + cursor: pointer; + height: 18px; + position: relative; + transition: border .25s .15s, box-shadow .25s .3s, padding .25s; + min-width: 36px; + width: 36px; + vertical-align: top; +} +/* Toggle Thumb */ +input[type="checkbox"]:after { + position: absolute; + background-color: var(--bg); + border: 1px solid var(--light-bg); + border-radius: 8px; + content: ''; + top: 0px; + left: 0px; + right: 16px; + display: block; + height: 16px; + transition: border .25s .15s, left .25s .1s, right .15s .175s; +} + +/* Toggle Body - Checked */ +input[type="checkbox"]:checked { + background-color: var(--green); + border-color: var(--green); +} +/* Toggle Thumb - Checked*/ +input[type="checkbox"]:checked:after { + border: 1px solid var(--green); + left: 16px; + right: 1px; + transition: border .25s, left .15s .25s, right .25s .175s; +}`,rt=c` +.row, .col { + display:flex; +} +.row { + align-items:center; + flex-direction: row; +} +.col { + align-items:stretch; + flex-direction: column; +} +`,st=c` +.m-table { + border-spacing: 0; + border-collapse: separate; + border: 1px solid var(--light-border-color); + border-radius: var(--border-radius); + margin: 0; + max-width: 100%; + direction: ltr; +} +.m-table tr:first-child td, +.m-table tr:first-child th { + border-top: 0 none; +} +.m-table td, +.m-table th { + font-size: var(--font-size-small); + line-height: calc(var(--font-size-small) + 4px); + padding: 4px 5px 4px; + vertical-align: top; +} + +.m-table.padded-12 td, +.m-table.padded-12 th { + padding: 12px; +} + +.m-table td:not([align]), +.m-table th:not([align]) { + text-align: left; +} + +.m-table th { + color: var(--fg2); + font-size: var(--font-size-small); + line-height: calc(var(--font-size-small) + 18px); + font-weight: 600; + letter-spacing: normal; + background-color: var(--bg2); + vertical-align: bottom; + border-bottom: 1px solid var(--light-border-color); +} + +.m-table > tbody > tr > td, +.m-table > tr > td { + border-top: 1px solid var(--light-border-color); + text-overflow: ellipsis; + overflow: hidden; +} +.table-title { + font-size:var(--font-size-small); + font-weight:bold; + vertical-align: middle; + margin: 12px 0 4px 0; +} +`,nt=c` +:host { + container-type: inline-size; +} +.only-large-screen { display:none; } +.endpoint-head .path { + display: flex; + font-family:var(--font-mono); + font-size: var(--font-size-small); + align-items: center; + overflow-wrap: break-word; + word-break: break-all; +} + +.endpoint-head .descr { + font-size: var(--font-size-small); + color:var(--light-fg); + font-weight:400; + align-items: center; + overflow-wrap: break-word; + word-break: break-all; + display:none; +} + +.m-endpoint.expanded { margin-bottom:16px; } +.m-endpoint > .endpoint-head{ + border-width:1px 1px 1px 5px; + border-style:solid; + border-color:transparent; + border-top-color:var(--light-border-color); + display:flex; + padding:6px 16px; + align-items: center; + cursor: pointer; +} +.m-endpoint > .endpoint-head.put:hover, +.m-endpoint > .endpoint-head.put.expanded { + border-color:var(--orange); + background-color:var(--light-orange); +} +.m-endpoint > .endpoint-head.post:hover, +.m-endpoint > .endpoint-head.post.expanded { + border-color:var(--green); + background-color:var(--light-green); +} +.m-endpoint > .endpoint-head.get:hover, +.m-endpoint > .endpoint-head.get.expanded { + border-color:var(--blue); + background-color:var(--light-blue); +} +.m-endpoint > .endpoint-head.delete:hover, +.m-endpoint > .endpoint-head.delete.expanded { + border-color:var(--red); + background-color:var(--light-red); +} + +.m-endpoint > .endpoint-head.head:hover, +.m-endpoint > .endpoint-head.head.expanded, +.m-endpoint > .endpoint-head.patch:hover, +.m-endpoint > .endpoint-head.patch.expanded, +.m-endpoint > .endpoint-head.options:hover, +.m-endpoint > .endpoint-head.options.expanded { + border-color:var(--yellow); + background-color:var(--light-yellow); +} + +.m-endpoint > .endpoint-head.deprecated:hover, +.m-endpoint > .endpoint-head.deprecated.expanded { + border-color:var(--border-color); + filter:opacity(0.6); +} + +.m-endpoint .endpoint-body { + flex-wrap:wrap; + padding:16px 0px 0 0px; + border-width:0px 1px 1px 5px; + border-style:solid; + box-shadow: 0px 4px 3px -3px rgba(0, 0, 0, 0.15); +} +.m-endpoint .endpoint-body.delete{ border-color:var(--red); } +.m-endpoint .endpoint-body.put{ border-color:var(--orange); } +.m-endpoint .endpoint-body.post { border-color:var(--green); } +.m-endpoint .endpoint-body.get { border-color:var(--blue); } +.m-endpoint .endpoint-body.head, +.m-endpoint .endpoint-body.patch, +.m-endpoint .endpoint-body.options { + border-color:var(--yellow); +} + +.m-endpoint .endpoint-body.deprecated { + border-color:var(--border-color); + filter:opacity(0.6); +} + +.endpoint-head .deprecated { + color: var(--light-fg); + filter:opacity(0.6); +} + +.summary{ + padding:8px 8px; +} +.summary .title { + font-size:calc(var(--font-size-regular) + 2px); + margin-bottom: 6px; + word-break: break-all; +} + +.endpoint-head .method { + padding:2px 5px; + vertical-align: middle; + font-size:var(--font-size-small); + height: calc(var(--font-size-small) + 16px); + line-height: calc(var(--font-size-small) + 8px); + width: 60px; + border-radius: 2px; + display:inline-block; + text-align: center; + font-weight: bold; + text-transform:uppercase; + margin-right:5px; +} +.endpoint-head .method.delete{ border: 2px solid var(--red);} +.endpoint-head .method.put{ border: 2px solid var(--orange); } +.endpoint-head .method.post{ border: 2px solid var(--green); } +.endpoint-head .method.get{ border: 2px solid var(--blue); } +.endpoint-head .method.get.deprecated{ border: 2px solid var(--border-color); } +.endpoint-head .method.head, +.endpoint-head .method.patch, +.endpoint-head .method.options { + border: 2px solid var(--yellow); +} + +.req-resp-container { + display: flex; + margin-top:16px; + align-items: stretch; + flex-wrap: wrap; + flex-direction: column; + border-top:1px solid var(--light-border-color); +} + +.view-mode-request, +api-response.view-mode { + flex:1; + min-height:100px; + padding:16px 8px; + overflow:hidden; +} +.view-mode-request { + border-width:0 0 1px 0; + border-style:dashed; +} + +.head .view-mode-request, +.patch .view-mode-request, +.options .view-mode-request { + border-color:var(--yellow); +} +.put .view-mode-request { + border-color:var(--orange); +} +.post .view-mode-request { + border-color:var(--green); +} +.get .view-mode-request { + border-color:var(--blue); +} +.delete .view-mode-request { + border-color:var(--red); +} + +@container (min-width: 1024px) { + .only-large-screen { display:block; } + .endpoint-head .path{ + font-size: var(--font-size-regular); + } + .endpoint-head .descr{ + display: flex; + } + .endpoint-head .m-markdown-small, + .descr .m-markdown-small{ + display:block; + } + .req-resp-container{ + flex-direction: var(--layout, row); + flex-wrap: nowrap; + } + api-response.view-mode { + padding:16px; + } + .view-mode-request.row-layout { + border-width:0 1px 0 0; + padding:16px; + } + .summary{ + padding:8px 16px; + } +} +`,it=c` +code[class*="language-"], +pre[class*="language-"] { + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + tab-size: 2; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + white-space: normal; +} + +.token.comment, +.token.block-comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: var(--light-fg) +} + +.token.punctuation { + color: var(--fg); +} + +.token.tag, +.token.attr-name, +.token.namespace, +.token.deleted { + color:var(--pink); +} + +.token.function-name { + color: var(--blue); +} + +.token.boolean, +.token.number, +.token.function { + color: var(--red); +} + +.token.property, +.token.class-name, +.token.constant, +.token.symbol { + color: var(--code-property-color); +} + +.token.selector, +.token.important, +.token.atrule, +.token.keyword, +.token.builtin { + color: var(--code-keyword-color); +} + +.token.string, +.token.char, +.token.attr-value, +.token.regex, +.token.variable { + color: var(--green); +} + +.token.operator, +.token.entity, +.token.url { + color: var(--code-operator-color); +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + +.token.inserted { + color: green; +} +`,ot=c` +.tab-panel { + border: none; +} +.tab-buttons { + height:30px; + padding: 4px 4px 0 4px; + border-bottom: 1px solid var(--light-border-color) ; + align-items: stretch; + overflow-y: hidden; + overflow-x: auto; + scrollbar-width: thin; +} +.tab-buttons::-webkit-scrollbar { + height: 1px; + background-color: var(--border-color); +} +.tab-btn { + border: none; + border-bottom: 3px solid transparent; + color: var(--light-fg); + background-color: transparent; + white-space: nowrap; + cursor:pointer; + outline:none; + font-family:var(--font-regular); + font-size:var(--font-size-small); + margin-right:16px; + padding:1px; +} +.tab-btn.active { + border-bottom: 3px solid var(--primary-color); + font-weight:bold; + color:var(--primary-color); +} + +.tab-btn:hover { + color:var(--primary-color); +} +.tab-content { + margin:-1px 0 0 0; + position:relative; + min-height: 50px; +} +`,at=c` +.nav-bar-info:focus-visible, +.nav-bar-tag:focus-visible, +.nav-bar-path:focus-visible { + outline: 1px solid; + box-shadow: none; + outline-offset: -4px; +} +.nav-bar-expand-all:focus-visible, +.nav-bar-collapse-all:focus-visible, +.nav-bar-tag-icon:focus-visible { + outline: 1px solid; + box-shadow: none; + outline-offset: 2px; +} +.nav-bar { + width:0; + height:100%; + overflow: hidden; + color:var(--nav-text-color); + background-color: var(--nav-bg-color); + background-blend-mode: multiply; + line-height: calc(var(--font-size-small) + 4px); + display:none; + position:relative; + flex-direction:column; + flex-wrap:nowrap; + word-break:break-word; +} +::slotted([slot=nav-logo]) { + padding:16px 16px 0 16px; +} +.nav-scroll { + overflow-x: hidden; + overflow-y: auto; + overflow-y: overlay; + scrollbar-width: thin; + scrollbar-color: var(--nav-hover-bg-color) transparent; +} + +.nav-bar-tag { + display: flex; + align-items: center; + justify-content: space-between; + flex-direction: row; +} +.nav-bar.read .nav-bar-tag-icon { + display:none; +} +.nav-bar-paths-under-tag { + overflow:hidden; + transition: max-height .2s ease-out, visibility .3s; +} +.collapsed .nav-bar-paths-under-tag { + visibility: hidden; +} + +.nav-bar-expand-all { + transform: rotate(90deg); + cursor:pointer; + margin-right:10px; +} +.nav-bar-collapse-all { + transform: rotate(270deg); + cursor:pointer; +} +.nav-bar-expand-all:hover, .nav-bar-collapse-all:hover { + color: var(--primary-color); +} + +.nav-bar-tag-icon { + color: var(--nav-text-color); + font-size: 20px; +} +.nav-bar-tag-icon:hover { + color:var(--nav-hover-text-color); +} +.nav-bar.focused .nav-bar-tag-and-paths.collapsed .nav-bar-tag-icon::after { + content: '⌵'; + width:16px; + height:16px; + text-align: center; + display: inline-block; + transform: rotate(-90deg); + transition: transform 0.2s ease-out 0s; +} +.nav-bar.focused .nav-bar-tag-and-paths.expanded .nav-bar-tag-icon::after { + content: '⌵'; + width:16px; + height:16px; + text-align: center; + display: inline-block; + transition: transform 0.2s ease-out 0s; +} +.nav-scroll::-webkit-scrollbar { + width: var(--scroll-bar-width, 8px); +} +.nav-scroll::-webkit-scrollbar-track { + background:transparent; +} +.nav-scroll::-webkit-scrollbar-thumb { + background-color: var(--nav-hover-bg-color); +} + +.nav-bar-tag { + font-size: var(--font-size-regular); + color: var(--nav-accent-color); + border-left:4px solid transparent; + font-weight:bold; + padding: 15px 15px 15px 10px; + text-transform: capitalize; +} + +.nav-bar-components, +.nav-bar-h1, +.nav-bar-h2, +.nav-bar-info, +.nav-bar-tag, +.nav-bar-path { + display:flex; + cursor: pointer; + width: 100%; + border: none; + border-radius:4px; + color: var(--nav-text-color); + background: transparent; + border-left:4px solid transparent; +} + +.nav-bar-h1, +.nav-bar-h2, +.nav-bar-path { + font-size: calc(var(--font-size-small) + 1px); + padding: var(--nav-item-padding); +} +.nav-bar-path.small-font { + font-size: var(--font-size-small); +} + +.nav-bar-info { + font-size: var(--font-size-regular); + padding: 16px 10px; + font-weight:bold; +} +.nav-bar-section { + display: flex; + flex-direction: row; + justify-content: space-between; + font-size: var(--font-size-small); + color: var(--nav-text-color); + padding: var(--nav-item-padding); + font-weight:bold; +} +.nav-bar-section.operations { + cursor:pointer; +} +.nav-bar-section.operations:hover { + color:var(--nav-hover-text-color); + background-color:var(--nav-hover-bg-color); +} + +.nav-bar-section:first-child { + display: none; +} +.nav-bar-h2 {margin-left:12px;} + +.nav-bar-h1.left-bar.active, +.nav-bar-h2.left-bar.active, +.nav-bar-info.left-bar.active, +.nav-bar-tag.left-bar.active, +.nav-bar-path.left-bar.active, +.nav-bar-section.left-bar.operations.active { + border-left:4px solid var(--nav-accent-color); + color:var(--nav-hover-text-color); +} + +.nav-bar-h1.colored-block.active, +.nav-bar-h2.colored-block.active, +.nav-bar-info.colored-block.active, +.nav-bar-tag.colored-block.active, +.nav-bar-path.colored-block.active, +.nav-bar-section.colored-block.operations.active { + background-color: var(--nav-accent-color); + color: var(--nav-accent-text-color); + border-radius: 0; +} + +.nav-bar-h1:hover, +.nav-bar-h2:hover, +.nav-bar-info:hover, +.nav-bar-tag:hover, +.nav-bar-path:hover { + color:var(--nav-hover-text-color); + background-color:var(--nav-hover-bg-color); +} +`,lt=c` +#api-info { + font-size: calc(var(--font-size-regular) - 1px); + margin-top: 8px; + margin-left: -15px; +} + +#api-info span:before { + content: "|"; + display: inline-block; + opacity: 0.5; + width: 15px; + text-align: center; +} +#api-info span:first-child:before { + content: ""; + width: 0px; +} +`,ct=c` + +`;const pt=/[\s#:?&={}]/g,ut="_rapidoc_api_key";function dt(e){return new Promise((t=>setTimeout(t,e)))}function ht(e,t){const r=t.target,s=document.createElement("textarea");s.value=e,s.style.position="fixed",document.body.appendChild(s),s.focus(),s.select();try{document.execCommand("copy"),r.innerText="Copied",setTimeout((()=>{r.innerText="Copy"}),5e3)}catch(e){console.error("Unable to copy",e)}document.body.removeChild(s)}function mt(e,t,r=""){return`${t.method} ${t.path} ${t.summary||""} ${t.description||""} ${t.operationId||""} ${r}`.toLowerCase().includes(e.toLowerCase())}function ft(e,t=new Set){return e?(Object.keys(e).forEach((r=>{var s;if(t.add(r),e[r].properties)ft(e[r].properties,t);else if(null!==(s=e[r].items)&&void 0!==s&&s.properties){var n;ft(null===(n=e[r].items)||void 0===n?void 0:n.properties,t)}})),t):t}function gt(e,t){if(e){const r=document.createElement("a");document.body.appendChild(r),r.style="display: none",r.href=e,r.download=t,r.click(),r.remove()}}function yt(e){if(e){const t=document.createElement("a");document.body.appendChild(t),t.style="display: none",t.href=e,t.target="_blank",t.click(),t.remove()}}const vt=Object.freeze({url:"/"}),{fetch:bt,Response:xt,Headers:wt,Request:$t,FormData:St,File:Et,Blob:kt}=globalThis;function At(e,t){return t||"undefined"==typeof navigator||(t=navigator),t&&"ReactNative"===t.product?!(!e||"object"!=typeof e||"string"!=typeof e.uri):"undefined"!=typeof File&&e instanceof File||"undefined"!=typeof Blob&&e instanceof Blob||!!ArrayBuffer.isView(e)||null!==e&&"object"==typeof e&&"function"==typeof e.pipe}function Ot(e,t){return Array.isArray(e)&&e.some((e=>At(e,t)))}void 0===globalThis.fetch&&(globalThis.fetch=bt),void 0===globalThis.Headers&&(globalThis.Headers=wt),void 0===globalThis.Request&&(globalThis.Request=$t),void 0===globalThis.Response&&(globalThis.Response=xt),void 0===globalThis.FormData&&(globalThis.FormData=St),void 0===globalThis.File&&(globalThis.File=Et),void 0===globalThis.Blob&&(globalThis.Blob=kt);class jt extends File{constructor(e,t="",r={}){super([e],t,r),this.data=e}valueOf(){return this.data}toString(){return this.valueOf()}}function Tt(e,t="reserved"){return[...e].map((e=>{if((e=>/^[a-z0-9\-._~]+$/i.test(e))(e))return e;if((e=>":/?#[]@!$&'()*+,;=".indexOf(e)>-1)(e)&&"unsafe"===t)return e;const r=new TextEncoder;return Array.from(r.encode(e)).map((e=>`0${e.toString(16).toUpperCase()}`.slice(-2))).map((e=>`%${e}`)).join("")})).join("")}function Pt(e){const{value:t}=e;return Array.isArray(t)?function({key:e,value:t,style:r,explode:s,escape:n}){if("simple"===r)return t.map((e=>Ct(e,n))).join(",");if("label"===r)return`.${t.map((e=>Ct(e,n))).join(".")}`;if("matrix"===r)return t.map((e=>Ct(e,n))).reduce(((t,r)=>!t||s?`${t||""};${e}=${r}`:`${t},${r}`),"");if("form"===r){const r=s?`&${e}=`:",";return t.map((e=>Ct(e,n))).join(r)}if("spaceDelimited"===r){const r=s?`${e}=`:"";return t.map((e=>Ct(e,n))).join(` ${r}`)}if("pipeDelimited"===r){const r=s?`${e}=`:"";return t.map((e=>Ct(e,n))).join(`|${r}`)}}(e):"object"==typeof t?function({key:e,value:t,style:r,explode:s,escape:n}){const i=Object.keys(t);return"simple"===r?i.reduce(((e,r)=>{const i=Ct(t[r],n);return`${e?`${e},`:""}${r}${s?"=":","}${i}`}),""):"label"===r?i.reduce(((e,r)=>{const i=Ct(t[r],n);return`${e?`${e}.`:"."}${r}${s?"=":"."}${i}`}),""):"matrix"===r&&s?i.reduce(((e,r)=>`${e?`${e};`:";"}${r}=${Ct(t[r],n)}`),""):"matrix"===r?i.reduce(((r,s)=>{const i=Ct(t[s],n);return`${r?`${r},`:`;${e}=`}${s},${i}`}),""):"form"===r?i.reduce(((e,r)=>{const i=Ct(t[r],n);return`${e?`${e}${s?"&":","}`:""}${r}${s?"=":","}${i}`}),""):void 0}(e):function({key:e,value:t,style:r,escape:s}){return"simple"===r?Ct(t,s):"label"===r?`.${Ct(t,s)}`:"matrix"===r?`;${e}=${Ct(t,s)}`:"form"===r||"deepObject"===r?Ct(t,s):void 0}(e)}function Ct(e,t=!1){return Array.isArray(e)||null!==e&&"object"==typeof e?e=JSON.stringify(e):"number"!=typeof e&&"boolean"!=typeof e||(e=String(e)),t&&e.length>0?Tt(e,t):e}const It={form:",",spaceDelimited:"%20",pipeDelimited:"|"},_t={csv:",",ssv:"%20",tsv:"%09",pipes:"|"};function Rt(e,t,r=!1){const{collectionFormat:s,allowEmptyValue:n,serializationOption:i,encoding:o}=t,a="object"!=typeof t||Array.isArray(t)?t:t.value,l=r?e=>e.toString():e=>encodeURIComponent(e),c=l(e);if(void 0===a&&n)return[[c,""]];if(At(a)||Ot(a))return[[c,a]];if(i)return Ft(e,a,r,i);if(o){if([typeof o.style,typeof o.explode,typeof o.allowReserved].some((e=>"undefined"!==e))){const{style:t,explode:s,allowReserved:n}=o;return Ft(e,a,r,{style:t,explode:s,allowReserved:n})}if("string"==typeof o.contentType){if(o.contentType.startsWith("application/json")){const e=l("string"==typeof a?a:JSON.stringify(a));return[[c,new jt(e,"blob",{type:o.contentType})]]}const e=l(String(a));return[[c,new jt(e,"blob",{type:o.contentType})]]}return"object"!=typeof a?[[c,l(a)]]:Array.isArray(a)&&a.every((e=>"object"!=typeof e))?[[c,a.map(l).join(",")]]:[[c,l(JSON.stringify(a))]]}return"object"!=typeof a?[[c,l(a)]]:Array.isArray(a)?"multi"===s?[[c,a.map(l)]]:[[c,a.map(l).join(_t[s||"csv"])]]:[[c,""]]}function Ft(e,t,r,s){const n=s.style||"form",i=void 0===s.explode?"form"===n:s.explode,o=!r&&(s&&s.allowReserved?"unsafe":"reserved"),a=e=>Ct(e,o),l=r?e=>e:e=>a(e);return"object"!=typeof t?[[l(e),a(t)]]:Array.isArray(t)?i?[[l(e),t.map(a)]]:[[l(e),t.map(a).join(It[n])]]:"deepObject"===n?Object.keys(t).map((r=>[l(`${e}[${r}]`),a(t[r])])):i?Object.keys(t).map((e=>[l(e),a(t[e])])):[[l(e),Object.keys(t).map((e=>[`${l(e)},${a(t[e])}`])).join(",")]]}function Mt(e){return((e,{encode:t=!0}={})=>{const r=(e,t,s)=>(null==s?e.append(t,""):Array.isArray(s)?s.reduce(((s,n)=>r(e,t,n)),e):s instanceof Date?e.append(t,s.toISOString()):"object"==typeof s?Object.entries(s).reduce(((s,[n,i])=>r(e,`${t}[${n}]`,i)),e):e.append(t,s),e),s=Object.entries(e).reduce(((e,[t,s])=>r(e,t,s)),new URLSearchParams),n=String(s);return t?n:decodeURIComponent(n)})(Object.keys(e).reduce(((t,r)=>{for(const[s,n]of Rt(r,e[r]))t[s]=n instanceof jt?n.valueOf():n;return t}),{}),{encode:!1})}function Lt(e={}){const{url:t="",query:r,form:s}=e;if(s){const t=Object.keys(s).some((e=>{const{value:t}=s[e];return At(t)||Ot(t)})),r=e.headers["content-type"]||e.headers["Content-Type"];if(t||/multipart\/form-data/i.test(r)){const t=(n=e.form,Object.entries(n).reduce(((e,[t,r])=>{for(const[s,n]of Rt(t,r,!0))if(Array.isArray(n))for(const t of n)if(ArrayBuffer.isView(t)){const r=new Blob([t]);e.append(s,r)}else e.append(s,t);else if(ArrayBuffer.isView(n)){const t=new Blob([n]);e.append(s,t)}else e.append(s,n);return e}),new FormData));e.formdata=t,e.body=t}else e.body=Mt(s);delete e.form}var n;if(r){const[s,n]=t.split("?");let i="";if(n){const e=new URLSearchParams(n);Object.keys(r).forEach((t=>e.delete(t))),i=String(e)}const o=((...e)=>{const t=e.filter((e=>e)).join("&");return t?`?${t}`:""})(i,Mt(r));e.url=s+o,delete e.query}return e}function Dt(e){return null==e}var Bt={isNothing:Dt,isObject:function(e){return"object"==typeof e&&null!==e},toArray:function(e){return Array.isArray(e)?e:Dt(e)?[]:[e]},repeat:function(e,t){var r,s="";for(r=0;ra&&(t=s-a+(i=" ... ").length),r-s>a&&(r=s+a-(o=" ...").length),{str:i+e.slice(t,r).replace(/\t/g,"→")+o,pos:s-t+i.length}}function Ht(e,t){return Bt.repeat(" ",t-e.length)+e}var Vt=function(e,t){if(t=Object.create(t||null),!e.buffer)return null;t.maxLength||(t.maxLength=79),"number"!=typeof t.indent&&(t.indent=1),"number"!=typeof t.linesBefore&&(t.linesBefore=3),"number"!=typeof t.linesAfter&&(t.linesAfter=2);for(var r,s=/\r?\n|\r|\0/g,n=[0],i=[],o=-1;r=s.exec(e.buffer);)i.push(r.index),n.push(r.index+r[0].length),e.position<=r.index&&o<0&&(o=n.length-2);o<0&&(o=n.length-1);var a,l,c="",p=Math.min(e.line+t.linesAfter,i.length).toString().length,u=t.maxLength-(t.indent+p+3);for(a=1;a<=t.linesBefore&&!(o-a<0);a++)l=zt(e.buffer,n[o-a],i[o-a],e.position-(n[o]-n[o-a]),u),c=Bt.repeat(" ",t.indent)+Ht((e.line-a+1).toString(),p)+" | "+l.str+"\n"+c;for(l=zt(e.buffer,n[o],i[o],e.position,u),c+=Bt.repeat(" ",t.indent)+Ht((e.line+1).toString(),p)+" | "+l.str+"\n",c+=Bt.repeat("-",t.indent+p+3+l.pos)+"^\n",a=1;a<=t.linesAfter&&!(o+a>=i.length);a++)l=zt(e.buffer,n[o+a],i[o+a],e.position-(n[o]-n[o+a]),u),c+=Bt.repeat(" ",t.indent)+Ht((e.line+a+1).toString(),p)+" | "+l.str+"\n";return c.replace(/\n$/,"")},Wt=["kind","multi","resolve","construct","instanceOf","predicate","represent","representName","defaultStyle","styleAliases"],Gt=["scalar","sequence","mapping"],Jt=function(e,t){if(t=t||{},Object.keys(t).forEach((function(t){if(-1===Wt.indexOf(t))throw new Ut('Unknown option "'+t+'" is met in definition of "'+e+'" YAML type.')})),this.options=t,this.tag=e,this.kind=t.kind||null,this.resolve=t.resolve||function(){return!0},this.construct=t.construct||function(e){return e},this.instanceOf=t.instanceOf||null,this.predicate=t.predicate||null,this.represent=t.represent||null,this.representName=t.representName||null,this.defaultStyle=t.defaultStyle||null,this.multi=t.multi||!1,this.styleAliases=function(e){var t={};return null!==e&&Object.keys(e).forEach((function(r){e[r].forEach((function(e){t[String(e)]=r}))})),t}(t.styleAliases||null),-1===Gt.indexOf(this.kind))throw new Ut('Unknown kind "'+this.kind+'" is specified for "'+e+'" YAML type.')};function Kt(e,t){var r=[];return e[t].forEach((function(e){var t=r.length;r.forEach((function(r,s){r.tag===e.tag&&r.kind===e.kind&&r.multi===e.multi&&(t=s)})),r[t]=e})),r}function Yt(e){return this.extend(e)}Yt.prototype.extend=function(e){var t=[],r=[];if(e instanceof Jt)r.push(e);else if(Array.isArray(e))r=r.concat(e);else{if(!e||!Array.isArray(e.implicit)&&!Array.isArray(e.explicit))throw new Ut("Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })");e.implicit&&(t=t.concat(e.implicit)),e.explicit&&(r=r.concat(e.explicit))}t.forEach((function(e){if(!(e instanceof Jt))throw new Ut("Specified list of YAML types (or a single Type object) contains a non-Type object.");if(e.loadKind&&"scalar"!==e.loadKind)throw new Ut("There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.");if(e.multi)throw new Ut("There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.")})),r.forEach((function(e){if(!(e instanceof Jt))throw new Ut("Specified list of YAML types (or a single Type object) contains a non-Type object.")}));var s=Object.create(Yt.prototype);return s.implicit=(this.implicit||[]).concat(t),s.explicit=(this.explicit||[]).concat(r),s.compiledImplicit=Kt(s,"implicit"),s.compiledExplicit=Kt(s,"explicit"),s.compiledTypeMap=function(){var e,t,r={scalar:{},sequence:{},mapping:{},fallback:{},multi:{scalar:[],sequence:[],mapping:[],fallback:[]}};function s(e){e.multi?(r.multi[e.kind].push(e),r.multi.fallback.push(e)):r[e.kind][e.tag]=r.fallback[e.tag]=e}for(e=0,t=arguments.length;e=0?"0b"+e.toString(2):"-0b"+e.toString(2).slice(1)},octal:function(e){return e>=0?"0o"+e.toString(8):"-0o"+e.toString(8).slice(1)},decimal:function(e){return e.toString(10)},hexadecimal:function(e){return e>=0?"0x"+e.toString(16).toUpperCase():"-0x"+e.toString(16).toUpperCase().slice(1)}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}}),ar=new RegExp("^(?:[-+]?(?:[0-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$"),lr=/^[-+]?[0-9]+e/,cr=new Jt("tag:yaml.org,2002:float",{kind:"scalar",resolve:function(e){return null!==e&&!(!ar.test(e)||"_"===e[e.length-1])},construct:function(e){var t,r;return r="-"===(t=e.replace(/_/g,"").toLowerCase())[0]?-1:1,"+-".indexOf(t[0])>=0&&(t=t.slice(1)),".inf"===t?1===r?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:".nan"===t?NaN:r*parseFloat(t,10)},predicate:function(e){return"[object Number]"===Object.prototype.toString.call(e)&&(e%1!=0||Bt.isNegativeZero(e))},represent:function(e,t){var r;if(isNaN(e))switch(t){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===e)switch(t){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===e)switch(t){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(Bt.isNegativeZero(e))return"-0.0";return r=e.toString(10),lr.test(r)?r.replace("e",".e"):r},defaultStyle:"lowercase"}),pr=tr.extend({implicit:[rr,sr,or,cr]}),ur=pr,dr=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),hr=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$"),mr=new Jt("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:function(e){return null!==e&&(null!==dr.exec(e)||null!==hr.exec(e))},construct:function(e){var t,r,s,n,i,o,a,l,c=0,p=null;if(null===(t=dr.exec(e))&&(t=hr.exec(e)),null===t)throw new Error("Date resolve error");if(r=+t[1],s=+t[2]-1,n=+t[3],!t[4])return new Date(Date.UTC(r,s,n));if(i=+t[4],o=+t[5],a=+t[6],t[7]){for(c=t[7].slice(0,3);c.length<3;)c+="0";c=+c}return t[9]&&(p=6e4*(60*+t[10]+ +(t[11]||0)),"-"===t[9]&&(p=-p)),l=new Date(Date.UTC(r,s,n,i,o,a,c)),p&&l.setTime(l.getTime()-p),l},instanceOf:Date,represent:function(e){return e.toISOString()}}),fr=new Jt("tag:yaml.org,2002:merge",{kind:"scalar",resolve:function(e){return"<<"===e||null===e}}),gr="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r",yr=new Jt("tag:yaml.org,2002:binary",{kind:"scalar",resolve:function(e){if(null===e)return!1;var t,r,s=0,n=e.length,i=gr;for(r=0;r64)){if(t<0)return!1;s+=6}return s%8==0},construct:function(e){var t,r,s=e.replace(/[\r\n=]/g,""),n=s.length,i=gr,o=0,a=[];for(t=0;t>16&255),a.push(o>>8&255),a.push(255&o)),o=o<<6|i.indexOf(s.charAt(t));return 0==(r=n%4*6)?(a.push(o>>16&255),a.push(o>>8&255),a.push(255&o)):18===r?(a.push(o>>10&255),a.push(o>>2&255)):12===r&&a.push(o>>4&255),new Uint8Array(a)},predicate:function(e){return"[object Uint8Array]"===Object.prototype.toString.call(e)},represent:function(e){var t,r,s="",n=0,i=e.length,o=gr;for(t=0;t>18&63],s+=o[n>>12&63],s+=o[n>>6&63],s+=o[63&n]),n=(n<<8)+e[t];return 0==(r=i%3)?(s+=o[n>>18&63],s+=o[n>>12&63],s+=o[n>>6&63],s+=o[63&n]):2===r?(s+=o[n>>10&63],s+=o[n>>4&63],s+=o[n<<2&63],s+=o[64]):1===r&&(s+=o[n>>2&63],s+=o[n<<4&63],s+=o[64],s+=o[64]),s}}),vr=Object.prototype.hasOwnProperty,br=Object.prototype.toString,xr=new Jt("tag:yaml.org,2002:omap",{kind:"sequence",resolve:function(e){if(null===e)return!0;var t,r,s,n,i,o=[],a=e;for(t=0,r=a.length;t>10),56320+(e-65536&1023))}for(var qr=new Array(256),Nr=new Array(256),Ur=0;Ur<256;Ur++)qr[Ur]=Dr(Ur)?1:0,Nr[Ur]=Dr(Ur);function zr(e,t){this.input=e,this.filename=t.filename||null,this.schema=t.schema||kr,this.onWarning=t.onWarning||null,this.legacy=t.legacy||!1,this.json=t.json||!1,this.listener=t.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=e.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.firstTabInLine=-1,this.documents=[]}function Hr(e,t){var r={name:e.filename,buffer:e.input.slice(0,-1),position:e.position,line:e.line,column:e.position-e.lineStart};return r.snippet=Vt(r),new Ut(t,r)}function Vr(e,t){throw Hr(e,t)}function Wr(e,t){e.onWarning&&e.onWarning.call(null,Hr(e,t))}var Gr={YAML:function(e,t,r){var s,n,i;null!==e.version&&Vr(e,"duplication of %YAML directive"),1!==r.length&&Vr(e,"YAML directive accepts exactly one argument"),null===(s=/^([0-9]+)\.([0-9]+)$/.exec(r[0]))&&Vr(e,"ill-formed argument of the YAML directive"),n=parseInt(s[1],10),i=parseInt(s[2],10),1!==n&&Vr(e,"unacceptable YAML version of the document"),e.version=r[0],e.checkLineBreaks=i<2,1!==i&&2!==i&&Wr(e,"unsupported YAML version of the document")},TAG:function(e,t,r){var s,n;2!==r.length&&Vr(e,"TAG directive accepts exactly two arguments"),s=r[0],n=r[1],Pr.test(s)||Vr(e,"ill-formed tag handle (first argument) of the TAG directive"),Ar.call(e.tagMap,s)&&Vr(e,'there is a previously declared suffix for "'+s+'" tag handle'),Cr.test(n)||Vr(e,"ill-formed tag prefix (second argument) of the TAG directive");try{n=decodeURIComponent(n)}catch(t){Vr(e,"tag prefix is malformed: "+n)}e.tagMap[s]=n}};function Jr(e,t,r,s){var n,i,o,a;if(t1&&(e.result+=Bt.repeat("\n",t-1))}function ts(e,t){var r,s,n=e.tag,i=e.anchor,o=[],a=!1;if(-1!==e.firstTabInLine)return!1;for(null!==e.anchor&&(e.anchorMap[e.anchor]=o),s=e.input.charCodeAt(e.position);0!==s&&(-1!==e.firstTabInLine&&(e.position=e.firstTabInLine,Vr(e,"tab characters must not be used in indentation")),45===s)&&Fr(e.input.charCodeAt(e.position+1));)if(a=!0,e.position++,Zr(e,!0,-1)&&e.lineIndent<=t)o.push(null),s=e.input.charCodeAt(e.position);else if(r=e.line,ns(e,t,3,!1,!0),o.push(e.result),Zr(e,!0,-1),s=e.input.charCodeAt(e.position),(e.line===r||e.lineIndent>t)&&0!==s)Vr(e,"bad indentation of a sequence entry");else if(e.lineIndentt?m=1:e.lineIndent===t?m=0:e.lineIndentt?m=1:e.lineIndent===t?m=0:e.lineIndentt)&&(y&&(o=e.line,a=e.lineStart,l=e.position),ns(e,t,4,!0,n)&&(y?f=e.result:g=e.result),y||(Yr(e,d,h,m,f,g,o,a,l),m=f=g=null),Zr(e,!0,-1),c=e.input.charCodeAt(e.position)),(e.line===i||e.lineIndent>t)&&0!==c)Vr(e,"bad indentation of a mapping entry");else if(e.lineIndent=0))break;0===n?Vr(e,"bad explicit indentation width of a block scalar; it cannot be less than one"):c?Vr(e,"repeat of an indentation width identifier"):(p=t+n-1,c=!0)}if(Rr(i)){do{i=e.input.charCodeAt(++e.position)}while(Rr(i));if(35===i)do{i=e.input.charCodeAt(++e.position)}while(!_r(i)&&0!==i)}for(;0!==i;){for(Xr(e),e.lineIndent=0,i=e.input.charCodeAt(e.position);(!c||e.lineIndentp&&(p=e.lineIndent),_r(i))u++;else{if(e.lineIndent0){for(n=o,i=0;n>0;n--)(o=Lr(a=e.input.charCodeAt(++e.position)))>=0?i=(i<<4)+o:Vr(e,"expected hexadecimal character");e.result+=Br(i),e.position++}else Vr(e,"unknown escape sequence");r=s=e.position}else _r(a)?(Jr(e,r,s,!0),es(e,Zr(e,!1,t)),r=s=e.position):e.position===e.lineStart&&Qr(e)?Vr(e,"unexpected end of the document within a double quoted scalar"):(e.position++,s=e.position)}Vr(e,"unexpected end of the stream within a double quoted scalar")}(e,d)?g=!0:function(e){var t,r,s;if(42!==(s=e.input.charCodeAt(e.position)))return!1;for(s=e.input.charCodeAt(++e.position),t=e.position;0!==s&&!Fr(s)&&!Mr(s);)s=e.input.charCodeAt(++e.position);return e.position===t&&Vr(e,"name of an alias node must contain at least one character"),r=e.input.slice(t,e.position),Ar.call(e.anchorMap,r)||Vr(e,'unidentified alias "'+r+'"'),e.result=e.anchorMap[r],Zr(e,!0,-1),!0}(e)?(g=!0,null===e.tag&&null===e.anchor||Vr(e,"alias node should not have any properties")):function(e,t,r){var s,n,i,o,a,l,c,p,u=e.kind,d=e.result;if(Fr(p=e.input.charCodeAt(e.position))||Mr(p)||35===p||38===p||42===p||33===p||124===p||62===p||39===p||34===p||37===p||64===p||96===p)return!1;if((63===p||45===p)&&(Fr(s=e.input.charCodeAt(e.position+1))||r&&Mr(s)))return!1;for(e.kind="scalar",e.result="",n=i=e.position,o=!1;0!==p;){if(58===p){if(Fr(s=e.input.charCodeAt(e.position+1))||r&&Mr(s))break}else if(35===p){if(Fr(e.input.charCodeAt(e.position-1)))break}else{if(e.position===e.lineStart&&Qr(e)||r&&Mr(p))break;if(_r(p)){if(a=e.line,l=e.lineStart,c=e.lineIndent,Zr(e,!1,-1),e.lineIndent>=t){o=!0,p=e.input.charCodeAt(e.position);continue}e.position=i,e.line=a,e.lineStart=l,e.lineIndent=c;break}}o&&(Jr(e,n,i,!1),es(e,e.line-a),n=i=e.position,o=!1),Rr(p)||(i=e.position+1),p=e.input.charCodeAt(++e.position)}return Jr(e,n,i,!1),!!e.result||(e.kind=u,e.result=d,!1)}(e,d,1===r)&&(g=!0,null===e.tag&&(e.tag="?")),null!==e.anchor&&(e.anchorMap[e.anchor]=e.result)):0===m&&(g=a&&ts(e,h))),null===e.tag)null!==e.anchor&&(e.anchorMap[e.anchor]=e.result);else if("?"===e.tag){for(null!==e.result&&"scalar"!==e.kind&&Vr(e,'unacceptable node kind for ! tag; it should be "scalar", not "'+e.kind+'"'),l=0,c=e.implicitTypes.length;l"),null!==e.result&&u.kind!==e.kind&&Vr(e,"unacceptable node kind for !<"+e.tag+'> tag; it should be "'+u.kind+'", not "'+e.kind+'"'),u.resolve(e.result,e.tag)?(e.result=u.construct(e.result,e.tag),null!==e.anchor&&(e.anchorMap[e.anchor]=e.result)):Vr(e,"cannot resolve a node with !<"+e.tag+"> explicit tag")}return null!==e.listener&&e.listener("close",e),null!==e.tag||null!==e.anchor||g}function is(e){var t,r,s,n,i=e.position,o=!1;for(e.version=null,e.checkLineBreaks=e.legacy,e.tagMap=Object.create(null),e.anchorMap=Object.create(null);0!==(n=e.input.charCodeAt(e.position))&&(Zr(e,!0,-1),n=e.input.charCodeAt(e.position),!(e.lineIndent>0||37!==n));){for(o=!0,n=e.input.charCodeAt(++e.position),t=e.position;0!==n&&!Fr(n);)n=e.input.charCodeAt(++e.position);for(s=[],(r=e.input.slice(t,e.position)).length<1&&Vr(e,"directive name must not be less than one character in length");0!==n;){for(;Rr(n);)n=e.input.charCodeAt(++e.position);if(35===n){do{n=e.input.charCodeAt(++e.position)}while(0!==n&&!_r(n));break}if(_r(n))break;for(t=e.position;0!==n&&!Fr(n);)n=e.input.charCodeAt(++e.position);s.push(e.input.slice(t,e.position))}0!==n&&Xr(e),Ar.call(Gr,r)?Gr[r](e,r,s):Wr(e,'unknown document directive "'+r+'"')}Zr(e,!0,-1),0===e.lineIndent&&45===e.input.charCodeAt(e.position)&&45===e.input.charCodeAt(e.position+1)&&45===e.input.charCodeAt(e.position+2)?(e.position+=3,Zr(e,!0,-1)):o&&Vr(e,"directives end mark is expected"),ns(e,e.lineIndent-1,4,!1,!0),Zr(e,!0,-1),e.checkLineBreaks&&jr.test(e.input.slice(i,e.position))&&Wr(e,"non-ASCII line breaks are interpreted as content"),e.documents.push(e.result),e.position===e.lineStart&&Qr(e)?46===e.input.charCodeAt(e.position)&&(e.position+=3,Zr(e,!0,-1)):e.position=55296&&s<=56319&&t+1=56320&&r<=57343?1024*(s-55296)+r-56320+65536:s}function Ss(e){return/^\n* /.test(e)}function Es(e,t,r,s,n){e.dump=function(){if(0===t.length)return 2===e.quotingType?'""':"''";if(!e.noCompatMode&&(-1!==ds.indexOf(t)||hs.test(t)))return 2===e.quotingType?'"'+t+'"':"'"+t+"'";var i=e.indent*Math.max(1,r),o=-1===e.lineWidth?-1:Math.max(Math.min(e.lineWidth,40),e.lineWidth-i),a=s||e.flowLevel>-1&&r>=e.flowLevel;switch(function(e,t,r,s,n,i,o,a){var l,c,p=0,u=null,d=!1,h=!1,m=-1!==s,f=-1,g=bs(c=$s(e,0))&&65279!==c&&!vs(c)&&45!==c&&63!==c&&58!==c&&44!==c&&91!==c&&93!==c&&123!==c&&125!==c&&35!==c&&38!==c&&42!==c&&33!==c&&124!==c&&61!==c&&62!==c&&39!==c&&34!==c&&37!==c&&64!==c&&96!==c&&function(e){return!vs(e)&&58!==e}($s(e,e.length-1));if(t||o)for(l=0;l=65536?l+=2:l++){if(!bs(p=$s(e,l)))return 5;g=g&&ws(p,u,a),u=p}else{for(l=0;l=65536?l+=2:l++){if(10===(p=$s(e,l)))d=!0,m&&(h=h||l-f-1>s&&" "!==e[f+1],f=l);else if(!bs(p))return 5;g=g&&ws(p,u,a),u=p}h=h||m&&l-f-1>s&&" "!==e[f+1]}return d||h?r>9&&Ss(e)?5:o?2===i?5:2:h?4:3:!g||o||n(e)?2===i?5:2:1}(t,a,e.indent,o,(function(t){return function(e,t){var r,s;for(r=0,s=e.implicitTypes.length;r"+ks(t,e.indent)+As(gs(function(e,t){for(var r,s,n,i=/(\n+)([^\n]*)/g,o=(n=-1!==(n=e.indexOf("\n"))?n:e.length,i.lastIndex=n,Os(e.slice(0,n),t)),a="\n"===e[0]||" "===e[0];s=i.exec(e);){var l=s[1],c=s[2];r=" "===c[0],o+=l+(a||r||""===c?"":"\n")+Os(c,t),a=r}return o}(t,o),i));case 5:return'"'+function(e){for(var t,r="",s=0,n=0;n=65536?n+=2:n++)s=$s(e,n),!(t=us[s])&&bs(s)?(r+=e[n],s>=65536&&(r+=e[n+1])):r+=t||ms(s);return r}(t)+'"';default:throw new Ut("impossible error: invalid scalar style")}}()}function ks(e,t){var r=Ss(e)?String(t):"",s="\n"===e[e.length-1];return r+(!s||"\n"!==e[e.length-2]&&"\n"!==e?s?"":"-":"+")+"\n"}function As(e){return"\n"===e[e.length-1]?e.slice(0,-1):e}function Os(e,t){if(""===e||" "===e[0])return e;for(var r,s,n=/ [^ ]/g,i=0,o=0,a=0,l="";r=n.exec(e);)(a=r.index)-i>t&&(s=o>i?o:a,l+="\n"+e.slice(i,s),i=s+1),o=a;return l+="\n",e.length-i>t&&o>i?l+=e.slice(i,o)+"\n"+e.slice(o+1):l+=e.slice(i),l.slice(1)}function js(e,t,r,s){var n,i,o,a="",l=e.tag;for(n=0,i=r.length;n tag resolver accepts not "'+l+'" style');s=a.represent[l](t,l)}e.dump=s}return!0}return!1}function Ps(e,t,r,s,n,i,o){e.tag=null,e.dump=r,Ts(e,r,!1)||Ts(e,r,!0);var a,l=cs.call(e.dump),c=s;s&&(s=e.flowLevel<0||e.flowLevel>t);var p,u,d="[object Object]"===l||"[object Array]"===l;if(d&&(u=-1!==(p=e.duplicates.indexOf(r))),(null!==e.tag&&"?"!==e.tag||u||2!==e.indent&&t>0)&&(n=!1),u&&e.usedDuplicates[p])e.dump="*ref_"+p;else{if(d&&u&&!e.usedDuplicates[p]&&(e.usedDuplicates[p]=!0),"[object Object]"===l)s&&0!==Object.keys(e.dump).length?(function(e,t,r,s){var n,i,o,a,l,c,p="",u=e.tag,d=Object.keys(r);if(!0===e.sortKeys)d.sort();else if("function"==typeof e.sortKeys)d.sort(e.sortKeys);else if(e.sortKeys)throw new Ut("sortKeys must be a boolean or a function");for(n=0,i=d.length;n1024)&&(e.dump&&10===e.dump.charCodeAt(0)?c+="?":c+="? "),c+=e.dump,l&&(c+=ys(e,t)),Ps(e,t+1,a,!0,l)&&(e.dump&&10===e.dump.charCodeAt(0)?c+=":":c+=": ",p+=c+=e.dump));e.tag=u,e.dump=p||"{}"}(e,t,e.dump,n),u&&(e.dump="&ref_"+p+e.dump)):(function(e,t,r){var s,n,i,o,a,l="",c=e.tag,p=Object.keys(r);for(s=0,n=p.length;s1024&&(a+="? "),a+=e.dump+(e.condenseFlow?'"':"")+":"+(e.condenseFlow?"":" "),Ps(e,t,o,!1,!1)&&(l+=a+=e.dump));e.tag=c,e.dump="{"+l+"}"}(e,t,e.dump),u&&(e.dump="&ref_"+p+" "+e.dump));else if("[object Array]"===l)s&&0!==e.dump.length?(e.noArrayIndent&&!o&&t>0?js(e,t-1,e.dump,n):js(e,t,e.dump,n),u&&(e.dump="&ref_"+p+e.dump)):(function(e,t,r){var s,n,i,o="",a=e.tag;for(s=0,n=r.length;s",e.dump=a+" "+e.dump)}return!0}function Cs(e,t){var r,s,n=[],i=[];for(Is(e,n,i),r=0,s=i.length;r(e[t]=function(e){return e.includes(", ")?e.split(", "):e}(r),e)),{})}function Ys(e,t,{loadSpec:r=!1}={}){const s={ok:e.ok,url:e.url||t,status:e.status,statusText:e.statusText,headers:Ks(e.headers)},n=s.headers["content-type"],i=r||((e="")=>/(json|xml|yaml|text)\b/.test(e))(n);return(i?e.text:e.blob||e.buffer).call(e).then((e=>{if(s.text=e,s.data=e,i)try{const t=function(e,t){return t&&(0===t.indexOf("application/json")||t.indexOf("+json")>0)?JSON.parse(e):Js.load(e)}(e,n);s.body=t,s.obj=t}catch(e){s.parseError=e}return s}))}async function Xs(e,t={}){"object"==typeof e&&(e=(t=e).url),t.headers=t.headers||{},(t=Lt(t)).headers&&Object.keys(t.headers).forEach((e=>{const r=t.headers[e];"string"==typeof r&&(t.headers[e]=r.replace(/\n+/g," "))})),t.requestInterceptor&&(t=await t.requestInterceptor(t)||t);const r=t.headers["content-type"]||t.headers["Content-Type"];let s;/multipart\/form-data/i.test(r)&&(delete t.headers["content-type"],delete t.headers["Content-Type"]);try{s=await(t.userFetch||fetch)(t.url,t),s=await Ys(s,e,t),t.responseInterceptor&&(s=await t.responseInterceptor(s)||s)}catch(e){if(!s)throw e;const t=new Error(s.statusText||`response status is ${s.status}`);throw t.status=s.status,t.statusCode=s.status,t.responseError=e,t}if(!s.ok){const e=new Error(s.statusText||`response status is ${s.status}`);throw e.status=s.status,e.statusCode=s.status,e.response=s,e}return s}function Zs(e,t={}){const{requestInterceptor:r,responseInterceptor:s}=t,n=e.withCredentials?"include":"same-origin";return t=>e({url:t,loadSpec:!0,requestInterceptor:r,responseInterceptor:s,headers:{Accept:"application/json, application/yaml"},credentials:n}).then((e=>e.body))}const Qs=e=>{var t,r;const{baseDoc:s,url:n}=e,i=null!==(t=null!=s?s:n)&&void 0!==t?t:"";return"string"==typeof(null===(r=globalThis.document)||void 0===r?void 0:r.baseURI)?String(new URL(i,globalThis.document.baseURI)):i},en=e=>{const{fetch:t,http:r}=e;return t||r||Xs};var tn,rn=(tn=function(e,t){return tn=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])},tn(e,t)},function(e,t){function r(){this.constructor=e}tn(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}),sn=Object.prototype.hasOwnProperty;function nn(e,t){return sn.call(e,t)}function on(e){if(Array.isArray(e)){for(var t=new Array(e.length),r=0;r=48&&t<=57))return!1;r++}return!0}function cn(e){return-1===e.indexOf("/")&&-1===e.indexOf("~")?e:e.replace(/~/g,"~0").replace(/\//g,"~1")}function pn(e){return e.replace(/~1/g,"/").replace(/~0/g,"~")}function un(e){if(void 0===e)return!0;if(e)if(Array.isArray(e)){for(var t=0,r=e.length;t0&&"constructor"==a[c-1]))throw new TypeError("JSON-Patch: modifying `__proto__` or `constructor/prototype` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README");if(r&&void 0===u&&(void 0===l[d]?u=a.slice(0,c).join("/"):c==p-1&&(u=t.path),void 0!==u&&h(t,0,e,u)),c++,Array.isArray(l)){if("-"===d)d=l.length;else{if(r&&!ln(d))throw new mn("Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index","OPERATION_PATH_ILLEGAL_ARRAY_INDEX",i,t,e);ln(d)&&(d=~~d)}if(c>=p){if(r&&"add"===t.op&&d>l.length)throw new mn("The specified index MUST NOT be greater than the number of elements in the array","OPERATION_VALUE_OUT_OF_BOUNDS",i,t,e);if(!1===(o=yn[t.op].call(t,l,d,e)).test)throw new mn("Test operation failed","TEST_OPERATION_FAILED",i,t,e);return o}}else if(c>=p){if(!1===(o=gn[t.op].call(t,l,d,e)).test)throw new mn("Test operation failed","TEST_OPERATION_FAILED",i,t,e);return o}if(l=l[d],r&&c0)throw new mn('Operation `path` property must start with "/"',"OPERATION_PATH_INVALID",t,e,r);if(("move"===e.op||"copy"===e.op)&&"string"!=typeof e.from)throw new mn("Operation `from` property is not present (applicable in `move` and `copy` operations)","OPERATION_FROM_REQUIRED",t,e,r);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&void 0===e.value)throw new mn("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_REQUIRED",t,e,r);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&un(e.value))throw new mn("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED",t,e,r);if(r)if("add"==e.op){var n=e.path.split("/").length,i=s.split("/").length;if(n!==i+1&&n!==i)throw new mn("Cannot perform an `add` operation at the desired path","OPERATION_PATH_CANNOT_ADD",t,e,r)}else if("replace"===e.op||"remove"===e.op||"_get"===e.op){if(e.path!==s)throw new mn("Cannot perform the operation at a path that does not exist","OPERATION_PATH_UNRESOLVABLE",t,e,r)}else if("move"===e.op||"copy"===e.op){var o=$n([{op:"_get",path:e.from,value:void 0}],r);if(o&&"OPERATION_PATH_UNRESOLVABLE"===o.name)throw new mn("Cannot perform the operation from a path that does not exist","OPERATION_FROM_UNRESOLVABLE",t,e,r)}}function $n(e,t,r){try{if(!Array.isArray(e))throw new mn("Patch sequence must be an array","SEQUENCE_NOT_AN_ARRAY");if(t)xn(an(t),an(e),r||!0);else{r=r||wn;for(var s=0;s0&&(e.patches=[],e.callback&&e.callback(s)),s}function Tn(e,t,r,s,n){if(t!==e){"function"==typeof t.toJSON&&(t=t.toJSON());for(var i=on(t),o=on(e),a=!1,l=o.length-1;l>=0;l--){var c=e[u=o[l]];if(!nn(t,u)||void 0===t[u]&&void 0!==c&&!1===Array.isArray(t))Array.isArray(e)===Array.isArray(t)?(n&&r.push({op:"test",path:s+"/"+cn(u),value:an(c)}),r.push({op:"remove",path:s+"/"+cn(u)}),a=!0):(n&&r.push({op:"test",path:s,value:e}),r.push({op:"replace",path:s,value:t}));else{var p=t[u];"object"==typeof c&&null!=c&&"object"==typeof p&&null!=p&&Array.isArray(c)===Array.isArray(p)?Tn(c,p,r,s+"/"+cn(u),n):c!==p&&(n&&r.push({op:"test",path:s+"/"+cn(u),value:an(c)}),r.push({op:"replace",path:s+"/"+cn(u),value:an(p)}))}}if(a||i.length!=o.length)for(l=0;lvoid 0!==t&&e?e[t]:e),e)},applyPatch:function(e,t,r){if(r=r||{},"merge"===(t={...t,path:t.path&&Nn(t.path)}).op){const r=ti(e,t.path);Object.assign(r,t.value),xn(e,[Un(t.path,r)])}else if("mergeDeep"===t.op){const r=ti(e,t.path),s=Bn(r,t.value);e=xn(e,[Un(t.path,s)]).newDocument}else if("add"===t.op&&""===t.path&&Kn(t.value)){const r=Object.keys(t.value).reduce(((e,r)=>(e.push({op:"add",path:`/${Nn(r)}`,value:t.value[r]}),e)),[]);xn(e,r)}else if("replace"===t.op&&""===t.path){let{value:s}=t;r.allowMetaPatches&&t.meta&&Qn(t)&&(Array.isArray(t.value)||Kn(t.value))&&(s={...s,...t.meta}),e=s}else if(xn(e,[t]),r.allowMetaPatches&&t.meta&&Qn(t)&&(Array.isArray(t.value)||Kn(t.value))){const r={...ti(e,t.path),...t.meta};xn(e,[Un(t.path,r)])}return e},parentPathMatch:function(e,t){if(!Array.isArray(t))return!1;for(let r=0,s=t.length;r(e+"").replace(/~/g,"~0").replace(/\//g,"~1"))).join("/")}`:e}function Un(e,t,r){return{op:"replace",path:e,value:t,meta:r}}function zn(e,t,r){return Jn(Gn(e.filter(Qn).map((e=>t(e.value,r,e.path)))||[]))}function Hn(e,t,r){return r=r||[],Array.isArray(e)?e.map(((e,s)=>Hn(e,t,r.concat(s)))):Kn(e)?Object.keys(e).map((s=>Hn(e[s],t,r.concat(s)))):t(e,r[r.length-1],r)}function Vn(e,t,r){let s=[];if((r=r||[]).length>0){const n=t(e,r[r.length-1],r);n&&(s=s.concat(n))}if(Array.isArray(e)){const n=e.map(((e,s)=>Vn(e,t,r.concat(s))));n&&(s=s.concat(n))}else if(Kn(e)){const n=Object.keys(e).map((s=>Vn(e[s],t,r.concat(s))));n&&(s=s.concat(n))}return s=Gn(s),s}function Wn(e){return Array.isArray(e)?e:[e]}function Gn(e){return[].concat(...e.map((e=>Array.isArray(e)?Gn(e):e)))}function Jn(e){return e.filter((e=>void 0!==e))}function Kn(e){return e&&"object"==typeof e}function Yn(e){return e&&"function"==typeof e}function Xn(e){if(ei(e)){const{op:t}=e;return"add"===t||"remove"===t||"replace"===t}return!1}function Zn(e){return Xn(e)||ei(e)&&"mutation"===e.type}function Qn(e){return Zn(e)&&("add"===e.op||"replace"===e.op||"merge"===e.op||"mergeDeep"===e.op)}function ei(e){return e&&"object"==typeof e}function ti(e,t){try{return vn(e,t)}catch(e){return console.error(e),{}}}var ri=function(e){return e&&e.Math===Math&&e},si=ri("object"==typeof globalThis&&globalThis)||ri("object"==typeof window&&window)||ri("object"==typeof self&&self)||ri("object"==typeof global&&global)||ri(!1)||function(){return this}()||Function("return this")(),ni=function(e){try{return!!e()}catch(e){return!0}},ii=!ni((function(){var e=function(){}.bind();return"function"!=typeof e||e.hasOwnProperty("prototype")})),oi=ii,ai=Function.prototype,li=ai.apply,ci=ai.call,pi="object"==typeof Reflect&&Reflect.apply||(oi?ci.bind(li):function(){return ci.apply(li,arguments)}),ui=ii,di=Function.prototype,hi=di.call,mi=ui&&di.bind.bind(hi,hi),fi=ui?mi:function(e){return function(){return hi.apply(e,arguments)}},gi=fi,yi=gi({}.toString),vi=gi("".slice),bi=function(e){return vi(yi(e),8,-1)},xi=bi,wi=fi,$i=function(e){if("Function"===xi(e))return wi(e)},Si="object"==typeof document&&document.all,Ei=void 0===Si&&void 0!==Si?function(e){return"function"==typeof e||e===Si}:function(e){return"function"==typeof e},ki={},Ai=!ni((function(){return 7!==Object.defineProperty({},1,{get:function(){return 7}})[1]})),Oi=ii,ji=Function.prototype.call,Ti=Oi?ji.bind(ji):function(){return ji.apply(ji,arguments)},Pi={},Ci={}.propertyIsEnumerable,Ii=Object.getOwnPropertyDescriptor,_i=Ii&&!Ci.call({1:2},1);Pi.f=_i?function(e){var t=Ii(this,e);return!!t&&t.enumerable}:Ci;var Ri,Fi,Mi=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}},Li=ni,Di=bi,Bi=Object,qi=fi("".split),Ni=Li((function(){return!Bi("z").propertyIsEnumerable(0)}))?function(e){return"String"===Di(e)?qi(e,""):Bi(e)}:Bi,Ui=function(e){return null==e},zi=Ui,Hi=TypeError,Vi=function(e){if(zi(e))throw new Hi("Can't call method on "+e);return e},Wi=Ni,Gi=Vi,Ji=function(e){return Wi(Gi(e))},Ki=Ei,Yi=function(e){return"object"==typeof e?null!==e:Ki(e)},Xi={},Zi=Xi,Qi=si,eo=Ei,to=function(e){return eo(e)?e:void 0},ro=function(e,t){return arguments.length<2?to(Zi[e])||to(Qi[e]):Zi[e]&&Zi[e][t]||Qi[e]&&Qi[e][t]},so=fi({}.isPrototypeOf),no=si.navigator,io=no&&no.userAgent,oo=si,ao=io?String(io):"",lo=oo.process,co=oo.Deno,po=lo&&lo.versions||co&&co.version,uo=po&&po.v8;uo&&(Fi=(Ri=uo.split("."))[0]>0&&Ri[0]<4?1:+(Ri[0]+Ri[1])),!Fi&&ao&&(!(Ri=ao.match(/Edge\/(\d+)/))||Ri[1]>=74)&&(Ri=ao.match(/Chrome\/(\d+)/))&&(Fi=+Ri[1]);var ho=Fi,mo=ni,fo=si.String,go=!!Object.getOwnPropertySymbols&&!mo((function(){var e=Symbol("symbol detection");return!fo(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&ho&&ho<41})),yo=go&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,vo=ro,bo=Ei,xo=so,wo=Object,$o=yo?function(e){return"symbol"==typeof e}:function(e){var t=vo("Symbol");return bo(t)&&xo(t.prototype,wo(e))},So=String,Eo=function(e){try{return So(e)}catch(e){return"Object"}},ko=Ei,Ao=Eo,Oo=TypeError,jo=function(e){if(ko(e))return e;throw new Oo(Ao(e)+" is not a function")},To=jo,Po=Ui,Co=function(e,t){var r=e[t];return Po(r)?void 0:To(r)},Io=Ti,_o=Ei,Ro=Yi,Fo=TypeError,Mo={exports:{}},Lo=si,Do=Object.defineProperty,Bo=si,qo=Mo.exports=Bo.o||function(e,t){try{Do(Lo,e,{value:t,configurable:!0,writable:!0})}catch(r){Lo[e]=t}return t}("__core-js_shared__",{});(qo.versions||(qo.versions=[])).push({version:"3.38.1",mode:"pure",copyright:"© 2014-2024 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.38.1/LICENSE",source:"https://github.com/zloirock/core-js"});var No=Mo.exports,Uo=No,zo=function(e,t){return Uo[e]||(Uo[e]=t||{})},Ho=Vi,Vo=Object,Wo=function(e){return Vo(Ho(e))},Go=Wo,Jo=fi({}.hasOwnProperty),Ko=Object.hasOwn||function(e,t){return Jo(Go(e),t)},Yo=fi,Xo=0,Zo=Math.random(),Qo=Yo(1..toString),ea=function(e){return"Symbol("+(void 0===e?"":e)+")_"+Qo(++Xo+Zo,36)},ta=zo,ra=Ko,sa=ea,na=go,ia=yo,oa=si.Symbol,aa=ta("wks"),la=ia?oa.for||oa:oa&&oa.withoutSetter||sa,ca=function(e){return ra(aa,e)||(aa[e]=na&&ra(oa,e)?oa[e]:la("Symbol."+e)),aa[e]},pa=Ti,ua=Yi,da=$o,ha=Co,ma=TypeError,fa=ca("toPrimitive"),ga=function(e,t){if(!ua(e)||da(e))return e;var r,s=ha(e,fa);if(s){if(void 0===t&&(t="default"),r=pa(s,e,t),!ua(r)||da(r))return r;throw new ma("Can't convert object to primitive value")}return void 0===t&&(t="number"),function(e,t){var r,s;if("string"===t&&_o(r=e.toString)&&!Ro(s=Io(r,e)))return s;if(_o(r=e.valueOf)&&!Ro(s=Io(r,e)))return s;if("string"!==t&&_o(r=e.toString)&&!Ro(s=Io(r,e)))return s;throw new Fo("Can't convert object to primitive value")}(e,t)},ya=$o,va=function(e){var t=ga(e,"string");return ya(t)?t:t+""},ba=Yi,xa=si.document,wa=ba(xa)&&ba(xa.createElement),$a=function(e){return wa?xa.createElement(e):{}},Sa=$a,Ea=!Ai&&!ni((function(){return 7!==Object.defineProperty(Sa("div"),"a",{get:function(){return 7}}).a})),ka=Ai,Aa=Ti,Oa=Pi,ja=Mi,Ta=Ji,Pa=va,Ca=Ko,Ia=Ea,_a=Object.getOwnPropertyDescriptor;ki.f=ka?_a:function(e,t){if(e=Ta(e),t=Pa(t),Ia)try{return _a(e,t)}catch(e){}if(Ca(e,t))return ja(!Aa(Oa.f,e,t),e[t])};var Ra=ni,Fa=Ei,Ma=/#|\.prototype\./,La=function(e,t){var r=Ba[Da(e)];return r===Na||r!==qa&&(Fa(t)?Ra(t):!!t)},Da=La.normalize=function(e){return String(e).replace(Ma,".").toLowerCase()},Ba=La.data={},qa=La.NATIVE="N",Na=La.POLYFILL="P",Ua=La,za=jo,Ha=ii,Va=$i($i.bind),Wa=function(e,t){return za(e),void 0===t?e:Ha?Va(e,t):function(){return e.apply(t,arguments)}},Ga={},Ja=Ai&&ni((function(){return 42!==Object.defineProperty((function(){}),"prototype",{value:42,writable:!1}).prototype})),Ka=Yi,Ya=String,Xa=TypeError,Za=function(e){if(Ka(e))return e;throw new Xa(Ya(e)+" is not an object")},Qa=Ai,el=Ea,tl=Ja,rl=Za,sl=va,nl=TypeError,il=Object.defineProperty,ol=Object.getOwnPropertyDescriptor;Ga.f=Qa?tl?function(e,t,r){if(rl(e),t=sl(t),rl(r),"function"==typeof e&&"prototype"===t&&"value"in r&&"writable"in r&&!r.writable){var s=ol(e,t);s&&s.writable&&(e[t]=r.value,r={configurable:"configurable"in r?r.configurable:s.configurable,enumerable:"enumerable"in r?r.enumerable:s.enumerable,writable:!1})}return il(e,t,r)}:il:function(e,t,r){if(rl(e),t=sl(t),rl(r),el)try{return il(e,t,r)}catch(e){}if("get"in r||"set"in r)throw new nl("Accessors not supported");return"value"in r&&(e[t]=r.value),e};var al=Ga,ll=Mi,cl=Ai?function(e,t,r){return al.f(e,t,ll(1,r))}:function(e,t,r){return e[t]=r,e},pl=si,ul=pi,dl=$i,hl=Ei,ml=ki.f,fl=Ua,gl=Xi,yl=Wa,vl=cl,bl=Ko,xl=function(e){var t=function(r,s,n){if(this instanceof t){switch(arguments.length){case 0:return new e;case 1:return new e(r);case 2:return new e(r,s)}return new e(r,s,n)}return ul(e,this,arguments)};return t.prototype=e.prototype,t},wl=function(e,t){var r,s,n,i,o,a,l,c,p,u=e.target,d=e.global,h=e.stat,m=e.proto,f=d?pl:h?pl[u]:pl[u]&&pl[u].prototype,g=d?gl:gl[u]||vl(gl,u,{})[u],y=g.prototype;for(i in t)s=!(r=fl(d?i:u+(h?".":"#")+i,e.forced))&&f&&bl(f,i),a=g[i],s&&(l=e.dontCallGetSet?(p=ml(f,i))&&p.value:f[i]),o=s&&l?l:t[i],(r||m||typeof a!=typeof o)&&(c=e.bind&&s?yl(o,pl):e.wrap&&s?xl(o):m&&hl(o)?dl(o):o,(e.sham||o&&o.sham||a&&a.sham)&&vl(c,"sham",!0),vl(g,i,c),m&&(bl(gl,n=u+"Prototype")||vl(gl,n,{}),vl(gl[n],i,o),e.real&&y&&(r||!y[i])&&vl(y,i,o)))},$l=ea,Sl=zo("keys"),El=function(e){return Sl[e]||(Sl[e]=$l(e))},kl=!ni((function(){function e(){}return e.prototype.constructor=null,Object.getPrototypeOf(new e)!==e.prototype})),Al=Ko,Ol=Ei,jl=Wo,Tl=kl,Pl=El("IE_PROTO"),Cl=Object,Il=Cl.prototype,_l=Tl?Cl.getPrototypeOf:function(e){var t=jl(e);if(Al(t,Pl))return t[Pl];var r=t.constructor;return Ol(r)&&t instanceof r?r.prototype:t instanceof Cl?Il:null},Rl=fi,Fl=jo,Ml=Yi,Ll=String,Dl=TypeError,Bl=Yi,ql=Vi,Nl=function(e){if(function(e){return Ml(e)||null===e}(e))return e;throw new Dl("Can't set "+Ll(e)+" as a prototype")},Ul=Object.setPrototypeOf||("__proto__"in{}?function(){var e,t=!1,r={};try{(e=function(e,t,r){try{return Rl(Fl(Object.getOwnPropertyDescriptor(e,t)[r]))}catch(e){}}(Object.prototype,"__proto__","set"))(r,[]),t=r instanceof Array}catch(e){}return function(r,s){return ql(r),Nl(s),Bl(r)?(t?e(r,s):r.__proto__=s,r):r}}():void 0),zl={},Hl=Math.ceil,Vl=Math.floor,Wl=Math.trunc||function(e){var t=+e;return(t>0?Vl:Hl)(t)},Gl=function(e){var t=+e;return t!=t||0===t?0:Wl(t)},Jl=Gl,Kl=Math.max,Yl=Math.min,Xl=Gl,Zl=Math.min,Ql=function(e){return function(e){var t=Xl(e);return t>0?Zl(t,9007199254740991):0}(e.length)},ec=Ji,tc=Ql,rc=function(e){return function(t,r,s){var n=ec(t),i=tc(n);if(0===i)return!e&&-1;var o,a=function(e,t){var r=Jl(e);return r<0?Kl(r+t,0):Yl(r,t)}(s,i);if(e&&r!=r){for(;i>a;)if((o=n[a++])!=o)return!0}else for(;i>a;a++)if((e||a in n)&&n[a]===r)return e||a||0;return!e&&-1}},sc={includes:rc(!0),indexOf:rc(!1)},nc={},ic=Ko,oc=Ji,ac=sc.indexOf,lc=nc,cc=fi([].push),pc=function(e,t){var r,s=oc(e),n=0,i=[];for(r in s)!ic(lc,r)&&ic(s,r)&&cc(i,r);for(;t.length>n;)ic(s,r=t[n++])&&(~ac(i,r)||cc(i,r));return i},uc=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],dc=pc,hc=uc.concat("length","prototype");zl.f=Object.getOwnPropertyNames||function(e){return dc(e,hc)};var mc={};mc.f=Object.getOwnPropertySymbols;var fc=ro,gc=zl,yc=mc,vc=Za,bc=fi([].concat),xc=fc("Reflect","ownKeys")||function(e){var t=gc.f(vc(e)),r=yc.f;return r?bc(t,r(e)):t},wc=Ko,$c=xc,Sc=ki,Ec=Ga,kc=function(e,t,r){for(var s=$c(t),n=Ec.f,i=Sc.f,o=0;oo;)Ic.f(e,r=n[o++],s[r]);return e};var Mc,Lc=ro("document","documentElement"),Dc=Za,Bc=Ac,qc=uc,Nc=nc,Uc=Lc,zc=$a,Hc=El("IE_PROTO"),Vc=function(){},Wc=function(e){return" + + + + + {navLogoImg} +
    + + +
    +
    + + + diff --git a/qqq-openapi/src/main/resources/rapidoc/rapidoc-overrides.css b/qqq-openapi/src/main/resources/rapidoc/rapidoc-overrides.css new file mode 100644 index 00000000..a11010f5 --- /dev/null +++ b/qqq-openapi/src/main/resources/rapidoc/rapidoc-overrides.css @@ -0,0 +1,76 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#api-info +{ + margin-left: 0 !important; +} + +#api-info button +{ + width: auto !important; +} + +#api-title span +{ + font-size: 24px !important; + margin-left: 8px; +} + +.nav-scroll +{ + padding-left: 16px; +} + +.tag-description.expanded +{ + max-height: initial !important; +} + +.tag-description .m-markdown p +{ + margin-block-end: 0.5em !important; +} + +api-response +{ + margin-bottom: 50vh; + display: inline-block; +} + +#otherVersions +{ + margin-bottom: 2em; +} + +#navLogo +{ + width: fit-content; + max-width: 280px; + margin-left: auto; + margin-right: auto; + visibility: hidden; +} + +#otherVersions +{ + visibility: hidden; +} \ No newline at end of file diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java index ad8166b0..e78227fd 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java @@ -28,7 +28,6 @@ import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; import com.kingsrook.qqq.backend.javalin.QJavalinMetaData; import com.kingsrook.sampleapp.metadata.SampleMetaDataProvider; import io.javalin.Javalin; -import io.javalin.plugin.bundled.CorsPluginConfig; /******************************************************************************* @@ -73,10 +72,10 @@ public class SampleJavalinServer javalinService = Javalin.create(config -> { + config.router.apiBuilder(qJavalinImplementation.getRoutes()); // todo - not all? - config.plugins.enableCors(cors -> cors.add(CorsPluginConfig::anyHost)); + config.bundledPlugins.enableCors(cors -> cors.addRule(corsRule -> corsRule.anyHost())); }).start(PORT); - javalinService.routes(qJavalinImplementation.getRoutes()); ///////////////////////////////////////////////////////////////// // set the server to hot-swap the q instance before all routes //