From 78ba2b591c5add54dfd57745d3be6e201b7f406a Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Thu, 8 Jun 2023 14:22:52 -0500 Subject: [PATCH 01/35] Update for next development version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2bea6202..5347aa5a 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ - 0.14.0 + 0.15.0-SNAPSHOT UTF-8 UTF-8 From a18d2afee5112673dcb1daadcfefde1c33e65c59 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Thu, 8 Jun 2023 14:27:29 -0500 Subject: [PATCH 02/35] Updating to 0.15.0 --- qqq-dev-tools/CURRENT-SNAPSHOT-VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qqq-dev-tools/CURRENT-SNAPSHOT-VERSION b/qqq-dev-tools/CURRENT-SNAPSHOT-VERSION index a803cc22..a5510516 100644 --- a/qqq-dev-tools/CURRENT-SNAPSHOT-VERSION +++ b/qqq-dev-tools/CURRENT-SNAPSHOT-VERSION @@ -1 +1 @@ -0.14.0 +0.15.0 From eb151f0610a06855ef3e3bd43013002b1fc8943a Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 8 Jun 2023 14:37:59 -0500 Subject: [PATCH 03/35] Allow full JDBC URL to be set in RDBMS meta-data, used directly (w/ a known vendor) --- .../module/rdbms/jdbc/ConnectionManager.java | 29 +++++++--------- .../model/metadata/RDBMSBackendMetaData.java | 33 +++++++++++++++++++ 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java index 6f2488e1..6714979b 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.module.rdbms.jdbc; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; @@ -41,28 +42,20 @@ public class ConnectionManager { String jdbcURL; - switch (backend.getVendor()) + if(StringUtils.hasContent(backend.getJdbcUrl())) { - case "aurora": + jdbcURL = backend.getJdbcUrl(); + } + else + { + switch(backend.getVendor()) { // TODO aws-mysql-jdbc driver not working when running on AWS // jdbcURL = "jdbc:mysql:aws://" + backend.getHostName() + ":" + backend.getPort() + "/" + backend.getDatabaseName() + "?rewriteBatchedStatements=true&zeroDateTimeBehavior=CONVERT_TO_NULL"; - jdbcURL = "jdbc:mysql://" + backend.getHostName() + ":" + backend.getPort() + "/" + backend.getDatabaseName() + "?rewriteBatchedStatements=true&zeroDateTimeBehavior=convertToNull&useSSL=false"; - break; - } - case "mysql": - { - jdbcURL = "jdbc:mysql://" + backend.getHostName() + ":" + backend.getPort() + "/" + backend.getDatabaseName() + "?rewriteBatchedStatements=true&zeroDateTimeBehavior=convertToNull"; - break; - } - case "h2": - { - jdbcURL = "jdbc:h2:" + backend.getHostName() + ":" + backend.getDatabaseName() + ";MODE=MySQL;DB_CLOSE_DELAY=-1"; - break; - } - default: - { - throw new IllegalArgumentException("Unsupported rdbms backend vendor: " + backend.getVendor()); + case "aurora" -> jdbcURL = "jdbc:mysql://" + backend.getHostName() + ":" + backend.getPort() + "/" + backend.getDatabaseName() + "?rewriteBatchedStatements=true&zeroDateTimeBehavior=convertToNull&useSSL=false"; + case "mysql" -> jdbcURL = "jdbc:mysql://" + backend.getHostName() + ":" + backend.getPort() + "/" + backend.getDatabaseName() + "?rewriteBatchedStatements=true&zeroDateTimeBehavior=convertToNull"; + case "h2" -> jdbcURL = "jdbc:h2:" + backend.getHostName() + ":" + backend.getDatabaseName() + ";MODE=MySQL;DB_CLOSE_DELAY=-1"; + default -> throw new IllegalArgumentException("Unsupported rdbms backend vendor: " + backend.getVendor()); } } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/RDBMSBackendMetaData.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/RDBMSBackendMetaData.java index 5425c3e1..6ecc6e8c 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/RDBMSBackendMetaData.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/RDBMSBackendMetaData.java @@ -39,6 +39,8 @@ public class RDBMSBackendMetaData extends QBackendMetaData private String username; private String password; + private String jdbcUrl; + /******************************************************************************* @@ -281,4 +283,35 @@ public class RDBMSBackendMetaData extends QBackendMetaData password = interpreter.interpret(password); } + + + /******************************************************************************* + ** Getter for jdbcUrl + *******************************************************************************/ + public String getJdbcUrl() + { + return (this.jdbcUrl); + } + + + + /******************************************************************************* + ** Setter for jdbcUrl + *******************************************************************************/ + public void setJdbcUrl(String jdbcUrl) + { + this.jdbcUrl = jdbcUrl; + } + + + + /******************************************************************************* + ** Fluent setter for jdbcUrl + *******************************************************************************/ + public RDBMSBackendMetaData withJdbcUrl(String jdbcUrl) + { + this.jdbcUrl = jdbcUrl; + return (this); + } + } From 4ccc726f2e975a6a8936279d2f2be089b5e211cd Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 8 Jun 2023 14:38:27 -0500 Subject: [PATCH 04/35] Fix binding of long values --- .../module/rdbms/jdbc/QueryManager.java | 19 ++++++++- .../module/rdbms/jdbc/QueryManagerTest.java | 39 +++++++++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java index 1f9dfbdd..2096c93b 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java @@ -681,7 +681,7 @@ public class QueryManager } else if(value instanceof Long l) { - bindParam(statement, index, l.intValue()); + bindParam(statement, index, l.longValue()); return (1); } else if(value instanceof Double d) @@ -859,6 +859,23 @@ public class QueryManager + /******************************************************************************* + * + *******************************************************************************/ + public static void bindParam(PreparedStatement statement, int index, Long value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.INTEGER); + } + else + { + statement.setLong(index, value); + } + } + + + /******************************************************************************* * *******************************************************************************/ diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java index 2b46289b..70664e3d 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java @@ -48,6 +48,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /******************************************************************************* @@ -70,7 +71,8 @@ class QueryManagerTest extends BaseTest datetime_col DATETIME, char_col CHAR(1), date_col DATE, - time_col TIME + time_col TIME, + long_col LONG ) """); } @@ -151,6 +153,33 @@ class QueryManagerTest extends BaseTest QueryManager.bindParam(ps, 1, new GregorianCalendar()); QueryManager.bindParam(ps, 1, LocalDate.now()); QueryManager.bindParam(ps, 1, LocalDateTime.now()); + + //////////////////////////////////////////////////////////////////////////////////////////////// + // originally longs were being downgraded to int when binding, so, verify that doesn't happen // + //////////////////////////////////////////////////////////////////////////////////////////////// + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testLongBinding() throws SQLException + { + Long biggerThanMaxInteger = 2147483648L; + + Connection connection = getConnection(); + PreparedStatement ps = connection.prepareStatement("INSERT INTO test_table (long_col) VALUES (?)"); + QueryManager.bindParam(ps, 1, biggerThanMaxInteger); + ps.execute(); + + ps = connection.prepareStatement("SELECT long_col FROM test_table WHERE long_col = ?"); + QueryManager.bindParam(ps, 1, biggerThanMaxInteger); + ps.execute(); + ResultSet rs = ps.getResultSet(); + assertTrue(rs.next()); + assertEquals(biggerThanMaxInteger, QueryManager.getLong(rs, "long_col")); } @@ -161,9 +190,11 @@ class QueryManagerTest extends BaseTest @Test void testGetValueMethods() throws SQLException { + Long biggerThanMaxInteger = 2147483648L; + Connection connection = getConnection(); - QueryManager.executeUpdate(connection, "INSERT INTO test_table (int_col, datetime_col, char_col) VALUES (1, now(), 'A')"); - PreparedStatement preparedStatement = connection.prepareStatement("SELECT * from test_table"); + QueryManager.executeUpdate(connection, "INSERT INTO test_table (int_col, datetime_col, char_col, long_col) VALUES (1, now(), 'A', " + biggerThanMaxInteger + ")"); + PreparedStatement preparedStatement = connection.prepareStatement("SELECT int_col, datetime_col, char_col, long_col from test_table"); preparedStatement.execute(); ResultSet rs = preparedStatement.getResultSet(); rs.next(); @@ -194,6 +225,8 @@ class QueryManagerTest extends BaseTest assertNotNull(QueryManager.getTimestamp(rs, 2)); assertEquals("A", QueryManager.getObject(rs, "char_col")); assertEquals("A", QueryManager.getObject(rs, 3)); + assertEquals(biggerThanMaxInteger, QueryManager.getLong(rs, "long_col")); + assertEquals(biggerThanMaxInteger, QueryManager.getLong(rs, 4)); } From 6a01754479512c1654832a0ca936d76e2fb7d973 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 8 Jun 2023 18:24:56 -0500 Subject: [PATCH 05/35] 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 06/35] 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 6b590324beadcdc4e5d0993e8eaabc378ee30282 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Mon, 12 Jun 2023 16:04:46 -0500 Subject: [PATCH 07/35] initial version of attempting to downgrade logs if a warning or error has already been logged from the stack of throwables --- .../backend/core/exceptions/QException.java | 105 ++++++++++ .../qqq/backend/core/logging/QLogger.java | 54 ++++- .../backend/core/utils/ExceptionUtils.java | 41 ++++ .../qqq/backend/core/logging/QLoggerTest.java | 190 ++++++++++++++++++ 4 files changed, 382 insertions(+), 8 deletions(-) create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/logging/QLoggerTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QException.java index 651919fa..67b4aba6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QException.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QException.java @@ -22,12 +22,19 @@ package com.kingsrook.qqq.backend.core.exceptions; +import org.apache.logging.log4j.Level; + + /******************************************************************************* * Base class for checked exceptions thrown in qqq. * *******************************************************************************/ public class QException extends Exception { + private boolean hasLoggedWarning; + private boolean hasLoggedError; + + /******************************************************************************* ** Constructor of message @@ -59,4 +66,102 @@ public class QException extends Exception { super(message, cause); } + + + + /******************************************************************************* + ** Getter for hasLoggedWarning + *******************************************************************************/ + public boolean getHasLoggedWarning() + { + return (this.hasLoggedWarning); + } + + + + /******************************************************************************* + ** Setter for hasLoggedWarning + *******************************************************************************/ + public void setHasLoggedWarning(boolean hasLoggedWarning) + { + this.hasLoggedWarning = hasLoggedWarning; + } + + + + /******************************************************************************* + ** Fluent setter for hasLoggedWarning + *******************************************************************************/ + public QException withHasLoggedWarning(boolean hasLoggedWarning) + { + this.hasLoggedWarning = hasLoggedWarning; + return (this); + } + + + + /******************************************************************************* + ** Getter for hasLoggedError + *******************************************************************************/ + public boolean getHasLoggedError() + { + return (this.hasLoggedError); + } + + + + /******************************************************************************* + ** Setter for hasLoggedError + *******************************************************************************/ + public void setHasLoggedError(boolean hasLoggedError) + { + this.hasLoggedError = hasLoggedError; + } + + + + /******************************************************************************* + ** Fluent setter for hasLoggedError + *******************************************************************************/ + public QException withHasLoggedError(boolean hasLoggedError) + { + this.hasLoggedError = hasLoggedError; + return (this); + } + + + + /******************************************************************************* + ** helper function for getting if level logged + *******************************************************************************/ + public boolean hasLoggedLevel(Level level) + { + if(Level.WARN.equals(level)) + { + return (hasLoggedWarning); + } + if(Level.ERROR.equals(level)) + { + return (hasLoggedError); + } + return (false); + } + + + + /******************************************************************************* + ** helper function for setting if level logged + *******************************************************************************/ + public void setHasLoggedLevel(Level level) + { + if(Level.WARN.equals(level)) + { + setHasLoggedWarning(true); + } + if(Level.ERROR.equals(level)) + { + setHasLoggedError(true); + } + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QLogger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QLogger.java index d057b8fb..5135b836 100755 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QLogger.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QLogger.java @@ -29,10 +29,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -392,7 +394,7 @@ public class QLogger *******************************************************************************/ public void warn(String message, Throwable t) { - logger.warn(makeJsonString(message, t)); + logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(message, t)); } @@ -402,7 +404,7 @@ public class QLogger *******************************************************************************/ public void warn(String message, Throwable t, LogPair... logPairs) { - logger.warn(makeJsonString(message, t, logPairs)); + logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(message, t, logPairs)); } @@ -412,7 +414,7 @@ public class QLogger *******************************************************************************/ public void warn(Throwable t) { - logger.warn(makeJsonString(null, t)); + logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(null, t)); } @@ -452,7 +454,7 @@ public class QLogger *******************************************************************************/ public void error(String message, Throwable t) { - logger.error(makeJsonString(message, t)); + logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(message, t)); } @@ -462,7 +464,7 @@ public class QLogger *******************************************************************************/ public void error(String message, Throwable t, LogPair... logPairs) { - logger.error(makeJsonString(message, t, logPairs)); + logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(message, t, logPairs)); } @@ -472,7 +474,7 @@ public class QLogger *******************************************************************************/ public void error(Throwable t) { - logger.error(makeJsonString(null, t)); + logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(null, t)); } @@ -532,7 +534,7 @@ public class QLogger if(t != null) { - logPairList.add(logPair("stackTrace", LogUtils.filterStackTrace(ExceptionUtils.getStackTrace(t)))); + logPairList.add(logPair("stackTrace", LogUtils.filterStackTrace(org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace(t)))); } return (LogUtils.jsonLog(logPairList)); @@ -582,4 +584,40 @@ public class QLogger } } } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected Level determineIfShouldDowngrade(Throwable t, Level level) + { + ////////////////////////////////////////////////////////////////////////////////////// + // look for QExceptions in the chain, if none found, return the log level passed in // + ////////////////////////////////////////////////////////////////////////////////////// + List exceptionList = ExceptionUtils.getClassListFromRootChain(t, QException.class); + if(CollectionUtils.nullSafeIsEmpty(exceptionList)) + { + return (level); + } + + //////////////////////////////////////////////////////////////////// + // check if any QException in this chain to see if it has already // + // logged this level, if so, downgrade to INFO // + //////////////////////////////////////////////////////////////////// + for(QException qException : exceptionList) + { + if(qException.hasLoggedLevel(level)) + { + log(Level.INFO, "Downgrading log message from " + level.toString() + " to " + Level.INFO, t); + return (Level.INFO); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////////// + // if it has not logged at this level, set that it has in QException, and return passed in level // + /////////////////////////////////////////////////////////////////////////////////////////////////// + exceptionList.get(0).setHasLoggedLevel(level); + return (level); + } } 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..b9809b88 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; @@ -60,6 +62,45 @@ public class ExceptionUtils + /******************************************************************************* + ** Find a list of exceptions of the given class in an exception's caused-by chain. + ** Returns empty list if none found. + ** + *******************************************************************************/ + public static List getClassListFromRootChain(Throwable e, Class targetClass) + { + List throwableList = new ArrayList<>(); + if(targetClass.isInstance(e)) + { + throwableList.add(targetClass.cast(e)); + } + + /////////////////////////////////////////////////// + // iterate through the chain with a limit of 100 // + /////////////////////////////////////////////////// + int counter = 0; + while(counter++ < 100) + { + //////////////////////////////////////////////////////////////////////// + // look for the same class from the last throwable found of that type // + //////////////////////////////////////////////////////////////////////// + e = findClassInRootChain(e.getCause(), targetClass); + if(e == null) + { + break; + } + + //////////////////////////////////////////////////////////////////////// + // if we did not break, higher one must have been found, keep looking // + //////////////////////////////////////////////////////////////////////// + throwableList.add(targetClass.cast(e)); + } + + return (throwableList); + } + + + /******************************************************************************* ** Get the root exception in a caused-by-chain. ** diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/logging/QLoggerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/logging/QLoggerTest.java new file mode 100644 index 00000000..1c28982c --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/logging/QLoggerTest.java @@ -0,0 +1,190 @@ +/* + * 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.logging; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.filter.LevelRangeFilter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + + +/******************************************************************************* + ** Unit test for com.kingsrook.qqq.backend.core.logging.QLogger + ** + *******************************************************************************/ +@Disabled // disabled because could never get the custom appender class to receive logEvents that have their levels set (always null) +class QLoggerTest extends BaseTest +{ + private static final QLogger LOG = QLogger.getLogger(QLoggerTest.class); + private static final ListAppender listAppender = ListAppender.createAppender(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeAll() throws Exception + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDowngradingWarnings() throws Exception + { + org.apache.logging.log4j.core.Logger logger = (org.apache.logging.log4j.core.Logger) LogManager.getLogger(QLoggerTest.class); + logger.addAppender(listAppender); + listAppender.start(); + + try + { + try + { + try + { + try + { + throw (new QException("Some deepest exception...")); + } + catch(Exception e) + { + String warning = "Less deep warning"; + LOG.warn(warning, e); + throw (new QException(warning, e)); + } + } + catch(Exception e2) + { + String warning = "Middle warning"; + LOG.warn(warning, e2); + throw (new QException(warning, e2)); + } + } + catch(Exception e2) + { + String warning = "Last warning"; + LOG.warn(warning, e2); + throw (new QException(warning, e2)); + } + } + catch(Exception e3) + { + ///////////////////////// + // check results below // + ///////////////////////// + } + + assertThat(listAppender.getEventList()).isNotNull(); + assertThat(listAppender.getEventList().size()).isEqualTo(5); + int counter = 0; + for(LogEvent logEvent : listAppender.getEventList()) + { + if(counter == 0) + { + assertThat(logEvent.getLevel()).isEqualTo(Level.WARN); + } + else + { + assertThat(logEvent.getLevel()).isEqualTo(Level.INFO); + } + counter++; + } + } + + + + /******************************************************************************* + ** appender to add to logger to keep a list of log events + *******************************************************************************/ + @Plugin(name = "ListAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) + public static class ListAppender extends AbstractAppender + { + private List eventList = new ArrayList<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected ListAppender(final String name, final Filter filter, final Layout layout, final boolean ignoreExceptions, final Property[] properties) + { + super(name, filter, layout, ignoreExceptions, properties); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @PluginFactory + public static ListAppender createAppender() + { + LevelRangeFilter levelRangeFilter = LevelRangeFilter.createFilter(Level.TRACE, Level.ERROR, Filter.Result.ACCEPT, Filter.Result.ACCEPT); + // return (new ListAppender("ListApppender", levelRangeFilter, null, true, null)); + return (new ListAppender("ListApppender", null, null, true, null)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void append(LogEvent event) + { + eventList.add(event); + } + + + + /******************************************************************************* + ** Getter for eventList + *******************************************************************************/ + public List getEventList() + { + return (this.eventList); + } + } + +} From eee7354e771d4b556d13fc4c9fd0ec93a6414f4d Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 12 Jun 2023 18:19:48 -0500 Subject: [PATCH 08/35] 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 09/35] 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 10/35] 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 11/35] 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 12/35] 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 13/35] 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 14/35] 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 Date: Fri, 16 Jun 2023 16:44:53 -0500 Subject: [PATCH 15/35] Add override executeWithStringDetails --- .../backend/core/actions/audits/AuditAction.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java index 4bbd6881..41015e57 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java @@ -76,6 +76,21 @@ public class AuditAction extends AbstractQActionFunction securityKeyValues, String message, List detailMessages) + { + List detailRecords = null; + if(CollectionUtils.nullSafeHasContents(detailMessages)) + { + detailRecords = detailMessages.stream().map(m -> new QRecord().withValue("message", m)).toList(); + } + execute(tableName, recordId, securityKeyValues, message, detailRecords); + } + + + /******************************************************************************* ** Execute to insert 1 audit, with a list of detail child records *******************************************************************************/ From 1cf83fb44176a005d69f27a0dc7ae4b82449871c Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 16 Jun 2023 16:45:10 -0500 Subject: [PATCH 16/35] Add factory method: newForTable --- .../possiblevalues/QPossibleValueSource.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java index 76a03b90..c0d0076b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java @@ -77,6 +77,21 @@ public class QPossibleValueSource implements TopLevelMetaDataInterface + /******************************************************************************* + ** Create a new possible value source, for a table, with default settings. + ** e.g., name & table name from the tableName parameter; type=TABLE; and LABEL_ONLY format + *******************************************************************************/ + public static QPossibleValueSource newForTable(String tableName) + { + return new QPossibleValueSource() + .withName(tableName) + .withType(QPossibleValueSourceType.TABLE) + .withTableName(tableName) + .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY); + } + + + /******************************************************************************* ** *******************************************************************************/ From 3772cf725f03e5d2abbc43da21b2666faaaeaf51 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 19 Jun 2023 12:03:50 -0500 Subject: [PATCH 17/35] Make table-triggers respect saved filters --- .../PollingAutomationPerTableRunner.java | 47 ++- .../core/model/automation/TableTrigger.java | 3 +- .../core/model/savedfilters/SavedFilter.java | 283 ++++++++++++++++++ .../SavedFiltersMetaDataProvider.java | 88 ++++++ .../scripts/ScriptsMetaDataProvider.java | 7 +- 5 files changed, 416 insertions(+), 12 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFilter.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java index 2f0b3106..2bb0a470 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java @@ -39,12 +39,15 @@ import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; 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.reporting.RecordPipe; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +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.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; @@ -61,11 +64,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.Automatio import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; import org.apache.commons.lang.NotImplementedException; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* @@ -270,16 +276,37 @@ public class PollingAutomationPerTableRunner implements Runnable QueryOutput queryOutput = new QueryAction().execute(queryInput); for(QRecord record : queryOutput.getRecords()) { - // todo - get filter if there is/was one - rs.add(new TableAutomationAction() - .withName("Script:" + record.getValue("scriptId")) - .withFilter(null) - .withTriggerEvent(triggerEvent) - .withPriority(record.getValueInteger("priority")) - .withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class)) - .withValues(MapBuilder.of("scriptId", record.getValue("scriptId"))) - .withIncludeRecordAssociations(true) - ); + TableTrigger tableTrigger = new TableTrigger(record); + + try + { + Integer filterId = tableTrigger.getFilterId(); + + GetInput getInput = new GetInput(); + getInput.setTableName(SavedFilter.TABLE_NAME); + getInput.setPrimaryKey(filterId); + GetOutput getOutput = new GetAction().execute(getInput); + QQueryFilter filter = null; + if(getOutput.getRecord() != null) + { + SavedFilter savedFilter = new SavedFilter(getOutput.getRecord()); + filter = JsonUtils.toObject(savedFilter.getFilterJson(), QQueryFilter.class); + } + + rs.add(new TableAutomationAction() + .withName("Script:" + tableTrigger.getScriptId()) + .withFilter(filter) + .withTriggerEvent(triggerEvent) + .withPriority(tableTrigger.getPriority()) + .withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class)) + .withValues(MapBuilder.of("scriptId", tableTrigger.getScriptId())) + .withIncludeRecordAssociations(true) + ); + } + catch(Exception e) + { + LOG.error("Error setting up table trigger", e, logPair("tableTriggerId", tableTrigger.getId())); + } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/TableTrigger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/TableTrigger.java index a21aedf0..a3c82b4f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/TableTrigger.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/TableTrigger.java @@ -28,6 +28,7 @@ import com.kingsrook.qqq.backend.core.model.data.QField; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; import com.kingsrook.qqq.backend.core.model.scripts.Script; @@ -50,7 +51,7 @@ public class TableTrigger extends QRecordEntity @QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME) private String tableName; - @QField(/* todo possibleValueSourceName = */) + @QField(possibleValueSourceName = SavedFilter.TABLE_NAME) private Integer filterId; @QField(possibleValueSourceName = Script.TABLE_NAME) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFilter.java new file mode 100644 index 00000000..9263b5d5 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFilter.java @@ -0,0 +1,283 @@ +/* + * 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.savedfilters; + + +import java.time.Instant; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.data.QField; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; + + +/******************************************************************************* + ** Entity bean for the saved filter table + *******************************************************************************/ +public class SavedFilter extends QRecordEntity +{ + public static final String TABLE_NAME = "savedFilter"; + + @QField(isEditable = false) + private Integer id; + + @QField(isEditable = false) + private Instant createDate; + + @QField(isEditable = false) + private Instant modifyDate; + + @QField(isRequired = true) + private String label; + + @QField(isEditable = false) + private String tableName; + + @QField(isEditable = false) + private String userId; + + @QField(isEditable = false) + private String filterJson; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public SavedFilter() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public SavedFilter(QRecord qRecord) throws QException + { + populateFromQRecord(qRecord); + } + + + + /******************************************************************************* + ** Getter for id + ** + *******************************************************************************/ + public Integer getId() + { + return id; + } + + + + /******************************************************************************* + ** Setter for id + ** + *******************************************************************************/ + public void setId(Integer id) + { + this.id = id; + } + + + + /******************************************************************************* + ** Getter for createDate + ** + *******************************************************************************/ + public Instant getCreateDate() + { + return createDate; + } + + + + /******************************************************************************* + ** Setter for createDate + ** + *******************************************************************************/ + public void setCreateDate(Instant createDate) + { + this.createDate = createDate; + } + + + + /******************************************************************************* + ** Getter for modifyDate + ** + *******************************************************************************/ + public Instant getModifyDate() + { + return modifyDate; + } + + + + /******************************************************************************* + ** Setter for modifyDate + ** + *******************************************************************************/ + public void setModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + } + + + + /******************************************************************************* + ** Getter for label + ** + *******************************************************************************/ + public String getLabel() + { + return label; + } + + + + /******************************************************************************* + ** Setter for label + ** + *******************************************************************************/ + public void setLabel(String label) + { + this.label = label; + } + + + + /******************************************************************************* + ** Fluent setter for label + ** + *******************************************************************************/ + public SavedFilter withLabel(String label) + { + this.label = label; + return (this); + } + + + + /******************************************************************************* + ** Getter for tableName + ** + *******************************************************************************/ + public String getTableName() + { + return tableName; + } + + + + /******************************************************************************* + ** Setter for tableName + ** + *******************************************************************************/ + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + + + /******************************************************************************* + ** Fluent setter for tableName + ** + *******************************************************************************/ + public SavedFilter withTableName(String tableName) + { + this.tableName = tableName; + return (this); + } + + + + /******************************************************************************* + ** Getter for userId + ** + *******************************************************************************/ + public String getUserId() + { + return userId; + } + + + + /******************************************************************************* + ** Setter for userId + ** + *******************************************************************************/ + public void setUserId(String userId) + { + this.userId = userId; + } + + + + /******************************************************************************* + ** Fluent setter for userId + ** + *******************************************************************************/ + public SavedFilter withUserId(String userId) + { + this.userId = userId; + return (this); + } + + + + /******************************************************************************* + ** Getter for filterJson + ** + *******************************************************************************/ + public String getFilterJson() + { + return filterJson; + } + + + + /******************************************************************************* + ** Setter for filterJson + ** + *******************************************************************************/ + public void setFilterJson(String filterJson) + { + this.filterJson = filterJson; + } + + + + /******************************************************************************* + ** Fluent setter for filterJson + ** + *******************************************************************************/ + public SavedFilter withFilterJson(String filterJson) + { + this.filterJson = filterJson; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java new file mode 100644 index 00000000..dd004fa0 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java @@ -0,0 +1,88 @@ +/* + * 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.backend.core.model.savedfilters; + + +import java.util.function.Consumer; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.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.tables.QTableMetaData; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class SavedFiltersMetaDataProvider +{ + + + /******************************************************************************* + ** + *******************************************************************************/ + public void defineAll(QInstance instance, String backendName, Consumer backendDetailEnricher) throws QException + { + instance.addTable(defineSavedFilterTable(backendName, backendDetailEnricher)); + instance.addPossibleValueSource(defineSavedFilterPossibleValueSource()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QTableMetaData defineSavedFilterTable(String backendName, Consumer backendDetailEnricher) throws QException + { + QTableMetaData table = new QTableMetaData() + .withName(SavedFilter.TABLE_NAME) + .withLabel("Saved Filter") + .withRecordLabelFormat("%s") + .withRecordLabelFields("label") + .withBackendName(backendName) + .withPrimaryKeyField("id") + .withFieldsFromEntity(SavedFilter.class); + + if(backendDetailEnricher != null) + { + backendDetailEnricher.accept(table); + } + + return (table); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QPossibleValueSource defineSavedFilterPossibleValueSource() + { + return new QPossibleValueSource() + .withName(SavedFilter.TABLE_NAME) + .withType(QPossibleValueSourceType.TABLE) + .withTableName(SavedFilter.TABLE_NAME) + .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY); + } + +} 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..1cb972a6 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 @@ -327,7 +327,12 @@ public class ScriptsMetaDataProvider .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))); tableMetaData.getField("scriptId").withPossibleValueSourceFilter(new QQueryFilter( - new QFilterCriteria("scriptType.name", QCriteriaOperator.EQUALS, SCRIPT_TYPE_NAME_RECORD) + new QFilterCriteria("scriptType.name", QCriteriaOperator.EQUALS, SCRIPT_TYPE_NAME_RECORD), + new QFilterCriteria("script.tableName", QCriteriaOperator.EQUALS, "${input.tableName}") + )); + + tableMetaData.getField("filterId").withPossibleValueSourceFilter(new QQueryFilter( + new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, "${input.tableName}") )); return tableMetaData; From 2c192a3fd91689640b852c7e5e94ab9994326e71 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 19 Jun 2023 12:14:26 -0500 Subject: [PATCH 18/35] Add SavedFiltersMetaDataProvider (as we've introduced a dependency between it and ScriptsMetaDataProvider (through tableTriggers) --- .../java/com/kingsrook/qqq/backend/javalin/TestUtils.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java index aaf4dd0e..0245e235 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -66,6 +66,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType; import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFiltersMetaDataProvider; import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider; import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; @@ -156,6 +157,7 @@ public class TestUtils qInstance.addBackend(defineMemoryBackend()); try { + new SavedFiltersMetaDataProvider().defineAll(qInstance, defineMemoryBackend().getName(), null); new ScriptsMetaDataProvider().defineAll(qInstance, defineMemoryBackend().getName(), null); } catch(Exception e) @@ -362,8 +364,7 @@ public class TestUtils .withType(QPossibleValueSourceType.TABLE) .withTableName(TABLE_NAME_PERSON) .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_PARENS_ID) - .withOrderByField("id") - ); + .withOrderByField("id")); } From 79304adcb05ba2fd7ff4ed1d2ee80661f5018a06 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 19 Jun 2023 12:19:11 -0500 Subject: [PATCH 19/35] Move saved filter processes to qqq --- .../SavedFiltersMetaDataProvider.java | 6 + .../DeleteSavedFilterProcess.java | 88 +++++++++++++ .../savedfilters/QuerySavedFilterProcess.java | 117 ++++++++++++++++++ .../savedfilters/StoreSavedFilterProcess.java | 117 ++++++++++++++++++ 4 files changed, 328 insertions(+) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/DeleteSavedFilterProcess.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/QuerySavedFilterProcess.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/StoreSavedFilterProcess.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java index dd004fa0..cb03e071 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java @@ -29,6 +29,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueForm 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.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.processes.implementations.savedfilters.DeleteSavedFilterProcess; +import com.kingsrook.qqq.backend.core.processes.implementations.savedfilters.QuerySavedFilterProcess; +import com.kingsrook.qqq.backend.core.processes.implementations.savedfilters.StoreSavedFilterProcess; /******************************************************************************* @@ -45,6 +48,9 @@ public class SavedFiltersMetaDataProvider { instance.addTable(defineSavedFilterTable(backendName, backendDetailEnricher)); instance.addPossibleValueSource(defineSavedFilterPossibleValueSource()); + instance.addProcess(QuerySavedFilterProcess.getProcessMetaData()); + instance.addProcess(StoreSavedFilterProcess.getProcessMetaData()); + instance.addProcess(DeleteSavedFilterProcess.getProcessMetaData()); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/DeleteSavedFilterProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/DeleteSavedFilterProcess.java new file mode 100644 index 00000000..a0a6f1f8 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/DeleteSavedFilterProcess.java @@ -0,0 +1,88 @@ +/* + * 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.savedfilters; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +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.savedfilters.SavedFilter; + + +/******************************************************************************* + ** Process used by the delete filter dialog + *******************************************************************************/ +public class DeleteSavedFilterProcess implements BackendStep +{ + private static final QLogger LOG = QLogger.getLogger(DeleteSavedFilterProcess.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QProcessMetaData getProcessMetaData() + { + return (new QProcessMetaData() + .withName("deleteSavedFilter") + .withStepList(List.of( + new QBackendStepMetaData() + .withCode(new QCodeReference(DeleteSavedFilterProcess.class)) + .withName("delete") + ))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + ActionHelper.validateSession(runBackendStepInput); + + try + { + Integer savedFilterId = runBackendStepInput.getValueInteger("id"); + + DeleteInput input = new DeleteInput(); + input.setTableName(SavedFilter.TABLE_NAME); + input.setPrimaryKeys(List.of(savedFilterId)); + new DeleteAction().execute(input); + } + catch(Exception e) + { + LOG.warn("Error deleting saved filter", e); + throw (e); + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/QuerySavedFilterProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/QuerySavedFilterProcess.java new file mode 100644 index 00000000..dc50ed17 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/QuerySavedFilterProcess.java @@ -0,0 +1,117 @@ +/* + * 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.savedfilters; + + +import java.io.Serializable; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +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; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; + + +/******************************************************************************* + ** Process used by the saved filter dialogs + *******************************************************************************/ +public class QuerySavedFilterProcess implements BackendStep +{ + private static final QLogger LOG = QLogger.getLogger(QuerySavedFilterProcess.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QProcessMetaData getProcessMetaData() + { + return (new QProcessMetaData() + .withName("querySavedFilter") + .withStepList(List.of( + new QBackendStepMetaData() + .withCode(new QCodeReference(QuerySavedFilterProcess.class)) + .withName("query") + ))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + ActionHelper.validateSession(runBackendStepInput); + + try + { + Integer savedFilterId = runBackendStepInput.getValueInteger("id"); + if(savedFilterId != null) + { + GetInput input = new GetInput(); + input.setTableName(SavedFilter.TABLE_NAME); + input.setPrimaryKey(savedFilterId); + + GetOutput output = new GetAction().execute(input); + runBackendStepOutput.addRecord(output.getRecord()); + runBackendStepOutput.addValue("savedFilter", output.getRecord()); + runBackendStepOutput.addValue("savedFilterList", (Serializable) List.of(output.getRecord())); + } + else + { + String tableName = runBackendStepInput.getValueString("tableName"); + + QueryInput input = new QueryInput(); + input.setTableName(SavedFilter.TABLE_NAME); + input.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, tableName)) + .withOrderBy(new QFilterOrderBy("label"))); + + QueryOutput output = new QueryAction().execute(input); + runBackendStepOutput.setRecords(output.getRecords()); + runBackendStepOutput.addValue("savedFilterList", (Serializable) output.getRecords()); + } + } + catch(Exception e) + { + LOG.warn("Error deleting saved filter", e); + throw (e); + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/StoreSavedFilterProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/StoreSavedFilterProcess.java new file mode 100644 index 00000000..37bc167e --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/StoreSavedFilterProcess.java @@ -0,0 +1,117 @@ +/* + * 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.savedfilters; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +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; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +import com.kingsrook.qqq.backend.core.model.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.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; + + +/******************************************************************************* + ** Process used by the saved filter dialog + *******************************************************************************/ +public class StoreSavedFilterProcess implements BackendStep +{ + private static final QLogger LOG = QLogger.getLogger(StoreSavedFilterProcess.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QProcessMetaData getProcessMetaData() + { + return (new QProcessMetaData() + .withName("storeSavedFilter") + .withStepList(List.of( + new QBackendStepMetaData() + .withCode(new QCodeReference(StoreSavedFilterProcess.class)) + .withName("store") + ))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + ActionHelper.validateSession(runBackendStepInput); + + try + { + QRecord qRecord = new QRecord() + .withValue("id", runBackendStepInput.getValueInteger("id")) + .withValue("label", runBackendStepInput.getValueString("label")) + .withValue("tableName", runBackendStepInput.getValueString("tableName")) + .withValue("filterJson", runBackendStepInput.getValueString("filterJson")) + .withValue("userId", runBackendStepInput.getSession().getUser().getIdReference()); + + List savedFilterList = new ArrayList<>(); + if(qRecord.getValueInteger("id") == null) + { + InsertInput input = new InsertInput(); + input.setTableName(SavedFilter.TABLE_NAME); + input.setRecords(List.of(qRecord)); + + InsertOutput output = new InsertAction().execute(input); + savedFilterList = output.getRecords(); + } + else + { + UpdateInput input = new UpdateInput(); + input.setTableName(SavedFilter.TABLE_NAME); + input.setRecords(List.of(qRecord)); + + UpdateOutput output = new UpdateAction().execute(input); + savedFilterList = output.getRecords(); + } + + runBackendStepOutput.addValue("savedFilterList", (Serializable) savedFilterList); + } + catch(Exception e) + { + LOG.warn("Error storing data saved filter", e); + throw (e); + } + } +} From 3e113a12b3e59017643d359b33257dd0a4066f92 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 19 Jun 2023 12:30:10 -0500 Subject: [PATCH 20/35] Initial checkin --- .../savedfilters/SavedFilterProcessTests.java | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/SavedFilterProcessTests.java diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/SavedFilterProcessTests.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/SavedFilterProcessTests.java new file mode 100644 index 00000000..d3c0bf5b --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/SavedFilterProcessTests.java @@ -0,0 +1,143 @@ +/* + * 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.savedfilters; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFiltersMetaDataProvider; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +/******************************************************************************* + ** Unit test for all saved filter processes + *******************************************************************************/ +class SavedFilterProcessTests extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + QInstance qInstance = QContext.getQInstance(); + new SavedFiltersMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null); + String tableName = TestUtils.TABLE_NAME_PERSON_MEMORY; + + { + /////////////////////////////////////////// + // query - should be no filters to start // + /////////////////////////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(QuerySavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("tableName", tableName); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + assertEquals(0, ((List) runProcessOutput.getValues().get("savedFilterList")).size()); + } + + Integer savedFilterId; + { + //////////////////////// + // store a new filter // + //////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(StoreSavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("label", "My Filter"); + runProcessInput.addValue("tableName", tableName); + runProcessInput.addValue("filterJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47)))); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + List savedFilterList = (List) runProcessOutput.getValues().get("savedFilterList"); + assertEquals(1, savedFilterList.size()); + savedFilterId = savedFilterList.get(0).getValueInteger("id"); + assertNotNull(savedFilterId); + } + + { + //////////////////////////////////// + // query - should find our filter // + //////////////////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(QuerySavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("tableName", tableName); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + List savedFilterList = (List) runProcessOutput.getValues().get("savedFilterList"); + assertEquals(1, savedFilterList.size()); + assertEquals(1, savedFilterList.get(0).getValueInteger("id")); + assertEquals("My Filter", savedFilterList.get(0).getValueString("label")); + } + + { + /////////////////////// + // update our filter // + /////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(StoreSavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("id", savedFilterId); + runProcessInput.addValue("label", "My Updated Filter"); + runProcessInput.addValue("tableName", tableName); + runProcessInput.addValue("filterJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47)))); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + List savedFilterList = (List) runProcessOutput.getValues().get("savedFilterList"); + assertEquals(1, savedFilterList.size()); + assertEquals(1, savedFilterList.get(0).getValueInteger("id")); + assertEquals("My Updated Filter", savedFilterList.get(0).getValueString("label")); + } + + { + /////////////////////// + // delete our filter // + /////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(DeleteSavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("id", savedFilterId); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + } + + { + //////////////////////////////////////// + // query - should be no filters again // + //////////////////////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(QuerySavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("tableName", tableName); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + assertEquals(0, ((List) runProcessOutput.getValues().get("savedFilterList")).size()); + } + + } + +} \ No newline at end of file From 0f799339d648219f0a229b904cd1febbc150e68f Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 19 Jun 2023 12:33:01 -0500 Subject: [PATCH 21/35] Set user in new sessions --- .../test/java/com/kingsrook/qqq/backend/core/BaseTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/BaseTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/BaseTest.java index f436eb11..1cae9018 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/BaseTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/BaseTest.java @@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.model.session.QUser; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.AfterEach; @@ -47,7 +48,10 @@ public class BaseTest @BeforeEach void baseBeforeEach() { - QContext.init(TestUtils.defineInstance(), new QSession()); + QContext.init(TestUtils.defineInstance(), new QSession() + .withUser(new QUser() + .withIdReference("001") + .withFullName("Anonymous"))); resetMemoryRecordStore(); } From 3791c069c7c6e210eca69f8b422863d796e0eded Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 20 Jun 2023 09:06:57 -0500 Subject: [PATCH 22/35] Add convertObjectToJava to code executors - for converting language objects to java objects --- .../actions/scripts/ExecuteCodeAction.java | 11 +++ .../core/actions/scripts/QCodeExecutor.java | 10 ++ .../actions/scripts/QCodeExecutorAware.java | 36 +++++++ .../javascript/QJavaScriptExecutor.java | 59 +++++++++++ .../javascript/ExecuteCodeActionTest.java | 97 +++++++++++++++++++ .../qqq/api/utils/ApiScriptUtils.java | 65 ++++++++++++- 6 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorAware.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java index c142fc1f..875cf2f5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java @@ -101,6 +101,17 @@ public class ExecuteCodeAction context.putAll(input.getInput()); } + ///////////////////////////////////////////////////////////////////////////////// + // set the qCodeExecutor into any context objects which are QCodeExecutorAware // + ///////////////////////////////////////////////////////////////////////////////// + for(Serializable value : context.values()) + { + if(value instanceof QCodeExecutorAware qCodeExecutorAware) + { + qCodeExecutorAware.setQCodeExecutor(qCodeExecutor); + } + } + Serializable codeOutput = qCodeExecutor.execute(codeReference, context, executionLogger); output.setOutput(codeOutput); executionLogger.acceptExecutionEnd(codeOutput); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java index e6cd808a..a0e06f81 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java @@ -41,4 +41,14 @@ public interface QCodeExecutor *******************************************************************************/ Serializable execute(QCodeReference codeReference, Map inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException; + /******************************************************************************* + ** Process an object from the script's language/runtime into a (more) native java object. + ** e.g., a Nashorn ScriptObjectMirror will end up as a "primitive", or a List or Map of such + ** + *******************************************************************************/ + default Object convertObjectToJava(Object object) + { + return (object); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorAware.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorAware.java new file mode 100644 index 00000000..e15dd2a6 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorAware.java @@ -0,0 +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.backend.core.actions.scripts; + + +/******************************************************************************* + ** Interface for classes that can accept a QCodeExecutor object via a setter. + *******************************************************************************/ +public interface QCodeExecutorAware +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void setQCodeExecutor(QCodeExecutor qCodeExecutor); + +} diff --git a/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java b/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java index 15f7ac98..7a708ba7 100644 --- a/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java +++ b/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java @@ -27,6 +27,10 @@ import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutor; import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; @@ -36,8 +40,10 @@ import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.apache.commons.lang.NotImplementedException; import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory; +import org.openjdk.nashorn.api.scripting.ScriptObjectMirror; import org.openjdk.nashorn.internal.runtime.ECMAException; import org.openjdk.nashorn.internal.runtime.ParserException; +import org.openjdk.nashorn.internal.runtime.Undefined; /******************************************************************************* @@ -59,6 +65,59 @@ public class QJavaScriptExecutor implements QCodeExecutor + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Object convertObjectToJava(Object object) + { + if(object == null || object instanceof String || object instanceof Boolean || object instanceof Integer || object instanceof Long || object instanceof BigDecimal) + { + return (object); + } + else if(object instanceof Float f) + { + return (new BigDecimal(f)); + } + else if(object instanceof Double d) + { + return (new BigDecimal(d)); + } + else if(object instanceof Undefined) + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // well, we always said we wanted javascript to treat null & undefined the same way... here's our chance // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + return (null); + } + + if(object instanceof ScriptObjectMirror scriptObjectMirror) + { + if(scriptObjectMirror.isArray()) + { + List result = new ArrayList<>(); + for(String key : scriptObjectMirror.keySet()) + { + result.add(Integer.parseInt(key), convertObjectToJava(scriptObjectMirror.get(key))); + } + return (result); + } + else + { + Map result = new HashMap<>(); + for(String key : scriptObjectMirror.keySet()) + { + result.put(key, convertObjectToJava(scriptObjectMirror.get(key))); + } + return (result); + } + } + + return QCodeExecutor.super.convertObjectToJava(object); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java index 3c1bf009..43338983 100644 --- a/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java +++ b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java @@ -23,7 +23,12 @@ package com.kingsrook.qqq.languages.javascript; import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import com.kingsrook.qqq.backend.core.actions.scripts.ExecuteCodeAction; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutor; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutorAware; import com.kingsrook.qqq.backend.core.exceptions.QCodeException; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput; @@ -31,6 +36,7 @@ import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput; 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.code.QCodeType; +import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @@ -241,10 +247,50 @@ class ExecuteCodeActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testConvertObjectToJava() throws QException + { + TestQCodeExecutorAware converter = new TestQCodeExecutorAware(); + testOne(1, """ + converter.convertObject("one", 1); + converter.convertObject("two", "two"); + converter.convertObject("true", true); + converter.convertObject("null", null); + converter.convertObject("undefined", undefined); + converter.convertObject("flatMap", {"a": 1, "b": "c"}); + converter.convertObject("flatList", ["a", 1, "b", "c"]); + converter.convertObject("mixedMap", {"a": [1, {"2": "3"}], "b": {"c": ["d"]}}); + """, MapBuilder.of("converter", converter)); + + assertEquals(1, converter.getConvertedObject("one")); + assertEquals("two", converter.getConvertedObject("two")); + assertEquals(true, converter.getConvertedObject("true")); + assertNull(converter.getConvertedObject("null")); + assertNull(converter.getConvertedObject("undefined")); + assertEquals(Map.of("a", 1, "b", "c"), converter.getConvertedObject("flatMap")); + assertEquals(List.of("a", 1, "b", "c"), converter.getConvertedObject("flatList")); + assertEquals(Map.of("a", List.of(1, Map.of("2", "3")), "b", Map.of("c", List.of("d"))), converter.getConvertedObject("mixedMap")); + } + + + /******************************************************************************* ** *******************************************************************************/ private OneTestOutput testOne(Integer inputValueC, String code) throws QException + { + return (testOne(inputValueC, code, null)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private OneTestOutput testOne(Integer inputValueC, String code, Map additionalContext) throws QException { System.out.println(); QInstance instance = TestUtils.defineInstance(); @@ -259,6 +305,14 @@ class ExecuteCodeActionTest extends BaseTest input.withContext("input", testInput); input.withContext("output", testOutput); + if(additionalContext != null) + { + for(Map.Entry entry : additionalContext.entrySet()) + { + input.withContext(entry.getKey(), entry.getValue()); + } + } + ExecuteCodeOutput output = new ExecuteCodeOutput(); ExecuteCodeAction executeCodeAction = new ExecuteCodeAction(); @@ -269,6 +323,49 @@ class ExecuteCodeActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + public static class TestQCodeExecutorAware implements QCodeExecutorAware, Serializable + { + private QCodeExecutor qCodeExecutor; + + private Map convertedObjectMap = new HashMap<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void setQCodeExecutor(QCodeExecutor qCodeExecutor) + { + this.qCodeExecutor = qCodeExecutor; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void convertObject(String name, Object inputObject) + { + convertedObjectMap.put(name, qCodeExecutor.convertObjectToJava(inputObject)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Object getConvertedObject(String name) + { + return (convertedObjectMap.get(name)); + } + } + + + /******************************************************************************* ** *******************************************************************************/ 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..af776782 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 @@ -32,19 +32,24 @@ import com.kingsrook.qqq.api.actions.QRecordApiAdapter; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutor; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutorAware; 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.JsonUtils; /******************************************************************************* ** Object injected into script context, for interfacing with a QQQ API. *******************************************************************************/ -public class ApiScriptUtils implements Serializable +public class ApiScriptUtils implements QCodeExecutorAware, Serializable { private String apiName; private String apiVersion; + private QCodeExecutor qCodeExecutor; + /******************************************************************************* @@ -165,6 +170,7 @@ public class ApiScriptUtils implements Serializable public Map insert(String tableApiName, Object body) throws QException { validateApiNameAndVersion("insert(" + tableApiName + ")"); + body = processBodyToJsonString(body); return (ApiImplementation.insert(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); } @@ -176,6 +182,7 @@ public class ApiScriptUtils implements Serializable public List> bulkInsert(String tableApiName, Object body) throws QException { validateApiNameAndVersion("bulkInsert(" + tableApiName + ")"); + body = processBodyToJsonString(body); return (ApiImplementation.bulkInsert(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); } @@ -187,17 +194,61 @@ public class ApiScriptUtils implements Serializable public void update(String tableApiName, Object primaryKey, Object body) throws QException { validateApiNameAndVersion("update(" + tableApiName + "," + primaryKey + ")"); + body = processBodyToJsonString(body); ApiImplementation.update(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(primaryKey), String.valueOf(body)); } + /******************************************************************************* + ** Take a "body" object, which maybe defined in the script's language/run-time, + ** and try to process it into a JSON String (which is what the API Implementation wants) + *******************************************************************************/ + private Object processBodyToJsonString(Object body) + { + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the caller already supplied the object as a string, then return that string. // + // and in case it can't be parsed as json, well, let that error come out of the api implementation, and go back to the caller. // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(body instanceof String) + { + return (body); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the input body wasn't a json string, try to convert it from a language-type object (e.g., javscript) to a java-object, // + // then make JSON out of that for the APIImplementation // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Object bodyJavaObject = processInputObjectViaCodeExecutor(body); + return JsonUtils.toJson(bodyJavaObject); + } + + + + /******************************************************************************* + ** Use the QCodeExecutor (if we have one) to process an input object from the + ** script's language into a (more) native java object. + ** e.g., a Nashorn ScriptObjectMirror will end up as a "primitive", or a List or Map of such + *******************************************************************************/ + private Object processInputObjectViaCodeExecutor(Object body) + { + if(qCodeExecutor == null || body == null) + { + return (body); + } + + return (qCodeExecutor.convertObjectToJava(body)); + } + + + /******************************************************************************* ** *******************************************************************************/ public List> bulkUpdate(String tableApiName, Object body) throws QException { validateApiNameAndVersion("bulkUpdate(" + tableApiName + ")"); + body = processBodyToJsonString(body); return (ApiImplementation.bulkUpdate(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); } @@ -220,6 +271,7 @@ public class ApiScriptUtils implements Serializable public List> bulkDelete(String tableApiName, Object body) throws QException { validateApiNameAndVersion("bulkDelete(" + tableApiName + ")"); + body = processBodyToJsonString(body); return (ApiImplementation.bulkDelete(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); } @@ -257,4 +309,15 @@ public class ApiScriptUtils implements Serializable } return paramMap; } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void setQCodeExecutor(QCodeExecutor qCodeExecutor) + { + this.qCodeExecutor = qCodeExecutor; + } } From 57569e4c846a6aef35b31ab515d0916dbc9490e9 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 20 Jun 2023 09:07:15 -0500 Subject: [PATCH 23/35] Escape identifiers in column names --- .../qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java index 26af479b..035794ee 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java @@ -248,7 +248,7 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte private String writeUpdateSQLPrefix(QTableMetaData table, List fieldsBeingUpdated) { String columns = fieldsBeingUpdated.stream() - .map(f -> this.getColumnName(table.getField(f)) + " = ?") + .map(f -> escapeIdentifier(this.getColumnName(table.getField(f))) + " = ?") .collect(Collectors.joining(", ")); String tableName = escapeIdentifier(getTableName(table)); From 7af5ad2655400c02a2cfe4f8a535e1322bf31d37 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 20 Jun 2023 10:22:21 -0500 Subject: [PATCH 24/35] Fix to support null-filter id on table-triggers --- .../PollingAutomationPerTableRunner.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java index 2bb0a470..52cc7646 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java @@ -280,17 +280,19 @@ public class PollingAutomationPerTableRunner implements Runnable try { - Integer filterId = tableTrigger.getFilterId(); - - GetInput getInput = new GetInput(); - getInput.setTableName(SavedFilter.TABLE_NAME); - getInput.setPrimaryKey(filterId); - GetOutput getOutput = new GetAction().execute(getInput); - QQueryFilter filter = null; - if(getOutput.getRecord() != null) + QQueryFilter filter = null; + Integer filterId = tableTrigger.getFilterId(); + if(filterId != null) { - SavedFilter savedFilter = new SavedFilter(getOutput.getRecord()); - filter = JsonUtils.toObject(savedFilter.getFilterJson(), QQueryFilter.class); + GetInput getInput = new GetInput(); + getInput.setTableName(SavedFilter.TABLE_NAME); + getInput.setPrimaryKey(filterId); + GetOutput getOutput = new GetAction().execute(getInput); + if(getOutput.getRecord() != null) + { + SavedFilter savedFilter = new SavedFilter(getOutput.getRecord()); + filter = JsonUtils.toObject(savedFilter.getFilterJson(), QQueryFilter.class); + } } rs.add(new TableAutomationAction() From 9d3cd50c7b11932e6dfb36a23eeb08bd304787a7 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 20 Jun 2023 11:36:34 -0500 Subject: [PATCH 25/35] Add 'tag' field to ApiProcessMetaData; use that when generating spec (for non-table processes for now) --- .../actions/GenerateOpenApiSpecAction.java | 15 +++++++-- .../processes/ApiProcessMetaData.java | 32 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 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 6fde1064..cf00a815 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 @@ -738,11 +738,20 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction Date: Tue, 20 Jun 2023 11:43:30 -0500 Subject: [PATCH 26/35] Add message, current, and total to get-job-status 202(ACCEPTED) response spec --- .../qqq/api/actions/GenerateOpenApiSpecAction.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 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 cf00a815..9908775f 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 @@ -1043,8 +1043,10 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction Date: Wed, 21 Jun 2023 16:16:38 -0500 Subject: [PATCH 27/35] Add labelMappings to instanceEnricher --- .../core/instances/QInstanceEnricher.java | 51 ++++++++++++++++++- .../core/instances/QInstanceEnricherTest.java | 10 ++++ 2 files changed, 60 insertions(+), 1 deletion(-) 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 e66c187d..81a64968 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 @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -97,6 +98,13 @@ public class QInstanceEnricher ////////////////////////////////////////////////////////// private boolean configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels = true; + ////////////////////////////////////////////////////////////////////////////////////////////////// + // let an instance define mappings to be applied during name-to-label enrichments, // + // e.g., to avoid ever incorrectly camel-casing an acronym (e.g., "Tla" shoudl always be "TLA") // + // or to expand abbreviations in code (e.g., "Addr" should always be "Address" // + ////////////////////////////////////////////////////////////////////////////////////////////////// + private static final Map labelMappings = new LinkedHashMap<>(); + /******************************************************************************* @@ -647,7 +655,17 @@ public class QInstanceEnricher //////////////////////////////////////////////////////////////// .replaceAll("([0-9])([A-Za-z])", "$1 $2"); - return (name.substring(0, 1).toUpperCase(Locale.ROOT) + suffix); + String label = name.substring(0, 1).toUpperCase(Locale.ROOT) + suffix; + + ///////////////////////////////////////////////////////////////////////////////////////////// + // apply any label mappings - e.g., to force app-specific acronyms/initialisms to all-caps // + ///////////////////////////////////////////////////////////////////////////////////////////// + for(Map.Entry entry : labelMappings.entrySet()) + { + label = label.replaceAll(entry.getKey(), entry.getValue()); + } + + return (label); } @@ -1111,4 +1129,35 @@ public class QInstanceEnricher { return (this.joinGraph); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void addLabelMapping(String from, String to) + { + labelMappings.put(from, to); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void removeLabelMapping(String from) + { + labelMappings.remove(from); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void clearLabelMappings() + { + labelMappings.clear(); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java index 4deb3559..2e3ff3ec 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java @@ -197,6 +197,16 @@ class QInstanceEnricherTest extends BaseTest assertEquals("Something USA", QInstanceEnricher.nameToLabel("somethingUSA")); assertEquals("Number 1 Dad", QInstanceEnricher.nameToLabel("number1Dad")); assertEquals("Number 417 Dad", QInstanceEnricher.nameToLabel("number417Dad")); + + assertEquals("Default Wms System Id", QInstanceEnricher.nameToLabel("defaultWmsSystemId")); + QInstanceEnricher.addLabelMapping("\\bWms\\b", "WMS"); + assertEquals("Default WMS System Id", QInstanceEnricher.nameToLabel("defaultWmsSystemId")); + QInstanceEnricher.clearLabelMappings(); + + assertEquals("Api Client Id", QInstanceEnricher.nameToLabel("apiClientId")); + QInstanceEnricher.addLabelMapping("\\bApi\\b", "API"); + assertEquals("API Client Id", QInstanceEnricher.nameToLabel("apiClientId")); + QInstanceEnricher.clearLabelMappings(); } From 6bfe0cd3eabd1bc96123e0e7444da5168e807d39 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 21 Jun 2023 16:17:01 -0500 Subject: [PATCH 28/35] Give error about null recordSecurityLock (vs null list of locks) --- .../qqq/backend/core/instances/QInstanceValidator.java | 5 +++++ 1 file changed, 5 insertions(+) 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 b9bb2d47..5b65f4c1 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 @@ -573,6 +573,11 @@ public class QInstanceValidator RECORD_SECURITY_LOCKS_LOOP: for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks())) { + if(!assertCondition(recordSecurityLock != null, prefix + "has a null recordSecurityLock (did you mean to give it a null list of locks?)")) + { + continue; + } + String securityKeyTypeName = recordSecurityLock.getSecurityKeyType(); if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType")) { From 900484c01cf6811b697cf35938fcd1dc3abf2b98 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 21 Jun 2023 16:17:32 -0500 Subject: [PATCH 29/35] More consistent behavior of field labels & descriptions everywhere --- .../actions/GenerateOpenApiSpecAction.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 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 9908775f..9fbb0663 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 @@ -913,7 +913,13 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction Date: Wed, 21 Jun 2023 16:17:53 -0500 Subject: [PATCH 30/35] For runProcess, send the input object through processBodyToJsonString (e.g., for js to java object conversion0 --- .../main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java | 1 + 1 file changed, 1 insertion(+) 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 47028cd8..10a104ce 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 @@ -299,6 +299,7 @@ public class ApiScriptUtils implements QCodeExecutorAware, Serializable validateApiNameAndVersion("runProcess(" + processApiName + ")"); Map paramMap = new LinkedHashMap<>(); + params = processBodyToJsonString(params); String paramsString = ValueUtils.getValueAsString(params); if(StringUtils.hasContent(paramsString)) { From a367ec717ce977ff678abe7858ed305e1564be1d Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 21 Jun 2023 16:18:13 -0500 Subject: [PATCH 31/35] Add `of` factory method --- .../actions/tables/aggregate/AggregateOperator.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateOperator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateOperator.java index 513e95c7..b530fe23 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateOperator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateOperator.java @@ -56,4 +56,14 @@ public enum AggregateOperator { return sqlPrefix; } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Aggregate of(String fieldName) + { + return (new Aggregate(fieldName, this)); + } } From 18c11d3869a6a947e78e18c6d0c83dc33dd323e0 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 21 Jun 2023 16:23:19 -0500 Subject: [PATCH 32/35] Fix NPEs --- .../api/actions/GenerateOpenApiSpecAction.java | 2 +- .../qqq/api/utils/ApiScriptUtils.java | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 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 9fbb0663..acb85f13 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 @@ -1083,7 +1083,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction paramMap = new LinkedHashMap<>(); - params = processBodyToJsonString(params); - String paramsString = ValueUtils.getValueAsString(params); - if(StringUtils.hasContent(paramsString)) + Map paramMap = new LinkedHashMap<>(); + + if(params != null) { - JSONObject paramsJSON = new JSONObject(paramsString); - for(String fieldName : paramsJSON.keySet()) + params = processBodyToJsonString(params); + String paramsString = ValueUtils.getValueAsString(params); + if(StringUtils.hasContent(paramsString)) { - paramMap.put(fieldName, paramsJSON.optString(fieldName)); + JSONObject paramsJSON = new JSONObject(paramsString); + for(String fieldName : paramsJSON.keySet()) + { + paramMap.put(fieldName, paramsJSON.optString(fieldName)); + } } } From 7eea0c08bb43d0cdb46c80db07ee5f5dbd456b1e Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Thu, 22 Jun 2023 10:19:08 -0500 Subject: [PATCH 33/35] update to get rid of a few warnings --- .../qqq/backend/core/actions/values/QValueFormatter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java index c58d78cc..85691e74 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java @@ -136,7 +136,7 @@ public class QValueFormatter { return formatValue(displayFormat, ValueUtils.getValueAsBigDecimal(value)); } - else if(e.getMessage().equals("d != java.math.BigDecimal")) + else if(e.getMessage().equals("d != java.math.BigDecimal") || e.getMessage().equals("d != java.lang.String")) { return formatValue(displayFormat, ValueUtils.getValueAsInteger(value)); } From a73bd40c6f3847bce6f56601fb11c385a6d0dab3 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Thu, 22 Jun 2023 10:32:56 -0500 Subject: [PATCH 34/35] dowgraded log to a debug --- .../java/com/kingsrook/qqq/backend/core/logging/QLogger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QLogger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QLogger.java index 5135b836..55d58086 100755 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QLogger.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/logging/QLogger.java @@ -609,7 +609,7 @@ public class QLogger { if(qException.hasLoggedLevel(level)) { - log(Level.INFO, "Downgrading log message from " + level.toString() + " to " + Level.INFO, t); + log(Level.DEBUG, "Downgrading log message from " + level.toString() + " to " + Level.INFO, t); return (Level.INFO); } } From bf80eb1845d2e60bc539f967b26fb3309c1fd13d Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Thu, 22 Jun 2023 10:38:40 -0500 Subject: [PATCH 35/35] Update versions for release --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5347aa5a..eebcd486 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ - 0.15.0-SNAPSHOT + 0.15.0 UTF-8 UTF-8