From 6a01754479512c1654832a0ca936d76e2fb7d973 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 8 Jun 2023 18:24:56 -0500 Subject: [PATCH 1/9] Renaming MiddlewareMetaData to SupplementalMetaData --- .../core/instances/QInstanceEnricher.java | 6 +-- .../core/instances/QInstanceValidator.java | 10 ++--- .../core/model/metadata/QInstance.java | 38 +++++++++---------- ...ava => QSupplementalInstanceMetaData.java} | 7 ++-- .../model/metadata/fields/QFieldMetaData.java | 38 +++++++++---------- ...a.java => QSupplementalFieldMetaData.java} | 7 ++-- ...a.java => QSupplementalTableMetaData.java} | 7 ++-- .../model/metadata/tables/QTableMetaData.java | 38 +++++++++---------- ...lewareType.java => ApiSupplementType.java} | 2 +- .../ApiInstanceMetaDataContainer.java | 10 ++--- .../fields/ApiFieldMetaDataContainer.java | 8 ++-- .../metadata/tables/ApiTableMetaData.java | 12 +++--- .../tables/ApiTableMetaDataContainer.java | 8 ++-- .../java/com/kingsrook/qqq/api/TestUtils.java | 22 +++++------ .../GenerateOpenApiSpecActionTest.java | 14 +++---- .../actions/GetTableApiFieldsActionTest.java | 16 ++++---- .../api/javalin/QJavalinApiHandlerTest.java | 2 +- .../metadata/ApiInstanceMetaDataTest.java | 8 ++-- 18 files changed, 128 insertions(+), 125 deletions(-) rename qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/{QMiddlewareInstanceMetaData.java => QSupplementalInstanceMetaData.java} (92%) rename qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/{QMiddlewareFieldMetaData.java => QSupplementalFieldMetaData.java} (90%) rename qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/{QMiddlewareTableMetaData.java => QSupplementalTableMetaData.java} (91%) rename qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/{ApiMiddlewareType.java => ApiSupplementType.java} (97%) 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 587addbb..cf6afa63 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 @@ -62,7 +62,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QMiddlewareTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QSupplementalTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.delete.BulkDeleteLoadStep; @@ -261,9 +261,9 @@ public class QInstanceEnricher { table.getFields().values().forEach(this::enrichField); - for(QMiddlewareTableMetaData middlewareTableMetaData : CollectionUtils.nonNullMap(table.getMiddlewareMetaData()).values()) + for(QSupplementalTableMetaData supplementalTableMetaData : CollectionUtils.nonNullMap(table.getSupplementalMetaData()).values()) { - middlewareTableMetaData.enrich(table); + supplementalTableMetaData.enrich(table); } } 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 3bc59276..efa03b2e 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 @@ -48,7 +48,7 @@ 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.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.QMiddlewareInstanceMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; @@ -158,7 +158,7 @@ public class QInstanceValidator validateQueuesAndProviders(qInstance); validateJoins(qInstance); validateSecurityKeyTypes(qInstance); - validateMiddlewareMetaData(qInstance); + validateSupplementalMetaData(qInstance); validateUniqueTopLevelNames(qInstance); } @@ -182,11 +182,11 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private void validateMiddlewareMetaData(QInstance qInstance) + private void validateSupplementalMetaData(QInstance qInstance) { - for(QMiddlewareInstanceMetaData middlewareInstanceMetaData : CollectionUtils.nonNullMap(qInstance.getMiddlewareMetaData()).values()) + for(QSupplementalInstanceMetaData supplementalInstanceMetaData : CollectionUtils.nonNullMap(qInstance.getSupplementalMetaData()).values()) { - middlewareInstanceMetaData.validate(qInstance, this); + supplementalInstanceMetaData.validate(qInstance, this); } } 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 0b9f45ee..4c05c923 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 @@ -91,7 +91,7 @@ public class QInstance private Map queueProviders = new LinkedHashMap<>(); private Map queues = new LinkedHashMap<>(); - private Map middlewareMetaData = new LinkedHashMap<>(); + private Map supplementalMetaData = new LinkedHashMap<>(); private Map environmentValues = new LinkedHashMap<>(); private String defaultTimeZoneId = "UTC"; @@ -1083,60 +1083,60 @@ public class QInstance /******************************************************************************* - ** Getter for middlewareMetaData + ** Getter for supplementalMetaData *******************************************************************************/ - public Map getMiddlewareMetaData() + public Map getSupplementalMetaData() { - return (this.middlewareMetaData); + return (this.supplementalMetaData); } /******************************************************************************* - ** Getter for middlewareMetaData + ** Getter for supplementalMetaData *******************************************************************************/ - public QMiddlewareInstanceMetaData getMiddlewareMetaData(String type) + public QSupplementalInstanceMetaData getSupplementalMetaData(String type) { - if(this.middlewareMetaData == null) + if(this.supplementalMetaData == null) { return (null); } - return this.middlewareMetaData.get(type); + return this.supplementalMetaData.get(type); } /******************************************************************************* - ** Setter for middlewareMetaData + ** Setter for supplementalMetaData *******************************************************************************/ - public void setMiddlewareMetaData(Map middlewareMetaData) + public void setSupplementalMetaData(Map supplementalMetaData) { - this.middlewareMetaData = middlewareMetaData; + this.supplementalMetaData = supplementalMetaData; } /******************************************************************************* - ** Fluent setter for middlewareMetaData + ** Fluent setter for supplementalMetaData *******************************************************************************/ - public QInstance withMiddlewareMetaData(Map middlewareMetaData) + public QInstance withSupplementalMetaData(Map supplementalMetaData) { - this.middlewareMetaData = middlewareMetaData; + this.supplementalMetaData = supplementalMetaData; return (this); } /******************************************************************************* - ** Fluent setter for middlewareMetaData + ** Fluent setter for supplementalMetaData *******************************************************************************/ - public QInstance withMiddlewareMetaData(QMiddlewareInstanceMetaData middlewareMetaData) + public QInstance withSupplementalMetaData(QSupplementalInstanceMetaData supplementalMetaData) { - if(this.middlewareMetaData == null) + if(this.supplementalMetaData == null) { - this.middlewareMetaData = new HashMap<>(); + this.supplementalMetaData = new HashMap<>(); } - this.middlewareMetaData.put(middlewareMetaData.getType(), middlewareMetaData); + this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData); return (this); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMiddlewareInstanceMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QSupplementalInstanceMetaData.java similarity index 92% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMiddlewareInstanceMetaData.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QSupplementalInstanceMetaData.java index 5c20f8bf..709417e2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMiddlewareInstanceMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QSupplementalInstanceMetaData.java @@ -27,9 +27,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; /******************************************************************************* - ** Base-class for instance-level meta-data defined for a specific middleware. + ** Base-class for instance-level meta-data defined by some supplemental module, etc, + ** outside of qqq core *******************************************************************************/ -public abstract class QMiddlewareInstanceMetaData +public abstract class QSupplementalInstanceMetaData { protected String type; @@ -58,7 +59,7 @@ public abstract class QMiddlewareInstanceMetaData /******************************************************************************* ** Fluent setter for type *******************************************************************************/ - public QMiddlewareInstanceMetaData withType(String type) + public QSupplementalInstanceMetaData withType(String type) { this.type = type; return (this); 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 e99e5db0..44d023d5 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 @@ -85,7 +85,7 @@ public class QFieldMetaData implements Cloneable private List adornments; - private Map middlewareMetaData; + private Map supplementalMetaData; @@ -840,60 +840,60 @@ public class QFieldMetaData implements Cloneable /******************************************************************************* - ** Getter for middlewareMetaData + ** Getter for supplementalMetaData *******************************************************************************/ - public Map getMiddlewareMetaData() + public Map getSupplementalMetaData() { - return (this.middlewareMetaData); + return (this.supplementalMetaData); } /******************************************************************************* - ** Getter for middlewareMetaData + ** Getter for supplementalMetaData *******************************************************************************/ - public QMiddlewareFieldMetaData getMiddlewareMetaData(String type) + public QSupplementalFieldMetaData getSupplementalMetaData(String type) { - if(this.middlewareMetaData == null) + if(this.supplementalMetaData == null) { return (null); } - return this.middlewareMetaData.get(type); + return this.supplementalMetaData.get(type); } /******************************************************************************* - ** Setter for middlewareMetaData + ** Setter for supplementalMetaData *******************************************************************************/ - public void setMiddlewareMetaData(Map middlewareMetaData) + public void setSupplementalMetaData(Map supplementalMetaData) { - this.middlewareMetaData = middlewareMetaData; + this.supplementalMetaData = supplementalMetaData; } /******************************************************************************* - ** Fluent setter for middlewareMetaData + ** Fluent setter for supplementalMetaData *******************************************************************************/ - public QFieldMetaData withMiddlewareMetaData(Map middlewareMetaData) + public QFieldMetaData withSupplementalMetaData(Map supplementalMetaData) { - this.middlewareMetaData = middlewareMetaData; + this.supplementalMetaData = supplementalMetaData; return (this); } /******************************************************************************* - ** Fluent setter for middlewareMetaData + ** Fluent setter for supplementalMetaData *******************************************************************************/ - public QFieldMetaData withMiddlewareMetaData(QMiddlewareFieldMetaData middlewareMetaData) + public QFieldMetaData withSupplementalMetaData(QSupplementalFieldMetaData supplementalMetaData) { - if(this.middlewareMetaData == null) + if(this.supplementalMetaData == null) { - this.middlewareMetaData = new HashMap<>(); + this.supplementalMetaData = new HashMap<>(); } - this.middlewareMetaData.put(middlewareMetaData.getType(), middlewareMetaData); + this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData); return (this); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QMiddlewareFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QSupplementalFieldMetaData.java similarity index 90% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QMiddlewareFieldMetaData.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QSupplementalFieldMetaData.java index 454187d4..16200ba2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QMiddlewareFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QSupplementalFieldMetaData.java @@ -23,9 +23,10 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields; /******************************************************************************* - ** Base-class for field-level meta-data defined for a specific middleware. + ** Base-class for field-level meta-data defined by some supplemental module, etc, + ** outside of qqq core *******************************************************************************/ -public abstract class QMiddlewareFieldMetaData +public abstract class QSupplementalFieldMetaData { protected String type; @@ -54,7 +55,7 @@ public abstract class QMiddlewareFieldMetaData /******************************************************************************* ** Fluent setter for type *******************************************************************************/ - public QMiddlewareFieldMetaData withType(String type) + public QSupplementalFieldMetaData withType(String type) { this.type = type; return (this); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QMiddlewareTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QSupplementalTableMetaData.java similarity index 91% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QMiddlewareTableMetaData.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QSupplementalTableMetaData.java index f6707861..1fb759db 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QMiddlewareTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QSupplementalTableMetaData.java @@ -23,9 +23,10 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables; /******************************************************************************* - ** Base-class for table-level meta-data defined for a specific middleware. + ** Base-class for table-level meta-data defined by some supplemental module, etc, + ** outside of qqq core *******************************************************************************/ -public abstract class QMiddlewareTableMetaData +public abstract class QSupplementalTableMetaData { protected String type; @@ -54,7 +55,7 @@ public abstract class QMiddlewareTableMetaData /******************************************************************************* ** Fluent setter for type *******************************************************************************/ - public QMiddlewareTableMetaData withType(String type) + public QSupplementalTableMetaData withType(String type) { this.type = type; return (this); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java index cba66e16..153ec9b9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java @@ -99,7 +99,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData private CacheOf cacheOf; - private Map middlewareMetaData; + private Map supplementalMetaData; private List exposedJoins; @@ -1189,60 +1189,60 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData /******************************************************************************* - ** Getter for middlewareMetaData + ** Getter for supplementalMetaData *******************************************************************************/ - public Map getMiddlewareMetaData() + public Map getSupplementalMetaData() { - return (this.middlewareMetaData); + return (this.supplementalMetaData); } /******************************************************************************* - ** Getter for middlewareMetaData + ** Getter for supplementalMetaData *******************************************************************************/ - public QMiddlewareTableMetaData getMiddlewareMetaData(String type) + public QSupplementalTableMetaData getSupplementalMetaData(String type) { - if(this.middlewareMetaData == null) + if(this.supplementalMetaData == null) { return (null); } - return this.middlewareMetaData.get(type); + return this.supplementalMetaData.get(type); } /******************************************************************************* - ** Setter for middlewareMetaData + ** Setter for supplementalMetaData *******************************************************************************/ - public void setMiddlewareMetaData(Map middlewareMetaData) + public void setSupplementalMetaData(Map supplementalMetaData) { - this.middlewareMetaData = middlewareMetaData; + this.supplementalMetaData = supplementalMetaData; } /******************************************************************************* - ** Fluent setter for middlewareMetaData + ** Fluent setter for supplementalMetaData *******************************************************************************/ - public QTableMetaData withMiddlewareMetaData(Map middlewareMetaData) + public QTableMetaData withSupplementalMetaData(Map supplementalMetaData) { - this.middlewareMetaData = middlewareMetaData; + this.supplementalMetaData = supplementalMetaData; return (this); } /******************************************************************************* - ** Fluent setter for middlewareMetaData + ** Fluent setter for supplementalMetaData *******************************************************************************/ - public QTableMetaData withMiddlewareMetaData(QMiddlewareTableMetaData middlewareMetaData) + public QTableMetaData withSupplementalMetaData(QSupplementalTableMetaData supplementalMetaData) { - if(this.middlewareMetaData == null) + if(this.supplementalMetaData == null) { - this.middlewareMetaData = new HashMap<>(); + this.supplementalMetaData = new HashMap<>(); } - this.middlewareMetaData.put(middlewareMetaData.getType(), middlewareMetaData); + this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData); return (this); } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/ApiMiddlewareType.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/ApiSupplementType.java similarity index 97% rename from qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/ApiMiddlewareType.java rename to qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/ApiSupplementType.java index 800ca283..cefaa68b 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/ApiMiddlewareType.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/ApiSupplementType.java @@ -25,7 +25,7 @@ package com.kingsrook.qqq.api; /******************************************************************************* ** *******************************************************************************/ -public interface ApiMiddlewareType +public interface ApiSupplementType { String NAME = "api"; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java index c26a9d39..cbb7aabd 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java @@ -24,17 +24,17 @@ package com.kingsrook.qqq.api.model.metadata; import java.util.LinkedHashMap; import java.util.Map; -import com.kingsrook.qqq.api.ApiMiddlewareType; +import com.kingsrook.qqq.api.ApiSupplementType; 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.QMiddlewareInstanceMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* ** *******************************************************************************/ -public class ApiInstanceMetaDataContainer extends QMiddlewareInstanceMetaData +public class ApiInstanceMetaDataContainer extends QSupplementalInstanceMetaData { private Map apis; @@ -46,7 +46,7 @@ public class ApiInstanceMetaDataContainer extends QMiddlewareInstanceMetaData *******************************************************************************/ public ApiInstanceMetaDataContainer() { - setType(ApiMiddlewareType.NAME); + setType(ApiSupplementType.NAME); } @@ -56,7 +56,7 @@ public class ApiInstanceMetaDataContainer extends QMiddlewareInstanceMetaData *******************************************************************************/ public static ApiInstanceMetaDataContainer of(QInstance qInstance) { - return ((ApiInstanceMetaDataContainer) qInstance.getMiddlewareMetaData(ApiMiddlewareType.NAME)); + return ((ApiInstanceMetaDataContainer) qInstance.getSupplementalMetaData(ApiSupplementType.NAME)); } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java index d370b20f..cc6e62d2 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java @@ -24,15 +24,15 @@ package com.kingsrook.qqq.api.model.metadata.fields; import java.util.LinkedHashMap; import java.util.Map; -import com.kingsrook.qqq.api.ApiMiddlewareType; +import com.kingsrook.qqq.api.ApiSupplementType; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QMiddlewareFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QSupplementalFieldMetaData; /******************************************************************************* ** *******************************************************************************/ -public class ApiFieldMetaDataContainer extends QMiddlewareFieldMetaData +public class ApiFieldMetaDataContainer extends QSupplementalFieldMetaData { private Map apis; @@ -54,7 +54,7 @@ public class ApiFieldMetaDataContainer extends QMiddlewareFieldMetaData *******************************************************************************/ public static ApiFieldMetaDataContainer of(QFieldMetaData field) { - return ((ApiFieldMetaDataContainer) field.getMiddlewareMetaData(ApiMiddlewareType.NAME)); + return ((ApiFieldMetaDataContainer) field.getSupplementalMetaData(ApiSupplementType.NAME)); } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaData.java index 5b3a1aa0..ec21098b 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaData.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaData.java @@ -27,7 +27,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; -import com.kingsrook.qqq.api.ApiMiddlewareType; +import com.kingsrook.qqq.api.ApiSupplementType; import com.kingsrook.qqq.api.model.APIVersionRange; import com.kingsrook.qqq.api.model.metadata.ApiOperation; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; @@ -81,7 +81,7 @@ public class ApiTableMetaData implements ApiOperation.EnabledOperationsProvider { for(QFieldMetaData field : table.getFields().values()) { - ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiMiddlewareMetaData(apiName, field); + ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiSupplementalMetaData(apiName, field); if(apiFieldMetaData.getInitialVersion() == null) { apiFieldMetaData.setInitialVersion(initialVersion); @@ -90,7 +90,7 @@ public class ApiTableMetaData implements ApiOperation.EnabledOperationsProvider for(QFieldMetaData field : CollectionUtils.nonNullList(removedApiFields)) { - ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiMiddlewareMetaData(apiName, field); + ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiSupplementalMetaData(apiName, field); if(apiFieldMetaData.getInitialVersion() == null) { apiFieldMetaData.setInitialVersion(initialVersion); @@ -104,11 +104,11 @@ public class ApiTableMetaData implements ApiOperation.EnabledOperationsProvider /******************************************************************************* ** *******************************************************************************/ - private static ApiFieldMetaData ensureFieldHasApiMiddlewareMetaData(String apiName, QFieldMetaData field) + private static ApiFieldMetaData ensureFieldHasApiSupplementalMetaData(String apiName, QFieldMetaData field) { - if(field.getMiddlewareMetaData(ApiMiddlewareType.NAME) == null) + if(field.getSupplementalMetaData(ApiSupplementType.NAME) == null) { - field.withMiddlewareMetaData(new ApiFieldMetaDataContainer()); + field.withSupplementalMetaData(new ApiFieldMetaDataContainer()); } ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.of(field); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java index 0b38caed..8dd779fc 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/tables/ApiTableMetaDataContainer.java @@ -24,8 +24,8 @@ package com.kingsrook.qqq.api.model.metadata.tables; import java.util.LinkedHashMap; import java.util.Map; -import com.kingsrook.qqq.api.ApiMiddlewareType; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QMiddlewareTableMetaData; +import com.kingsrook.qqq.api.ApiSupplementType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QSupplementalTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -33,7 +33,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* ** *******************************************************************************/ -public class ApiTableMetaDataContainer extends QMiddlewareTableMetaData +public class ApiTableMetaDataContainer extends QSupplementalTableMetaData { private Map apis; @@ -55,7 +55,7 @@ public class ApiTableMetaDataContainer extends QMiddlewareTableMetaData *******************************************************************************/ public static ApiTableMetaDataContainer of(QTableMetaData table) { - return ((ApiTableMetaDataContainer) table.getMiddlewareMetaData(ApiMiddlewareType.NAME)); + return ((ApiTableMetaDataContainer) table.getSupplementalMetaData(ApiSupplementType.NAME)); } 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 428a56a6..48b0eade 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 @@ -105,7 +105,7 @@ public class TestUtils qInstance.setAuthentication(new Auth0AuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS).withName("anonymous")); - qInstance.withMiddlewareMetaData(new ApiInstanceMetaDataContainer() + qInstance.withSupplementalMetaData(new ApiInstanceMetaDataContainer() .withApiInstanceMetaData(new ApiInstanceMetaData() .withName(API_NAME) .withPath("/api/") @@ -204,7 +204,7 @@ public class TestUtils /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // make some changes to this table in the "main" api (but leave it like the backend in the ALTERNATIVE_API_NAME) // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - table.withMiddlewareMetaData(new ApiTableMetaDataContainer() + table.withSupplementalMetaData(new ApiTableMetaDataContainer() .withApiTableMetaData(API_NAME, new ApiTableMetaData() .withInitialVersion(V2022_Q4) @@ -212,7 +212,7 @@ public class TestUtils // in 2022.Q4, this table had a "shoeCount" field. but for the 2023.Q1 version, we renamed it to noOfShoes! // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// .withRemovedApiField(new QFieldMetaData("shoeCount", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS) - .withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, + .withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withFinalVersion(V2022_Q4).withReplacedByFieldName("noOfShoes")))) ) .withApiTableMetaData(ALTERNATIVE_API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))); @@ -220,18 +220,18 @@ public class TestUtils ///////////////////////////////////////////////////// // change the name for this field for the main api // ///////////////////////////////////////////////////// - table.getField("birthDate").withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withApiFieldName("birthDay"))); + table.getField("birthDate").withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withApiFieldName("birthDay"))); //////////////////////////////////////////////////////////////////////////////// // See above - we renamed this field (in the backend) for the 2023_Q1 version // //////////////////////////////////////////////////////////////////////////////// - table.getField("noOfShoes").withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withInitialVersion(V2023_Q1))); + table.getField("noOfShoes").withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withInitialVersion(V2023_Q1))); ///////////////////////////////////////////////////////////////////////////////////////////////// // 2 new fields - one will appear in a future version of the API, the other is always excluded // ///////////////////////////////////////////////////////////////////////////////////////////////// - table.getField("cost").withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withInitialVersion(V2023_Q2))); - table.getField("price").withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withIsExcluded(true))); + table.getField("cost").withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withInitialVersion(V2023_Q2))); + table.getField("price").withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(API_NAME, new ApiFieldMetaData().withIsExcluded(true))); return (table); } @@ -248,7 +248,7 @@ public class TestUtils .withCustomizer(TableCustomizers.PRE_INSERT_RECORD.getRole(), new QCodeReference(OrderPreInsertCustomizer.class)) .withCustomizer(TableCustomizers.PRE_UPDATE_RECORD.getRole(), new QCodeReference(OrderPreUpdateCustomizer.class)) .withBackendName(MEMORY_BACKEND_NAME) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) .withPrimaryKeyField("id") .withAssociation(new Association().withName("orderLines").withAssociatedTableName(TABLE_NAME_LINE_ITEM).withJoinName("orderLineItem")) .withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_ORDER_EXTRINSIC).withJoinName("orderOrderExtrinsic")) @@ -271,7 +271,7 @@ public class TestUtils return new QTableMetaData() .withName(TABLE_NAME_LINE_ITEM) .withBackendName(MEMORY_BACKEND_NAME) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) .withPrimaryKeyField("id") .withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_LINE_ITEM_EXTRINSIC).withJoinName("lineItemLineItemExtrinsic")) .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) @@ -293,7 +293,7 @@ public class TestUtils return new QTableMetaData() .withName(TABLE_NAME_LINE_ITEM_EXTRINSIC) .withBackendName(MEMORY_BACKEND_NAME) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) @@ -313,7 +313,7 @@ public class TestUtils return new QTableMetaData() .withName(TABLE_NAME_ORDER_EXTRINSIC) .withBackendName(MEMORY_BACKEND_NAME) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion(V2022_Q4))) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecActionTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecActionTest.java index acd4e4f1..c9a3d7e3 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecActionTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecActionTest.java @@ -120,7 +120,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withInitialVersion(TestUtils.V2022_Q4)))); qInstance.addTable(new QTableMetaData() @@ -129,7 +129,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) .withIsHidden(true) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withInitialVersion(TestUtils.V2022_Q4)))); qInstance.addTable(new QTableMetaData() @@ -137,7 +137,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withIsExcluded(true)))); qInstance.addTable(new QTableMetaData() @@ -151,7 +151,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withInitialVersion(TestUtils.V2023_Q2)))); qInstance.addTable(new QTableMetaData() @@ -159,7 +159,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withInitialVersion(TestUtils.V2022_Q4) .withFinalVersion(TestUtils.V2022_Q4)))); @@ -169,7 +169,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) .withoutCapabilities(Capability.TABLE_QUERY, Capability.TABLE_GET, Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withInitialVersion(TestUtils.V2022_Q4)))); GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion(TestUtils.CURRENT_API_VERSION).withApiName(TestUtils.API_NAME)); @@ -198,7 +198,7 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withApiTableName("externalName") .withInitialVersion(TestUtils.V2022_Q4)))); diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsActionTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsActionTest.java index 0285b790..ee9bfc8f 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsActionTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/actions/GetTableApiFieldsActionTest.java @@ -74,11 +74,11 @@ class GetTableApiFieldsActionTest extends BaseTest QInstance qInstance = QContext.getQInstance(); qInstance.addTable(new QTableMetaData() .withName(TABLE_NAME) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("1"))) + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("1"))) .withField(new QFieldMetaData("a", STRING)) // inherit versionRange from the table - .withField(new QFieldMetaData("b", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1")))) - .withField(new QFieldMetaData("c", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("2")))) - .withField(new QFieldMetaData("d", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("3")))) + .withField(new QFieldMetaData("b", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1")))) + .withField(new QFieldMetaData("c", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("2")))) + .withField(new QFieldMetaData("d", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("3")))) ); new QInstanceEnricher(qInstance).enrich(); @@ -98,13 +98,13 @@ class GetTableApiFieldsActionTest extends BaseTest QInstance qInstance = QContext.getQInstance(); qInstance.addTable(new QTableMetaData() .withName(TABLE_NAME) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("1") - .withRemovedApiField(new QFieldMetaData("c", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1").withFinalVersion("2")))) + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("1") + .withRemovedApiField(new QFieldMetaData("c", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1").withFinalVersion("2")))) )) .withField(new QFieldMetaData("a", STRING)) // inherit versionRange from the table - .withField(new QFieldMetaData("b", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1")))) + .withField(new QFieldMetaData("b", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("1")))) // we used to have "c" here... now it's in the removed list above! - .withField(new QFieldMetaData("d", STRING).withMiddlewareMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("3")))) + .withField(new QFieldMetaData("d", STRING).withSupplementalMetaData(new ApiFieldMetaDataContainer().withApiFieldMetaData(TestUtils.API_NAME, new ApiFieldMetaData().withInitialVersion("3")))) ); new QInstanceEnricher(qInstance).enrich(); 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 0d288270..6a274cc2 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 @@ -98,7 +98,7 @@ class QJavalinApiHandlerTest extends BaseTest .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withApiTableName("externalName") .withInitialVersion(TestUtils.V2022_Q4)))); diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataTest.java index fcbb5ac7..bc2ac00e 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataTest.java @@ -114,11 +114,11 @@ class ApiInstanceMetaDataTest qInstance.addTable(new QTableMetaData() .withName("myValidTable") - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("2023.Q1")))); + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("2023.Q1")))); qInstance.addTable(new QTableMetaData() .withName("myInvalidTable") - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("notAVersion")))); + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("notAVersion")))); assertValidationErrors(qInstance, makeBaselineValidApiInstanceMetaData() .withCurrentVersion(new APIVersion("2023.Q1")) @@ -127,7 +127,7 @@ class ApiInstanceMetaDataTest qInstance.addTable(new QTableMetaData() .withName("myFutureValidTable") - .withMiddlewareMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("2024.Q1")))); + .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData().withInitialVersion("2024.Q1")))); assertValidationErrors(qInstance, makeBaselineValidApiInstanceMetaData() .withCurrentVersion(new APIVersion("2023.Q1")) @@ -195,7 +195,7 @@ class ApiInstanceMetaDataTest *******************************************************************************/ private void assertValidationErrors(QInstance qInstance, ApiInstanceMetaData apiInstanceMetaData, List expectedErrors) { - qInstance.withMiddlewareMetaData(new ApiInstanceMetaDataContainer().withApiInstanceMetaData(apiInstanceMetaData)); + qInstance.withSupplementalMetaData(new ApiInstanceMetaDataContainer().withApiInstanceMetaData(apiInstanceMetaData)); QInstanceValidator validator = new QInstanceValidator(); apiInstanceMetaData.validate(apiInstanceMetaData.getName(), qInstance, validator); From a340299c679160cd7a50f6cb4b44b6fbc4a85e1e Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 12 Jun 2023 10:58:30 -0500 Subject: [PATCH 2/9] Initial implementation of api processes --- .../core/instances/QInstanceEnricher.java | 6 + .../actions/processes/RunProcessInput.java | 80 ++- .../actions/processes/RunProcessOutput.java | 109 ++++ .../metadata/processes/QProcessMetaData.java | 61 +++ .../QSupplementalProcessMetaData.java | 78 +++ .../qqq/api/actions/ApiImplementation.java | 272 +++++++++- .../qqq/api/javalin/QJavalinApiHandler.java | 137 +++++ .../processes/ApiProcessCustomizers.java | 87 +++ .../processes/ApiProcessMetaData.java | 496 ++++++++++++++++++ .../ApiProcessMetaDataContainer.java | 138 +++++ .../PostRunApiProcessCustomizer.java | 20 + .../processes/PreRunApiProcessCustomizer.java | 19 + .../qqq/api/model/openapi/HttpMethod.java | 14 + .../kingsrook/qqq/api/GetPersonInfoStep.java | 27 + .../java/com/kingsrook/qqq/api/TestUtils.java | 94 ++++ .../api/javalin/QJavalinApiHandlerTest.java | 14 + 16 files changed, 1644 insertions(+), 8 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessCustomizers.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java create mode 100644 qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/GetPersonInfoStep.java 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 cf6afa63..79fa519e 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 @@ -57,6 +57,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponen 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.QStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; @@ -367,6 +368,11 @@ public class QInstanceEnricher process.getStepList().forEach(this::enrichStep); } + for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values()) + { + supplementalProcessMetaData.enrich(process); + } + enrichPermissionRules(process); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessInput.java index 15e0e063..ec7b7cc0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessInput.java @@ -23,6 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.processes; import java.io.Serializable; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback; @@ -31,6 +35,7 @@ import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; /******************************************************************************* @@ -183,6 +188,17 @@ public class RunProcessInput extends AbstractActionInput + /******************************************************************************* + ** + *******************************************************************************/ + public RunProcessInput withValue(String fieldName, Serializable value) + { + this.processState.getValues().put(fieldName, value); + return (this); + } + + + /******************************************************************************* ** Setter for values ** @@ -258,7 +274,7 @@ public class RunProcessInput extends AbstractActionInput *******************************************************************************/ public String getValueString(String fieldName) { - return ((String) getValue(fieldName)); + return (ValueUtils.getValueAsString(getValue(fieldName))); } @@ -269,7 +285,67 @@ public class RunProcessInput extends AbstractActionInput *******************************************************************************/ public Integer getValueInteger(String fieldName) { - return ((Integer) getValue(fieldName)); + return (ValueUtils.getValueAsInteger(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public BigDecimal getValueBigDecimal(String fieldName) + { + return (ValueUtils.getValueAsBigDecimal(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Boolean getValueBoolean(String fieldName) + { + return (ValueUtils.getValueAsBoolean(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public LocalTime getValueLocalTime(String fieldName) + { + return (ValueUtils.getValueAsLocalTime(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public LocalDate getValueLocalDate(String fieldName) + { + return (ValueUtils.getValueAsLocalDate(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public byte[] getValueByteArray(String fieldName) + { + return (ValueUtils.getValueAsByteArray(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Instant getValueInstant(String fieldName) + { + return (ValueUtils.getValueAsInstant(getValue(fieldName))); } 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 c088f48b..466e02c4 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 @@ -23,11 +23,16 @@ package com.kingsrook.qqq.backend.core.model.actions.processes; import java.io.Serializable; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.List; import java.util.Map; 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.utils.ValueUtils; /******************************************************************************* @@ -122,6 +127,99 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab + /******************************************************************************* + ** Getter for a single field's value + ** + *******************************************************************************/ + public Serializable getValue(String fieldName) + { + return (this.processState.getValues().get(fieldName)); + } + + + + /******************************************************************************* + ** Getter for a single field's value + ** + *******************************************************************************/ + public String getValueString(String fieldName) + { + return (ValueUtils.getValueAsString(getValue(fieldName))); + } + + + + /******************************************************************************* + ** Getter for a single field's value + ** + *******************************************************************************/ + public Integer getValueInteger(String fieldName) + { + return (ValueUtils.getValueAsInteger(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public BigDecimal getValueBigDecimal(String fieldName) + { + return (ValueUtils.getValueAsBigDecimal(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Boolean getValueBoolean(String fieldName) + { + return (ValueUtils.getValueAsBoolean(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public LocalTime getValueLocalTime(String fieldName) + { + return (ValueUtils.getValueAsLocalTime(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public LocalDate getValueLocalDate(String fieldName) + { + return (ValueUtils.getValueAsLocalDate(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public byte[] getValueByteArray(String fieldName) + { + return (ValueUtils.getValueAsByteArray(getValue(fieldName))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Instant getValueInstant(String fieldName) + { + return (ValueUtils.getValueAsInstant(getValue(fieldName))); + } + + + /******************************************************************************* ** Setter for values ** @@ -133,6 +231,17 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab + /******************************************************************************* + ** + *******************************************************************************/ + public RunProcessOutput withValue(String fieldName, Serializable value) + { + this.processState.getValues().put(fieldName, value); + return (this); + } + + + /******************************************************************************* ** Setter for values ** 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 4f29ac04..5295cad9 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 @@ -61,6 +61,7 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi private QScheduleMetaData schedule; + private Map supplementalMetaData; /******************************************************************************* @@ -544,4 +545,64 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi qInstance.addProcess(this); } + + + /******************************************************************************* + ** Getter for supplementalMetaData + *******************************************************************************/ + public Map getSupplementalMetaData() + { + return (this.supplementalMetaData); + } + + + + /******************************************************************************* + ** Getter for supplementalMetaData + *******************************************************************************/ + public QSupplementalProcessMetaData getSupplementalMetaData(String type) + { + if(this.supplementalMetaData == null) + { + return (null); + } + return this.supplementalMetaData.get(type); + } + + + + /******************************************************************************* + ** Setter for supplementalMetaData + *******************************************************************************/ + public void setSupplementalMetaData(Map supplementalMetaData) + { + this.supplementalMetaData = supplementalMetaData; + } + + + + /******************************************************************************* + ** Fluent setter for supplementalMetaData + *******************************************************************************/ + public QProcessMetaData withSupplementalMetaData(Map supplementalMetaData) + { + this.supplementalMetaData = supplementalMetaData; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for supplementalMetaData + *******************************************************************************/ + public QProcessMetaData withSupplementalMetaData(QSupplementalProcessMetaData supplementalMetaData) + { + if(this.supplementalMetaData == null) + { + this.supplementalMetaData = new HashMap<>(); + } + this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java new file mode 100644 index 00000000..c60e01b3 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java @@ -0,0 +1,78 @@ +/* + * 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.backend.core.model.metadata.processes; + + +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; + + +/******************************************************************************* + ** Base-class for process-level meta-data defined by some supplemental module, etc, + ** outside of qqq core + *******************************************************************************/ +public abstract class QSupplementalProcessMetaData +{ + protected String type; + + + + /******************************************************************************* + ** 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 QSupplementalProcessMetaData withType(String type) + { + this.type = type; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void enrich(QProcessMetaData process) + { + //////////////////////// + // noop in base class // + //////////////////////// + } +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java index 96660f60..7c94f10f 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java @@ -30,14 +30,23 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import com.kingsrook.qqq.api.javalin.QBadRequestException; import com.kingsrook.qqq.api.model.APIVersion; +import com.kingsrook.qqq.api.model.APIVersionRange; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiOperation; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessCustomizers; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaDataContainer; +import com.kingsrook.qqq.api.model.metadata.processes.PostRunApiProcessCustomizer; +import com.kingsrook.qqq.api.model.metadata.processes.PreRunApiProcessCustomizer; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.actions.tables.GetAction; @@ -49,6 +58,8 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.logging.LogPair; 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.QInputSource; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; @@ -67,8 +78,10 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; 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.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.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.NotFoundStatusMessage; @@ -96,10 +109,11 @@ public class ApiImplementation { private static final QLogger LOG = QLogger.getLogger(ApiImplementation.class); - ///////////////////////////////////// - // key: Pair // - ///////////////////////////////////// - private static Map, Map> tableApiNameMap = new HashMap<>(); + /////////////////////////////////////////////////////////////////// + // key: Pair, value: Map metaData> // + /////////////////////////////////////////////////////////////////// + private static Map, Map> tableApiNameMap = new HashMap<>(); + private static Map, Map> processApiNameMap = new HashMap<>(); @@ -896,6 +910,99 @@ public class ApiImplementation + /******************************************************************************* + ** + *******************************************************************************/ + public static Map runProcess(ApiInstanceMetaData apiInstanceMetaData, String version, String processApiName, Map paramMap) throws QException + { + QProcessMetaData process = validateProcessAndVersion(apiInstanceMetaData, version, processApiName); + String processName = process.getName(); + ApiProcessMetaData apiProcessMetaData = getApiProcessMetaDataIfProcessIsInApi(apiInstanceMetaData, process); + + List badRequestMessages = new ArrayList<>(); + Map output = new LinkedHashMap<>(); + + String processUUID = UUID.randomUUID().toString(); + + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(processName); + runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP); + runProcessInput.setProcessUUID(processUUID); + // todo i don't think runProcessInput.setCallback(); + // todo i don't think runProcessInput.setAsyncJobCallback(); + + ////////////////////// + // map input values // + ////////////////////// + for(QFieldMetaData inputField : CollectionUtils.nonNullList(apiProcessMetaData.getInputFields())) + { + String value = paramMap.get(inputField.getName()); + if(!StringUtils.hasContent(value) && inputField.getIsRequired()) + { + badRequestMessages.add("Missing value for required input field " + inputField.getName()); + continue; + } + + // todo - types? + + runProcessInput.addValue(inputField.getName(), value); + } + + // todo! runProcessInput.setRecords(records); + + ///////////////////////////////////////// + // throw if bad inputs have been noted // + ///////////////////////////////////////// + if(!badRequestMessages.isEmpty()) + { + if(badRequestMessages.size() == 1) + { + throw (new QBadRequestException(badRequestMessages.get(0))); + } + else + { + throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join(" \n", badRequestMessages))); + } + } + + ///////////////////////////////////////// + // run pre-customizer, if there is one // + ///////////////////////////////////////// + Map customizers = apiProcessMetaData.getCustomizers(); + if(customizers != null && customizers.containsKey(ApiProcessCustomizers.PRE_RUN.getRole())) + { + PreRunApiProcessCustomizer preRunCustomizer = QCodeLoader.getAdHoc(PreRunApiProcessCustomizer.class, customizers.get(ApiProcessCustomizers.PRE_RUN.getRole())); + preRunCustomizer.preApiRun(runProcessInput); + } + + ///////////////////// + // run the process // + ///////////////////// + RunProcessAction runProcessAction = new RunProcessAction(); + RunProcessOutput runProcessOutput = runProcessAction.execute(runProcessInput); + + ///////////////////////////////////////// + // run post-customizer, if there is one // + ///////////////////////////////////////// + if(customizers != null && customizers.containsKey(ApiProcessCustomizers.POST_RUN.getRole())) + { + PostRunApiProcessCustomizer postRunCustomizer = QCodeLoader.getAdHoc(PostRunApiProcessCustomizer.class, customizers.get(ApiProcessCustomizers.POST_RUN.getRole())); + postRunCustomizer.postApiRun(runProcessInput, runProcessOutput); + } + + /////////////////////// + // map output values // + /////////////////////// + for(QFieldMetaData outputField : apiProcessMetaData.getOutputFields()) + { + output.put(outputField.getName(), runProcessOutput.getValues().get(outputField.getName())); + } + + return (output); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -1078,14 +1185,14 @@ public class ApiImplementation ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); if(apiTableMetaDataContainer == null) { - LOG.info("404 because table apiMetaDataContainer is null", logPairs); + LOG.info("404 because table apiTableMetaDataContainer is null", logPairs); throw (new QNotFoundException("Could not find a table named " + tableApiName + " in this api.")); } ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApiTableMetaData(apiInstanceMetaData.getName()); if(apiTableMetaData == null) { - LOG.info("404 because table apiMetaData is null", logPairs); + LOG.info("404 because table apiTableMetaData is null", logPairs); throw (new QNotFoundException("Could not find a table named " + tableApiName + " in this api.")); } @@ -1126,6 +1233,65 @@ public class ApiImplementation + /******************************************************************************* + ** + *******************************************************************************/ + public static QProcessMetaData validateProcessAndVersion(ApiInstanceMetaData apiInstanceMetaData, String version, String processApiName) throws QNotFoundException + { + QProcessMetaData process = getProcessByApiName(apiInstanceMetaData.getName(), version, processApiName); + LogPair[] logPairs = new LogPair[] { logPair("apiName", apiInstanceMetaData.getName()), logPair("version", version), logPair("processApiName", processApiName) }; + + if(process == null) + { + LOG.info("404 because process is null (processApiName=" + processApiName + ")", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + if(BooleanUtils.isTrue(process.getIsHidden())) + { + LOG.info("404 because process isHidden", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); + if(apiProcessMetaDataContainer == null) + { + LOG.info("404 because process apiProcessMetaDataContainer is null", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiInstanceMetaData.getName()); + if(apiProcessMetaData == null) + { + LOG.info("404 because process apiProcessMetaData is null", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + if(BooleanUtils.isTrue(apiProcessMetaData.getIsExcluded())) + { + LOG.info("404 because process is excluded", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + APIVersion requestApiVersion = new APIVersion(version); + List supportedVersions = apiInstanceMetaData.getSupportedVersions(); + if(CollectionUtils.nullSafeIsEmpty(supportedVersions) || !supportedVersions.contains(requestApiVersion)) + { + LOG.info("404 because requested version is not supported", logPairs); + throw (new QNotFoundException(version + " is not a supported version in this api.")); + } + + if(!apiProcessMetaData.getApiVersionRange().includes(requestApiVersion)) + { + LOG.info("404 because process version range does not include requested version", logPairs); + throw (new QNotFoundException(version + " is not a supported version for process " + processApiName + " in this api.")); + } + + return (process); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -1167,6 +1333,100 @@ public class ApiImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private static QProcessMetaData getProcessByApiName(String apiName, String version, String processApiName) + { + ///////////////////////////////////////////////////////////////////////////////////////////// + // processApiNameMap is a map of (apiName,apiVersion) => Map. // + // that is to say, a 2-level map. The first level is keyed by (apiName,apiVersion) pairs. // + // the second level is keyed by processApiNames. // + ///////////////////////////////////////////////////////////////////////////////////////////// + Pair key = new Pair<>(apiName, version); + if(processApiNameMap.get(key) == null) + { + Map map = new HashMap<>(); + + for(QProcessMetaData process : QContext.getQInstance().getProcesses().values()) + { + ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); + if(apiProcessMetaDataContainer != null) + { + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiName); + if(apiProcessMetaData != null) + { + String name = process.getName(); + if(StringUtils.hasContent(apiProcessMetaData.getApiProcessName())) + { + name = apiProcessMetaData.getApiProcessName(); + } + map.put(name, process); + } + } + } + + processApiNameMap.put(key, map); + } + + return (processApiNameMap.get(key).get(processApiName)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static ApiProcessMetaData getApiProcessMetaDataIfProcessIsInApi(ApiInstanceMetaData apiInstanceMetaData, QProcessMetaData process) + { + if(BooleanUtils.isTrue(process.getIsHidden())) + { + LOG.trace("excluding process because it is hidden (process=" + process.getName() + ")"); + return (null); + } + + ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); + if(apiProcessMetaDataContainer == null) + { + LOG.trace("excluding process because apiProcessMetaDataContainer is null (process=" + process.getName() + ")"); + return (null); + } + + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiInstanceMetaData.getName()); + if(apiProcessMetaData == null) + { + LOG.trace("excluding process because apiProcessMetaData is null (process=" + process.getName() + ")"); + return (null); + } + + if(BooleanUtils.isTrue(apiProcessMetaData.getIsExcluded())) + { + LOG.trace("excluding process because is excluded (process=" + process.getName() + ")"); + return (null); + } + + boolean isProcessInAnySupportedVersions = false; + List supportedVersions = apiInstanceMetaData.getSupportedVersions(); + APIVersionRange apiVersionRange = apiProcessMetaData.getApiVersionRange(); + for(APIVersion supportedVersion : supportedVersions) + { + if(apiVersionRange.includes(supportedVersion)) + { + isProcessInAnySupportedVersions = true; + } + } + + if(!isProcessInAnySupportedVersions) + { + LOG.trace("excluding process because it is not in any supported versions (process=" + process.getName() + ")"); + return (null); + } + + return (apiProcessMetaData); + } + + + /******************************************************************************* ** *******************************************************************************/ 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 98484dce..764884cc 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 @@ -29,6 +29,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -43,8 +44,10 @@ import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataProvider; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData; 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; @@ -54,6 +57,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; 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.QRuntimeException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; @@ -66,6 +70,8 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0AuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +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.model.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QUser; @@ -156,10 +162,43 @@ public class QJavalinApiHandler //////////////////////////////////////////// ApiBuilder.get("/", context -> doSpecHtml(context, apiInstanceMetaData)); + /////////////////////////////////////////// + // add known paths for specs & docs page // + /////////////////////////////////////////// ApiBuilder.get("/openapi.yaml", context -> doSpecYaml(context, apiInstanceMetaData)); ApiBuilder.get("/openapi.json", context -> doSpecJson(context, apiInstanceMetaData)); ApiBuilder.get("/openapi.html", context -> doSpecHtml(context, apiInstanceMetaData)); + /////////////////// + // add processes // + /////////////////// + for(QProcessMetaData process : qInstance.getProcesses().values()) + { + ApiProcessMetaData apiProcessMetaData = ApiImplementation.getApiProcessMetaDataIfProcessIsInApi(apiInstanceMetaData, process); + if(apiProcessMetaData != null) + { + String path = getProcessApiPath(process, apiProcessMetaData, apiInstanceMetaData); + HttpMethod method = apiProcessMetaData.getMethod(); + switch(method) + { + case GET -> ApiBuilder.get(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData)); + case POST -> ApiBuilder.post(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData)); + case PUT -> ApiBuilder.put(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData)); + case PATCH -> ApiBuilder.patch(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData)); + case DELETE -> ApiBuilder.delete(path, context -> runProcess(context, process, apiProcessMetaData, apiInstanceMetaData)); + default -> throw (new QRuntimeException("Unrecognized http method [" + method + "] for process [" + process.getName() + "]")); + } + + if(doesProcessSupportAsync(apiInstanceMetaData, process)) + { + ApiBuilder.get(path + "/status/{processId}", context -> getProcessStatus(context, apiInstanceMetaData)); + } + } + } + + /////////////////////////////////// + // add wildcard paths for tables // + /////////////////////////////////// ApiBuilder.path("/{tableName}", () -> { ApiBuilder.get("/openapi.yaml", context -> doSpecYaml(context, apiInstanceMetaData)); @@ -208,6 +247,104 @@ public class QJavalinApiHandler + /******************************************************************************* + ** + *******************************************************************************/ + private void getProcessStatus(Context context, ApiInstanceMetaData apiInstanceMetaData) + { + // todo! + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("checkstyle:indentation") + private void runProcess(Context context, QProcessMetaData processMetaData, ApiProcessMetaData apiProcessMetaData, ApiInstanceMetaData apiInstanceMetaData) + { + String version = context.pathParam("version"); + APILog apiLog = newAPILog(context); + + try + { + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiRunProcess", logPair("process", processMetaData.getName())); + + Map parameters = new LinkedHashMap<>(); + for(QFieldMetaData inputField : CollectionUtils.nonNullList(apiProcessMetaData.getInputFields())) + { + String value = switch(apiProcessMetaData.getMethod()) + { + case GET -> context.queryParam(inputField.getName()); + // todo - other methods (all from a JSON body??) + default -> throw new QException("Http method " + apiLog.getMethod() + " is not yet implemented for reading parameters"); + }; + parameters.put(inputField.getName(), value); + } + + Map outputRecord = ApiImplementation.runProcess(apiInstanceMetaData, version, apiProcessMetaData.getApiProcessName(), parameters); + + QJavalinAccessLogger.logEndSuccess(); + String resultString = toJson(outputRecord); + context.result(resultString); + storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); + } + catch(Exception e) + { + QJavalinAccessLogger.logEndFail(e); + handleException(context, e, apiLog); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private boolean doesProcessSupportAsync(ApiInstanceMetaData apiInstanceMetaData, QProcessMetaData process) + { + // todo - implement + return false; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String getProcessApiPath(QProcessMetaData process, ApiProcessMetaData apiProcessMetaData, ApiInstanceMetaData apiInstanceMetaData) + { + if(StringUtils.hasContent(apiProcessMetaData.getPath())) + { + return apiProcessMetaData.getPath() + "/" + apiProcessMetaData.getApiProcessName(); + } + else if(StringUtils.hasContent(process.getTableName())) + { + QTableMetaData table = qInstance.getTable(process.getTableName()); + String tablePathPart = table.getName(); + ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); + if(apiTableMetaDataContainer != null) + { + ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApis().get(apiInstanceMetaData.getName()); + if(apiTableMetaData != null) + { + if(StringUtils.hasContent(apiTableMetaData.getApiTableName())) + { + tablePathPart = apiTableMetaData.getApiTableName(); + } + } + } + return tablePathPart + "/" + apiProcessMetaData.getApiProcessName(); + } + else + { + return apiProcessMetaData.getApiProcessName(); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessCustomizers.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessCustomizers.java new file mode 100644 index 00000000..daf01f93 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessCustomizers.java @@ -0,0 +1,87 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api.model.metadata.processes; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum ApiProcessCustomizers +{ + PRE_RUN("preRun", PreRunApiProcessCustomizer.class), + POST_RUN("postRun", PreRunApiProcessCustomizer.class); + + private final String role; + private final Class expectedType; + + + + /******************************************************************************* + ** + *******************************************************************************/ + ApiProcessCustomizers(String role, Class expectedType) + { + this.role = role; + this.expectedType = expectedType; + } + + + + /******************************************************************************* + ** Get the FilesystemTableCustomer for a given role (e.g., the role used in meta-data, not + ** the enum-constant name). + *******************************************************************************/ + public static ApiProcessCustomizers forRole(String name) + { + for(ApiProcessCustomizers value : values()) + { + if(value.role.equals(name)) + { + return (value); + } + } + + return (null); + } + + + + /******************************************************************************* + ** Getter for role + ** + *******************************************************************************/ + public String getRole() + { + return role; + } + + + + /******************************************************************************* + ** Getter for expectedType + ** + *******************************************************************************/ + public Class getExpectedType() + { + return expectedType; + } +} 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 new file mode 100644 index 00000000..d3920a95 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaData.java @@ -0,0 +1,496 @@ +/* + * 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.api.model.metadata.processes; + + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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.model.metadata.code.QCodeReference; +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.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessMetaData +{ + private String initialVersion; + private String finalVersion; + + private String apiProcessName; + private Boolean isExcluded; + + private String path; + private HttpMethod method; + + private List inputFields; + private List outputFields; + + private Map customizers; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public ApiProcessMetaData withInferredInputFields(QProcessMetaData processMetaData) + { + inputFields = new ArrayList<>(); + for(QStepMetaData stepMetaData : CollectionUtils.nonNullList(processMetaData.getStepList())) + { + if(stepMetaData instanceof QFrontendStepMetaData frontendStep) + { + inputFields.addAll(frontendStep.getInputFields()); + } + } + + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public ApiProcessMetaData withInferredOutputFields(QProcessMetaData processMetaData) + { + outputFields = new ArrayList<>(); + for(QStepMetaData stepMetaData : CollectionUtils.nonNullList(processMetaData.getStepList())) + { + if(stepMetaData instanceof QFrontendStepMetaData frontendStep) + { + outputFields.addAll(frontendStep.getOutputFields()); + } + } + + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public APIVersionRange getApiVersionRange() + { + if(getInitialVersion() == null) + { + return APIVersionRange.none(); + } + + return (getFinalVersion() != null + ? APIVersionRange.betweenAndIncluding(getInitialVersion(), getFinalVersion()) + : APIVersionRange.afterAndIncluding(getInitialVersion())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + public void enrich(String apiName, QProcessMetaData process) + { + if(!StringUtils.hasContent(getApiProcessName())) + { + setApiProcessName(process.getName()); + } + + if(initialVersion != null) + { + /////////////////////////////////////////////////////////////// + // make sure all fields have at least an initial version set // + /////////////////////////////////////////////////////////////// + for(QFieldMetaData field : CollectionUtils.mergeLists(getInputFields(), getOutputFields())) + { + ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiSupplementalMetaData(apiName, field); + if(apiFieldMetaData.getInitialVersion() == null) + { + apiFieldMetaData.setInitialVersion(initialVersion); + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static ApiFieldMetaData ensureFieldHasApiSupplementalMetaData(String apiName, QFieldMetaData field) + { + if(field.getSupplementalMetaData(ApiSupplementType.NAME) == null) + { + field.withSupplementalMetaData(new ApiFieldMetaDataContainer()); + } + + ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.of(field); + if(apiFieldMetaDataContainer.getApiFieldMetaData(apiName) == null) + { + apiFieldMetaDataContainer.withApiFieldMetaData(apiName, new ApiFieldMetaData()); + } + + return (apiFieldMetaDataContainer.getApiFieldMetaData(apiName)); + } + + + + /******************************************************************************* + ** Fluent setter for a single outputField + *******************************************************************************/ + public ApiProcessMetaData withOutputField(QFieldMetaData outputField) + { + if(this.outputFields == null) + { + this.outputFields = new ArrayList<>(); + } + this.outputFields.add(outputField); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for a single inputField + *******************************************************************************/ + public ApiProcessMetaData withInputField(QFieldMetaData inputField) + { + if(this.inputFields == null) + { + this.inputFields = new ArrayList<>(); + } + this.inputFields.add(inputField); + return (this); + } + + + + /******************************************************************************* + ** Getter for initialVersion + *******************************************************************************/ + public String getInitialVersion() + { + return (this.initialVersion); + } + + + + /******************************************************************************* + ** Setter for initialVersion + *******************************************************************************/ + public void setInitialVersion(String initialVersion) + { + this.initialVersion = initialVersion; + } + + + + /******************************************************************************* + ** Fluent setter for initialVersion + *******************************************************************************/ + public ApiProcessMetaData withInitialVersion(String initialVersion) + { + this.initialVersion = initialVersion; + return (this); + } + + + + /******************************************************************************* + ** Getter for finalVersion + *******************************************************************************/ + public String getFinalVersion() + { + return (this.finalVersion); + } + + + + /******************************************************************************* + ** Setter for finalVersion + *******************************************************************************/ + public void setFinalVersion(String finalVersion) + { + this.finalVersion = finalVersion; + } + + + + /******************************************************************************* + ** Fluent setter for finalVersion + *******************************************************************************/ + public ApiProcessMetaData withFinalVersion(String finalVersion) + { + this.finalVersion = finalVersion; + return (this); + } + + + + /******************************************************************************* + ** Getter for apiProcessName + *******************************************************************************/ + public String getApiProcessName() + { + return (this.apiProcessName); + } + + + + /******************************************************************************* + ** Setter for apiProcessName + *******************************************************************************/ + public void setApiProcessName(String apiProcessName) + { + this.apiProcessName = apiProcessName; + } + + + + /******************************************************************************* + ** Fluent setter for apiProcessName + *******************************************************************************/ + public ApiProcessMetaData withApiProcessName(String apiProcessName) + { + this.apiProcessName = apiProcessName; + return (this); + } + + + + /******************************************************************************* + ** Getter for isExcluded + *******************************************************************************/ + public Boolean getIsExcluded() + { + return (this.isExcluded); + } + + + + /******************************************************************************* + ** Setter for isExcluded + *******************************************************************************/ + public void setIsExcluded(Boolean isExcluded) + { + this.isExcluded = isExcluded; + } + + + + /******************************************************************************* + ** Fluent setter for isExcluded + *******************************************************************************/ + public ApiProcessMetaData withIsExcluded(Boolean isExcluded) + { + this.isExcluded = isExcluded; + return (this); + } + + + + /******************************************************************************* + ** Getter for method + *******************************************************************************/ + public HttpMethod getMethod() + { + return (this.method); + } + + + + /******************************************************************************* + ** Setter for method + *******************************************************************************/ + public void setMethod(HttpMethod method) + { + this.method = method; + } + + + + /******************************************************************************* + ** Fluent setter for method + *******************************************************************************/ + public ApiProcessMetaData withMethod(HttpMethod method) + { + this.method = method; + return (this); + } + + + + /******************************************************************************* + ** 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 ApiProcessMetaData withPath(String path) + { + this.path = path; + return (this); + } + + + + /******************************************************************************* + ** Getter for inputFields + *******************************************************************************/ + public List getInputFields() + { + return (this.inputFields); + } + + + + /******************************************************************************* + ** Setter for inputFields + *******************************************************************************/ + public void setInputFields(List inputFields) + { + this.inputFields = inputFields; + } + + + + /******************************************************************************* + ** Fluent setter for inputFields + *******************************************************************************/ + public ApiProcessMetaData withInputFields(List inputFields) + { + this.inputFields = inputFields; + return (this); + } + + + + /******************************************************************************* + ** Getter for outputFields + *******************************************************************************/ + public List getOutputFields() + { + return (this.outputFields); + } + + + + /******************************************************************************* + ** Setter for outputFields + *******************************************************************************/ + public void setOutputFields(List outputFields) + { + this.outputFields = outputFields; + } + + + + /******************************************************************************* + ** Fluent setter for outputFields + *******************************************************************************/ + public ApiProcessMetaData withOutputFields(List outputFields) + { + this.outputFields = outputFields; + return (this); + } + + + + /******************************************************************************* + ** Getter for customizers + *******************************************************************************/ + public Map getCustomizers() + { + return (this.customizers); + } + + + + /******************************************************************************* + ** Setter for customizers + *******************************************************************************/ + public void setCustomizers(Map customizers) + { + this.customizers = customizers; + } + + + + /******************************************************************************* + ** Fluent setter for customizers + *******************************************************************************/ + public ApiProcessMetaData withCustomizers(Map customizers) + { + this.customizers = customizers; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public ApiProcessMetaData withCustomizer(String role, QCodeReference customizer) + { + if(this.customizers == null) + { + this.customizers = new HashMap<>(); + } + + if(this.customizers.containsKey(role)) + { + throw (new IllegalArgumentException("Attempt to add a second customizer with role [" + role + "] to apiProcess [" + apiProcessName + "].")); + } + this.customizers.put(role, customizer); + return (this); + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java new file mode 100644 index 00000000..36a2b352 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java @@ -0,0 +1,138 @@ +/* + * 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.api.model.metadata.processes; + + +import java.util.LinkedHashMap; +import java.util.Map; +import com.kingsrook.qqq.api.ApiSupplementType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData +{ + private Map apis; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ApiProcessMetaDataContainer() + { + setType("api"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static ApiProcessMetaDataContainer of(QProcessMetaData process) + { + return ((ApiProcessMetaDataContainer) process.getSupplementalMetaData(ApiSupplementType.NAME)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void enrich(QProcessMetaData process) + { + super.enrich(process); + + for(Map.Entry entry : CollectionUtils.nonNullMap(apis).entrySet()) + { + entry.getValue().enrich(entry.getKey(), process); + } + } + + + + /******************************************************************************* + ** Getter for apis + *******************************************************************************/ + public Map getApis() + { + return (this.apis); + } + + + + /******************************************************************************* + ** Getter for apis + *******************************************************************************/ + public ApiProcessMetaData getApiProcessMetaData(String apiName) + { + if(this.apis == null) + { + return (null); + } + + return (this.apis.get(apiName)); + } + + + + /******************************************************************************* + ** Setter for apis + *******************************************************************************/ + public void setApis(Map apis) + { + this.apis = apis; + } + + + + /******************************************************************************* + ** Fluent setter for apis + *******************************************************************************/ + public ApiProcessMetaDataContainer withApis(Map apis) + { + this.apis = apis; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for apis + *******************************************************************************/ + public ApiProcessMetaDataContainer withApiProcessMetaData(String apiName, ApiProcessMetaData apiProcessMetaData) + { + if(this.apis == null) + { + this.apis = new LinkedHashMap<>(); + } + this.apis.put(apiName, apiProcessMetaData); + return (this); + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java new file mode 100644 index 00000000..06ba64f8 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java @@ -0,0 +1,20 @@ +package com.kingsrook.qqq.api.model.metadata.processes; + + +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; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface PostRunApiProcessCustomizer +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void postApiRun(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException; + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java new file mode 100644 index 00000000..e1836708 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java @@ -0,0 +1,19 @@ +package com.kingsrook.qqq.api.model.metadata.processes; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface PreRunApiProcessCustomizer +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void preApiRun(RunProcessInput runProcessInput) throws QException; + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java new file mode 100644 index 00000000..c04d577f --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java @@ -0,0 +1,14 @@ +package com.kingsrook.qqq.api.model.openapi; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum HttpMethod +{ + GET, + POST, + PUT, + PATCH, + DELETE +} diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/GetPersonInfoStep.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/GetPersonInfoStep.java new file mode 100644 index 00000000..96191b58 --- /dev/null +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/GetPersonInfoStep.java @@ -0,0 +1,27 @@ +package com.kingsrook.qqq.api; + + +import java.math.BigDecimal; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class GetPersonInfoStep implements BackendStep +{ + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + runBackendStepOutput.addValue("density", new BigDecimal("3.50")); + runBackendStepOutput.addValue("daysOld", runBackendStepInput.getValueInteger("age") * 365); + runBackendStepOutput.addValue("nickname", "Guy from " + runBackendStepInput.getValueString("homeTown")); + } + +} 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 48b0eade..fdef3cd5 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 @@ -29,8 +29,11 @@ import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaData; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessMetaDataContainer; 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; @@ -45,12 +48,23 @@ 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.Auth0AuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.HtmlWrapper; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine; import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; 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.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.possiblevalues.PVSValueFormatAndFields; +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.NoCodeWidgetFrontendComponentMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +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.tables.Association; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; @@ -59,6 +73,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.SystemErrorStatusMessage; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; /******************************************************************************* @@ -74,6 +89,8 @@ public class TestUtils public static final String TABLE_NAME_LINE_ITEM_EXTRINSIC = "orderLineExtrinsic"; public static final String TABLE_NAME_ORDER_EXTRINSIC = "orderExtrinsic"; + public static final String PROCESS_NAME_GET_PERSON_INFO = "getPersonInfo"; + public static final String API_NAME = "test-api"; public static final String ALTERNATIVE_API_NAME = "person-api"; @@ -103,6 +120,9 @@ public class TestUtils qInstance.addJoin(defineJoinLineItemLineItemExtrinsic()); qInstance.addJoin(defineJoinOrderOrderExtrinsic()); + qInstance.addPossibleValueSource(definePersonPossibleValueSource()); + qInstance.addProcess(defineProcessGetPersonInfo()); + qInstance.setAuthentication(new Auth0AuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS).withName("anonymous")); qInstance.withSupplementalMetaData(new ApiInstanceMetaDataContainer() @@ -133,6 +153,80 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + private static QPossibleValueSource definePersonPossibleValueSource() + { + return new QPossibleValueSource() + .withName(TABLE_NAME_PERSON) + .withType(QPossibleValueSourceType.TABLE) + .withTableName(TABLE_NAME_PERSON) + .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QProcessMetaData defineProcessGetPersonInfo() + { + QProcessMetaData process = new QProcessMetaData() + .withName(PROCESS_NAME_GET_PERSON_INFO) + .withLabel("Get Person Info") + .withTableName(TABLE_NAME_PERSON) + .addStep(new QFrontendStepMetaData() + .withName("enterInputs") + .withLabel("Person Info Input") + .withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM)) + + .withFormField(new QFieldMetaData("age", QFieldType.INTEGER)) + .withFormField(new QFieldMetaData("partnerPersonId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_PERSON)) + .withFormField(new QFieldMetaData("heightInches", QFieldType.DECIMAL)) + .withFormField(new QFieldMetaData("weightPounds", QFieldType.INTEGER)) + .withFormField(new QFieldMetaData("homeTown", QFieldType.STRING)) + + .withComponent(new NoCodeWidgetFrontendComponentMetaData() + + .withOutput(new WidgetHtmlLine() + .withWrapper(HtmlWrapper.divWithStyles(HtmlWrapper.STYLE_FLOAT_RIGHT, HtmlWrapper.STYLE_MEDIUM_CENTERED, HtmlWrapper.styleWidth("50%"))) + .withVelocityTemplate(""" + Density:
$density
+ """)) + + .withOutput(new WidgetHtmlLine() + .withVelocityTemplate(""" + Days old: $daysOld
+ Nickname: $nickname
+ """)) + )) + + .addStep(new QBackendStepMetaData() + .withName("execute") + .withCode(new QCodeReference(GetPersonInfoStep.class))) + + .addStep(new QFrontendStepMetaData() + .withName("dummyStep") + ); + + process.withSupplementalMetaData(new ApiProcessMetaDataContainer() + .withApiProcessMetaData(API_NAME, new ApiProcessMetaData() + .withInitialVersion(CURRENT_API_VERSION) + .withMethod(HttpMethod.GET) + .withInferredInputFields(process) + .withOutputFields(ListBuilder.of( + new QFieldMetaData("density", QFieldType.DECIMAL), + new QFieldMetaData("daysOld", QFieldType.INTEGER), + new QFieldMetaData("nickname", QFieldType.STRING) + )) + )); + + return (process); + } + + + /******************************************************************************* ** Define the in-memory backend used in standard tests *******************************************************************************/ 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 6a274cc2..b7889b1d 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 @@ -1439,6 +1439,20 @@ class QJavalinApiHandlerTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testProcess() throws QException + { + HttpResponse response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/getPersonInfo?age=43&partnerPersonId=1&heightInches=72&weightPounds=220&homeTown=Chester").asString(); + assertEquals(HttpStatus.OK_200, response.getStatus()); + JSONObject jsonObject = new JSONObject(response.getBody()); + System.out.println(jsonObject.toString(3)); + } + + + /******************************************************************************* ** *******************************************************************************/ From eee7354e771d4b556d13fc4c9fd0ec93a6414f4d Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 12 Jun 2023 18:19:48 -0500 Subject: [PATCH 3/9] Checkpoint, WIP on processes in api --- .../core/instances/QInstanceEnricher.java | 2 +- .../metadata/processes/QProcessMetaData.java | 66 ++++ .../QSupplementalProcessMetaData.java | 4 +- ...indQueryFilterForExtractStepException.java | 19 ++ .../ExtractViaQueryStep.java | 2 +- .../StreamedETLWithFrontendProcess.java | 24 ++ .../qqq/backend/core/utils/ObjectUtils.java | 43 +++ .../core/utils/collections/MapBuilder.java | 14 +- .../utils/collections/MapBuilderTest.java | 2 +- .../qqq/api/actions/ApiImplementation.java | 287 +++++++----------- .../actions/GenerateOpenApiSpecAction.java | 188 +++++++++++- .../qqq/api/javalin/QJavalinApiHandler.java | 132 +++++--- .../api/model/actions/HttpApiResponse.java | 122 ++++++++ .../metadata/processes/ApiProcessInput.java | 130 ++++++++ .../ApiProcessInputFieldsContainer.java | 116 +++++++ .../processes/ApiProcessMetaData.java | 241 ++++++--------- .../ApiProcessMetaDataContainer.java | 7 +- .../processes/ApiProcessObjectOutput.java | 85 ++++++ .../processes/ApiProcessOutputInterface.java | 30 ++ .../ApiProcessSummaryListOutput.java | 138 +++++++++ .../metadata/processes/ApiProcessUtils.java | 169 +++++++++++ .../java/com/kingsrook/qqq/api/TestUtils.java | 53 +++- .../qqq/api/TransformPersonStep.java | 59 ++++ .../api/javalin/QJavalinApiHandlerTest.java | 34 ++- 24 files changed, 1571 insertions(+), 396 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/CouldNotFindQueryFilterForExtractStepException.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/HttpApiResponse.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInput.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInputFieldsContainer.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessObjectOutput.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessUtils.java create mode 100644 qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TransformPersonStep.java 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 79fa519e..e66c187d 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 @@ -370,7 +370,7 @@ public class QInstanceEnricher for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values()) { - supplementalProcessMetaData.enrich(process); + supplementalProcessMetaData.enrich(this, process); } enrichPermissionRules(process); 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 5295cad9..8a1a5eae 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 @@ -54,6 +54,9 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi private BasepullConfiguration basepullConfiguration; private QPermissionRules permissionRules; + private Integer minInputRecords = null; + private Integer maxInputRecords = null; + 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 @@ -64,6 +67,7 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi private Map supplementalMetaData; + /******************************************************************************* ** *******************************************************************************/ @@ -605,4 +609,66 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi return (this); } + + + /******************************************************************************* + ** Getter for minInputRecords + *******************************************************************************/ + public Integer getMinInputRecords() + { + return (this.minInputRecords); + } + + + + /******************************************************************************* + ** Setter for minInputRecords + *******************************************************************************/ + public void setMinInputRecords(Integer minInputRecords) + { + this.minInputRecords = minInputRecords; + } + + + + /******************************************************************************* + ** Fluent setter for minInputRecords + *******************************************************************************/ + public QProcessMetaData withMinInputRecords(Integer minInputRecords) + { + this.minInputRecords = minInputRecords; + return (this); + } + + + + /******************************************************************************* + ** Getter for maxInputRecords + *******************************************************************************/ + public Integer getMaxInputRecords() + { + return (this.maxInputRecords); + } + + + + /******************************************************************************* + ** Setter for maxInputRecords + *******************************************************************************/ + public void setMaxInputRecords(Integer maxInputRecords) + { + this.maxInputRecords = maxInputRecords; + } + + + + /******************************************************************************* + ** Fluent setter for maxInputRecords + *******************************************************************************/ + public QProcessMetaData withMaxInputRecords(Integer maxInputRecords) + { + this.maxInputRecords = maxInputRecords; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java index c60e01b3..5a478053 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java @@ -22,7 +22,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.processes; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; /******************************************************************************* @@ -69,7 +69,7 @@ public abstract class QSupplementalProcessMetaData /******************************************************************************* ** *******************************************************************************/ - public void enrich(QProcessMetaData process) + public void enrich(QInstanceEnricher qInstanceEnricher, QProcessMetaData process) { //////////////////////// // noop in base class // diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/CouldNotFindQueryFilterForExtractStepException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/CouldNotFindQueryFilterForExtractStepException.java new file mode 100644 index 00000000..2dea8212 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/CouldNotFindQueryFilterForExtractStepException.java @@ -0,0 +1,19 @@ +package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class CouldNotFindQueryFilterForExtractStepException extends QException +{ + /******************************************************************************* + ** + *******************************************************************************/ + public CouldNotFindQueryFilterForExtractStepException(String message) + { + super(message); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java index c4d7ff89..1f3e776d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java @@ -223,7 +223,7 @@ public class ExtractViaQueryStep extends AbstractExtractStep return (new QQueryFilter().withCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, idStrings))); } - throw (new QException("Could not find query filter for Extract step.")); + throw (new CouldNotFindQueryFilterForExtractStepException("Could not find query filter for Extract step.")); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java index 126735ef..46138ec9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLWithFrontendProcess.java @@ -407,6 +407,30 @@ public class StreamedETLWithFrontendProcess + /******************************************************************************* + ** Fluent setter for minInputRecords + ** + *******************************************************************************/ + public Builder withMinInputRecords(Integer minInputRecords) + { + processMetaData.setMinInputRecords(minInputRecords); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for maxInputRecords + ** + *******************************************************************************/ + public Builder withMaxInputRecords(Integer maxInputRecords) + { + processMetaData.setMaxInputRecords(maxInputRecords); + return (this); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ObjectUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ObjectUtils.java index 3c16e64f..80901c44 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ObjectUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ObjectUtils.java @@ -22,6 +22,9 @@ package com.kingsrook.qqq.backend.core.utils; +import java.util.function.Consumer; +import java.util.function.Predicate; +import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeConsumer; import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier; @@ -96,4 +99,44 @@ public class ObjectUtils return (defaultIfThrew); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void ifNotNull(T object, Consumer consumer) + { + if(object != null) + { + consumer.accept(object); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void ifNotNullUnsafe(T object, UnsafeConsumer consumer) throws E + { + if(object != null) + { + consumer.run(object); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static T requireConditionElse(T a, Predicate condition, T b) + { + if(condition.test(a)) + { + return (a); + } + return (b); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilder.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilder.java index 0a94ddfd..e03d4283 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilder.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilder.java @@ -35,18 +35,18 @@ import java.util.function.Supplier; ** ** Can use it 2 ways: ** MapBuilder.of(key, value, key2, value2, ...) => Map (a HashMap) - ** MapBuilder.of(SomeMap::new).with(key, value).with(key2, value2)...build() => SomeMap (the type you specify) + ** MapBuilder.of(() -> new SomeMap()).with(key, value).with(key2, value2)...build() => SomeMap (the type you specify) *******************************************************************************/ -public class MapBuilder +public class MapBuilder> { - private Map map; + private M map; /******************************************************************************* ** *******************************************************************************/ - private MapBuilder(Map map) + private MapBuilder(M map) { this.map = map; } @@ -56,7 +56,7 @@ public class MapBuilder /******************************************************************************* ** *******************************************************************************/ - public static MapBuilder of(Supplier> mapSupplier) + public static > MapBuilder of(Supplier mapSupplier) { return (new MapBuilder<>(mapSupplier.get())); } @@ -66,7 +66,7 @@ public class MapBuilder /******************************************************************************* ** *******************************************************************************/ - public MapBuilder with(K key, V value) + public MapBuilder with(K key, V value) { map.put(key, value); return (this); @@ -77,7 +77,7 @@ public class MapBuilder /******************************************************************************* ** *******************************************************************************/ - public Map build() + public M build() { return (this.map); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilderTest.java index b4b17600..5bafa2bc 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilderTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MapBuilderTest.java @@ -78,7 +78,7 @@ class MapBuilderTest @Test void testTypeYouRequest() { - Map myTreeMap = MapBuilder.of(TreeMap::new).with("1", 1).with("2", 2).build(); + TreeMap myTreeMap = MapBuilder.of(() -> new TreeMap()).with("1", 1).with("2", 2).build(); assertTrue(myTreeMap instanceof TreeMap); } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java index 7c94f10f..8a2f7dcd 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.api.actions; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -33,12 +34,15 @@ import java.util.Set; import java.util.UUID; import com.kingsrook.qqq.api.javalin.QBadRequestException; import com.kingsrook.qqq.api.model.APIVersion; -import com.kingsrook.qqq.api.model.APIVersionRange; +import com.kingsrook.qqq.api.model.actions.HttpApiResponse; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiOperation; import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessCustomizers; +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.metadata.processes.ApiProcessOutputInterface; +import com.kingsrook.qqq.api.model.metadata.processes.ApiProcessUtils; import com.kingsrook.qqq.api.model.metadata.processes.PostRunApiProcessCustomizer; import com.kingsrook.qqq.api.model.metadata.processes.PreRunApiProcessCustomizer; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; @@ -46,6 +50,7 @@ import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; +import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback; import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; @@ -89,6 +94,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessa import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.QStatusMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.CouldNotFindQueryFilterForExtractStepException; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.Pair; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -100,6 +106,7 @@ import org.json.JSONArray; import org.json.JSONObject; import org.json.JSONTokener; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; +import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IN; /******************************************************************************* @@ -113,7 +120,6 @@ public class ApiImplementation // key: Pair, value: Map metaData> // /////////////////////////////////////////////////////////////////// private static Map, Map> tableApiNameMap = new HashMap<>(); - private static Map, Map> processApiNameMap = new HashMap<>(); @@ -913,14 +919,15 @@ public class ApiImplementation /******************************************************************************* ** *******************************************************************************/ - public static Map runProcess(ApiInstanceMetaData apiInstanceMetaData, String version, String processApiName, Map paramMap) throws QException + public static HttpApiResponse runProcess(ApiInstanceMetaData apiInstanceMetaData, String version, String processApiName, Map paramMap) throws QException { - QProcessMetaData process = validateProcessAndVersion(apiInstanceMetaData, version, processApiName); - String processName = process.getName(); - ApiProcessMetaData apiProcessMetaData = getApiProcessMetaDataIfProcessIsInApi(apiInstanceMetaData, process); + Pair pair = ApiProcessUtils.getProcessMetaDataPair(apiInstanceMetaData, version, processApiName); - List badRequestMessages = new ArrayList<>(); - Map output = new LinkedHashMap<>(); + ApiProcessMetaData apiProcessMetaData = pair.getA(); + QProcessMetaData process = pair.getB(); + String processName = process.getName(); + + List badRequestMessages = new ArrayList<>(); String processUUID = UUID.randomUUID().toString(); @@ -928,27 +935,37 @@ public class ApiImplementation runProcessInput.setProcessName(processName); runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP); runProcessInput.setProcessUUID(processUUID); - // todo i don't think runProcessInput.setCallback(); // todo i don't think runProcessInput.setAsyncJobCallback(); ////////////////////// // map input values // ////////////////////// - for(QFieldMetaData inputField : CollectionUtils.nonNullList(apiProcessMetaData.getInputFields())) + ApiProcessInput apiProcessInput = apiProcessMetaData.getInput(); + if(apiProcessInput != null) { - String value = paramMap.get(inputField.getName()); - if(!StringUtils.hasContent(value) && inputField.getIsRequired()) - { - badRequestMessages.add("Missing value for required input field " + inputField.getName()); - continue; - } - - // todo - types? - - runProcessInput.addValue(inputField.getName(), value); + processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getQueryStringParams()); + processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getFormParams()); + processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getObjectBodyParams()); } - // todo! runProcessInput.setRecords(records); + //////////////////////////////////////// + // get records for process, if needed // + //////////////////////////////////////// + if(process.getMinInputRecords() != null && process.getMinInputRecords() > 0) + { + if(apiProcessInput != null && apiProcessInput.getRecordIdsParamName() != null) + { + String idParam = apiProcessInput.getRecordIdsParamName(); + if(StringUtils.hasContent(idParam) && StringUtils.hasContent(paramMap.get(idParam))) + { + String[] ids = paramMap.get(idParam).split(","); + + QTableMetaData table = QContext.getQInstance().getTable(process.getTableName()); + QQueryFilter filter = new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), IN, Arrays.asList(ids))); + runProcessInput.setCallback(getCallback(filter)); + } + } + } ///////////////////////////////////////// // throw if bad inputs have been noted // @@ -978,8 +995,17 @@ public class ApiImplementation ///////////////////// // run the process // ///////////////////// - RunProcessAction runProcessAction = new RunProcessAction(); - RunProcessOutput runProcessOutput = runProcessAction.execute(runProcessInput); + RunProcessOutput runProcessOutput; + + try + { + RunProcessAction runProcessAction = new RunProcessAction(); + runProcessOutput = runProcessAction.execute(runProcessInput); + } + catch(CouldNotFindQueryFilterForExtractStepException e) + { + throw (new QBadRequestException("Records to run through this process were not specified.")); + } ///////////////////////////////////////// // run post-customizer, if there is one // @@ -993,12 +1019,42 @@ public class ApiImplementation /////////////////////// // map output values // /////////////////////// - for(QFieldMetaData outputField : apiProcessMetaData.getOutputFields()) + ApiProcessOutputInterface output = apiProcessMetaData.getOutput(); + if(output != null) { - output.put(outputField.getName(), runProcessOutput.getValues().get(outputField.getName())); + return (new HttpApiResponse(output.getSuccessStatusCode(runProcessInput, runProcessOutput), output.getOutputForProcess(runProcessInput, runProcessOutput))); + } + else + { + return (new HttpApiResponse(HttpStatus.Code.NO_CONTENT, "")); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void processProcessInputFields(Map paramMap, List badRequestMessages, RunProcessInput runProcessInput, ApiProcessInputFieldsContainer fieldsContainer) + { + if(fieldsContainer == null) + { + return; } - return (output); + for(QFieldMetaData inputField : CollectionUtils.nonNullList(fieldsContainer.getFields())) + { + String value = paramMap.get(inputField.getName()); + if(!StringUtils.hasContent(value) && inputField.getIsRequired()) + { + badRequestMessages.add("Missing value for required input field " + inputField.getName()); + continue; + } + + // todo - types? + + runProcessInput.addValue(inputField.getName(), value); + } } @@ -1233,65 +1289,6 @@ public class ApiImplementation - /******************************************************************************* - ** - *******************************************************************************/ - public static QProcessMetaData validateProcessAndVersion(ApiInstanceMetaData apiInstanceMetaData, String version, String processApiName) throws QNotFoundException - { - QProcessMetaData process = getProcessByApiName(apiInstanceMetaData.getName(), version, processApiName); - LogPair[] logPairs = new LogPair[] { logPair("apiName", apiInstanceMetaData.getName()), logPair("version", version), logPair("processApiName", processApiName) }; - - if(process == null) - { - LOG.info("404 because process is null (processApiName=" + processApiName + ")", logPairs); - throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); - } - - if(BooleanUtils.isTrue(process.getIsHidden())) - { - LOG.info("404 because process isHidden", logPairs); - throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); - } - - ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); - if(apiProcessMetaDataContainer == null) - { - LOG.info("404 because process apiProcessMetaDataContainer is null", logPairs); - throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); - } - - ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiInstanceMetaData.getName()); - if(apiProcessMetaData == null) - { - LOG.info("404 because process apiProcessMetaData is null", logPairs); - throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); - } - - if(BooleanUtils.isTrue(apiProcessMetaData.getIsExcluded())) - { - LOG.info("404 because process is excluded", logPairs); - throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); - } - - APIVersion requestApiVersion = new APIVersion(version); - List supportedVersions = apiInstanceMetaData.getSupportedVersions(); - if(CollectionUtils.nullSafeIsEmpty(supportedVersions) || !supportedVersions.contains(requestApiVersion)) - { - LOG.info("404 because requested version is not supported", logPairs); - throw (new QNotFoundException(version + " is not a supported version in this api.")); - } - - if(!apiProcessMetaData.getApiVersionRange().includes(requestApiVersion)) - { - LOG.info("404 because process version range does not include requested version", logPairs); - throw (new QNotFoundException(version + " is not a supported version for process " + processApiName + " in this api.")); - } - - return (process); - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -1333,99 +1330,6 @@ public class ApiImplementation - /******************************************************************************* - ** - *******************************************************************************/ - private static QProcessMetaData getProcessByApiName(String apiName, String version, String processApiName) - { - ///////////////////////////////////////////////////////////////////////////////////////////// - // processApiNameMap is a map of (apiName,apiVersion) => Map. // - // that is to say, a 2-level map. The first level is keyed by (apiName,apiVersion) pairs. // - // the second level is keyed by processApiNames. // - ///////////////////////////////////////////////////////////////////////////////////////////// - Pair key = new Pair<>(apiName, version); - if(processApiNameMap.get(key) == null) - { - Map map = new HashMap<>(); - - for(QProcessMetaData process : QContext.getQInstance().getProcesses().values()) - { - ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); - if(apiProcessMetaDataContainer != null) - { - ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiName); - if(apiProcessMetaData != null) - { - String name = process.getName(); - if(StringUtils.hasContent(apiProcessMetaData.getApiProcessName())) - { - name = apiProcessMetaData.getApiProcessName(); - } - map.put(name, process); - } - } - } - - processApiNameMap.put(key, map); - } - - return (processApiNameMap.get(key).get(processApiName)); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public static ApiProcessMetaData getApiProcessMetaDataIfProcessIsInApi(ApiInstanceMetaData apiInstanceMetaData, QProcessMetaData process) - { - if(BooleanUtils.isTrue(process.getIsHidden())) - { - LOG.trace("excluding process because it is hidden (process=" + process.getName() + ")"); - return (null); - } - - ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); - if(apiProcessMetaDataContainer == null) - { - LOG.trace("excluding process because apiProcessMetaDataContainer is null (process=" + process.getName() + ")"); - return (null); - } - - ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiInstanceMetaData.getName()); - if(apiProcessMetaData == null) - { - LOG.trace("excluding process because apiProcessMetaData is null (process=" + process.getName() + ")"); - return (null); - } - - if(BooleanUtils.isTrue(apiProcessMetaData.getIsExcluded())) - { - LOG.trace("excluding process because is excluded (process=" + process.getName() + ")"); - return (null); - } - - boolean isProcessInAnySupportedVersions = false; - List supportedVersions = apiInstanceMetaData.getSupportedVersions(); - APIVersionRange apiVersionRange = apiProcessMetaData.getApiVersionRange(); - for(APIVersion supportedVersion : supportedVersions) - { - if(apiVersionRange.includes(supportedVersion)) - { - isProcessInAnySupportedVersions = true; - } - } - - if(!isProcessInAnySupportedVersions) - { - LOG.trace("excluding process because it is not in any supported versions (process=" + process.getName() + ")"); - return (null); - } - - return (apiProcessMetaData); - } - - /******************************************************************************* ** @@ -1445,4 +1349,29 @@ public class ApiImplementation return errors.stream().anyMatch(e -> (e instanceof NotFoundStatusMessage)); } + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QProcessCallback getCallback(QQueryFilter filter) + { + return new QProcessCallback() + { + @Override + public QQueryFilter getQueryFilter() + { + return (filter); + } + + + + @Override + public Map getFieldValues(List fields) + { + return (Collections.emptyMap()); + } + }; + } + } 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 1aa18b5c..a37efeac 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 @@ -41,6 +41,11 @@ import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.ApiOperation; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; +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.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.Components; @@ -73,12 +78,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; 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.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; 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.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.ObjectUtils; +import com.kingsrook.qqq.backend.core.utils.Pair; 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; @@ -192,8 +199,9 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction tables = new ArrayList<>(qInstance.getTables().values()); + List tables = new ArrayList<>(qInstance.getTables().values()); + Set usedProcessNames = new HashSet<>(); tables.sort(Comparator.comparing(t -> ObjectUtils.requireNonNullElse(t.getLabel(), t.getName(), ""))); for(QTableMetaData table : tables) { @@ -330,7 +339,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction> apiProcessMetaDataList = getProcessesUnderTable(table, apiName, apiVersion); + + if(!getEnabled && !queryByQueryStringEnabled && !insertEnabled && !insertBulkEnabled && !updateEnabled && !updateBulkEnabled && !deleteEnabled && !deleteBulkEnabled && !CollectionUtils.nullSafeHasContents(apiProcessMetaDataList)) { - LOG.debug("Omitting table [" + tableName + "] because it does not have any supported capabilities / enabled operations"); + LOG.debug("Omitting table [" + tableName + "] because it does not have any supported capabilities / enabled operations or processes"); continue; } @@ -687,6 +698,35 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction pair : CollectionUtils.nonNullList(apiProcessMetaDataList)) + { + ApiProcessMetaData apiProcessMetaData = pair.getA(); + QProcessMetaData processMetaData = pair.getB(); + + String processApiPath = ApiProcessUtils.getProcessApiPath(qInstance, processMetaData, apiProcessMetaData, apiInstanceMetaData); + Path path = generateProcessSpecPathObject(apiInstanceMetaData, apiProcessMetaData, processMetaData, ListBuilder.of(tableLabel)); + openAPI.getPaths().put(basePath + processApiPath, path); + + usedProcessNames.add(processMetaData.getName()); + } + } + + ///////////////////////////// + // add non-table processes // + ///////////////////////////// + List> processesNotUnderTables = getProcessesNotUnderTables(apiName, apiVersion, usedProcessNames); + for(Pair pair : CollectionUtils.nonNullList(processesNotUnderTables)) + { + ApiProcessMetaData apiProcessMetaData = pair.getA(); + QProcessMetaData processMetaData = pair.getB(); + + String processApiPath = ApiProcessUtils.getProcessApiPath(qInstance, processMetaData, apiProcessMetaData, apiInstanceMetaData); + Path path = generateProcessSpecPathObject(apiInstanceMetaData, apiProcessMetaData, processMetaData, ListBuilder.of(processMetaData.getLabel())); + openAPI.getPaths().put(basePath + processApiPath, path); + + usedProcessNames.add(processMetaData.getName()); } componentResponses.put("error" + HttpStatus.BAD_REQUEST.getCode(), buildStandardErrorResponse("Bad Request. Some portion of the request's content was not acceptable to the server. See error message in body for details.", "Parameter id should be given an integer value, but received string: \"Foo\"")); @@ -710,6 +750,140 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction> getProcessesNotUnderTables(String apiName, APIVersion apiVersion, Set usedProcessNames) + { + List> apiProcessMetaDataList = new ArrayList<>(); + for(QProcessMetaData processMetaData : CollectionUtils.nonNullMap(QContext.getQInstance().getProcesses()).values()) + { + if(usedProcessNames.contains(processMetaData.getName())) + { + continue; + } + + ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(processMetaData); + if(apiProcessMetaDataContainer == null) + { + continue; + } + + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApis().get(apiName); + if(apiProcessMetaData == null) + { + continue; + } + + if(!apiProcessMetaData.getApiVersionRange().includes(apiVersion)) + { + continue; + } + + apiProcessMetaDataList.add(Pair.of(apiProcessMetaData, processMetaData)); + } + return (apiProcessMetaDataList); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Path generateProcessSpecPathObject(ApiInstanceMetaData apiInstanceMetaData, ApiProcessMetaData apiProcessMetaData, QProcessMetaData processMetaData, List tags) + { + Method methodForProcess = new Method() + .withOperationId(apiProcessMetaData.getApiProcessName()) + .withTags(tags) + .withSummary(processMetaData.getLabel()) // todo - add optional summary to meta data + .withDescription("Run the process named " + processMetaData.getLabel())// todo - add optional description to meta data, .withDescription() + .withSecurity(getSecurity(apiInstanceMetaData, "todo - process name")); + + List parameters = new ArrayList<>(); + ApiProcessInput apiProcessInput = apiProcessMetaData.getInput(); + if(apiProcessInput != null) + { + ApiProcessInputFieldsContainer queryStringParams = apiProcessInput.getQueryStringParams(); + if(queryStringParams != null) + { + for(QFieldMetaData field : CollectionUtils.nonNullList(queryStringParams.getFields())) + { + parameters.add(new Parameter() + .withName(field.getName()) + // todo - add description to meta data .withDescription("Which page of results to return. Starts at 1.") + .withDescription("Value for the " + field.getLabel() + " field.") + .withIn("query") + .withRequired(field.getIsRequired()) + .withSchema(new Schema().withType(getFieldType(field)))); + } + } + } + + if(CollectionUtils.nullSafeHasContents(parameters)) + { + methodForProcess.setParameters(parameters); + } + + // todo methodForProcess.withRequestBody(); + + // todo methodForProcess.withResponse(); + + methodForProcess.withResponse(HttpStatus.OK.getCode(), new Response() + .withDescription("Successfully ran the process") + .withContent(MapBuilder.of("application/json", new Content()))); + + @SuppressWarnings("checkstyle:indentation") + Path path = switch(apiProcessMetaData.getMethod()) + { + case GET -> new Path().withGet(methodForProcess); + case POST -> new Path().withPost(methodForProcess); + case PUT -> new Path().withPut(methodForProcess); + case PATCH -> new Path().withPatch(methodForProcess); + case DELETE -> new Path().withDelete(methodForProcess); + }; + + return (path); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private List> getProcessesUnderTable(QTableMetaData table, String apiName, APIVersion apiVersion) + { + List> apiProcessMetaDataList = new ArrayList<>(); + for(QProcessMetaData processMetaData : CollectionUtils.nonNullMap(QContext.getQInstance().getProcesses()).values()) + { + if(!table.getName().equals(processMetaData.getTableName())) + { + continue; + } + + ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(processMetaData); + if(apiProcessMetaDataContainer == null) + { + continue; + } + + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApis().get(apiName); + if(apiProcessMetaData == null) + { + continue; + } + + if(!apiProcessMetaData.getApiVersionRange().includes(apiVersion)) + { + continue; + } + + apiProcessMetaDataList.add(Pair.of(apiProcessMetaData, processMetaData)); + } + return (apiProcessMetaDataList); + } + + + /******************************************************************************* ** written for the use-case of, generating a single table's api, but it has ** associations that it references, so we need their schemas too - so, make 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 764884cc..12dd78aa 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 @@ -28,11 +28,13 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; import java.util.Base64; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.BiFunction; import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonInclude; import com.kingsrook.qqq.api.actions.ApiImplementation; @@ -41,10 +43,15 @@ import com.kingsrook.qqq.api.model.APILog; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput; +import com.kingsrook.qqq.api.model.actions.HttpApiResponse; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataProvider; +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.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; @@ -80,6 +87,7 @@ import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModu import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; +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.backend.javalin.QJavalinAccessLogger; @@ -91,6 +99,7 @@ import io.javalin.http.Context; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.BooleanUtils; import org.eclipse.jetty.http.HttpStatus; +import org.json.JSONObject; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; import static com.kingsrook.qqq.backend.javalin.QJavalinImplementation.SLOW_LOG_THRESHOLD_MS; @@ -102,6 +111,8 @@ public class QJavalinApiHandler { private static final QLogger LOG = QLogger.getLogger(QJavalinApiHandler.class); + private static final ApiProcessMetaDataContainer EMPTY_CONTAINER = new ApiProcessMetaDataContainer().withApis(Collections.emptyMap()); + private static QInstance qInstance; private static Map apiLogUserIdCache = new HashMap<>(); @@ -174,10 +185,11 @@ public class QJavalinApiHandler /////////////////// for(QProcessMetaData process : qInstance.getProcesses().values()) { - ApiProcessMetaData apiProcessMetaData = ApiImplementation.getApiProcessMetaDataIfProcessIsInApi(apiInstanceMetaData, process); + ApiProcessMetaDataContainer apiProcessMetaDataContainer = Objects.requireNonNullElse(ApiProcessMetaDataContainer.of(process), EMPTY_CONTAINER); + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApis().get(apiInstanceMetaData.getName()); if(apiProcessMetaData != null) { - String path = getProcessApiPath(process, apiProcessMetaData, apiInstanceMetaData); + String path = ApiProcessUtils.getProcessApiPath(qInstance, process, apiProcessMetaData, apiInstanceMetaData); HttpMethod method = apiProcessMetaData.getMethod(); switch(method) { @@ -189,6 +201,8 @@ public class QJavalinApiHandler default -> throw (new QRuntimeException("Unrecognized http method [" + method + "] for process [" + process.getName() + "]")); } + make405sForOtherMethods(method, path); + if(doesProcessSupportAsync(apiInstanceMetaData, process)) { ApiBuilder.get(path + "/status/{processId}", context -> getProcessStatus(context, apiInstanceMetaData)); @@ -247,6 +261,49 @@ public class QJavalinApiHandler + /******************************************************************************* + ** + *******************************************************************************/ + private void make405sForOtherMethods(HttpMethod allowedMethod, String path) + { + if(!allowedMethod.equals(HttpMethod.GET)) + { + ApiBuilder.get(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod)); + } + + if(!allowedMethod.equals(HttpMethod.POST)) + { + ApiBuilder.post(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod)); + } + + if(!allowedMethod.equals(HttpMethod.PUT)) + { + ApiBuilder.put(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod)); + } + + if(!allowedMethod.equals(HttpMethod.PATCH)) + { + ApiBuilder.patch(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod)); + } + + if(!allowedMethod.equals(HttpMethod.DELETE)) + { + ApiBuilder.delete(path, (Context c) -> QJavalinApiHandler.return405(c, allowedMethod)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void return405(Context context, HttpMethod allowedMethod) + { + respondWithError(context, HttpStatus.Code.METHOD_NOT_ALLOWED, "This path only supports method: " + allowedMethod, newAPILog(context)); // 405 + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -272,21 +329,26 @@ public class QJavalinApiHandler QJavalinAccessLogger.logStart("apiRunProcess", logPair("process", processMetaData.getName())); Map parameters = new LinkedHashMap<>(); - for(QFieldMetaData inputField : CollectionUtils.nonNullList(apiProcessMetaData.getInputFields())) + + ApiProcessInput input = apiProcessMetaData.getInput(); + if(input != null) { - String value = switch(apiProcessMetaData.getMethod()) + processProcessInputFieldsContainer(context, parameters, input.getQueryStringParams(), Context::queryParam); + processProcessInputFieldsContainer(context, parameters, input.getFormParams(), Context::formParam); + + ApiProcessInputFieldsContainer objectBodyParams = input.getObjectBodyParams(); + if(objectBodyParams != null) { - case GET -> context.queryParam(inputField.getName()); - // todo - other methods (all from a JSON body??) - default -> throw new QException("Http method " + apiLog.getMethod() + " is not yet implemented for reading parameters"); - }; - parameters.put(inputField.getName(), value); + JSONObject jsonObject = new JSONObject(context.body()); + processProcessInputFieldsContainer(context, parameters, objectBodyParams, (ctx, name) -> jsonObject.optString(name, null)); + } } - Map outputRecord = ApiImplementation.runProcess(apiInstanceMetaData, version, apiProcessMetaData.getApiProcessName(), parameters); + HttpApiResponse response = ApiImplementation.runProcess(apiInstanceMetaData, version, apiProcessMetaData.getApiProcessName(), parameters); + context.status(response.getStatusCode().getCode()); QJavalinAccessLogger.logEndSuccess(); - String resultString = toJson(outputRecord); + String resultString = toJson(Objects.requireNonNullElse(response.getResponseBodyObject(), "")); context.result(resultString); storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); } @@ -302,10 +364,22 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private boolean doesProcessSupportAsync(ApiInstanceMetaData apiInstanceMetaData, QProcessMetaData process) + private static void processProcessInputFieldsContainer(Context context, Map parameters, ApiProcessInputFieldsContainer fieldsContainer, BiFunction paramAccessor) { - // todo - implement - return false; + if(fieldsContainer != null) + { + List fields = CollectionUtils.nonNullList(fieldsContainer.getFields()); + ObjectUtils.ifNotNull(fieldsContainer.getRecordIdsField(), fields::add); + for(QFieldMetaData field : fields) + { + String queryParamValue = paramAccessor.apply(context, field.getName()); + if(queryParamValue != null) + { + String backendName = ObjectUtils.requireConditionElse(field.getBackendName(), StringUtils::hasContent, field.getName()); + parameters.put(backendName, queryParamValue); + } + } + } } @@ -313,34 +387,10 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - private String getProcessApiPath(QProcessMetaData process, ApiProcessMetaData apiProcessMetaData, ApiInstanceMetaData apiInstanceMetaData) + private boolean doesProcessSupportAsync(ApiInstanceMetaData apiInstanceMetaData, QProcessMetaData process) { - if(StringUtils.hasContent(apiProcessMetaData.getPath())) - { - return apiProcessMetaData.getPath() + "/" + apiProcessMetaData.getApiProcessName(); - } - else if(StringUtils.hasContent(process.getTableName())) - { - QTableMetaData table = qInstance.getTable(process.getTableName()); - String tablePathPart = table.getName(); - ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); - if(apiTableMetaDataContainer != null) - { - ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApis().get(apiInstanceMetaData.getName()); - if(apiTableMetaData != null) - { - if(StringUtils.hasContent(apiTableMetaData.getApiTableName())) - { - tablePathPart = apiTableMetaData.getApiTableName(); - } - } - } - return tablePathPart + "/" + apiProcessMetaData.getApiProcessName(); - } - else - { - return apiProcessMetaData.getApiProcessName(); - } + // todo - implement + return false; } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/HttpApiResponse.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/HttpApiResponse.java new file mode 100644 index 00000000..5d3b1c66 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/HttpApiResponse.java @@ -0,0 +1,122 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api.model.actions; + + +import java.io.Serializable; +import org.eclipse.jetty.http.HttpStatus; + + +/******************************************************************************* + ** class to contain http api responses. + ** + *******************************************************************************/ +public class HttpApiResponse +{ + private HttpStatus.Code statusCode; + private Serializable responseBodyObject; + + + + /******************************************************************************* + ** Default Constructor + ** + *******************************************************************************/ + public HttpApiResponse() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public HttpApiResponse(HttpStatus.Code statusCode, Serializable responseBodyObject) + { + this.statusCode = statusCode; + this.responseBodyObject = responseBodyObject; + } + + + + /******************************************************************************* + ** Getter for statusCode + *******************************************************************************/ + public HttpStatus.Code getStatusCode() + { + return (this.statusCode); + } + + + + /******************************************************************************* + ** Setter for statusCode + *******************************************************************************/ + public void setStatusCode(HttpStatus.Code statusCode) + { + this.statusCode = statusCode; + } + + + + /******************************************************************************* + ** Fluent setter for statusCode + *******************************************************************************/ + public HttpApiResponse withStatusCode(HttpStatus.Code statusCode) + { + this.statusCode = statusCode; + return (this); + } + + + + /******************************************************************************* + ** Getter for responseBodyObject + *******************************************************************************/ + public Serializable getResponseBodyObject() + { + return (this.responseBodyObject); + } + + + + /******************************************************************************* + ** Setter for responseBodyObject + *******************************************************************************/ + public void setResponseBodyObject(Serializable responseBodyObject) + { + this.responseBodyObject = responseBodyObject; + } + + + + /******************************************************************************* + ** Fluent setter for responseBodyObject + *******************************************************************************/ + public HttpApiResponse withResponseBodyObject(Serializable responseBodyObject) + { + this.responseBodyObject = responseBodyObject; + return (this); + } + +} \ No newline at end of file diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInput.java new file mode 100644 index 00000000..f533b9ae --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInput.java @@ -0,0 +1,130 @@ +package com.kingsrook.qqq.api.model.metadata.processes; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessInput +{ + private ApiProcessInputFieldsContainer queryStringParams; + private ApiProcessInputFieldsContainer formParams; + private ApiProcessInputFieldsContainer recordBodyParams; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String getRecordIdsParamName() + { + if(queryStringParams != null && queryStringParams.getRecordIdsField() != null) + { + return (queryStringParams.getRecordIdsField().getName()); + } + + if(formParams != null && formParams.getRecordIdsField() != null) + { + return (formParams.getRecordIdsField().getName()); + } + + if(recordBodyParams != null && recordBodyParams.getRecordIdsField() != null) + { + return (recordBodyParams.getRecordIdsField().getName()); + } + + return (null); + } + + + + /******************************************************************************* + ** Getter for queryStringParams + *******************************************************************************/ + public ApiProcessInputFieldsContainer getQueryStringParams() + { + return (this.queryStringParams); + } + + + + /******************************************************************************* + ** Setter for queryStringParams + *******************************************************************************/ + public void setQueryStringParams(ApiProcessInputFieldsContainer queryStringParams) + { + this.queryStringParams = queryStringParams; + } + + + + /******************************************************************************* + ** Fluent setter for queryStringParams + *******************************************************************************/ + public ApiProcessInput withQueryStringParams(ApiProcessInputFieldsContainer queryStringParams) + { + this.queryStringParams = queryStringParams; + return (this); + } + + + + /******************************************************************************* + ** Getter for formParams + *******************************************************************************/ + public ApiProcessInputFieldsContainer getFormParams() + { + return (this.formParams); + } + + + + /******************************************************************************* + ** Setter for formParams + *******************************************************************************/ + public void setFormParams(ApiProcessInputFieldsContainer formParams) + { + this.formParams = formParams; + } + + + + /******************************************************************************* + ** Fluent setter for formParams + *******************************************************************************/ + public ApiProcessInput withFormParams(ApiProcessInputFieldsContainer formParams) + { + this.formParams = formParams; + return (this); + } + + + + /******************************************************************************* + ** Getter for recordBodyParams + *******************************************************************************/ + public ApiProcessInputFieldsContainer getObjectBodyParams() + { + return (this.recordBodyParams); + } + + + + /******************************************************************************* + ** Setter for recordBodyParams + *******************************************************************************/ + public void setRecordBodyParams(ApiProcessInputFieldsContainer recordBodyParams) + { + this.recordBodyParams = recordBodyParams; + } + + + + /******************************************************************************* + ** Fluent setter for recordBodyParams + *******************************************************************************/ + public ApiProcessInput withRecordBodyParams(ApiProcessInputFieldsContainer recordBodyParams) + { + this.recordBodyParams = recordBodyParams; + return (this); + } +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInputFieldsContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInputFieldsContainer.java new file mode 100644 index 00000000..7c5b0060 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInputFieldsContainer.java @@ -0,0 +1,116 @@ +package com.kingsrook.qqq.api.model.metadata.processes; + + +import java.util.ArrayList; +import java.util.List; +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.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessInputFieldsContainer +{ + private QFieldMetaData recordIdsField; + private List fields; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public ApiProcessInputFieldsContainer withInferredInputFields(QProcessMetaData processMetaData) + { + fields = new ArrayList<>(); + for(QStepMetaData stepMetaData : CollectionUtils.nonNullList(processMetaData.getStepList())) + { + if(stepMetaData instanceof QFrontendStepMetaData frontendStep) + { + fields.addAll(frontendStep.getInputFields()); + } + } + + return (this); + } + + + + /******************************************************************************* + ** Getter for recordIdsField + *******************************************************************************/ + public QFieldMetaData getRecordIdsField() + { + return (this.recordIdsField); + } + + + + /******************************************************************************* + ** Setter for recordIdsField + *******************************************************************************/ + public void setRecordIdsField(QFieldMetaData recordIdsField) + { + this.recordIdsField = recordIdsField; + } + + + + /******************************************************************************* + ** Fluent setter for recordIdsField + *******************************************************************************/ + public ApiProcessInputFieldsContainer withRecordIdsField(QFieldMetaData recordIdsField) + { + this.recordIdsField = recordIdsField; + return (this); + } + + + + /******************************************************************************* + ** Getter for fields + *******************************************************************************/ + public List getFields() + { + return (this.fields); + } + + + + /******************************************************************************* + ** Setter for fields + *******************************************************************************/ + public void setFields(List fields) + { + this.fields = fields; + } + + + + /******************************************************************************* + ** Fluent setter for fields + *******************************************************************************/ + public ApiProcessInputFieldsContainer withField(QFieldMetaData field) + { + if(this.fields == null) + { + this.fields = new ArrayList<>(); + } + this.fields.add(field); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for fields + *******************************************************************************/ + public ApiProcessInputFieldsContainer withFields(List fields) + { + this.fields = fields; + return (this); + } +} 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 d3920a95..b3cfe92a 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 @@ -22,7 +22,6 @@ package com.kingsrook.qqq.api.model.metadata.processes; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -31,13 +30,13 @@ 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.model.metadata.code.QCodeReference; 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.model.metadata.processes.QProcessMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; 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; /******************************************************************************* @@ -54,51 +53,13 @@ public class ApiProcessMetaData private String path; private HttpMethod method; - private List inputFields; - private List outputFields; + private ApiProcessInput input; + private ApiProcessOutputInterface output; private Map customizers; - /******************************************************************************* - ** - *******************************************************************************/ - public ApiProcessMetaData withInferredInputFields(QProcessMetaData processMetaData) - { - inputFields = new ArrayList<>(); - for(QStepMetaData stepMetaData : CollectionUtils.nonNullList(processMetaData.getStepList())) - { - if(stepMetaData instanceof QFrontendStepMetaData frontendStep) - { - inputFields.addAll(frontendStep.getInputFields()); - } - } - - return (this); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public ApiProcessMetaData withInferredOutputFields(QProcessMetaData processMetaData) - { - outputFields = new ArrayList<>(); - for(QStepMetaData stepMetaData : CollectionUtils.nonNullList(processMetaData.getStepList())) - { - if(stepMetaData instanceof QFrontendStepMetaData frontendStep) - { - outputFields.addAll(frontendStep.getOutputFields()); - } - } - - return (this); - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -119,8 +80,7 @@ public class ApiProcessMetaData /******************************************************************************* ** *******************************************************************************/ - @SuppressWarnings("unchecked") - public void enrich(String apiName, QProcessMetaData process) + public void enrich(QInstanceEnricher qInstanceEnricher, String apiName, QProcessMetaData process) { if(!StringUtils.hasContent(getApiProcessName())) { @@ -129,15 +89,19 @@ public class ApiProcessMetaData if(initialVersion != null) { - /////////////////////////////////////////////////////////////// - // make sure all fields have at least an initial version set // - /////////////////////////////////////////////////////////////// - for(QFieldMetaData field : CollectionUtils.mergeLists(getInputFields(), getOutputFields())) + if(getOutput() instanceof ApiProcessObjectOutput outputObject) { - ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiSupplementalMetaData(apiName, field); - if(apiFieldMetaData.getInitialVersion() == null) + enrichFieldList(qInstanceEnricher, apiName, outputObject.getOutputFields()); + } + + if(input != null) + { + for(ApiProcessInputFieldsContainer fieldsContainer : ListBuilder.of(input.getQueryStringParams(), input.getFormParams(), input.getObjectBodyParams())) { - apiFieldMetaData.setInitialVersion(initialVersion); + if(fieldsContainer != null) + { + enrichFieldList(qInstanceEnricher, apiName, fieldsContainer.getFields()); + } } } } @@ -145,6 +109,25 @@ public class ApiProcessMetaData + /******************************************************************************* + ** + *******************************************************************************/ + private void enrichFieldList(QInstanceEnricher qInstanceEnricher, String apiName, List fields) + { + for(QFieldMetaData field : CollectionUtils.nonNullList(fields)) + { + ApiFieldMetaData apiFieldMetaData = ensureFieldHasApiSupplementalMetaData(apiName, field); + if(apiFieldMetaData.getInitialVersion() == null) + { + apiFieldMetaData.setInitialVersion(initialVersion); + } + + qInstanceEnricher.enrichField(field); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -166,36 +149,6 @@ public class ApiProcessMetaData - /******************************************************************************* - ** Fluent setter for a single outputField - *******************************************************************************/ - public ApiProcessMetaData withOutputField(QFieldMetaData outputField) - { - if(this.outputFields == null) - { - this.outputFields = new ArrayList<>(); - } - this.outputFields.add(outputField); - return (this); - } - - - - /******************************************************************************* - ** Fluent setter for a single inputField - *******************************************************************************/ - public ApiProcessMetaData withInputField(QFieldMetaData inputField) - { - if(this.inputFields == null) - { - this.inputFields = new ArrayList<>(); - } - this.inputFields.add(inputField); - return (this); - } - - - /******************************************************************************* ** Getter for initialVersion *******************************************************************************/ @@ -382,68 +335,6 @@ public class ApiProcessMetaData - /******************************************************************************* - ** Getter for inputFields - *******************************************************************************/ - public List getInputFields() - { - return (this.inputFields); - } - - - - /******************************************************************************* - ** Setter for inputFields - *******************************************************************************/ - public void setInputFields(List inputFields) - { - this.inputFields = inputFields; - } - - - - /******************************************************************************* - ** Fluent setter for inputFields - *******************************************************************************/ - public ApiProcessMetaData withInputFields(List inputFields) - { - this.inputFields = inputFields; - return (this); - } - - - - /******************************************************************************* - ** Getter for outputFields - *******************************************************************************/ - public List getOutputFields() - { - return (this.outputFields); - } - - - - /******************************************************************************* - ** Setter for outputFields - *******************************************************************************/ - public void setOutputFields(List outputFields) - { - this.outputFields = outputFields; - } - - - - /******************************************************************************* - ** Fluent setter for outputFields - *******************************************************************************/ - public ApiProcessMetaData withOutputFields(List outputFields) - { - this.outputFields = outputFields; - return (this); - } - - - /******************************************************************************* ** Getter for customizers *******************************************************************************/ @@ -493,4 +384,66 @@ public class ApiProcessMetaData return (this); } + + + /******************************************************************************* + ** Getter for output + *******************************************************************************/ + public ApiProcessOutputInterface getOutput() + { + return (this.output); + } + + + + /******************************************************************************* + ** Setter for output + *******************************************************************************/ + public void setOutput(ApiProcessOutputInterface output) + { + this.output = output; + } + + + + /******************************************************************************* + ** Fluent setter for output + *******************************************************************************/ + public ApiProcessMetaData withOutput(ApiProcessOutputInterface output) + { + this.output = output; + return (this); + } + + + + /******************************************************************************* + ** Getter for input + *******************************************************************************/ + public ApiProcessInput getInput() + { + return (this.input); + } + + + + /******************************************************************************* + ** Setter for input + *******************************************************************************/ + public void setInput(ApiProcessInput input) + { + this.input = input; + } + + + + /******************************************************************************* + ** Fluent setter for input + *******************************************************************************/ + public ApiProcessMetaData withInput(ApiProcessInput input) + { + this.input = input; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java index 36a2b352..5fe3351a 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.api.model.metadata.processes; import java.util.LinkedHashMap; import java.util.Map; import com.kingsrook.qqq.api.ApiSupplementType; +import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -64,13 +65,13 @@ public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData ** *******************************************************************************/ @Override - public void enrich(QProcessMetaData process) + public void enrich(QInstanceEnricher qInstanceEnricher, QProcessMetaData process) { - super.enrich(process); + super.enrich(qInstanceEnricher, process); for(Map.Entry entry : CollectionUtils.nonNullMap(apis).entrySet()) { - entry.getValue().enrich(entry.getKey(), process); + entry.getValue().enrich(qInstanceEnricher, entry.getKey(), process); } } 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 new file mode 100644 index 00000000..e92556a6 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessObjectOutput.java @@ -0,0 +1,85 @@ +package com.kingsrook.qqq.api.model.metadata.processes; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +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; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessObjectOutput implements ApiProcessOutputInterface +{ + private List outputFields; + + + + /******************************************************************************* + ** + ******************************************************************************/ + @Override + public Serializable getOutputForProcess(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) + { + LinkedHashMap outputMap = new LinkedHashMap<>(); + + for(QFieldMetaData outputField : CollectionUtils.nonNullList(getOutputFields())) + { + outputMap.put(outputField.getName(), runProcessOutput.getValues().get(outputField.getName())); + } + + return (outputMap); + } + + + + /******************************************************************************* + ** Getter for outputFields + *******************************************************************************/ + public List getOutputFields() + { + return (this.outputFields); + } + + + + /******************************************************************************* + ** Setter for outputFields + *******************************************************************************/ + public void setOutputFields(List outputFields) + { + this.outputFields = outputFields; + } + + + + /******************************************************************************* + ** Fluent setter for outputFields + *******************************************************************************/ + public ApiProcessObjectOutput withOutputFields(List outputFields) + { + this.outputFields = outputFields; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for a single outputField + *******************************************************************************/ + public ApiProcessObjectOutput withOutputField(QFieldMetaData outputField) + { + if(this.outputFields == null) + { + this.outputFields = new ArrayList<>(); + } + this.outputFields.add(outputField); + return (this); + } + +} 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 new file mode 100644 index 00000000..476bb072 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessOutputInterface.java @@ -0,0 +1,30 @@ +package com.kingsrook.qqq.api.model.metadata.processes; + + +import java.io.Serializable; +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 org.eclipse.jetty.http.HttpStatus; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface ApiProcessOutputInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + Serializable getOutputForProcess(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException; + + /******************************************************************************* + ** + *******************************************************************************/ + default HttpStatus.Code getSuccessStatusCode(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) + { + return (HttpStatus.Code.OK); + } + +} 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 new file mode 100644 index 00000000..9894f96a --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessSummaryListOutput.java @@ -0,0 +1,138 @@ +package com.kingsrook.qqq.api.model.metadata.processes; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +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.actions.processes.ProcessSummaryFilterLink; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryRecordLink; +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.CollectionUtils; +import org.apache.commons.lang.NotImplementedException; +import org.eclipse.jetty.http.HttpStatus; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessSummaryListOutput implements ApiProcessOutputInterface +{ + private static final QLogger LOG = QLogger.getLogger(ApiProcessSummaryListOutput.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public HttpStatus.Code getSuccessStatusCode(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) + { + List processSummaryLineInterfaces = (List) runProcessOutput.getValues().get("processResults"); + if(processSummaryLineInterfaces.isEmpty()) + { + ////////////////////////////////////////////////////////////////////////// + // if there are no summary lines, all we can return is 204 - no content // + ////////////////////////////////////////////////////////////////////////// + return (HttpStatus.Code.NO_CONTENT); + } + else + { + /////////////////////////////////////////////////////////////////////////////////// + // else if there are summary lines, we'll represent them as a 207 - multi-status // + /////////////////////////////////////////////////////////////////////////////////// + return (HttpStatus.Code.MULTI_STATUS); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Serializable getOutputForProcess(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException + { + try + { + ArrayList apiOutput = new ArrayList<>(); + List processSummaryLineInterfaces = (List) runProcessOutput.getValues().get("processResults"); + for(ProcessSummaryLineInterface processSummaryLineInterface : processSummaryLineInterfaces) + { + if(processSummaryLineInterface instanceof ProcessSummaryLine processSummaryLine) + { + processSummaryLine.setCount(1); + processSummaryLine.prepareForFrontend(true); + + List primaryKeys = processSummaryLine.getPrimaryKeys(); + if(CollectionUtils.nullSafeHasContents(primaryKeys)) + { + for(Serializable primaryKey : primaryKeys) + { + HashMap map = toMap(processSummaryLine); + map.put("id", primaryKey); + apiOutput.add(map); + } + } + else + { + apiOutput.add(toMap(processSummaryLine)); + } + } + else if(processSummaryLineInterface instanceof ProcessSummaryRecordLink processSummaryRecordLink) + { + throw new NotImplementedException("ProcessSummaryRecordLink handling"); + } + else if(processSummaryLineInterface instanceof ProcessSummaryFilterLink processSummaryFilterLink) + { + throw new NotImplementedException("ProcessSummaryFilterLink handling"); + } + else + { + throw new NotImplementedException("Unknown ProcessSummaryLineInterface handling"); + } + } + + return (apiOutput); + } + catch(Exception e) + { + LOG.warn("Error getting api output for process", e); + throw (new QException("Error generating process output", e)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("checkstyle:indentation") + private static HashMap toMap(ProcessSummaryLine processSummaryLine) + { + HashMap map = new HashMap<>(); + HttpStatus.Code code = switch(processSummaryLine.getStatus()) + { + case OK, WARNING, INFO -> HttpStatus.Code.OK; + case ERROR -> HttpStatus.Code.INTERNAL_SERVER_ERROR; + }; + + String messagePrefix = switch(processSummaryLine.getStatus()) + { + case OK, INFO, ERROR -> ""; + case WARNING -> "Warning: "; + }; + + map.put("statusCode", code.getCode()); + map.put("statusText", code.getMessage()); + map.put("message", messagePrefix + processSummaryLine.getMessage()); + + return (map); + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessUtils.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessUtils.java new file mode 100644 index 00000000..dd1a1321 --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessUtils.java @@ -0,0 +1,169 @@ +package com.kingsrook.qqq.api.model.metadata.processes; + + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.api.model.APIVersion; +import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; +import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; +import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; +import com.kingsrook.qqq.backend.core.logging.LogPair; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +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.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.Pair; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import org.apache.commons.lang.BooleanUtils; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ApiProcessUtils +{ + private static final QLogger LOG = QLogger.getLogger(ApiProcessUtils.class); + + private static Map, Map> processApiNameMap = new HashMap<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Pair getProcessMetaDataPair(ApiInstanceMetaData apiInstanceMetaData, String version, String processApiName) throws QNotFoundException + { + QProcessMetaData process = getProcessByApiName(apiInstanceMetaData.getName(), version, processApiName); + LogPair[] logPairs = new LogPair[] { logPair("apiName", apiInstanceMetaData.getName()), logPair("version", version), logPair("processApiName", processApiName) }; + + if(process == null) + { + LOG.info("404 because process is null (processApiName=" + processApiName + ")", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + if(BooleanUtils.isTrue(process.getIsHidden())) + { + LOG.info("404 because process isHidden", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); + if(apiProcessMetaDataContainer == null) + { + LOG.info("404 because process apiProcessMetaDataContainer is null", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiInstanceMetaData.getName()); + if(apiProcessMetaData == null) + { + LOG.info("404 because process apiProcessMetaData is null", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + if(BooleanUtils.isTrue(apiProcessMetaData.getIsExcluded())) + { + LOG.info("404 because process is excluded", logPairs); + throw (new QNotFoundException("Could not find a process named " + processApiName + " in this api.")); + } + + APIVersion requestApiVersion = new APIVersion(version); + List supportedVersions = apiInstanceMetaData.getSupportedVersions(); + if(CollectionUtils.nullSafeIsEmpty(supportedVersions) || !supportedVersions.contains(requestApiVersion)) + { + LOG.info("404 because requested version is not supported", logPairs); + throw (new QNotFoundException(version + " is not a supported version in this api.")); + } + + if(!apiProcessMetaData.getApiVersionRange().includes(requestApiVersion)) + { + LOG.info("404 because process version range does not include requested version", logPairs); + throw (new QNotFoundException(version + " is not a supported version for process " + processApiName + " in this api.")); + } + + return (Pair.of(apiProcessMetaData, process)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QProcessMetaData getProcessByApiName(String apiName, String version, String processApiName) + { + ///////////////////////////////////////////////////////////////////////////////////////////// + // processApiNameMap is a map of (apiName,apiVersion) => Map. // + // that is to say, a 2-level map. The first level is keyed by (apiName,apiVersion) pairs. // + // the second level is keyed by processApiNames. // + ///////////////////////////////////////////////////////////////////////////////////////////// + Pair key = new Pair<>(apiName, version); + if(processApiNameMap.get(key) == null) + { + Map map = new HashMap<>(); + + for(QProcessMetaData process : QContext.getQInstance().getProcesses().values()) + { + ApiProcessMetaDataContainer apiProcessMetaDataContainer = ApiProcessMetaDataContainer.of(process); + if(apiProcessMetaDataContainer != null) + { + ApiProcessMetaData apiProcessMetaData = apiProcessMetaDataContainer.getApiProcessMetaData(apiName); + if(apiProcessMetaData != null) + { + String name = process.getName(); + if(StringUtils.hasContent(apiProcessMetaData.getApiProcessName())) + { + name = apiProcessMetaData.getApiProcessName(); + } + map.put(name, process); + } + } + } + + processApiNameMap.put(key, map); + } + + return (processApiNameMap.get(key).get(processApiName)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String getProcessApiPath(QInstance qInstance, QProcessMetaData process, ApiProcessMetaData apiProcessMetaData, ApiInstanceMetaData apiInstanceMetaData) + { + if(StringUtils.hasContent(apiProcessMetaData.getPath())) + { + return apiProcessMetaData.getPath() + "/" + apiProcessMetaData.getApiProcessName(); + } + else if(StringUtils.hasContent(process.getTableName())) + { + QTableMetaData table = qInstance.getTable(process.getTableName()); + String tablePathPart = table.getName(); + ApiTableMetaDataContainer apiTableMetaDataContainer = ApiTableMetaDataContainer.of(table); + if(apiTableMetaDataContainer != null) + { + ApiTableMetaData apiTableMetaData = apiTableMetaDataContainer.getApis().get(apiInstanceMetaData.getName()); + if(apiTableMetaData != null) + { + if(StringUtils.hasContent(apiTableMetaData.getApiTableName())) + { + tablePathPart = apiTableMetaData.getApiTableName(); + } + } + } + return tablePathPart + "/" + apiProcessMetaData.getApiProcessName(); + } + else + { + return apiProcessMetaData.getApiProcessName(); + } + } + +} 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 fdef3cd5..20ea1005 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 @@ -29,8 +29,12 @@ import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; +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.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; @@ -72,8 +76,10 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.SystemErrorStatusMessage; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaUpdateStep; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; -import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; /******************************************************************************* @@ -89,7 +95,8 @@ public class TestUtils public static final String TABLE_NAME_LINE_ITEM_EXTRINSIC = "orderLineExtrinsic"; public static final String TABLE_NAME_ORDER_EXTRINSIC = "orderExtrinsic"; - public static final String PROCESS_NAME_GET_PERSON_INFO = "getPersonInfo"; + public static final String PROCESS_NAME_GET_PERSON_INFO = "getPersonInfo"; + public static final String PROCESS_NAME_TRANSFORM_PEOPLE = "transformPeople"; public static final String API_NAME = "test-api"; public static final String ALTERNATIVE_API_NAME = "person-api"; @@ -122,6 +129,7 @@ public class TestUtils qInstance.addPossibleValueSource(definePersonPossibleValueSource()); qInstance.addProcess(defineProcessGetPersonInfo()); + qInstance.addProcess(defineProcessTransformPeople()); qInstance.setAuthentication(new Auth0AuthenticationMetaData().withType(QAuthenticationType.FULLY_ANONYMOUS).withName("anonymous")); @@ -214,12 +222,12 @@ public class TestUtils .withApiProcessMetaData(API_NAME, new ApiProcessMetaData() .withInitialVersion(CURRENT_API_VERSION) .withMethod(HttpMethod.GET) - .withInferredInputFields(process) - .withOutputFields(ListBuilder.of( - new QFieldMetaData("density", QFieldType.DECIMAL), - new QFieldMetaData("daysOld", QFieldType.INTEGER), - new QFieldMetaData("nickname", QFieldType.STRING) - )) + .withInput(new ApiProcessInput() + .withQueryStringParams(new ApiProcessInputFieldsContainer().withInferredInputFields(process))) + .withOutput(new ApiProcessObjectOutput() + .withOutputField(new QFieldMetaData("density", QFieldType.DECIMAL)) + .withOutputField(new QFieldMetaData("daysOld", QFieldType.INTEGER)) + .withOutputField(new QFieldMetaData("nickname", QFieldType.STRING))) )); return (process); @@ -227,6 +235,35 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + private static QProcessMetaData defineProcessTransformPeople() + { + QProcessMetaData process = StreamedETLWithFrontendProcess.processMetaDataBuilder() + .withName(PROCESS_NAME_TRANSFORM_PEOPLE) + .withTableName(TABLE_NAME_PERSON) + .withSourceTable(TABLE_NAME_PERSON) + .withDestinationTable(TABLE_NAME_PERSON) + .withMinInputRecords(1) + .withExtractStepClass(ExtractViaQueryStep.class) + .withTransformStepClass(TransformPersonStep.class) + .withLoadStepClass(LoadViaUpdateStep.class) + .getProcessMetaData(); + + process.withSupplementalMetaData(new ApiProcessMetaDataContainer() + .withApiProcessMetaData(API_NAME, new ApiProcessMetaData() + .withInitialVersion(CURRENT_API_VERSION) + .withMethod(HttpMethod.POST) + .withInput(new ApiProcessInput() + .withQueryStringParams(new ApiProcessInputFieldsContainer().withRecordIdsField(new QFieldMetaData("id", QFieldType.STRING)))) + .withOutput(new ApiProcessSummaryListOutput()))); + + return (process); + } + + + /******************************************************************************* ** Define the in-memory backend used in standard tests *******************************************************************************/ diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TransformPersonStep.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TransformPersonStep.java new file mode 100644 index 00000000..6abda809 --- /dev/null +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TransformPersonStep.java @@ -0,0 +1,59 @@ +package com.kingsrook.qqq.api; + + +import java.util.ArrayList; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface; +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.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep; +import com.kingsrook.qqq.backend.core.processes.implementations.general.StandardProcessSummaryLineProducer; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class TransformPersonStep extends AbstractTransformStep +{ + private ProcessSummaryLine okLine = StandardProcessSummaryLineProducer.getOkToUpdateLine(); + private ProcessSummaryLine errorLine = StandardProcessSummaryLineProducer.getErrorLine(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public ArrayList getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen) + { + ArrayList rs = new ArrayList<>(); + okLine.addSelfToListIfAnyCount(rs); + errorLine.addSelfToListIfAnyCount(rs); + return (rs); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + for(QRecord record : runBackendStepInput.getRecords()) + { + Integer id = record.getValueInteger("id"); + if(id % 2 == 0) + { + okLine.incrementCountAndAddPrimaryKey(id); + } + else + { + errorLine.incrementCountAndAddPrimaryKey(id); + } + } + } + +} 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 b7889b1d..3db79362 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 @@ -1443,9 +1443,12 @@ class QJavalinApiHandlerTest extends BaseTest ** *******************************************************************************/ @Test - void testProcess() throws QException + void testGetProcessForObject() throws QException { - HttpResponse response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/getPersonInfo?age=43&partnerPersonId=1&heightInches=72&weightPounds=220&homeTown=Chester").asString(); + HttpResponse response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/getPersonInfo").asString(); + assertErrorResponse(HttpStatus.METHOD_NOT_ALLOWED_405, "This path only supports method: GET", response); + + response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/getPersonInfo?age=43&partnerPersonId=1&heightInches=72&weightPounds=220&homeTown=Chester").asString(); assertEquals(HttpStatus.OK_200, response.getStatus()); JSONObject jsonObject = new JSONObject(response.getBody()); System.out.println(jsonObject.toString(3)); @@ -1453,6 +1456,33 @@ class QJavalinApiHandlerTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPostProcessForProcessSummaryList() throws QException + { + insertSimpsons(); + + HttpResponse response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/transformPeople").asString(); + assertErrorResponse(HttpStatus.METHOD_NOT_ALLOWED_405, "This path only supports method: POST", response); + + response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/transformPeople").asString(); + assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Records to run through this process were not specified", response); + + response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/transformPeople?id=999").asString(); + assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus()); + assertEquals("", response.getBody()); + + response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/transformPeople?id=1,2,3").asString(); + assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus()); + JSONArray jsonArray = new JSONArray(response.getBody()); + assertEquals(3, jsonArray.length()); + System.out.println(jsonArray.toString(3)); + } + + + /******************************************************************************* ** *******************************************************************************/ From 19ee5bcb23e96dc3c147d9c4909e3ea65e8f5fc0 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 14 Jun 2023 11:53:43 -0500 Subject: [PATCH 4/9] Checkpoint, WIP on processes in api - mostly done --- .../core/actions/async/AsyncJobManager.java | 36 +- .../core/instances/QInstanceValidator.java | 10 +- .../processes/ProcessSummaryFilterLink.java | 31 ++ .../actions/processes/ProcessSummaryLine.java | 13 +- .../processes/ProcessSummaryRecordLink.java | 31 ++ .../QSupplementalProcessMetaData.java | 14 + .../StreamedETLPreviewStep.java | 39 +- .../backend/core/utils/ExceptionUtils.java | 37 ++ .../core/utils/ExceptionUtilsTest.java | 37 ++ .../qqq/api/actions/ApiImplementation.java | 197 +++++++-- .../actions/GenerateOpenApiSpecAction.java | 379 ++++++++++++++---- .../qqq/api/javalin/QJavalinApiHandler.java | 99 +++-- .../metadata/fields/ApiFieldMetaData.java | 99 +++++ .../fields/ApiFieldMetaDataContainer.java | 50 ++- .../metadata/processes/ApiProcessInput.java | 90 +++++ .../ApiProcessInputFieldsContainer.java | 51 ++- .../processes/ApiProcessMetaData.java | 131 ++++++ .../ApiProcessMetaDataContainer.java | 54 ++- .../processes/ApiProcessObjectOutput.java | 158 ++++++++ .../processes/ApiProcessOutputInterface.java | 36 +- .../ApiProcessSummaryListOutput.java | 154 ++++++- .../metadata/processes/ApiProcessUtils.java | 21 + .../PostRunApiProcessCustomizer.java | 21 + .../processes/PreRunApiProcessCustomizer.java | 21 + .../model/openapi/ExampleWithSingleValue.java | 11 +- .../qqq/api/model/openapi/Parameter.java | 32 ++ .../qqq/api/model/openapi/Schema.java | 22 + .../qqq/api/utils/ApiScriptUtils.java | 51 +++ .../resources/rapidoc/rapidoc-container.html | 2 +- .../java/com/kingsrook/qqq/api/TestUtils.java | 8 +- .../api/javalin/QJavalinApiHandlerTest.java | 31 +- .../qqq/api/utils/ApiScriptUtilsTest.java | 85 ++++ 32 files changed, 1875 insertions(+), 176 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobManager.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobManager.java index 2c16afd0..14b9fad6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobManager.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobManager.java @@ -39,6 +39,7 @@ import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider; import com.kingsrook.qqq.backend.core.state.StateProviderInterface; import com.kingsrook.qqq.backend.core.state.StateType; import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.apache.logging.log4j.Level; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -50,6 +51,7 @@ public class AsyncJobManager { private static final QLogger LOG = QLogger.getLogger(AsyncJobManager.class); + private String forcedJobUUID = null; /******************************************************************************* @@ -69,7 +71,8 @@ public class AsyncJobManager *******************************************************************************/ public T startJob(String jobName, long timeout, TimeUnit timeUnit, AsyncJob asyncJob) throws JobGoingAsyncException, QException { - UUIDAndTypeStateKey uuidAndTypeStateKey = new UUIDAndTypeStateKey(UUID.randomUUID(), StateType.ASYNC_JOB_STATUS); + UUID jobUUID = StringUtils.hasContent(forcedJobUUID) ? UUID.fromString(forcedJobUUID) : UUID.randomUUID(); + UUIDAndTypeStateKey uuidAndTypeStateKey = new UUIDAndTypeStateKey(jobUUID, StateType.ASYNC_JOB_STATUS); AsyncJobStatus asyncJobStatus = new AsyncJobStatus(); asyncJobStatus.setState(AsyncJobState.RUNNING); getStateProvider().put(uuidAndTypeStateKey, asyncJobStatus); @@ -205,4 +208,35 @@ public class AsyncJobManager jobStatus.ifPresent(asyncJobStatus -> asyncJobStatus.setCancelRequested(true)); } + + + /******************************************************************************* + ** Getter for forcedJobUUID + *******************************************************************************/ + public String getForcedJobUUID() + { + return (this.forcedJobUUID); + } + + + + /******************************************************************************* + ** Setter for forcedJobUUID + *******************************************************************************/ + public void setForcedJobUUID(String forcedJobUUID) + { + this.forcedJobUUID = forcedJobUUID; + } + + + + /******************************************************************************* + ** Fluent setter for forcedJobUUID + *******************************************************************************/ + public AsyncJobManager withForcedJobUUID(String forcedJobUUID) + { + this.forcedJobUUID = forcedJobUUID; + return (this); + } + } 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 efa03b2e..b9bb2d47 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 @@ -63,6 +63,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection; 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; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField; @@ -1226,6 +1227,11 @@ public class QInstanceValidator } } + for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values()) + { + supplementalProcessMetaData.validate(qInstance, process, this); + } + }); } } @@ -1703,7 +1709,7 @@ public class QInstanceValidator ** But if it throws, add the provided message to the list of errors (and return false, ** e.g., in case you need to stop evaluating rules to avoid exceptions). *******************************************************************************/ - private boolean assertNoException(UnsafeLambda unsafeLambda, String message) + public boolean assertNoException(UnsafeLambda unsafeLambda, String message) { try { @@ -1736,7 +1742,7 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private void warn(String message) + public void warn(String message) { if(printWarnings) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryFilterLink.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryFilterLink.java index b13e69e3..eee92ff2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryFilterLink.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryFilterLink.java @@ -22,8 +22,10 @@ package com.kingsrook.qqq.backend.core.model.actions.processes; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.kingsrook.qqq.backend.core.logging.LogPair; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -76,6 +78,35 @@ public class ProcessSummaryFilterLink implements ProcessSummaryLineInterface + /******************************************************************************* + ** + *******************************************************************************/ + @JsonIgnore + public String getFullText() + { + StringBuilder rs = new StringBuilder(); + + if(StringUtils.hasContent(linkPreText)) + { + rs.append(linkPreText).append(" "); + } + + if(StringUtils.hasContent(linkText)) + { + rs.append(linkText).append(" "); + } + + if(StringUtils.hasContent(linkPostText)) + { + rs.append(linkPostText).append(" "); + } + + rs.deleteCharAt(rs.length() - 1); + return (rs.toString()); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLine.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLine.java index 39ead861..786318e0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLine.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLine.java @@ -26,6 +26,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.logging.LogPair; +import com.kingsrook.qqq.backend.core.utils.ObjectUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -395,15 +396,19 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface { if(count != null) { + String baseMessage; if(count.equals(1)) { - setMessage((isPast ? getSingularPastMessage() : getSingularFutureMessage()) - + (messageSuffix == null ? "" : messageSuffix)); + baseMessage = isPast ? getSingularPastMessage() : getSingularFutureMessage(); } else { - setMessage((isPast ? getPluralPastMessage() : getPluralFutureMessage()) - + (messageSuffix == null ? "" : messageSuffix)); + baseMessage = isPast ? getPluralPastMessage() : getPluralFutureMessage(); + } + + if(StringUtils.hasContent(baseMessage)) + { + setMessage(baseMessage + ObjectUtils.requireConditionElse(messageSuffix, StringUtils::hasContent, "")); } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryRecordLink.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryRecordLink.java index fa8ae313..50e2c378 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryRecordLink.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryRecordLink.java @@ -23,7 +23,9 @@ package com.kingsrook.qqq.backend.core.model.actions.processes; import java.io.Serializable; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.kingsrook.qqq.backend.core.logging.LogPair; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -64,6 +66,35 @@ public class ProcessSummaryRecordLink implements ProcessSummaryLineInterface + /******************************************************************************* + ** + *******************************************************************************/ + @JsonIgnore + public String getFullText() + { + StringBuilder rs = new StringBuilder(); + + if(StringUtils.hasContent(linkPreText)) + { + rs.append(linkPreText).append(" "); + } + + if(StringUtils.hasContent(linkText)) + { + rs.append(linkText).append(" "); + } + + if(StringUtils.hasContent(linkPostText)) + { + rs.append(linkPostText).append(" "); + } + + rs.deleteCharAt(rs.length() - 1); + return (rs.toString()); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java index 5a478053..0a5dce8b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QSupplementalProcessMetaData.java @@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.model.metadata.processes; 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; /******************************************************************************* @@ -75,4 +77,16 @@ public abstract class QSupplementalProcessMetaData // noop in base class // //////////////////////// } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void validate(QInstance qInstance, QProcessMetaData process, QInstanceValidator qInstanceValidator) + { + //////////////////////// + // noop in base class // + //////////////////////// + } } 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 99045d3a..4921c9ce 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 @@ -27,6 +27,7 @@ import java.util.List; import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; +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.actions.processes.RunBackendStepInput; @@ -71,11 +72,14 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe return; } - if(runBackendStepInput.getFrontendStepBehavior() != null && runBackendStepInput.getFrontendStepBehavior().equals(RunProcessInput.FrontendStepBehavior.SKIP)) - { - LOG.debug("Skipping preview because frontend behavior is [" + RunProcessInput.FrontendStepBehavior.SKIP + "]."); - return; - } + ////////////////////////////// + // set up the extract steps // + ////////////////////////////// + AbstractExtractStep extractStep = getExtractStep(runBackendStepInput); + RecordPipe recordPipe = new RecordPipe(); + extractStep.setLimit(limit); + extractStep.setRecordPipe(recordPipe); + extractStep.preRun(runBackendStepInput, runBackendStepOutput); ///////////////////////////////////////////////////////////////// // if we're running inside an automation, then skip this step. // @@ -86,17 +90,26 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe return; } - ////////////////////////////////////////// - // set up the extract & transform steps // - ////////////////////////////////////////// - AbstractExtractStep extractStep = getExtractStep(runBackendStepInput); - RecordPipe recordPipe = new RecordPipe(); - extractStep.setLimit(limit); - extractStep.setRecordPipe(recordPipe); - extractStep.preRun(runBackendStepInput, runBackendStepOutput); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if skipping frontend steps, skip this action - // + // but, if inside an (ideally, only async) API call, at least do the count, so status calls can get x of y status // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(RunProcessInput.FrontendStepBehavior.SKIP.equals(runBackendStepInput.getFrontendStepBehavior())) + { + if(QContext.getQSession().getValue("apiVersion") != null) + { + countRecords(runBackendStepInput, runBackendStepOutput, extractStep); + } + + LOG.debug("Skipping preview because frontend behavior is [" + RunProcessInput.FrontendStepBehavior.SKIP + "]."); + return; + } countRecords(runBackendStepInput, runBackendStepOutput, extractStep); + ////////////////////////////// + // setup the transform step // + ////////////////////////////// AbstractTransformStep transformStep = getTransformStep(runBackendStepInput); transformStep.preRun(runBackendStepInput, runBackendStepOutput); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtils.java index c4d71714..b0c0817e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtils.java @@ -22,7 +22,9 @@ package com.kingsrook.qqq.backend.core.utils; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; @@ -88,4 +90,39 @@ public class ExceptionUtils return (root); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String concatenateMessagesFromChain(Exception exception) + { + if(exception == null) + { + return (null); + } + + List messages = new ArrayList<>(); + Throwable root = exception; + Set seen = new HashSet<>(); + + do + { + if(StringUtils.hasContent(root.getMessage())) + { + messages.add(root.getMessage()); + } + else + { + messages.add(root.getClass().getSimpleName()); + } + + seen.add(root); + root = root.getCause(); + } + while(root != null && !seen.contains(root)); + + return (StringUtils.join("; ", messages)); + } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtilsTest.java index c92d4fe5..4b68f28b 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ExceptionUtilsTest.java @@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; @@ -88,6 +89,33 @@ class ExceptionUtilsTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testConcatenateMessagesFromChain() + { + assertNull(ExceptionUtils.concatenateMessagesFromChain(null)); + assertEquals("QException", ExceptionUtils.concatenateMessagesFromChain(new QException((String) null))); + assertEquals("QException", ExceptionUtils.concatenateMessagesFromChain(new QException(""))); + assertEquals("foo; bar", ExceptionUtils.concatenateMessagesFromChain(new QException("foo", new QException("bar")))); + assertEquals("foo; QException; bar", ExceptionUtils.concatenateMessagesFromChain(new QException("foo", new QException(null, new QException("bar"))))); + + MyException selfCaused = new MyException("selfCaused"); + selfCaused.setCause(selfCaused); + assertEquals("selfCaused", ExceptionUtils.concatenateMessagesFromChain(selfCaused)); + + MyException cycle1 = new MyException("cycle1"); + MyException cycle2 = new MyException("cycle2"); + cycle1.setCause(cycle2); + cycle2.setCause(cycle1); + + assertEquals("cycle1; cycle2", ExceptionUtils.concatenateMessagesFromChain(cycle1)); + assertEquals("cycle2; cycle1", ExceptionUtils.concatenateMessagesFromChain(cycle2)); + } + + + /******************************************************************************* ** Test exception class - lets you set the cause, easier to create a loop. *******************************************************************************/ @@ -97,6 +125,9 @@ class ExceptionUtilsTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ public MyException(String message) { super(message); @@ -104,6 +135,9 @@ class ExceptionUtilsTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ public MyException(Throwable cause) { super(cause); @@ -111,6 +145,9 @@ class ExceptionUtilsTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ public void setCause(Throwable cause) { myCause = cause; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java index 8a2f7dcd..315d9ed4 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java @@ -30,8 +30,10 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.concurrent.TimeUnit; import com.kingsrook.qqq.api.javalin.QBadRequestException; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.actions.HttpApiResponse; @@ -47,6 +49,10 @@ import com.kingsrook.qqq.api.model.metadata.processes.PostRunApiProcessCustomize import com.kingsrook.qqq.api.model.metadata.processes.PreRunApiProcessCustomizer; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; +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.async.JobGoingAsyncException; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; @@ -63,6 +69,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.logging.LogPair; 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.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource; @@ -96,6 +103,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.QStatusMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.CouldNotFindQueryFilterForExtractStepException; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.Pair; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -119,7 +127,7 @@ public class ApiImplementation /////////////////////////////////////////////////////////////////// // key: Pair, value: Map metaData> // /////////////////////////////////////////////////////////////////// - private static Map, Map> tableApiNameMap = new HashMap<>(); + private static Map, Map> tableApiNameMap = new HashMap<>(); @@ -306,7 +314,7 @@ public class ApiImplementation } else { - throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join(" \n", badRequestMessages))); + throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join("\n", badRequestMessages))); } } @@ -946,24 +954,27 @@ public class ApiImplementation processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getQueryStringParams()); processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getFormParams()); processProcessInputFields(paramMap, badRequestMessages, runProcessInput, apiProcessInput.getObjectBodyParams()); + + if(apiProcessInput.getBodyField() != null) + { + processSingleProcessInputField(apiProcessInput.getBodyField(), paramMap, badRequestMessages, runProcessInput); + } } //////////////////////////////////////// // get records for process, if needed // //////////////////////////////////////// - if(process.getMinInputRecords() != null && process.getMinInputRecords() > 0) + // if(process.getMinInputRecords() != null && process.getMinInputRecords() > 0) + if(apiProcessInput != null && apiProcessInput.getRecordIdsParamName() != null) { - if(apiProcessInput != null && apiProcessInput.getRecordIdsParamName() != null) + String idParam = apiProcessInput.getRecordIdsParamName(); + if(StringUtils.hasContent(idParam) && StringUtils.hasContent(paramMap.get(idParam))) { - String idParam = apiProcessInput.getRecordIdsParamName(); - if(StringUtils.hasContent(idParam) && StringUtils.hasContent(paramMap.get(idParam))) - { - String[] ids = paramMap.get(idParam).split(","); + String[] ids = paramMap.get(idParam).split(","); - QTableMetaData table = QContext.getQInstance().getTable(process.getTableName()); - QQueryFilter filter = new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), IN, Arrays.asList(ids))); - runProcessInput.setCallback(getCallback(filter)); - } + QTableMetaData table = QContext.getQInstance().getTable(process.getTableName()); + QQueryFilter filter = new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), IN, Arrays.asList(ids))); + runProcessInput.setCallback(getCallback(filter)); } } @@ -978,7 +989,7 @@ public class ApiImplementation } else { - throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join(" \n", badRequestMessages))); + throw (new QBadRequestException("Request failed with " + badRequestMessages.size() + " reasons: " + StringUtils.join("\n", badRequestMessages))); } } @@ -992,6 +1003,44 @@ public class ApiImplementation preRunCustomizer.preApiRun(runProcessInput); } + boolean async = false; + if(ApiProcessMetaData.AsyncMode.ALWAYS.equals(apiProcessMetaData.getAsyncMode()) + || (ApiProcessMetaData.AsyncMode.OPTIONAL.equals(apiProcessMetaData.getAsyncMode()) && "true".equalsIgnoreCase(paramMap.get("async")))) + { + async = true; + } + + if(async) + { + try + { + ////////////////////////////////////////////////////////////////////////////////////////////////////// + // note - in other implementations, the process gets its own UUID (for process state to be stashed) // + // and the job gets its own (where we check in on running/complete). // + // but in this implementation, we want to just pass back one UUID to the caller, so make the job // + // manager use the process's uuid as the job uuid, and all will be revealed! // + ////////////////////////////////////////////////////////////////////////////////////////////////////// + // todo? to help w/ StreamedETLPreview "should i count?" runProcessInput.setIsAsync(true); + new AsyncJobManager().withForcedJobUUID(processUUID).startJob(processName, 0, TimeUnit.MILLISECONDS, (callback) -> + { + runProcessInput.setAsyncJobCallback(callback); + return (new RunProcessAction().execute(runProcessInput)); + }); + } + catch(JobGoingAsyncException jgae) + { + LinkedHashMap response = new LinkedHashMap<>(); + response.put("jobId", jgae.getJobUUID()); + return (new HttpApiResponse(HttpStatus.Code.ACCEPTED, response)); + } + + //////////////////////////////////////////////////////////////////////////////////////// + // passing 0 as the timeout to startJob *should* make it always throw the JGAE. But, // + // in case it didn't, we don't have a uuid to return to the caller, so that's a fail. // + //////////////////////////////////////////////////////////////////////////////////////// + throw (new QException("Error starting asynchronous job - no job id was returned.")); + } + ///////////////////// // run the process // ///////////////////// @@ -1006,10 +1055,26 @@ public class ApiImplementation { throw (new QBadRequestException("Records to run through this process were not specified.")); } + catch(Exception e) + { + String concatenation = ExceptionUtils.concatenateMessagesFromChain(e); + throw (new QException(concatenation, e)); + } - ///////////////////////////////////////// + return (buildResponseAfterProcess(apiProcessMetaData, runProcessInput, runProcessOutput)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static HttpApiResponse buildResponseAfterProcess(ApiProcessMetaData apiProcessMetaData, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws QException + { + ////////////////////////////////////////// // run post-customizer, if there is one // - ///////////////////////////////////////// + ////////////////////////////////////////// + Map customizers = apiProcessMetaData.getCustomizers(); if(customizers != null && customizers.containsKey(ApiProcessCustomizers.POST_RUN.getRole())) { PostRunApiProcessCustomizer postRunCustomizer = QCodeLoader.getAdHoc(PostRunApiProcessCustomizer.class, customizers.get(ApiProcessCustomizers.POST_RUN.getRole())); @@ -1044,16 +1109,101 @@ public class ApiImplementation for(QFieldMetaData inputField : CollectionUtils.nonNullList(fieldsContainer.getFields())) { - String value = paramMap.get(inputField.getName()); - if(!StringUtils.hasContent(value) && inputField.getIsRequired()) + processSingleProcessInputField(inputField, paramMap, badRequestMessages, runProcessInput); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void processSingleProcessInputField(QFieldMetaData inputField, Map paramMap, List badRequestMessages, RunProcessInput runProcessInput) + { + String value = paramMap.get(inputField.getName()); + + if(!StringUtils.hasContent(value) && inputField.getDefaultValue() != null) + { + value = ValueUtils.getValueAsString(inputField.getDefaultValue()); + } + + if(!StringUtils.hasContent(value) && inputField.getIsRequired()) + { + badRequestMessages.add("Missing value for required input field " + inputField.getName()); + return; + } + + // todo - types? + + runProcessInput.addValue(inputField.getName(), value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static HttpApiResponse getProcessStatus(ApiInstanceMetaData apiInstanceMetaData, String version, String apiProcessName, String jobUUID) throws QException + { + Optional optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID); + if(optionalJobStatus.isEmpty()) + { + throw (new QException("Could not find status of process job: " + jobUUID)); + } + + 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(jobUUID); + if(processState.isPresent()) { - badRequestMessages.add("Missing value for required input field " + inputField.getName()); - continue; + RunProcessOutput runProcessOutput = new RunProcessOutput(processState.get()); + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.seedFromProcessState(processState.get()); + + Pair pair = ApiProcessUtils.getProcessMetaDataPair(apiInstanceMetaData, version, apiProcessName); + + ApiProcessMetaData apiProcessMetaData = pair.getA(); + return (buildResponseAfterProcess(apiProcessMetaData, runProcessInput, runProcessOutput)); } - - // todo - types? - - runProcessInput.addValue(inputField.getName(), value); + else + { + throw (new QException("Could not find results for completed of process job: " + jobUUID)); + } + } + 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) + { + throw (new QException(jobStatus.getCaughtException())); + } + else + { + throw (new QException("Job failed with an unspecified error.")); + } + } + else + { + LinkedHashMap response = new LinkedHashMap<>(); + response.put("jobId", jobUUID); + response.put("message", jobStatus.getMessage()); + if(jobStatus.getCurrent() != null && jobStatus.getTotal() != null) + { + response.put("current", jobStatus.getCurrent()); + response.put("total", jobStatus.getTotal()); + } + return (new HttpApiResponse(HttpStatus.Code.ACCEPTED, response)); } } @@ -1330,7 +1480,6 @@ public class ApiImplementation - /******************************************************************************* ** *******************************************************************************/ 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 a37efeac..d38e5386 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 @@ -41,10 +41,12 @@ import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.api.model.metadata.ApiOperation; import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData; +import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer; 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.metadata.processes.ApiProcessOutputInterface; 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; @@ -90,6 +92,7 @@ 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 io.javalin.http.ContentType; import io.javalin.http.HttpStatus; import org.apache.commons.lang.BooleanUtils; @@ -296,13 +299,13 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction tagList = new ArrayList<>(); + Set usedProcessNames = new HashSet<>(); + /////////////////// // foreach table // /////////////////// - List tables = new ArrayList<>(qInstance.getTables().values()); - Set usedProcessNames = new HashSet<>(); - tables.sort(Comparator.comparing(t -> ObjectUtils.requireNonNullElse(t.getLabel(), t.getName(), ""))); - for(QTableMetaData table : tables) + for(QTableMetaData table : qInstance.getTables().values()) { String tableName = table.getName(); @@ -385,6 +388,10 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version).withApiName(apiName)).getFields(); + tagList.add(new Tag() + .withName(tableLabel) + .withDescription("Operations on the " + tableLabel + " table.")); + /////////////////////////////// // permissions for the table // /////////////////////////////// @@ -416,13 +423,6 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction pair : CollectionUtils.nonNullList(apiProcessMetaDataList)) { ApiProcessMetaData apiProcessMetaData = pair.getA(); QProcessMetaData processMetaData = pair.getB(); - String processApiPath = ApiProcessUtils.getProcessApiPath(qInstance, processMetaData, apiProcessMetaData, apiInstanceMetaData); - Path path = generateProcessSpecPathObject(apiInstanceMetaData, apiProcessMetaData, processMetaData, ListBuilder.of(tableLabel)); - openAPI.getPaths().put(basePath + processApiPath, path); + addProcessEndpoints(qInstance, apiInstanceMetaData, basePath, openAPI, tableProcessesTag, apiProcessMetaData, processMetaData); usedProcessNames.add(processMetaData.getName()); } @@ -716,19 +728,35 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction> processesNotUnderTables = getProcessesNotUnderTables(apiName, apiVersion, usedProcessNames); - for(Pair pair : CollectionUtils.nonNullList(processesNotUnderTables)) + if(input.getTableName() == null) { - ApiProcessMetaData apiProcessMetaData = pair.getA(); - QProcessMetaData processMetaData = pair.getB(); + List> processesNotUnderTables = getProcessesNotUnderTables(apiName, apiVersion, usedProcessNames); + for(Pair pair : CollectionUtils.nonNullList(processesNotUnderTables)) + { + ApiProcessMetaData apiProcessMetaData = pair.getA(); + QProcessMetaData processMetaData = pair.getB(); - String processApiPath = ApiProcessUtils.getProcessApiPath(qInstance, processMetaData, apiProcessMetaData, apiInstanceMetaData); - Path path = generateProcessSpecPathObject(apiInstanceMetaData, apiProcessMetaData, processMetaData, ListBuilder.of(processMetaData.getLabel())); - openAPI.getPaths().put(basePath + processApiPath, path); + String tag = processMetaData.getLabel(); + if(doesProcessLabelNeedTheWordProcessAppended(tag)) + { + tag += " process"; + } + tagList.add(new Tag() + .withName(tag) + .withDescription(tag)); - usedProcessNames.add(processMetaData.getName()); + addProcessEndpoints(qInstance, apiInstanceMetaData, basePath, openAPI, tag, apiProcessMetaData, processMetaData); + + usedProcessNames.add(processMetaData.getName()); + } } + tagList.sort(Comparator.comparing(Tag::getName)); + openAPI.setTags(tagList); + + //////////////////////////// + // define standard errors // + //////////////////////////// componentResponses.put("error" + HttpStatus.BAD_REQUEST.getCode(), buildStandardErrorResponse("Bad Request. Some portion of the request's content was not acceptable to the server. See error message in body for details.", "Parameter id should be given an integer value, but received string: \"Foo\"")); componentResponses.put("error" + HttpStatus.UNAUTHORIZED.getCode(), buildStandardErrorResponse("Unauthorized. The required authentication credentials were missing or invalid.", "The required authentication credentials were missing or invalid.")); componentResponses.put("error" + HttpStatus.FORBIDDEN.getCode(), buildStandardErrorResponse("Forbidden. You do not have permission to access the requested resource.", "You do not have permission to access the requested resource.")); @@ -750,6 +778,41 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction tags) { + String description = apiProcessMetaData.getDescription(); + if(!StringUtils.hasContent(description)) + { + description = "Run the " + processMetaData.getLabel(); + if(doesProcessLabelNeedTheWordProcessAppended(description)) + { + description += " process"; + } + } + + //////////////////////////////// + // start defining the process // + //////////////////////////////// Method methodForProcess = new Method() .withOperationId(apiProcessMetaData.getApiProcessName()) .withTags(tags) - .withSummary(processMetaData.getLabel()) // todo - add optional summary to meta data - .withDescription("Run the process named " + processMetaData.getLabel())// todo - add optional description to meta data, .withDescription() - .withSecurity(getSecurity(apiInstanceMetaData, "todo - process name")); + .withSummary(ObjectUtils.requireConditionElse(apiProcessMetaData.getSummary(), StringUtils::hasContent, processMetaData.getLabel())) + .withDescription(description) + .withSecurity(getSecurity(apiInstanceMetaData, processMetaData.getName())); + //////////////////////////////// + // add inputs for the process // + //////////////////////////////// List parameters = new ArrayList<>(); ApiProcessInput apiProcessInput = apiProcessMetaData.getInput(); if(apiProcessInput != null) @@ -806,17 +885,67 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction responses = new LinkedHashMap<>(); - // todo methodForProcess.withResponse(); + ApiProcessOutputInterface output = apiProcessMetaData.getOutput(); + if(!ApiProcessMetaData.AsyncMode.ALWAYS.equals(apiProcessMetaData.getAsyncMode())) + { + responses.putAll(output.getSpecResponses(apiInstanceMetaData.getName())); + } + if(!ApiProcessMetaData.AsyncMode.NEVER.equals(apiProcessMetaData.getAsyncMode())) + { + responses.put(HttpStatus.ACCEPTED.getCode(), new Response() + .withDescription("The process has been started asynchronously. You can call back later to check its status.") + .withContent(MapBuilder.of(ContentType.JSON, new Content() + .withSchema(new Schema() + .withType("object") + .withProperties(MapBuilder.of( + "jobId", new Schema().withType("string").withFormat("uuid").withDescription("id of the asynchronous job") + )) + ) + )) + ); + } - methodForProcess.withResponse(HttpStatus.OK.getCode(), new Response() - .withDescription("Successfully ran the process") - .withContent(MapBuilder.of("application/json", new Content()))); + responses.putAll(buildStandardErrorResponses(apiInstanceMetaData)); + methodForProcess.withResponses(responses); @SuppressWarnings("checkstyle:indentation") Path path = switch(apiProcessMetaData.getMethod()) @@ -847,6 +996,100 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction tags) + { + //////////////////////////////// + // start defining the process // + //////////////////////////////// + Method methodForProcess = new Method() + .withOperationId("getStatusFor" + StringUtils.ucFirst(apiProcessMetaData.getApiProcessName())) + .withTags(tags) + .withSummary("Get Status of Job: " + ObjectUtils.requireConditionElse(apiProcessMetaData.getSummary(), StringUtils::hasContent, processMetaData.getLabel())) + .withDescription("Get the status for a previous asynchronous call to the process named " + processMetaData.getLabel()) + .withSecurity(getSecurity(apiInstanceMetaData, processMetaData.getName())); + + //////////////////////////////////////////////////////// + // add the async input for optionally-async processes // + //////////////////////////////////////////////////////// + methodForProcess.setParameters(ListBuilder.of(new Parameter() + .withName("jobId") + .withIn("path") + .withRequired(true) + .withDescription("Id of the job, as returned by the API call that started it.") + .withSchema(new Schema().withType("string").withFormat("uuid")) + )); + + ////////////////////////////////// + // build all possible responses // + ////////////////////////////////// + Map responses = new LinkedHashMap<>(); + responses.put(HttpStatus.ACCEPTED.getCode(), new Response() + .withDescription("The process is still running. You can call back later to get its final status.") + .withContent(MapBuilder.of(ContentType.JSON, new Content() + .withSchema(new Schema() + .withType("object") + .withProperties(MapBuilder.of( + "jobId", new Schema().withType("string").withFormat("uuid").withDescription("id of the asynchronous job") + // todo - status?? + )) + ) + )) + ); + + ApiProcessOutputInterface output = apiProcessMetaData.getOutput(); + responses.putAll(output.getSpecResponses(apiInstanceMetaData.getName())); + responses.putAll(buildStandardErrorResponses(apiInstanceMetaData)); + + methodForProcess.withResponses(responses); + return (new Path().withGet(methodForProcess)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Parameter processFieldToParameter(ApiInstanceMetaData apiInstanceMetaData, QFieldMetaData field) + { + ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.ofOrNew(field); + ApiFieldMetaData apiFieldMetaData = apiFieldMetaDataContainer.getApiFieldMetaData(apiInstanceMetaData.getName()); + + String description = "Value for the " + field.getLabel() + " field."; + if(field.getDefaultValue() != null) + { + description += " Default value is " + field.getDefaultValue() + ", if not given."; + } + if(apiFieldMetaData != null && StringUtils.hasContent(apiFieldMetaData.getDescription())) + { + description = apiFieldMetaData.getDescription(); + } + + Parameter parameter = new Parameter() + .withName(field.getName()) + .withDescription(description) + .withRequired(field.getIsRequired()) + .withSchema(new Schema().withType(getFieldType(field))); + + if(apiFieldMetaData != null) + { + if(apiFieldMetaData.getExample() != null) + { + parameter.withExample(apiFieldMetaData.getExample()); + } + else if(apiFieldMetaData.getExamples() != null) + { + parameter.withExamples(apiFieldMetaData.getExamples()); + } + } + + return (parameter); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -1337,7 +1580,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction getProcessStatus(context, apiInstanceMetaData)); + ApiBuilder.get(path + "/status/{jobId}", context -> getProcessStatus(context, process, apiProcessMetaData, apiInstanceMetaData)); } } } @@ -304,16 +305,6 @@ public class QJavalinApiHandler - /******************************************************************************* - ** - *******************************************************************************/ - private void getProcessStatus(Context context, ApiInstanceMetaData apiInstanceMetaData) - { - // todo! - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -328,9 +319,11 @@ public class QJavalinApiHandler setupSession(context, null, version, apiInstanceMetaData); QJavalinAccessLogger.logStart("apiRunProcess", logPair("process", processMetaData.getName())); + //////////////////////////////////////////////////// + // process inputs into map for api implementation // + //////////////////////////////////////////////////// Map parameters = new LinkedHashMap<>(); - - ApiProcessInput input = apiProcessMetaData.getInput(); + ApiProcessInput input = apiProcessMetaData.getInput(); if(input != null) { processProcessInputFieldsContainer(context, parameters, input.getQueryStringParams(), Context::queryParam); @@ -342,12 +335,62 @@ public class QJavalinApiHandler JSONObject jsonObject = new JSONObject(context.body()); processProcessInputFieldsContainer(context, parameters, objectBodyParams, (ctx, name) -> jsonObject.optString(name, null)); } + + if(input.getBodyField() != null) + { + parameters.put(input.getBodyField().getName(), context.body()); + } } - HttpApiResponse response = ApiImplementation.runProcess(apiInstanceMetaData, version, apiProcessMetaData.getApiProcessName(), parameters); - context.status(response.getStatusCode().getCode()); + if(ApiProcessMetaData.AsyncMode.OPTIONAL.equals(apiProcessMetaData.getAsyncMode())) + { + parameters.put("async", context.queryParam("async")); + } + ///////////////////// + // run the process // + ///////////////////// + HttpApiResponse response = ApiImplementation.runProcess(apiInstanceMetaData, version, apiProcessMetaData.getApiProcessName(), parameters); + + ////////////////// + // log & return // + ////////////////// QJavalinAccessLogger.logEndSuccess(); + context.status(response.getStatusCode().getCode()); + String resultString = toJson(Objects.requireNonNullElse(response.getResponseBodyObject(), "")); + context.result(resultString); + storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); + } + catch(Exception e) + { + QJavalinAccessLogger.logEndFail(e); + handleException(context, e, apiLog); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void getProcessStatus(Context context, QProcessMetaData processMetaData, ApiProcessMetaData apiProcessMetaData, ApiInstanceMetaData apiInstanceMetaData) + { + String version = context.pathParam("version"); + APILog apiLog = newAPILog(context); + + try + { + setupSession(context, null, version, apiInstanceMetaData); + QJavalinAccessLogger.logStart("apiGetProcessStatus", logPair("process", processMetaData.getName())); + + String jobId = context.pathParam("jobId"); + HttpApiResponse response = ApiImplementation.getProcessStatus(apiInstanceMetaData, version, apiProcessMetaData.getApiProcessName(), jobId); + + ////////////////// + // log & return // + ////////////////// + QJavalinAccessLogger.logEndSuccess(); + context.status(response.getStatusCode().getCode()); String resultString = toJson(Objects.requireNonNullElse(response.getResponseBodyObject(), "")); context.result(resultString); storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); @@ -384,17 +427,6 @@ public class QJavalinApiHandler - /******************************************************************************* - ** - *******************************************************************************/ - private boolean doesProcessSupportAsync(ApiInstanceMetaData apiInstanceMetaData, QProcessMetaData process) - { - // todo - implement - return false; - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -1348,7 +1380,12 @@ public class QJavalinApiHandler // default exception handling // //////////////////////////////// LOG.warn("Exception in javalin request", e); - respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, e.getClass().getSimpleName() + " (" + e.getMessage() + ")", apiLog); // 500 + String message = e.getMessage(); + if(!StringUtils.hasContent(message)) + { + message = e.getClass().getSimpleName(); + } + respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, message, apiLog); // 500 return; } } 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 fabc02f0..d03c7fa4 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 @@ -22,6 +22,8 @@ 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.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -35,10 +37,14 @@ public class ApiFieldMetaData private String finalVersion; private String apiFieldName; + private String description; private Boolean isExcluded; private String replacedByFieldName; + private Example example; + private Map examples; + /******************************************************************************* @@ -214,4 +220,97 @@ public class ApiFieldMetaData return (this); } + + + /******************************************************************************* + ** Getter for description + *******************************************************************************/ + public String getDescription() + { + return (this.description); + } + + + + /******************************************************************************* + ** Setter for description + *******************************************************************************/ + public void setDescription(String description) + { + this.description = description; + } + + + + /******************************************************************************* + ** Fluent setter for description + *******************************************************************************/ + public ApiFieldMetaData withDescription(String description) + { + this.description = description; + return (this); + } + + + + /******************************************************************************* + ** Getter for example + *******************************************************************************/ + public Example getExample() + { + return (this.example); + } + + + + /******************************************************************************* + ** Setter for example + *******************************************************************************/ + public void setExample(Example example) + { + this.example = example; + } + + + + /******************************************************************************* + ** Fluent setter for example + *******************************************************************************/ + public ApiFieldMetaData withExample(Example example) + { + this.example = example; + 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 ApiFieldMetaData withExamples(Map examples) + { + this.examples = examples; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java index cc6e62d2..fa9fe94a 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaDataContainer.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.api.model.metadata.fields; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import com.kingsrook.qqq.api.ApiSupplementType; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QSupplementalFieldMetaData; @@ -36,6 +37,7 @@ public class ApiFieldMetaDataContainer extends QSupplementalFieldMetaData { private Map apis; + private ApiFieldMetaData defaultApiFieldMetaData; /******************************************************************************* @@ -59,6 +61,17 @@ public class ApiFieldMetaDataContainer extends QSupplementalFieldMetaData + /******************************************************************************* + ** either get the container attached to a field - or a new one - note - the new + ** one will NOT be attached to the field!! + *******************************************************************************/ + public static ApiFieldMetaDataContainer ofOrNew(QFieldMetaData field) + { + return (Objects.requireNonNullElseGet(of(field), ApiFieldMetaDataContainer::new)); + } + + + /******************************************************************************* ** Getter for apis *******************************************************************************/ @@ -70,16 +83,16 @@ public class ApiFieldMetaDataContainer extends QSupplementalFieldMetaData /******************************************************************************* - ** Getter for apis + ** Getter the apiFieldMetaData for a specific api, or the container's default *******************************************************************************/ public ApiFieldMetaData getApiFieldMetaData(String apiName) { if(this.apis == null) { - return (null); + return (defaultApiFieldMetaData); } - return (this.apis.get(apiName)); + return (this.apis.getOrDefault(apiName, defaultApiFieldMetaData)); } @@ -118,4 +131,35 @@ public class ApiFieldMetaDataContainer extends QSupplementalFieldMetaData return (this); } + + + /******************************************************************************* + ** Getter for defaultApiFieldMetaData + *******************************************************************************/ + public ApiFieldMetaData getDefaultApiFieldMetaData() + { + return (this.defaultApiFieldMetaData); + } + + + + /******************************************************************************* + ** Setter for defaultApiFieldMetaData + *******************************************************************************/ + public void setDefaultApiFieldMetaData(ApiFieldMetaData defaultApiFieldMetaData) + { + this.defaultApiFieldMetaData = defaultApiFieldMetaData; + } + + + + /******************************************************************************* + ** Fluent setter for defaultApiFieldMetaData + *******************************************************************************/ + public ApiFieldMetaDataContainer withDefaultApiFieldMetaData(ApiFieldMetaData defaultApiFieldMetaData) + { + this.defaultApiFieldMetaData = defaultApiFieldMetaData; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInput.java index f533b9ae..83b05691 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInput.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInput.java @@ -1,6 +1,30 @@ +/* + * 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.api.model.metadata.processes; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; + + /******************************************************************************* ** *******************************************************************************/ @@ -10,6 +34,9 @@ public class ApiProcessInput private ApiProcessInputFieldsContainer formParams; private ApiProcessInputFieldsContainer recordBodyParams; + private QFieldMetaData bodyField; + private String bodyFieldContentType; + /******************************************************************************* @@ -127,4 +154,67 @@ public class ApiProcessInput this.recordBodyParams = recordBodyParams; return (this); } + + + + /******************************************************************************* + ** Getter for bodyField + *******************************************************************************/ + public QFieldMetaData getBodyField() + { + return (this.bodyField); + } + + + + /******************************************************************************* + ** Setter for bodyField + *******************************************************************************/ + public void setBodyField(QFieldMetaData bodyField) + { + this.bodyField = bodyField; + } + + + + /******************************************************************************* + ** Fluent setter for bodyField + *******************************************************************************/ + public ApiProcessInput withBodyField(QFieldMetaData bodyField) + { + this.bodyField = bodyField; + return (this); + } + + + + /******************************************************************************* + ** Getter for bodyFieldContentType + *******************************************************************************/ + public String getBodyFieldContentType() + { + return (this.bodyFieldContentType); + } + + + + /******************************************************************************* + ** Setter for bodyFieldContentType + *******************************************************************************/ + public void setBodyFieldContentType(String bodyFieldContentType) + { + this.bodyFieldContentType = bodyFieldContentType; + } + + + + /******************************************************************************* + ** Fluent setter for bodyFieldContentType + *******************************************************************************/ + public ApiProcessInput withBodyFieldContentType(String bodyFieldContentType) + { + this.bodyFieldContentType = bodyFieldContentType; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInputFieldsContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInputFieldsContainer.java index 7c5b0060..67e2958f 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInputFieldsContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessInputFieldsContainer.java @@ -1,7 +1,30 @@ +/* + * 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.api.model.metadata.processes; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; @@ -21,16 +44,38 @@ public class ApiProcessInputFieldsContainer /******************************************************************************* - ** + ** find all input fields in frontend steps of the process, and add them as fields + ** in this container. *******************************************************************************/ public ApiProcessInputFieldsContainer withInferredInputFields(QProcessMetaData processMetaData) { - fields = new ArrayList<>(); + return (withInferredInputFieldsExcluding(processMetaData, Collections.emptySet())); + } + + + + /******************************************************************************* + ** find all input fields in frontend steps of the process, and add them as fields + ** in this container, unless they're in the collection to exclude. + *******************************************************************************/ + public ApiProcessInputFieldsContainer withInferredInputFieldsExcluding(QProcessMetaData processMetaData, Collection minusFieldNames) + { + if(fields == null) + { + fields = new ArrayList<>(); + } + for(QStepMetaData stepMetaData : CollectionUtils.nonNullList(processMetaData.getStepList())) { if(stepMetaData instanceof QFrontendStepMetaData frontendStep) { - fields.addAll(frontendStep.getInputFields()); + for(QFieldMetaData inputField : frontendStep.getInputFields()) + { + if(minusFieldNames != null && !minusFieldNames.contains(inputField.getName())) + { + fields.add(inputField); + } + } } } 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 b3cfe92a..9cb32dbd 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 @@ -31,12 +31,15 @@ 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; 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.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 org.apache.commons.lang.BooleanUtils; /******************************************************************************* @@ -52,6 +55,10 @@ public class ApiProcessMetaData private String path; private HttpMethod method; + private String summary; + private String description; + + private AsyncMode asyncMode = AsyncMode.OPTIONAL; private ApiProcessInput input; private ApiProcessOutputInterface output; @@ -60,6 +67,15 @@ public class ApiProcessMetaData + public enum AsyncMode + { + NEVER, + OPTIONAL, + ALWAYS + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -103,6 +119,10 @@ public class ApiProcessMetaData enrichFieldList(qInstanceEnricher, apiName, fieldsContainer.getFields()); } } + if(input.getBodyField() != null) + { + enrichFieldList(qInstanceEnricher, apiName, List.of(input.getBodyField())); + } } } } @@ -446,4 +466,115 @@ public class ApiProcessMetaData return (this); } + + + /******************************************************************************* + ** Getter for summary + *******************************************************************************/ + public String getSummary() + { + return (this.summary); + } + + + + /******************************************************************************* + ** Setter for summary + *******************************************************************************/ + public void setSummary(String summary) + { + this.summary = summary; + } + + + + /******************************************************************************* + ** Fluent setter for summary + *******************************************************************************/ + public ApiProcessMetaData withSummary(String summary) + { + this.summary = summary; + return (this); + } + + + + /******************************************************************************* + ** Getter for description + *******************************************************************************/ + public String getDescription() + { + return (this.description); + } + + + + /******************************************************************************* + ** Setter for description + *******************************************************************************/ + public void setDescription(String description) + { + this.description = description; + } + + + + /******************************************************************************* + ** Fluent setter for description + *******************************************************************************/ + public ApiProcessMetaData withDescription(String description) + { + this.description = description; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void validate(QInstance qInstance, QProcessMetaData process, QInstanceValidator qInstanceValidator, String apiName) + { + if(BooleanUtils.isTrue(getIsExcluded())) + { + ///////////////////////////////////////////////// + // no validation needed for excluded processes // + ///////////////////////////////////////////////// + return; + } + + qInstanceValidator.assertCondition(getMethod() != null, "Missing a method for api process meta data for process: " + process.getName() + ", apiName: " + apiName); + } + + + + /******************************************************************************* + ** Getter for asyncMode + *******************************************************************************/ + public AsyncMode getAsyncMode() + { + return (this.asyncMode); + } + + + + /******************************************************************************* + ** Setter for asyncMode + *******************************************************************************/ + public void setAsyncMode(AsyncMode asyncMode) + { + this.asyncMode = asyncMode; + } + + + + /******************************************************************************* + ** Fluent setter for asyncMode + *******************************************************************************/ + public ApiProcessMetaData withAsyncMode(AsyncMode asyncMode) + { + this.asyncMode = asyncMode; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java index 5fe3351a..496e8ea0 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessMetaDataContainer.java @@ -26,6 +26,8 @@ import java.util.LinkedHashMap; import java.util.Map; import com.kingsrook.qqq.api.ApiSupplementType; 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; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -46,7 +48,7 @@ public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData *******************************************************************************/ public ApiProcessMetaDataContainer() { - setType("api"); + setType(ApiSupplementType.NAME); } @@ -61,6 +63,23 @@ public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData + /******************************************************************************* + ** either get the container attached to a field - or create a new one and attach + ** it to the field, and return that. + *******************************************************************************/ + public static ApiProcessMetaDataContainer ofOrWithNew(QProcessMetaData process) + { + ApiProcessMetaDataContainer apiProcessMetaDataContainer = (ApiProcessMetaDataContainer) process.getSupplementalMetaData(ApiSupplementType.NAME); + if(apiProcessMetaDataContainer == null) + { + apiProcessMetaDataContainer = new ApiProcessMetaDataContainer(); + process.withSupplementalMetaData(apiProcessMetaDataContainer); + } + return (apiProcessMetaDataContainer); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -77,6 +96,22 @@ public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void validate(QInstance qInstance, QProcessMetaData process, QInstanceValidator qInstanceValidator) + { + super.validate(qInstance, process, qInstanceValidator); + + for(Map.Entry entry : CollectionUtils.nonNullMap(apis).entrySet()) + { + entry.getValue().validate(qInstance, process, qInstanceValidator, entry.getKey()); + } + } + + + /******************************************************************************* ** Getter for apis *******************************************************************************/ @@ -102,6 +137,22 @@ public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData + /******************************************************************************* + ** + *******************************************************************************/ + public ApiProcessMetaData getApiProcessMetaDataOrWithNew(String apiName) + { + ApiProcessMetaData apiProcessMetaData = getApiProcessMetaData(apiName); + if(apiProcessMetaData == null) + { + apiProcessMetaData = new ApiProcessMetaData(); + withApiProcessMetaData(apiName, apiProcessMetaData); + } + return (apiProcessMetaData); + } + + + /******************************************************************************* ** Setter for apis *******************************************************************************/ @@ -135,5 +186,4 @@ public class ApiProcessMetaDataContainer extends QSupplementalProcessMetaData this.apis.put(apiName, apiProcessMetaData); return (this); } - } 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 e92556a6..20cf7453 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 @@ -1,3 +1,24 @@ +/* + * 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.api.model.metadata.processes; @@ -5,10 +26,25 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +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; 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 io.javalin.http.ContentType; +import org.eclipse.jetty.http.HttpStatus; /******************************************************************************* @@ -18,6 +54,66 @@ public class ApiProcessObjectOutput implements ApiProcessOutputInterface { private List outputFields; + private String responseDescription; + private HttpStatus.Code successResponseCode; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public HttpStatus.Code getSuccessStatusCode(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) + { + return (HttpStatus.Code.OK); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Map getSpecResponses(String apiName) + { + Map properties = new LinkedHashMap<>(); + for(QFieldMetaData outputField : CollectionUtils.nonNullList(outputFields)) + { + ApiFieldMetaDataContainer apiFieldMetaDataContainer = ApiFieldMetaDataContainer.ofOrNew(outputField); + ApiFieldMetaData apiFieldMetaData = apiFieldMetaDataContainer.getApiFieldMetaData(apiName); + + Object example = null; + if(apiFieldMetaData != null) + { + if(apiFieldMetaData.getExample() instanceof ExampleWithSingleValue exampleWithSingleValue) + { + example = exampleWithSingleValue.getValue(); + } + else if(apiFieldMetaData.getExample() instanceof ExampleWithListValue exampleWithListValue) + { + example = exampleWithListValue.getValue(); + } + } + + properties.put(outputField.getName(), new Schema() + .withDescription(apiFieldMetaData == null ? null : apiFieldMetaData.getDescription()) + .withExample(example) + .withNullable(!outputField.getIsRequired()) + .withType(GenerateOpenApiSpecAction.getFieldType(outputField)) + ); + } + + return (MapBuilder.of( + Objects.requireNonNullElse(successResponseCode, HttpStatus.Code.OK).getCode(), + new Response() + .withDescription(ObjectUtils.requireConditionElse(responseDescription, StringUtils::hasContent, "Process has been successfully executed.")) + .withContent(MapBuilder.of(ContentType.JSON, new Content() + .withSchema(new Schema() + .withType("object") + .withProperties(properties)))) + )); + } + /******************************************************************************* @@ -82,4 +178,66 @@ public class ApiProcessObjectOutput implements ApiProcessOutputInterface return (this); } + + + /******************************************************************************* + ** Getter for responseDescription + *******************************************************************************/ + public String getResponseDescription() + { + return (this.responseDescription); + } + + + + /******************************************************************************* + ** Setter for responseDescription + *******************************************************************************/ + public void setResponseDescription(String responseDescription) + { + this.responseDescription = responseDescription; + } + + + + /******************************************************************************* + ** Fluent setter for responseDescription + *******************************************************************************/ + public ApiProcessObjectOutput withResponseDescription(String responseDescription) + { + this.responseDescription = responseDescription; + return (this); + } + + + + /******************************************************************************* + ** Getter for successResponseCode + *******************************************************************************/ + public HttpStatus.Code getSuccessResponseCode() + { + return (this.successResponseCode); + } + + + + /******************************************************************************* + ** Setter for successResponseCode + *******************************************************************************/ + public void setSuccessResponseCode(HttpStatus.Code successResponseCode) + { + this.successResponseCode = successResponseCode; + } + + + + /******************************************************************************* + ** Fluent setter for successResponseCode + *******************************************************************************/ + public ApiProcessObjectOutput withSuccessResponseCode(HttpStatus.Code successResponseCode) + { + this.successResponseCode = successResponseCode; + return (this); + } + } 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 476bb072..6e430aa4 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 @@ -1,10 +1,34 @@ +/* + * 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.api.model.metadata.processes; import java.io.Serializable; +import java.util.Map; +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 org.eclipse.jetty.http.HttpStatus; @@ -24,7 +48,17 @@ public interface ApiProcessOutputInterface *******************************************************************************/ default HttpStatus.Code getSuccessStatusCode(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) { - return (HttpStatus.Code.OK); + return (HttpStatus.Code.NO_CONTENT); } + /******************************************************************************* + ** + *******************************************************************************/ + default Map getSpecResponses(String apiName) + { + return (MapBuilder.of( + HttpStatus.Code.NO_CONTENT.getCode(), new Response() + .withDescription("Process has been successfully executed.") + )); + } } 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 9894f96a..847e4b8c 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 @@ -1,10 +1,36 @@ +/* + * 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.api.model.metadata.processes; import java.io.Serializable; import java.util.ArrayList; 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; @@ -14,6 +40,9 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryReco 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.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; +import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; +import io.javalin.http.ContentType; import org.apache.commons.lang.NotImplementedException; import org.eclipse.jetty.http.HttpStatus; @@ -52,6 +81,51 @@ public class ApiProcessSummaryListOutput implements ApiProcessOutputInterface + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Map getSpecResponses(String apiName) + { + Map propertiesFor207Object = new LinkedHashMap<>(); + propertiesFor207Object.put("id", new Schema().withType("integer").withDescription("Id of the record whose status is being described in the object")); + propertiesFor207Object.put("statusCode", new Schema().withType("integer").withDescription("HTTP Status code indicating the success or failure of the process on this record")); + propertiesFor207Object.put("statusText", new Schema().withType("string").withDescription("HTTP Status text indicating the success or failure of the process on this record")); + propertiesFor207Object.put("message", new Schema().withType("string").withDescription("Additional descriptive information about the result of the process on this record.")); + + List exampleFor207Object = ListBuilder.of(MapBuilder.of(LinkedHashMap::new) + .with("id", 42) + .with("statusCode", io.javalin.http.HttpStatus.OK.getCode()) + .with("statusText", io.javalin.http.HttpStatus.OK.getMessage()) + .with("message", "record was processed successfully.") + .build(), + MapBuilder.of(LinkedHashMap::new) + .with("id", 47) + .with("statusCode", io.javalin.http.HttpStatus.BAD_REQUEST.getCode()) + .with("statusText", io.javalin.http.HttpStatus.BAD_REQUEST.getMessage()) + .with("message", "error executing process on record.") + .build()); + + return MapBuilder.of( + HttpStatus.Code.MULTI_STATUS.getCode(), new Response() + .withDescription("For each input record, an object describing its status may be returned.") + .withContent(MapBuilder.of(ContentType.JSON, new Content() + .withSchema(new Schema() + .withType("array") + .withItems(new Schema() + .withType("object") + .withProperties(propertiesFor207Object)) + .withExample(exampleFor207Object) + ) + )), + + HttpStatus.Code.NO_CONTENT.getCode(), new Response() + .withDescription("If no records were found, there may be no content in the response.") + ); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -67,7 +141,7 @@ public class ApiProcessSummaryListOutput implements ApiProcessOutputInterface if(processSummaryLineInterface instanceof ProcessSummaryLine processSummaryLine) { processSummaryLine.setCount(1); - processSummaryLine.prepareForFrontend(true); + processSummaryLine.pickMessage(true); List primaryKeys = processSummaryLine.getPrimaryKeys(); if(CollectionUtils.nullSafeHasContents(primaryKeys)) @@ -86,11 +160,11 @@ public class ApiProcessSummaryListOutput implements ApiProcessOutputInterface } else if(processSummaryLineInterface instanceof ProcessSummaryRecordLink processSummaryRecordLink) { - throw new NotImplementedException("ProcessSummaryRecordLink handling"); + apiOutput.add(toMap(processSummaryRecordLink)); } else if(processSummaryLineInterface instanceof ProcessSummaryFilterLink processSummaryFilterLink) { - throw new NotImplementedException("ProcessSummaryFilterLink handling"); + apiOutput.add(toMap(processSummaryFilterLink)); } else { @@ -112,27 +186,81 @@ public class ApiProcessSummaryListOutput implements ApiProcessOutputInterface /******************************************************************************* ** *******************************************************************************/ - @SuppressWarnings("checkstyle:indentation") + private HashMap toMap(ProcessSummaryFilterLink processSummaryFilterLink) + { + HashMap map = initResultMapForProcessSummaryLine(processSummaryFilterLink); + + String messagePrefix = getResultMapMessagePrefix(processSummaryFilterLink); + map.put("message", messagePrefix + processSummaryFilterLink.getFullText()); + + return (map); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private HashMap toMap(ProcessSummaryRecordLink processSummaryRecordLink) + { + HashMap map = initResultMapForProcessSummaryLine(processSummaryRecordLink); + + String messagePrefix = getResultMapMessagePrefix(processSummaryRecordLink); + map.put("message", messagePrefix + processSummaryRecordLink.getFullText()); + + return (map); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ private static HashMap toMap(ProcessSummaryLine processSummaryLine) + { + HashMap map = initResultMapForProcessSummaryLine(processSummaryLine); + + String messagePrefix = getResultMapMessagePrefix(processSummaryLine); + map.put("message", messagePrefix + processSummaryLine.getMessage()); + + return (map); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static String getResultMapMessagePrefix(ProcessSummaryLineInterface processSummaryLine) + { + @SuppressWarnings("checkstyle:indentation") + String messagePrefix = switch(processSummaryLine.getStatus()) + { + case OK, INFO, ERROR -> ""; + case WARNING -> "Warning: "; + }; + return messagePrefix; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static HashMap initResultMapForProcessSummaryLine(ProcessSummaryLineInterface processSummaryLine) { HashMap map = new HashMap<>(); + + @SuppressWarnings("checkstyle:indentation") HttpStatus.Code code = switch(processSummaryLine.getStatus()) { case OK, WARNING, INFO -> HttpStatus.Code.OK; case ERROR -> HttpStatus.Code.INTERNAL_SERVER_ERROR; }; - String messagePrefix = switch(processSummaryLine.getStatus()) - { - case OK, INFO, ERROR -> ""; - case WARNING -> "Warning: "; - }; - map.put("statusCode", code.getCode()); map.put("statusText", code.getMessage()); - map.put("message", messagePrefix + processSummaryLine.getMessage()); - - return (map); + return map; } } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessUtils.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessUtils.java index dd1a1321..b69797e4 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessUtils.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/ApiProcessUtils.java @@ -1,3 +1,24 @@ +/* + * 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.api.model.metadata.processes; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java index 06ba64f8..e9fca81a 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PostRunApiProcessCustomizer.java @@ -1,3 +1,24 @@ +/* + * 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.api.model.metadata.processes; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java index e1836708..a463af66 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/processes/PreRunApiProcessCustomizer.java @@ -1,3 +1,24 @@ +/* + * 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.api.model.metadata.processes; diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithSingleValue.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithSingleValue.java index e365e16b..79ebab6c 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithSingleValue.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/ExampleWithSingleValue.java @@ -22,12 +22,15 @@ package com.kingsrook.qqq.api.model.openapi; +import java.io.Serializable; + + /******************************************************************************* ** *******************************************************************************/ public class ExampleWithSingleValue extends Example { - private String value; + private Serializable value; @@ -46,7 +49,7 @@ public class ExampleWithSingleValue extends Example /******************************************************************************* ** Getter for value *******************************************************************************/ - public String getValue() + public Serializable getValue() { return (this.value); } @@ -56,7 +59,7 @@ public class ExampleWithSingleValue extends Example /******************************************************************************* ** Setter for value *******************************************************************************/ - public void setValue(String value) + public void setValue(Serializable value) { this.value = value; } @@ -66,7 +69,7 @@ public class ExampleWithSingleValue extends Example /******************************************************************************* ** Fluent setter for value *******************************************************************************/ - public ExampleWithSingleValue withValue(String value) + public ExampleWithSingleValue withValue(Serializable value) { this.value = value; return (this); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Parameter.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Parameter.java index 4077f25b..9d43451f 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Parameter.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Parameter.java @@ -37,6 +37,7 @@ public class Parameter private Schema schema; private Boolean explode; private Map examples; + private Example example; @@ -255,4 +256,35 @@ public class Parameter return (this); } + + + /******************************************************************************* + ** Getter for example + *******************************************************************************/ + public Example getExample() + { + return (this.example); + } + + + + /******************************************************************************* + ** Setter for examplee + *******************************************************************************/ + public void setExample(Example example) + { + this.example = example; + } + + + + /******************************************************************************* + ** Fluent setter for examplee + *******************************************************************************/ + public Parameter withExample(Example example) + { + this.example = example; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java index 228bae49..3f858881 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.api.model.openapi; +import java.math.BigDecimal; import java.util.List; import java.util.Map; import com.fasterxml.jackson.annotation.JsonGetter; @@ -191,6 +192,27 @@ public class Schema + /******************************************************************************* + ** Setter for example + *******************************************************************************/ + public void setExample(BigDecimal example) + { + this.example = example; + } + + + + /******************************************************************************* + ** Fluent setter for example + *******************************************************************************/ + public Schema withExample(Object example) + { + this.example = example; + return (this); + } + + + /******************************************************************************* ** Fluent setter for example *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java index f06585d2..48056e27 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java @@ -30,11 +30,15 @@ import java.util.Map; import com.kingsrook.qqq.api.actions.ApiImplementation; import com.kingsrook.qqq.api.actions.QRecordApiAdapter; import com.kingsrook.qqq.api.model.APIVersion; +import com.kingsrook.qqq.api.model.actions.HttpApiResponse; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import org.json.JSONObject; /******************************************************************************* @@ -225,6 +229,53 @@ public class ApiScriptUtils implements Serializable + /******************************************************************************* + ** + *******************************************************************************/ + public Serializable runProcess(String processApiName) throws QException + { + return (runProcess(processApiName, null)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Serializable runProcess(String processApiName, Object params) throws QException + { + validateApiNameAndVersion("runProcess(" + processApiName + ")"); + + Map paramMap = new LinkedHashMap<>(); + String paramsString = ValueUtils.getValueAsString(params); + if(StringUtils.hasContent(paramsString)) + { + JSONObject paramsJSON = new JSONObject(paramsString); + for(String fieldName : paramsJSON.keySet()) + { + paramMap.put(fieldName, paramsJSON.optString(fieldName)); + } + } + + HttpApiResponse httpApiResponse = ApiImplementation.runProcess(getApiInstanceMetaData(), apiVersion, processApiName, paramMap); + return (httpApiResponse.getResponseBodyObject()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Serializable getProcessStatus(String processApiName, String jobId) throws QException + { + validateApiNameAndVersion("getProcessStatus(" + processApiName + ")"); + + HttpApiResponse httpApiResponse = ApiImplementation.getProcessStatus(getApiInstanceMetaData(), apiVersion, processApiName, jobId); + return (httpApiResponse.getResponseBodyObject()); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/resources/rapidoc/rapidoc-container.html b/qqq-middleware-api/src/main/resources/rapidoc/rapidoc-container.html index 8be1775d..540a5a24 100644 --- a/qqq-middleware-api/src/main/resources/rapidoc/rapidoc-container.html +++ b/qqq-middleware-api/src/main/resources/rapidoc/rapidoc-container.html @@ -38,7 +38,7 @@ show-header="false" allow-spec-file-download="true" primary-color="{primaryColor}" - sort-endpoints-by="method" + sort-endpoints-by="none" allow-authentication="true" persist-auth="true" render-style="focused" 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 20ea1005..fea81122 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 @@ -189,11 +189,11 @@ public class TestUtils .withLabel("Person Info Input") .withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM)) - .withFormField(new QFieldMetaData("age", QFieldType.INTEGER)) + .withFormField(new QFieldMetaData("age", QFieldType.INTEGER).withIsRequired(true)) .withFormField(new QFieldMetaData("partnerPersonId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_PERSON)) - .withFormField(new QFieldMetaData("heightInches", QFieldType.DECIMAL)) - .withFormField(new QFieldMetaData("weightPounds", QFieldType.INTEGER)) - .withFormField(new QFieldMetaData("homeTown", QFieldType.STRING)) + .withFormField(new QFieldMetaData("heightInches", QFieldType.DECIMAL).withIsRequired(true)) + .withFormField(new QFieldMetaData("weightPounds", QFieldType.INTEGER).withIsRequired(true)) + .withFormField(new QFieldMetaData("homeTown", QFieldType.STRING).withIsRequired(true)) .withComponent(new NoCodeWidgetFrontendComponentMetaData() 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 3db79362..c3d3e339 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 @@ -26,6 +26,7 @@ import java.time.LocalDate; import java.time.Month; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import com.kingsrook.qqq.api.BaseTest; import com.kingsrook.qqq.api.TestUtils; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; @@ -50,6 +51,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.modules.authentication.implementations.FullyAnonymousAuthenticationModule; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; import io.javalin.apibuilder.EndpointGroup; @@ -67,6 +69,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -1448,10 +1451,12 @@ class QJavalinApiHandlerTest extends BaseTest HttpResponse response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/getPersonInfo").asString(); assertErrorResponse(HttpStatus.METHOD_NOT_ALLOWED_405, "This path only supports method: GET", response); + response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/getPersonInfo").asString(); + assertErrorResponse(HttpStatus.BAD_REQUEST_400, "Request failed with 4 reasons: Missing value for required input field", response); + response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/getPersonInfo?age=43&partnerPersonId=1&heightInches=72&weightPounds=220&homeTown=Chester").asString(); assertEquals(HttpStatus.OK_200, response.getStatus()); JSONObject jsonObject = new JSONObject(response.getBody()); - System.out.println(jsonObject.toString(3)); } @@ -1478,11 +1483,33 @@ class QJavalinApiHandlerTest extends BaseTest assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus()); JSONArray jsonArray = new JSONArray(response.getBody()); assertEquals(3, jsonArray.length()); - System.out.println(jsonArray.toString(3)); } + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAsyncProcessAndGetStatus() throws QException + { + insertSimpsons(); + + HttpResponse response = Unirest.post(BASE_URL + "/api/" + VERSION + "/person/transformPeople?id=1,2,3&async=true").asString(); + assertEquals(HttpStatus.ACCEPTED_202, response.getStatus()); + JSONObject acceptedJSON = new JSONObject(response.getBody()); + String jobId = acceptedJSON.getString("jobId"); + assertNotNull(jobId); + + SleepUtils.sleep(100, TimeUnit.MILLISECONDS); + + response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/transformPeople/status/" + jobId).asString(); + assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus()); + JSONArray jsonArray = new JSONArray(response.getBody()); + assertEquals(3, jsonArray.length()); + } + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java index ec4cfb2b..fdc06e63 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/utils/ApiScriptUtilsTest.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.api.utils; import java.io.Serializable; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import com.kingsrook.qqq.api.BaseTest; import com.kingsrook.qqq.api.TestUtils; import com.kingsrook.qqq.api.javalin.QBadRequestException; @@ -33,9 +34,12 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.junit.jupiter.api.Test; import static com.kingsrook.qqq.api.TestUtils.insertSimpsons; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -262,6 +266,87 @@ class ApiScriptUtilsTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetProcessForObject() throws QException + { + ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils(); + + assertThatThrownBy(() -> apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_GET_PERSON_INFO)) + .isInstanceOf(QBadRequestException.class) + .hasMessageContaining("Request failed with 4 reasons: Missing value for required input field"); + + Object result = apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_GET_PERSON_INFO, """ + {"age": 43, "partnerPersonId": 1, "heightInches": 72, "weightPounds": 220, "homeTown": "Chester"} + """); + + assertThat(result).isInstanceOf(Map.class); + Map resultMap = (Map) result; + assertEquals(15695, resultMap.get("daysOld")); + assertEquals("Guy from Chester", resultMap.get("nickname")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPostProcessForProcessSummaryList() throws QException + { + insertSimpsons(); + + ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils(); + + assertThatThrownBy(() -> apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_TRANSFORM_PEOPLE, null)) + .isInstanceOf(QBadRequestException.class) + .hasMessageContaining("Records to run through this process were not specified"); + + Serializable emptyResult = apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_TRANSFORM_PEOPLE, JsonUtils.toJson(Map.of("id", 999))); + assertThat(emptyResult).isInstanceOf(List.class); + assertEquals(0, ((List) emptyResult).size()); + + Serializable result = apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_TRANSFORM_PEOPLE, JsonUtils.toJson(Map.of("id", "1,2,3"))); + assertThat(result).isInstanceOf(List.class); + List> resultList = (List>) result; + assertEquals(3, resultList.size()); + + assertThat(resultList.stream().filter(m -> m.get("id").equals(2)).findFirst()).isPresent().get().hasFieldOrPropertyWithValue("statusCode", 200); + assertThat(resultList.stream().filter(m -> m.get("id").equals(3)).findFirst()).isPresent().get().hasFieldOrPropertyWithValue("statusCode", 500); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAsyncProcessAndGetStatus() throws QException + { + insertSimpsons(); + + ApiScriptUtils apiScriptUtils = newDefaultApiScriptUtils(); + + Serializable asyncResult = apiScriptUtils.runProcess(TestUtils.PROCESS_NAME_TRANSFORM_PEOPLE, JsonUtils.toJson(Map.of("id", "1,2,3", "async", true))); + assertThat(asyncResult).isInstanceOf(Map.class); + String jobId = ValueUtils.getValueAsString(((Map) asyncResult).get("jobId")); + assertNotNull(jobId); + + SleepUtils.sleep(100, TimeUnit.MILLISECONDS); + + Serializable result = apiScriptUtils.getProcessStatus(TestUtils.PROCESS_NAME_TRANSFORM_PEOPLE, jobId); + assertThat(result).isInstanceOf(List.class); + List> resultList = (List>) result; + assertEquals(3, resultList.size()); + + assertThat(resultList.stream().filter(m -> m.get("id").equals(2)).findFirst()).isPresent().get().hasFieldOrPropertyWithValue("statusCode", 200); + assertThat(resultList.stream().filter(m -> m.get("id").equals(3)).findFirst()).isPresent().get().hasFieldOrPropertyWithValue("statusCode", 500); + } + + + /******************************************************************************* ** *******************************************************************************/ From 54a0d6720fe756ad64e1654bd14f26b69b6d4c17 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 14 Jun 2023 13:26:57 -0500 Subject: [PATCH 5/9] Missing copyright --- ...indQueryFilterForExtractStepException.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/CouldNotFindQueryFilterForExtractStepException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/CouldNotFindQueryFilterForExtractStepException.java index 2dea8212..cf405c4a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/CouldNotFindQueryFilterForExtractStepException.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/CouldNotFindQueryFilterForExtractStepException.java @@ -1,3 +1,24 @@ +/* + * 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.backend.core.processes.implementations.etl.streamedwithfrontend; From d0194d9580f4b7dea6724bd82d8098f66410dda6 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 14 Jun 2023 13:32:50 -0500 Subject: [PATCH 6/9] Missing copyright --- .../qqq/api/model/openapi/HttpMethod.java | 21 +++++++++++++++++++ .../kingsrook/qqq/api/GetPersonInfoStep.java | 21 +++++++++++++++++++ .../qqq/api/TransformPersonStep.java | 21 +++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java index c04d577f..6d6a8dad 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/HttpMethod.java @@ -1,3 +1,24 @@ +/* + * 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.api.model.openapi; diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/GetPersonInfoStep.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/GetPersonInfoStep.java index 96191b58..6ee02038 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/GetPersonInfoStep.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/GetPersonInfoStep.java @@ -1,3 +1,24 @@ +/* + * 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.api; diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TransformPersonStep.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TransformPersonStep.java index 6abda809..d3dfdd0a 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TransformPersonStep.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/TransformPersonStep.java @@ -1,3 +1,24 @@ +/* + * 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.api; From d273d091df4c70938db81fb7e6dc92d41f3e90ef Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 14 Jun 2023 15:50:38 -0500 Subject: [PATCH 7/9] Test fixes --- .../actions/GenerateOpenApiSpecAction.java | 2 +- .../GenerateOpenApiSpecActionTest.java | 5 +++++ .../api/javalin/QJavalinApiHandlerTest.java | 19 ++++++++++++++----- 3 files changed, 20 insertions(+), 6 deletions(-) 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 d38e5386..90ebf18d 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 @@ -204,7 +204,6 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction apiPaths = output.getOpenAPI().getPaths().keySet(); assertTrue(apiPaths.stream().anyMatch(s -> s.contains("/supportedTable/"))); @@ -201,6 +204,8 @@ class GenerateOpenApiSpecActionTest extends BaseTest .withSupplementalMetaData(new ApiTableMetaDataContainer().withApiTableMetaData(TestUtils.API_NAME, new ApiTableMetaData() .withApiTableName("externalName") .withInitialVersion(TestUtils.V2022_Q4)))); + + new QInstanceEnricher(qInstance).enrich(); GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion(TestUtils.CURRENT_API_VERSION).withApiName(TestUtils.API_NAME)); Set apiPaths = output.getOpenAPI().getPaths().keySet(); 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 c3d3e339..99ba063e 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 @@ -72,6 +72,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; /******************************************************************************* @@ -1501,12 +1502,20 @@ class QJavalinApiHandlerTest extends BaseTest String jobId = acceptedJSON.getString("jobId"); assertNotNull(jobId); - SleepUtils.sleep(100, TimeUnit.MILLISECONDS); + for(int i = 0; i < 10; i++) + { + SleepUtils.sleep(100, TimeUnit.MILLISECONDS); - response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/transformPeople/status/" + jobId).asString(); - assertEquals(HttpStatus.MULTI_STATUS_207, response.getStatus()); - JSONArray jsonArray = new JSONArray(response.getBody()); - assertEquals(3, jsonArray.length()); + response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/transformPeople/status/" + jobId).asString(); + if(response.getStatus() == HttpStatus.MULTI_STATUS_207) + { + JSONArray jsonArray = new JSONArray(response.getBody()); + assertEquals(3, jsonArray.length()); + return; + } + } + + fail("Never got back a 207, after many sleeps"); } From 1c1a0f99e8f8f951f0a96b1d84d69d5d76912dff Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 14 Jun 2023 16:42:04 -0500 Subject: [PATCH 8/9] changes to make script processes in api better --- .../scripts/ScriptsMetaDataProvider.java | 4 ++ .../actions/GenerateOpenApiSpecAction.java | 39 +++++++++++-------- .../processes/ApiProcessMetaData.java | 32 +++++++++++++++ .../metadata/processes/ApiProcessUtils.java | 15 ++++--- 4 files changed, 68 insertions(+), 22 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java index de3399b9..e61050b7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java @@ -102,6 +102,8 @@ public class ScriptsMetaDataProvider { return (new QProcessMetaData() .withName(STORE_SCRIPT_REVISION_PROCESS_NAME) + .withTableName(Script.TABLE_NAME) + .withIsHidden(true) .withStepList(List.of( new QBackendStepMetaData() .withName("main") @@ -118,6 +120,8 @@ public class ScriptsMetaDataProvider { return (new QProcessMetaData() .withName(TEST_SCRIPT_PROCESS_NAME) + .withTableName(Script.TABLE_NAME) + .withIsHidden(true) .withStepList(List.of( new QBackendStepMetaData() .withName("main") 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 90ebf18d..4e51f8ce 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 @@ -202,8 +202,8 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction supportedVersions = apiInstanceMetaData.getSupportedVersions(); if(CollectionUtils.nullSafeIsEmpty(supportedVersions) || !supportedVersions.contains(requestApiVersion)) From b58f93e627c188191ce19a7a0f1b37e15997596b Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 15 Jun 2023 08:19:04 -0500 Subject: [PATCH 9/9] Revert a few changes, to help with stability of generated api specs --- .../actions/GenerateOpenApiSpecAction.java | 66 ++++++++++--------- 1 file changed, 34 insertions(+), 32 deletions(-) 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 4e51f8ce..6fde1064 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 @@ -302,10 +302,12 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction tagList = new ArrayList<>(); Set usedProcessNames = new HashSet<>(); - /////////////////// - // foreach table // - /////////////////// - for(QTableMetaData table : qInstance.getTables().values()) + ///////////////////////////////////// + // foreach table (sorted by label) // + ///////////////////////////////////// + List tables = new ArrayList<>(qInstance.getTables().values()); + tables.sort(Comparator.comparing(t -> ObjectUtils.requireNonNullElse(t.getLabel(), t.getName(), ""))); + for(QTableMetaData table : tables) { String tableName = table.getName(); @@ -557,34 +559,6 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction