diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/TableCountExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/TableCountExecutor.java
new file mode 100644
index 00000000..0bc8d96f
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/TableCountExecutor.java
@@ -0,0 +1,103 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors;
+
+
+import java.util.Collections;
+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.tables.CountAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
+import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
+import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import com.kingsrook.qqq.middleware.javalin.executors.io.TableCountInput;
+import com.kingsrook.qqq.middleware.javalin.executors.io.TableCountOutputInterface;
+import org.apache.commons.lang3.BooleanUtils;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TableCountExecutor extends AbstractMiddlewareExecutor
+{
+ private static final QLogger LOG = QLogger.getLogger(TableCountExecutor.class);
+
+ protected static final Integer DEFAULT_QUERY_TIMEOUT_SECONDS = 60;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void execute(TableCountInput input, TableCountOutputInterface output) throws QException
+ {
+ try
+ {
+ ExecutorSessionUtils.setTableVariantInSession(input.getTableVariant());
+
+ CountInput countInput = new CountInput();
+ countInput.setTableName(input.getTableName());
+
+ PermissionsHelper.checkTablePermissionThrowing(countInput, TablePermissionSubType.READ);
+
+ countInput.setFilter(input.getFilter());
+ countInput.setQueryJoins(input.getJoins());
+ countInput.setIncludeDistinctCount(input.getIncludeDistinct());
+ countInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND);
+
+ if(countInput.getFilter() != null)
+ {
+ // todo
+ countInput.getFilter().interpretValues(Collections.emptyMap());
+ }
+
+ CountOutput countOutput = new CountAction().execute(countInput);
+ output.setCount(ValueUtils.getValueAsLong(countOutput.getCount()));
+
+ if(BooleanUtils.isTrue(input.getIncludeDistinct()))
+ {
+ output.setDistinctCount(ValueUtils.getValueAsLong(countOutput.getDistinctCount()));
+ }
+ }
+ catch(QException e)
+ {
+ QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class);
+ if(userFacingException != null)
+ {
+ throw userFacingException;
+ }
+
+ throw (e);
+ }
+ catch(Exception e)
+ {
+ throw (new QException("Unexpected error occurred while executing count query: " + e.getMessage(), e));
+ }
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/TableMetaDataExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/TableMetaDataExecutor.java
new file mode 100644
index 00000000..3c17294b
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/TableMetaDataExecutor.java
@@ -0,0 +1,67 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors;
+
+
+import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction;
+import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
+import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
+import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.middleware.javalin.executors.io.TableMetaDataInput;
+import com.kingsrook.qqq.middleware.javalin.executors.io.TableMetaDataOutputInterface;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TableMetaDataExecutor extends AbstractMiddlewareExecutor
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void execute(TableMetaDataInput input, TableMetaDataOutputInterface output) throws QException
+ {
+ com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput tableMetaDataInput = new com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput();
+
+ String tableName = input.getTableName();
+ QTableMetaData table = QContext.getQInstance().getTable(tableName);
+ if(table == null)
+ {
+ throw (new QNotFoundException("Table [" + tableName + "] was not found."));
+ }
+
+ tableMetaDataInput.setTableName(tableName);
+ PermissionsHelper.checkTablePermissionThrowing(tableMetaDataInput, TablePermissionSubType.READ);
+
+ TableMetaDataAction tableMetaDataAction = new TableMetaDataAction();
+ TableMetaDataOutput tableMetaDataOutput = tableMetaDataAction.execute(tableMetaDataInput);
+
+ output.setTableMetaData(tableMetaDataOutput.getTable());
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/TableQueryExecutor.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/TableQueryExecutor.java
new file mode 100644
index 00000000..e798972f
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/TableQueryExecutor.java
@@ -0,0 +1,109 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors;
+
+
+import java.util.Collections;
+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.tables.QueryAction;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
+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.utils.ExceptionUtils;
+import com.kingsrook.qqq.backend.javalin.QJavalinMetaData;
+import com.kingsrook.qqq.backend.javalin.QJavalinUtils;
+import com.kingsrook.qqq.middleware.javalin.executors.io.TableQueryInput;
+import com.kingsrook.qqq.middleware.javalin.executors.io.TableQueryOutputInterface;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TableQueryExecutor extends AbstractMiddlewareExecutor
+{
+ private static final QLogger LOG = QLogger.getLogger(TableQueryExecutor.class);
+
+ protected static final Integer DEFAULT_QUERY_TIMEOUT_SECONDS = 60;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void execute(TableQueryInput input, TableQueryOutputInterface output) throws QException
+ {
+ try
+ {
+ ExecutorSessionUtils.setTableVariantInSession(input.getTableVariant());
+
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(input.getTableName());
+
+ PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ);
+
+ queryInput.setFilter(input.getFilter());
+ queryInput.setQueryJoins(input.getJoins());
+ queryInput.setShouldGenerateDisplayValues(true);
+ queryInput.setShouldTranslatePossibleValues(true);
+ queryInput.setTimeoutSeconds(DEFAULT_QUERY_TIMEOUT_SECONDS); // todo param
+ queryInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND);
+
+ if(queryInput.getFilter() != null)
+ {
+ // todo
+ queryInput.getFilter().interpretValues(Collections.emptyMap());
+ }
+
+ if(queryInput.getFilter() == null || queryInput.getFilter().getLimit() == null)
+ {
+ QJavalinUtils.handleQueryNullLimit(QJavalinMetaData.of(QContext.getQInstance()), queryInput, null);
+ }
+
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ // todo not sure QValueFormatter.setBlobValuesToDownloadUrls(QContext.getQInstance().getTable(input.getTableName()), queryOutput.getRecords());
+
+ output.setRecords(queryOutput.getRecords());
+ }
+ catch(QException e)
+ {
+ QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class);
+ if(userFacingException != null)
+ {
+ throw userFacingException;
+ }
+
+ throw (e);
+ }
+ catch(Exception e)
+ {
+ throw (new QException("Unexpected error occurred while executing query: " + e.getMessage(), e));
+ }
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableCountInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableCountInput.java
new file mode 100644
index 00000000..36d3381d
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableCountInput.java
@@ -0,0 +1,63 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TableCountInput extends TableQueryOrCountInput
+{
+ private Boolean includeDistinct = false;
+
+
+
+ /*******************************************************************************
+ ** Getter for includeDistinct
+ *******************************************************************************/
+ public Boolean getIncludeDistinct()
+ {
+ return (this.includeDistinct);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for includeDistinct
+ *******************************************************************************/
+ public void setIncludeDistinct(Boolean includeDistinct)
+ {
+ this.includeDistinct = includeDistinct;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for includeDistinct
+ *******************************************************************************/
+ public TableCountInput withIncludeDistinct(Boolean includeDistinct)
+ {
+ this.includeDistinct = includeDistinct;
+ return (this);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableCountOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableCountOutputInterface.java
new file mode 100644
index 00000000..c6936dbe
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableCountOutputInterface.java
@@ -0,0 +1,40 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public interface TableCountOutputInterface extends AbstractMiddlewareOutputInterface
+{
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ void setCount(Long count);
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ void setDistinctCount(Long count);
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableMetaDataInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableMetaDataInput.java
new file mode 100644
index 00000000..4418f64f
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableMetaDataInput.java
@@ -0,0 +1,66 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TableMetaDataInput extends AbstractMiddlewareInput
+{
+ private String tableName;
+
+
+
+ /*******************************************************************************
+ ** Getter for tableName
+ **
+ *******************************************************************************/
+ public String getTableName()
+ {
+ return tableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for tableName
+ **
+ *******************************************************************************/
+ public void setTableName(String tableName)
+ {
+ this.tableName = tableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for tableName
+ **
+ *******************************************************************************/
+ public TableMetaDataInput withTableName(String tableName)
+ {
+ this.tableName = tableName;
+ return (this);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableMetaDataOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableMetaDataOutputInterface.java
new file mode 100644
index 00000000..1cc85005
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableMetaDataOutputInterface.java
@@ -0,0 +1,37 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public interface TableMetaDataOutputInterface extends AbstractMiddlewareOutputInterface
+{
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ void setTableMetaData(QFrontendTableMetaData tableMetaData);
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableQueryInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableQueryInput.java
new file mode 100644
index 00000000..5218537f
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableQueryInput.java
@@ -0,0 +1,31 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TableQueryInput extends TableQueryOrCountInput
+{
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableQueryOrCountInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableQueryOrCountInput.java
new file mode 100644
index 00000000..f3bd650e
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableQueryOrCountInput.java
@@ -0,0 +1,172 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.TableVariant;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public abstract class TableQueryOrCountInput extends AbstractMiddlewareInput
+{
+ private String tableName;
+ private QQueryFilter filter;
+ private List joins;
+
+ private TableVariant tableVariant;
+
+
+
+ /*******************************************************************************
+ ** Getter for tableName
+ **
+ *******************************************************************************/
+ public String getTableName()
+ {
+ return tableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for tableName
+ **
+ *******************************************************************************/
+ public void setTableName(String tableName)
+ {
+ this.tableName = tableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for tableName
+ **
+ *******************************************************************************/
+ public TableQueryOrCountInput withTableName(String tableName)
+ {
+ this.tableName = tableName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for filter
+ **
+ *******************************************************************************/
+ public QQueryFilter getFilter()
+ {
+ return filter;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for filter
+ **
+ *******************************************************************************/
+ public void setFilter(QQueryFilter filter)
+ {
+ this.filter = filter;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for filter
+ **
+ *******************************************************************************/
+ public TableQueryOrCountInput withFilter(QQueryFilter filter)
+ {
+ this.filter = filter;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for joins
+ *******************************************************************************/
+ public List getJoins()
+ {
+ return (this.joins);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for joins
+ *******************************************************************************/
+ public void setJoins(List joins)
+ {
+ this.joins = joins;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for joins
+ *******************************************************************************/
+ public TableQueryOrCountInput withJoins(List joins)
+ {
+ this.joins = joins;
+ return (this);
+ }
+
+
+ /*******************************************************************************
+ ** Getter for tableVariant
+ *******************************************************************************/
+ public TableVariant getTableVariant()
+ {
+ return (this.tableVariant);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for tableVariant
+ *******************************************************************************/
+ public void setTableVariant(TableVariant tableVariant)
+ {
+ this.tableVariant = tableVariant;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for tableVariant
+ *******************************************************************************/
+ public TableQueryOrCountInput withTableVariant(TableVariant tableVariant)
+ {
+ this.tableVariant = tableVariant;
+ return (this);
+ }
+
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableQueryOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableQueryOutputInterface.java
new file mode 100644
index 00000000..93776a30
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/TableQueryOutputInterface.java
@@ -0,0 +1,38 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public interface TableQueryOutputInterface extends AbstractMiddlewareOutputInterface
+{
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ void setRecords(List records);
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MiddlewareVersionV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MiddlewareVersionV1.java
index 6ea85ed3..ab3eeba4 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MiddlewareVersionV1.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MiddlewareVersionV1.java
@@ -44,6 +44,7 @@ public class MiddlewareVersionV1 extends AbstractMiddlewareVersion
list.add(new TableMetaDataSpecV1());
list.add(new TableQuerySpecV1());
+ list.add(new TableCountSpecV1());
list.add(new ProcessMetaDataSpecV1());
list.add(new ProcessInitSpecV1());
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableCountSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableCountSpecV1.java
new file mode 100644
index 00000000..350d2b62
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableCountSpecV1.java
@@ -0,0 +1,163 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1;
+
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.middleware.javalin.executors.TableCountExecutor;
+import com.kingsrook.qqq.middleware.javalin.executors.io.TableCountInput;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.TableCountResponseV1;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.QuerySpecUtils;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
+import com.kingsrook.qqq.openapi.model.Example;
+import com.kingsrook.qqq.openapi.model.HttpMethod;
+import com.kingsrook.qqq.openapi.model.In;
+import com.kingsrook.qqq.openapi.model.Parameter;
+import com.kingsrook.qqq.openapi.model.RequestBody;
+import com.kingsrook.qqq.openapi.model.Schema;
+import com.kingsrook.qqq.openapi.model.Type;
+import io.javalin.http.Context;
+import org.json.JSONObject;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TableCountSpecV1 extends AbstractEndpointSpec
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public BasicOperation defineBasicOperation()
+ {
+ return new BasicOperation()
+ .withPath("/table/{tableName}/count")
+ .withHttpMethod(HttpMethod.POST)
+ .withTag(TagsV1.TABLES)
+ .withShortSummary("Count records in a table")
+ .withLongDescription("""
+ Execute a query against a table, returning the number of records that match a filter."""
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public List defineRequestParameters()
+ {
+ return List.of(
+ new Parameter()
+ .withName("tableName")
+ .withDescription("Name of the table to count.")
+ .withRequired(true)
+ .withSchema(new Schema().withType(Type.STRING))
+ .withExample("person")
+ .withIn(In.PATH),
+ new Parameter()
+ .withName("includeDistinct")
+ .withDescription("Whether or not to also return the count distinct records from the main table (e.g., in case of a to-many join; by default, do not).")
+ .withRequired(true)
+ .withSchema(new Schema().withType(Type.BOOLEAN))
+ .withExample("true")
+ .withIn(In.QUERY)
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public RequestBody defineRequestBody()
+ {
+ return (QuerySpecUtils.defineQueryOrCountRequestBody());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public TableCountInput buildInput(Context context) throws Exception
+ {
+ TableCountInput input = new TableCountInput();
+ input.setTableName(getRequestParam(context, "tableName"));
+
+ JSONObject requestBody = getRequestBodyAsJsonObject(context);
+ if(requestBody != null)
+ {
+ input.setFilter(QuerySpecUtils.getFilterFromRequestBody(requestBody));
+ input.setJoins(QuerySpecUtils.getJoinsFromRequestBody(requestBody));
+ input.setTableVariant(QuerySpecUtils.getTableVariantFromRequestBody(requestBody));
+ }
+
+ String includeDistinctParam = getRequestParam(context, "includeDistinct");
+ if("true".equals(includeDistinctParam))
+ {
+ input.setIncludeDistinct(true);
+ }
+
+ return (input);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public Map defineComponentSchemas()
+ {
+ return Map.of(TableCountResponseV1.class.getSimpleName(), new TableCountResponseV1().toSchema());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public BasicResponse defineBasicSuccessResponse()
+ {
+ Map examples = new LinkedHashMap<>();
+ examples.put("TODO", new Example()
+ .withValue(new TableCountResponseV1().withCount(42L)));
+
+ return new BasicResponse("""
+ The number (count) of records matching the query""",
+ TableCountResponseV1.class.getSimpleName(),
+ examples
+ );
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableMetaDataSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableMetaDataSpecV1.java
new file mode 100644
index 00000000..4a52c6ca
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableMetaDataSpecV1.java
@@ -0,0 +1,143 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1;
+
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.middleware.javalin.executors.TableMetaDataExecutor;
+import com.kingsrook.qqq.middleware.javalin.executors.io.TableMetaDataInput;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.TableMetaDataResponseV1;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
+import com.kingsrook.qqq.openapi.model.Example;
+import com.kingsrook.qqq.openapi.model.HttpMethod;
+import com.kingsrook.qqq.openapi.model.In;
+import com.kingsrook.qqq.openapi.model.Parameter;
+import com.kingsrook.qqq.openapi.model.Schema;
+import com.kingsrook.qqq.openapi.model.Type;
+import io.javalin.http.Context;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TableMetaDataSpecV1 extends AbstractEndpointSpec
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public BasicOperation defineBasicOperation()
+ {
+ return new BasicOperation()
+ .withPath("/metaData/table/{tableName}")
+ .withHttpMethod(HttpMethod.GET)
+ .withTag(TagsV1.TABLES)
+ .withShortSummary("Get table metaData")
+ .withLongDescription("""
+ Load the full metadata for a single table, including all fields, which a frontend
+ needs to display to users."""
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public List defineRequestParameters()
+ {
+ return List.of(
+ new Parameter()
+ .withName("tableName")
+ .withDescription("Name of the table to load.")
+ .withRequired(true)
+ .withSchema(new Schema().withType(Type.STRING))
+ .withExample("person")
+ .withIn(In.PATH)
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public TableMetaDataInput buildInput(Context context) throws Exception
+ {
+ TableMetaDataInput input = new TableMetaDataInput();
+ input.setTableName(getRequestParam(context, "tableName"));
+ return (input);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public Map defineComponentSchemas()
+ {
+ return Map.of(TableMetaDataResponseV1.class.getSimpleName(), new TableMetaDataResponseV1().toSchema());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public BasicResponse defineBasicSuccessResponse()
+ {
+ QFrontendTableMetaData frontendTableMetaData = null; // todo
+
+ Map examples = new LinkedHashMap<>();
+ examples.put("TODO", new Example()
+ .withValue(new TableMetaDataResponseV1().withTableMetaData(frontendTableMetaData)));
+
+ return new BasicResponse("""
+ The full table metadata""",
+ TableMetaDataResponseV1.class.getSimpleName(),
+ examples
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void handleOutput(Context context, TableMetaDataResponseV1 output) throws Exception
+ {
+ context.result(JsonUtils.toJson(output.getTableMetaData()));
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableQuerySpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableQuerySpecV1.java
new file mode 100644
index 00000000..37bbc478
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableQuerySpecV1.java
@@ -0,0 +1,181 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1;
+
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.middleware.javalin.executors.TableQueryExecutor;
+import com.kingsrook.qqq.middleware.javalin.executors.io.TableQueryInput;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
+import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.TableQueryResponseV1;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.QuerySpecUtils;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.utils.TagsV1;
+import com.kingsrook.qqq.openapi.model.Example;
+import com.kingsrook.qqq.openapi.model.HttpMethod;
+import com.kingsrook.qqq.openapi.model.In;
+import com.kingsrook.qqq.openapi.model.Parameter;
+import com.kingsrook.qqq.openapi.model.RequestBody;
+import com.kingsrook.qqq.openapi.model.Schema;
+import com.kingsrook.qqq.openapi.model.Type;
+import io.javalin.http.Context;
+import org.json.JSONObject;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TableQuerySpecV1 extends AbstractEndpointSpec
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public BasicOperation defineBasicOperation()
+ {
+ return new BasicOperation()
+ .withPath("/table/{tableName}/query")
+ .withHttpMethod(HttpMethod.POST)
+ .withTag(TagsV1.TABLES)
+ .withShortSummary("Query for records from a table")
+ .withLongDescription("""
+ Execute a query against a table, returning records that match a filter."""
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public List defineRequestParameters()
+ {
+ return List.of(
+ new Parameter()
+ .withName("tableName")
+ .withDescription("Name of the table to query.")
+ .withRequired(true)
+ .withSchema(new Schema().withType(Type.STRING))
+ .withExample("person")
+ .withIn(In.PATH)
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public RequestBody defineRequestBody()
+ {
+ return (QuerySpecUtils.defineQueryOrCountRequestBody());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public TableQueryInput buildInput(Context context) throws Exception
+ {
+ TableQueryInput input = new TableQueryInput();
+ input.setTableName(getRequestParam(context, "tableName"));
+
+ JSONObject requestBody = getRequestBodyAsJsonObject(context);
+ if(requestBody != null)
+ {
+ input.setFilter(QuerySpecUtils.getFilterFromRequestBody(requestBody));
+ input.setJoins(QuerySpecUtils.getJoinsFromRequestBody(requestBody));
+ input.setTableVariant(QuerySpecUtils.getTableVariantFromRequestBody(requestBody));
+ }
+
+ return (input);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public Map defineComponentSchemas()
+ {
+ return Map.of(TableQueryResponseV1.class.getSimpleName(), new TableQueryResponseV1().toSchema());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public BasicResponse defineBasicSuccessResponse()
+ {
+ Map examples = new LinkedHashMap<>();
+ examples.put("TODO", new Example()
+ .withValue(new TableQueryResponseV1().withRecords(List.of(new QRecord()
+ .withRecordLabel("Item 17")
+ .withTableName("item")
+ .withValue("id", 17).withValue("quantity", 1000).withValue("storeId", 42)
+ .withDisplayValue("id", "17").withDisplayValue("quantity", "1,000").withDisplayValue("storeId", "QQQ-Mart")
+ ))));
+
+ return new BasicResponse("""
+ The records matching query""",
+ TableQueryResponseV1.class.getSimpleName(),
+ examples
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void handleOutput(Context context, TableQueryResponseV1 tableQueryResponseV1) throws Exception
+ {
+ if(CollectionUtils.nullSafeIsEmpty(tableQueryResponseV1.getRecords()))
+ {
+ ////////////////////////////////////////////////////////////////////////////////////
+ // special case here, where we want an empty list to be returned for the case //
+ // with no records found by default our serialization doesn't include empty lists //
+ ////////////////////////////////////////////////////////////////////////////////////
+ context.result(JsonUtils.toJson(tableQueryResponseV1, objectMapper -> objectMapper
+ .setSerializationInclusion(JsonInclude.Include.ALWAYS)));
+ }
+ else
+ {
+ super.handleOutput(context, tableQueryResponseV1);
+ }
+ }
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/TableCountResponseV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/TableCountResponseV1.java
new file mode 100644
index 00000000..5e46ac47
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/TableCountResponseV1.java
@@ -0,0 +1,102 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
+
+
+import com.kingsrook.qqq.middleware.javalin.executors.io.TableCountOutputInterface;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TableCountResponseV1 implements TableCountOutputInterface, ToSchema
+{
+ @OpenAPIDescription("Number (count) of records that satisfy the query request")
+ private Long count;
+
+ @OpenAPIDescription("Number (count) of distinct records that satisfy the query request. Only included if requested.")
+ private Long distinctCount;
+
+
+ /*******************************************************************************
+ ** Getter for count
+ *******************************************************************************/
+ public Long getCount()
+ {
+ return (this.count);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for count
+ *******************************************************************************/
+ public void setCount(Long count)
+ {
+ this.count = count;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for count
+ *******************************************************************************/
+ public TableCountResponseV1 withCount(Long count)
+ {
+ this.count = count;
+ return (this);
+ }
+
+
+ /*******************************************************************************
+ ** Getter for distinctCount
+ *******************************************************************************/
+ public Long getDistinctCount()
+ {
+ return (this.distinctCount);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for distinctCount
+ *******************************************************************************/
+ public void setDistinctCount(Long distinctCount)
+ {
+ this.distinctCount = distinctCount;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for distinctCount
+ *******************************************************************************/
+ public TableCountResponseV1 withDistinctCount(Long distinctCount)
+ {
+ this.distinctCount = distinctCount;
+ return (this);
+ }
+
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/TableMetaDataResponseV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/TableMetaDataResponseV1.java
new file mode 100644
index 00000000..780891da
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/TableMetaDataResponseV1.java
@@ -0,0 +1,84 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
+
+
+import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
+import com.kingsrook.qqq.middleware.javalin.executors.io.TableMetaDataOutputInterface;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.SchemaBuilder;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.TableMetaData;
+import com.kingsrook.qqq.openapi.model.Schema;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TableMetaDataResponseV1 implements TableMetaDataOutputInterface, ToSchema
+{
+ private TableMetaData tableMetaData;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setTableMetaData(QFrontendTableMetaData frontendTableMetaData)
+ {
+ this.tableMetaData = new TableMetaData(frontendTableMetaData);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for frontendTableMetaData
+ **
+ *******************************************************************************/
+ public TableMetaDataResponseV1 withTableMetaData(QFrontendTableMetaData frontendTableMetaData)
+ {
+ setTableMetaData(frontendTableMetaData);
+ return (this);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public Schema toSchema()
+ {
+ return new SchemaBuilder().classToSchema(TableMetaData.class);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for tableMetaData
+ **
+ *******************************************************************************/
+ public TableMetaData getTableMetaData()
+ {
+ return tableMetaData;
+ }
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/TableQueryResponseV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/TableQueryResponseV1.java
new file mode 100644
index 00000000..ab8dd426
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/TableQueryResponseV1.java
@@ -0,0 +1,82 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses;
+
+
+import java.util.List;
+import java.util.stream.Collectors;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.middleware.javalin.executors.io.TableQueryOutputInterface;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.OutputRecord;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TableQueryResponseV1 implements TableQueryOutputInterface, ToSchema
+{
+ @OpenAPIDescription("List of records that satisfy the query request")
+ @OpenAPIListItems(value = OutputRecord.class, useRef = true)
+ private List records;
+
+
+
+ /*******************************************************************************
+ ** Setter for records
+ *******************************************************************************/
+ @Override
+ public void setRecords(List records)
+ {
+ if(records == null)
+ {
+ this.records = null;
+ }
+ else
+ {
+ this.records = records.stream().map(qr -> new OutputRecord(qr)).collect(Collectors.toList());
+ }
+ }
+
+
+ /*******************************************************************************
+ ** Setter for records
+ *******************************************************************************/
+ public TableQueryResponseV1 withRecords(List records)
+ {
+ setRecords(records);
+ return this;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for records
+ *******************************************************************************/
+ public List getRecords()
+ {
+ return (this.records);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/ExposedJoin.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/ExposedJoin.java
new file mode 100644
index 00000000..88569e60
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/ExposedJoin.java
@@ -0,0 +1,108 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendExposedJoin;
+import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
+
+
+/***************************************************************************
+ **
+ ***************************************************************************/
+public class ExposedJoin implements ToSchema
+{
+ @OpenAPIExclude()
+ private QFrontendExposedJoin wrapped;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public ExposedJoin(QFrontendExposedJoin section)
+ {
+ this.wrapped = section;
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public ExposedJoin()
+ {
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("User-facing label to display for this join")
+ public String getLabel()
+ {
+ return (this.wrapped.getLabel());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Whether or not this join is 'to many' in nature")
+ public Boolean getIsMany()
+ {
+ return (this.wrapped.getIsMany());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("The meta-data for the joined table")
+ public TableMetaData getJoinTable()
+ {
+ return (new TableMetaData(this.wrapped.getJoinTable()));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("A list of joins that travel from the base table to the exposed join table")
+ @OpenAPIListItems(value = QJoinMetaData.class, useRef = false)
+ public List getJoinPath()
+ {
+ return (this.wrapped.getJoinPath());
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FilterCriteria.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FilterCriteria.java
new file mode 100644
index 00000000..611ddacd
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/FilterCriteria.java
@@ -0,0 +1,99 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
+
+
+import java.util.List;
+import java.util.stream.Collectors;
+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.utils.CollectionUtils;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
+
+
+/***************************************************************************
+ **
+ ***************************************************************************/
+public class FilterCriteria implements ToSchema
+{
+ @OpenAPIExclude()
+ private QFilterCriteria wrapped;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public FilterCriteria(QFilterCriteria wrapped)
+ {
+ this.wrapped = wrapped;
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public FilterCriteria()
+ {
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Field name that this criteria applies to")
+ public String getFieldName()
+ {
+ return (this.wrapped.getFieldName());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Logical operator that applies to this criteria")
+ public QCriteriaOperator getOperator()
+ {
+ return (this.wrapped.getOperator());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Values to apply via the operator to the field")
+ @OpenAPIListItems(value = String.class)
+ public List getValues()
+ {
+ return (CollectionUtils.nonNullList(this.wrapped.getValues()).stream().map(String::valueOf).collect(Collectors.toList()));
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/OrderBy.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/OrderBy.java
new file mode 100644
index 00000000..41ea518f
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/OrderBy.java
@@ -0,0 +1,82 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
+
+
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
+
+
+/***************************************************************************
+ **
+ ***************************************************************************/
+public class OrderBy implements ToSchema
+{
+ @OpenAPIExclude()
+ private QFilterOrderBy wrapped;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public OrderBy(QFilterOrderBy wrapped)
+ {
+ this.wrapped = wrapped;
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public OrderBy()
+ {
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Field name that this order-by applies to")
+ public String getFieldName()
+ {
+ return (this.wrapped.getFieldName());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Whether this order-by is ascending or descending")
+ public Boolean getIsAscending()
+ {
+ return (this.wrapped.getIsAscending());
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/OutputRecord.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/OutputRecord.java
new file mode 100644
index 00000000..7f1ed43e
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/OutputRecord.java
@@ -0,0 +1,110 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
+
+
+import java.io.Serializable;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
+
+
+/***************************************************************************
+ **
+ ***************************************************************************/
+public class OutputRecord implements ToSchema
+{
+ @OpenAPIExclude()
+ private QRecord wrapped;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public OutputRecord(QRecord wrapped)
+ {
+ this.wrapped = wrapped;
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public OutputRecord()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for tableName
+ **
+ *******************************************************************************/
+ @OpenAPIDescription("Name of the table that the record is from.")
+ public String getTableName()
+ {
+ return this.wrapped.getTableName();
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for tableName
+ **
+ *******************************************************************************/
+ @OpenAPIDescription("Label to identify the record to a user.")
+ public String getRecordLabel()
+ {
+ return this.wrapped.getRecordLabel();
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for values
+ **
+ *******************************************************************************/
+ @OpenAPIDescription("Raw values that make up the record. Keys are Strings, which match the table's field names. Values can be any type, as per the table's fields.")
+ public Map getValues()
+ {
+ return this.wrapped.getValues();
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for displayValues
+ **
+ *******************************************************************************/
+ @OpenAPIDescription("Formatted string versions of the values that make up the record. Keys are Strings, which match the table's field names. Values are all Strings.")
+ public Map getDisplayValues()
+ {
+ return this.wrapped.getDisplayValues();
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/QueryFilter.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/QueryFilter.java
new file mode 100644
index 00000000..7e729418
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/QueryFilter.java
@@ -0,0 +1,120 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
+
+
+/***************************************************************************
+ **
+ ***************************************************************************/
+public class QueryFilter implements ToSchema
+{
+ @OpenAPIExclude()
+ private QQueryFilter wrapped;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public QueryFilter(QQueryFilter wrapped)
+ {
+ this.wrapped = wrapped;
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public QueryFilter()
+ {
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Field level criteria that make up the query filter")
+ @OpenAPIListItems(value = FilterCriteria.class, useRef = true)
+ public List getCriteria()
+ {
+ return (CollectionUtils.nonNullList(this.wrapped.getCriteria()).stream().map(s -> new FilterCriteria(s)).toList());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("How the query's results should be ordered (sorted).")
+ @OpenAPIListItems(value = OrderBy.class, useRef = true)
+ public List getOrderBys()
+ {
+ return (CollectionUtils.nonNullList(this.wrapped.getOrderBys()).stream().map(s -> new OrderBy(s)).toList());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Boolean operator to apply between criteria")
+ public QQueryFilter.BooleanOperator getBooleanOperator()
+ {
+ return (this.wrapped.getBooleanOperator());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Records to skip (e.g., to implement pagination)")
+ public Integer getSkip()
+ {
+ return (this.wrapped.getSkip());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Maximum number of results to return.")
+ public Integer getLimit()
+ {
+ return (this.wrapped.getLimit());
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/QueryJoin.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/QueryJoin.java
new file mode 100644
index 00000000..59f7ffe3
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/QueryJoin.java
@@ -0,0 +1,125 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
+
+
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
+
+
+/***************************************************************************
+ **
+ ***************************************************************************/
+public class QueryJoin implements ToSchema
+{
+ @OpenAPIExclude()
+ private com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin wrapped;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public QueryJoin(com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin wrapped)
+ {
+ this.wrapped = wrapped;
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public QueryJoin()
+ {
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Table being joined into the query by this QueryJoin")
+ public String getJoinTable()
+ {
+ return (this.wrapped.getJoinTable());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Base table (or an alias) that this QueryJoin is joined against")
+ public String getBaseTableOrAlias()
+ {
+ return (this.wrapped.getBaseTableOrAlias());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Name of a join to use in case the baseTable and joinTable have more than one")
+ public String getJoinName()
+ {
+ return (this.wrapped.getJoinMetaData() == null ? null : this.wrapped.getJoinMetaData().getName());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Alias to apply to this table in the join query")
+ public String getAlias()
+ {
+ return (this.wrapped.getAlias());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Whether or not to select values from the join table")
+ public Boolean getSelect()
+ {
+ return (this.wrapped.getSelect());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Type of join being performed (SQL semantics)")
+ public com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin.Type getType()
+ {
+ return (this.wrapped.getType());
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableMetaData.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableMetaData.java
new file mode 100644
index 00000000..893afda5
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableMetaData.java
@@ -0,0 +1,158 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
+
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QSupplementalTableMetaData;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIIncludeProperties;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIListItems;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMapValueType;
+
+
+/***************************************************************************
+ **
+ ***************************************************************************/
+@OpenAPIIncludeProperties(ancestorClasses = TableMetaDataLight.class)
+public class TableMetaData extends TableMetaDataLight implements ToSchema
+{
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public TableMetaData(QFrontendTableMetaData wrapped)
+ {
+ super(wrapped);
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public TableMetaData()
+ {
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Fields in this table")
+ @OpenAPIMapValueType(value = FieldMetaData.class, useRef = true)
+ public Map getFields()
+ {
+ return (CollectionUtils.nonNullMap(this.wrapped.getFields()).values().stream()
+ .collect(Collectors.toMap(f -> f.getName(), f -> new FieldMetaData(f))));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Name of the primary key field in this table")
+ public String getPrimaryKeyField()
+ {
+ return (wrapped.getPrimaryKeyField());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Whether or not this table's backend uses variants.")
+ public Boolean getUsesVariants()
+ {
+ return (wrapped.getUsesVariants());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Sections to organize fields on screens for this record")
+ @OpenAPIListItems(value = TableSection.class, useRef = true)
+ public List getSections()
+ {
+ if(wrapped.getSections() == null)
+ {
+ return (null);
+ }
+
+ return (wrapped.getSections().stream().map(s -> new TableSection(s)).toList());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Sections to organize fields on screens for this record")
+ @OpenAPIListItems(value = ExposedJoin.class, useRef = true)
+ public List getExposedJoins()
+ {
+ if(wrapped.getExposedJoins() == null)
+ {
+ return (null);
+ }
+
+ return (wrapped.getExposedJoins().stream().map(s -> new ExposedJoin(s)).toList());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Additional meta data about the table, not necessarily known to QQQ.")
+ public Map getSupplementalMetaData()
+ {
+ return (wrapped.getSupplementalTableMetaData());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("For tables that support the sharing feature, meta-data about the sharing setup.")
+ @OpenAPIListItems(value = ShareableTableMetaData.class, useRef = false)
+ public ShareableTableMetaData getShareableTableMetaData()
+ {
+ return (wrapped.getShareableTableMetaData());
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableMetaDataLight.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableMetaDataLight.java
index c9ea3a8a..e70df644 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableMetaDataLight.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableMetaDataLight.java
@@ -40,7 +40,7 @@ import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIMap
public class TableMetaDataLight implements ToSchema
{
@OpenAPIExclude()
- private QFrontendTableMetaData wrapped;
+ protected QFrontendTableMetaData wrapped;
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableSection.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableSection.java
new file mode 100644
index 00000000..17d18917
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableSection.java
@@ -0,0 +1,161 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIExclude;
+
+
+/***************************************************************************
+ **
+ ***************************************************************************/
+public class TableSection implements ToSchema
+{
+ @OpenAPIExclude()
+ private QFieldSection wrapped;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public TableSection(QFieldSection section)
+ {
+ this.wrapped = section;
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public TableSection()
+ {
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Unique identifier for this section within this table")
+ public String getName()
+ {
+ return (this.wrapped.getName());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("User-facing label to display for this section")
+ public String getLabel()
+ {
+ return (this.wrapped.getLabel());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Importance of this section (T1, T2, or T3)")
+ public String getTier()
+ {
+ return (this.wrapped.getTier().name());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("List of field names to include in this section.")
+ public List getFieldNames()
+ {
+ return (this.wrapped.getFieldNames());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Name of a widget in this QQQ instance to include in this section (instead of fields).")
+ public String getWidgetName()
+ {
+ return (this.wrapped.getWidgetName());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Icon to display for the table")
+ public Icon getIcon()
+ {
+ return (this.wrapped.getIcon() == null ? null : new Icon(this.wrapped.getIcon()));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Whether or not to hide this section")
+ public Boolean isHidden()
+ {
+ return (this.wrapped.getIsHidden());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Layout suggestion, for how many columns of a 12-grid this section should use.")
+ public Integer getGridColumns()
+ {
+ return (this.wrapped.getGridColumns());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @OpenAPIDescription("Help Contents for this section table.") // todo describe more
+ public List getHelpContents()
+ {
+ return (this.wrapped.getHelpContents());
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableVariant.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableVariant.java
new file mode 100644
index 00000000..7ea932e6
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/responses/components/TableVariant.java
@@ -0,0 +1,141 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components;
+
+
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
+import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+
+
+/***************************************************************************
+ **
+ ***************************************************************************/
+public class TableVariant implements ToSchema
+{
+ @OpenAPIDescription("""
+ Specification for the variant type (variantTypeKey from QQQ BackendVariantsConfig)""")
+ private String type;
+
+ @OpenAPIDescription("""
+ Identifier for the variant option (record) being used.""")
+ private String id;
+
+ @OpenAPIDescription("""
+ A user-facing name (or label) for the variant option being used.""")
+ private String name;
+
+
+
+
+ /*******************************************************************************
+ ** 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 TableVariant withType(String type)
+ {
+ this.type = type;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for id
+ *******************************************************************************/
+ public String getId()
+ {
+ return (this.id);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for id
+ *******************************************************************************/
+ public void setId(String id)
+ {
+ this.id = id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for id
+ *******************************************************************************/
+ public TableVariant withId(String id)
+ {
+ this.id = id;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for name
+ *******************************************************************************/
+ public String getName()
+ {
+ return (this.name);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for name
+ *******************************************************************************/
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for name
+ *******************************************************************************/
+ public TableVariant withName(String name)
+ {
+ this.name = name;
+ return (this);
+ }
+
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/QuerySpecUtils.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/QuerySpecUtils.java
index d648ba54..eb2130ed 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/QuerySpecUtils.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/QuerySpecUtils.java
@@ -24,20 +24,19 @@ package com.kingsrook.qqq.middleware.javalin.specs.v1.utils;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.context.QContext;
-import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
-import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
+import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
-import com.kingsrook.qqq.backend.core.utils.StringUtils;
-import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
-import com.kingsrook.qqq.middleware.javalin.executors.io.QueryMiddlewareInput;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.TableVariant;
+import com.kingsrook.qqq.openapi.model.Content;
+import com.kingsrook.qqq.openapi.model.RequestBody;
import com.kingsrook.qqq.openapi.model.Schema;
import com.kingsrook.qqq.openapi.model.Type;
+import io.javalin.http.ContentType;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -51,99 +50,28 @@ public class QuerySpecUtils
/***************************************************************************
**
***************************************************************************/
- public static Schema defineQueryJoinsSchema()
+ public static RequestBody defineQueryOrCountRequestBody()
{
- Schema queryJoinsSchema = new Schema()
- .withType(Type.ARRAY)
- .withItems(new Schema()
- .withProperties(MapBuilder.of(
- "joinTable", new Schema()
- .withType(Type.STRING),
- "select", new Schema()
- .withType(Type.BOOLEAN),
- "type", new Schema()
- .withType(Type.STRING)
- .withEnumValues(Arrays.stream(QueryJoin.Type.values()).map(o -> o.name()).toList()),
- "alias", new Schema()
- .withType(Type.STRING),
- "baseTableOrAlias", new Schema()
- .withType(Type.STRING)
- ))
- );
- return queryJoinsSchema;
- }
-
-
-
- /***************************************************************************
- **
- ***************************************************************************/
- public static Schema defineQQueryFilterSchema()
- {
- Schema qQueryFilterSchema = new Schema()
- .withType(Type.OBJECT)
- .withExample(List.of(
- JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.LESS_THAN, 5)))
- ))
- .withProperties(MapBuilder.of(
- "criteria", new Schema()
- .withType(Type.ARRAY)
- .withItems(new Schema()
- .withProperties(MapBuilder.of(
- "fieldName", new Schema()
- .withType(Type.STRING),
- "operator", new Schema()
- .withType(Type.STRING)
- .withEnumValues(Arrays.stream(QCriteriaOperator.values()).map(o -> o.name()).toList()),
- "values", new Schema()
- .withType(Type.ARRAY)
- .withItems(new Schema().withOneOf(List.of(
- new Schema().withType(Type.INTEGER),
- new Schema().withType(Type.STRING)
- )))
- ))
- ),
- "orderBys", new Schema()
- .withType(Type.ARRAY)
- .withItems(new Schema()
- .withProperties(MapBuilder.of(
- "fieldName", new Schema()
- .withType(Type.STRING),
- "isAscending", new Schema()
- .withType(Type.BOOLEAN)))
- ),
- "booleanOperator", new Schema().withType(Type.STRING).withEnumValues(Arrays.stream(QQueryFilter.BooleanOperator.values()).map(o -> o.name()).toList()),
- "skip", new Schema().withType(Type.INTEGER),
- "limit", new Schema().withType(Type.INTEGER)
- // todo - subfilters??
- ));
- return qQueryFilterSchema;
- }
-
-
-
- /***************************************************************************
- **
- ***************************************************************************/
- public static Schema getQueryResponseSchema()
- {
- Schema schema = new Schema()
- .withDescription("Records found by the query. May be empty.")
- .withType(Type.OBJECT)
- .withProperties(MapBuilder.of(
- "records", new Schema()
- .withType(Type.ARRAY)
- .withItems(new Schema()
+ return new RequestBody()
+ .withContent(Map.of(
+ ContentType.APPLICATION_JSON.getMimeType(), new Content()
+ .withSchema(new Schema()
.withType(Type.OBJECT)
- .withProperties(MapBuilder.of(
- "recordLabel", new Schema().withType(Type.STRING),
- "tableName", new Schema().withType(Type.STRING),
- "values", new Schema().withType(Type.OBJECT).withDescription("Keys for each field in the table"),
- "displayValues", new Schema().withType(Type.OBJECT)
+ .withProperties(Map.of(
+ "filter", new Schema()
+ .withDescription("QueryFilter to specify matching records to be returned by the query")
+ .withRef("#/components/schemas/QueryFilter"),
+ "joins", new Schema()
+ .withDescription("QueryJoin objects to specify tables to be joined into the query")
+ .withType(Type.ARRAY)
+ .withItems(new Schema()
+ .withRef("#/components/schemas/QueryJoin")),
+ "tableVariant", new Schema()
+ .withDescription("For tables that use variant backends, specification of which variant to use.")
+ .withRef("#/components/schemas/TableVariant")
))
)
));
- return schema;
}
@@ -151,58 +79,76 @@ public class QuerySpecUtils
/***************************************************************************
**
***************************************************************************/
- public static QueryMiddlewareInput buildInput(Map paramMap) throws IOException
+ public static QQueryFilter getFilterFromRequestBody(JSONObject requestBody) throws IOException
{
-
- QQueryFilter filter = null;
- String filterParam = paramMap.get("filter");
- if(StringUtils.hasContent(filterParam))
+ if(requestBody.has("filter"))
{
- filter = JsonUtils.toObject(filterParam, QQueryFilter.class);
- }
-
- List queryJoins = null;
- String queryJoinsParam = paramMap.get("queryJoins");
- if(StringUtils.hasContent(queryJoinsParam))
- {
- queryJoins = new ArrayList<>();
-
- JSONArray queryJoinsJSON = new JSONArray(queryJoinsParam);
- for(int i = 0; i < queryJoinsJSON.length(); i++)
+ Object filterFromJson = requestBody.get("filter");
+ if(filterFromJson instanceof JSONObject filterJsonObject)
{
- QueryJoin queryJoin = new QueryJoin();
- queryJoins.add(queryJoin);
-
- JSONObject jsonObject = queryJoinsJSON.getJSONObject(i);
- queryJoin.setJoinTable(jsonObject.optString("joinTable"));
-
- if(jsonObject.has("baseTableOrAlias") && !jsonObject.isNull("baseTableOrAlias"))
- {
- queryJoin.setBaseTableOrAlias(jsonObject.optString("baseTableOrAlias"));
- }
-
- if(jsonObject.has("alias") && !jsonObject.isNull("alias"))
- {
- queryJoin.setAlias(jsonObject.optString("alias"));
- }
-
- queryJoin.setSelect(jsonObject.optBoolean("select"));
-
- if(jsonObject.has("type") && !jsonObject.isNull("type"))
- {
- queryJoin.setType(QueryJoin.Type.valueOf(jsonObject.getString("type")));
- }
-
- if(jsonObject.has("joinName") && !jsonObject.isNull("joinName"))
- {
- queryJoin.setJoinMetaData(QContext.getQInstance().getJoin(jsonObject.getString("joinName")));
- }
+ return (JsonUtils.toObject(filterJsonObject.toString(), QQueryFilter.class));
}
}
- return new QueryMiddlewareInput()
- .withTable(paramMap.get("table"))
- .withFilter(filter)
- .withQueryJoins(queryJoins);
+ return (null);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static List getJoinsFromRequestBody(JSONObject requestBody) throws IOException
+ {
+ if(requestBody.has("joins"))
+ {
+ Object joinsFromJson = requestBody.get("joins");
+ if(joinsFromJson instanceof JSONArray joinsJsonArray)
+ {
+ List joins = new ArrayList<>();
+ for(int i = 0; i < joinsJsonArray.length(); i++)
+ {
+ JSONObject joinJsonObject = joinsJsonArray.getJSONObject(i);
+ QJoinMetaData joinMetaData = null;
+ if(joinJsonObject.has("joinName"))
+ {
+ String joinName = joinJsonObject.getString("joinName");
+ joinMetaData = QContext.getQInstance().getJoin(joinName);
+ joinJsonObject.remove("joinName");
+ }
+
+ QueryJoin queryJoin = JsonUtils.toObject(joinJsonObject.toString(), QueryJoin.class);
+ queryJoin.setJoinMetaData(joinMetaData);
+
+ joins.add(queryJoin);
+ }
+ return (joins);
+ }
+ }
+
+ return null;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static TableVariant getTableVariantFromRequestBody(JSONObject requestBody)
+ {
+ if(requestBody.has("tableVariant"))
+ {
+ Object variantFromJson = requestBody.get("tableVariant");
+ if(variantFromJson instanceof JSONObject variantJsonObject)
+ {
+ TableVariant tableVariant = new TableVariant();
+ tableVariant.setType(variantJsonObject.optString("type"));
+ tableVariant.setId(variantJsonObject.optString("id"));
+ tableVariant.setName(variantJsonObject.optString("name"));
+ return (tableVariant);
+ }
+ }
+
+ return null;
}
}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/TagsV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/TagsV1.java
index 93fbdb20..6f85fbcc 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/TagsV1.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/utils/TagsV1.java
@@ -32,10 +32,7 @@ public enum TagsV1 implements TagsInterface
{
AUTHENTICATION("Authentication"),
GENERAL("General"),
- QUERY("Query"),
- INSERT("Insert"),
- UPDATE("Update"),
- DELETE("Delete"),
+ TABLES("Tables"),
PROCESSES("Processes"),
REPORTS("Reports"),
WIDGETS("Widgets");
diff --git a/qqq-middleware-javalin/src/main/resources/openapi/v1/openapi.yaml b/qqq-middleware-javalin/src/main/resources/openapi/v1/openapi.yaml
index b0aebaf4..3f91da88 100644
--- a/qqq-middleware-javalin/src/main/resources/openapi/v1/openapi.yaml
+++ b/qqq-middleware-javalin/src/main/resources/openapi/v1/openapi.yaml
@@ -130,6 +130,42 @@ components:
description: "Description of the error"
type: "string"
type: "object"
+ ExposedJoin:
+ properties:
+ isMany:
+ description: "Whether or not this join is 'to many' in nature"
+ type: "boolean"
+ joinPath:
+ description: "A list of joins that travel from the base table to the exposed\
+ \ join table"
+ items:
+ properties:
+ joinOns:
+ type: "array"
+ leftTable:
+ type: "string"
+ name:
+ type: "string"
+ orderBys:
+ type: "array"
+ rightTable:
+ type: "string"
+ type:
+ enum:
+ - "ONE_TO_ONE"
+ - "ONE_TO_MANY"
+ - "MANY_TO_ONE"
+ - "MANY_TO_MANY"
+ type: "string"
+ type: "object"
+ type: "array"
+ joinTable:
+ $ref: "#/components/schemas/TableMetaData"
+ description: "The meta-data for the joined table"
+ label:
+ description: "User-facing label to display for this join"
+ type: "string"
+ type: "object"
FieldAdornment:
properties:
type:
@@ -193,6 +229,45 @@ components:
description: "Data-type for this field"
type: "string"
type: "object"
+ FilterCriteria:
+ properties:
+ fieldName:
+ description: "Field name that this criteria applies to"
+ type: "string"
+ operator:
+ description: "Logical operator that applies to this criteria"
+ enum:
+ - "EQUALS"
+ - "NOT_EQUALS"
+ - "NOT_EQUALS_OR_IS_NULL"
+ - "IN"
+ - "NOT_IN"
+ - "IS_NULL_OR_IN"
+ - "LIKE"
+ - "NOT_LIKE"
+ - "STARTS_WITH"
+ - "ENDS_WITH"
+ - "CONTAINS"
+ - "NOT_STARTS_WITH"
+ - "NOT_ENDS_WITH"
+ - "NOT_CONTAINS"
+ - "LESS_THAN"
+ - "LESS_THAN_OR_EQUALS"
+ - "GREATER_THAN"
+ - "GREATER_THAN_OR_EQUALS"
+ - "IS_BLANK"
+ - "IS_NOT_BLANK"
+ - "BETWEEN"
+ - "NOT_BETWEEN"
+ - "TRUE"
+ - "FALSE"
+ type: "string"
+ values:
+ description: "Values to apply via the operator to the field"
+ items:
+ type: "string"
+ type: "array"
+ type: "object"
FrontendComponent:
properties:
type:
@@ -353,6 +428,34 @@ components:
\ has permission to see that they exist)."
type: "object"
type: "object"
+ OrderBy:
+ properties:
+ fieldName:
+ description: "Field name that this order-by applies to"
+ type: "string"
+ isAscending:
+ description: "Whether this order-by is ascending or descending"
+ type: "boolean"
+ type: "object"
+ OutputRecord:
+ properties:
+ displayValues:
+ description: "Formatted string versions of the values that make up the record.\
+ \ Keys are Strings, which match the table's field names. Values are all\
+ \ Strings."
+ type: "object"
+ recordLabel:
+ description: "Label to identify the record to a user."
+ type: "string"
+ tableName:
+ description: "Name of the table that the record is from."
+ type: "string"
+ values:
+ description: "Raw values that make up the record. Keys are Strings, which\
+ \ match the table's field names. Values can be any type, as per the table's\
+ \ fields."
+ type: "object"
+ type: "object"
ProcessMetaData:
properties:
frontendSteps:
@@ -720,6 +823,157 @@ components:
\ additional fields will be set."
type: "string"
type: "object"
+ QueryFilter:
+ properties:
+ booleanOperator:
+ description: "Boolean operator to apply between criteria"
+ enum:
+ - "AND"
+ - "OR"
+ type: "string"
+ criteria:
+ description: "Field level criteria that make up the query filter"
+ items:
+ $ref: "#/components/schemas/FilterCriteria"
+ type: "array"
+ limit:
+ description: "Maximum number of results to return."
+ type: "number"
+ orderBys:
+ description: "How the query's results should be ordered (sorted)."
+ items:
+ $ref: "#/components/schemas/OrderBy"
+ type: "array"
+ skip:
+ description: "Records to skip (e.g., to implement pagination)"
+ type: "number"
+ type: "object"
+ QueryJoin:
+ properties:
+ alias:
+ description: "Alias to apply to this table in the join query"
+ type: "string"
+ baseTableOrAlias:
+ description: "Base table (or an alias) that this QueryJoin is joined against"
+ type: "string"
+ joinName:
+ description: "Name of a join to use in case the baseTable and joinTable\
+ \ have more than one"
+ type: "string"
+ joinTable:
+ description: "Table being joined into the query by this QueryJoin"
+ type: "string"
+ select:
+ description: "Whether or not to select values from the join table"
+ type: "boolean"
+ type:
+ description: "Type of join being performed (SQL semantics)"
+ enum:
+ - "INNER"
+ - "LEFT"
+ - "RIGHT"
+ - "FULL"
+ type: "string"
+ type: "object"
+ TableCountResponseV1:
+ properties:
+ count:
+ description: "Number (count) of records that satisfy the query request"
+ type: "number"
+ distinctCount:
+ description: "Number (count) of distinct records that satisfy the query\
+ \ request. Only included if requested."
+ type: "number"
+ type: "object"
+ TableMetaData:
+ properties:
+ capabilities:
+ description: "List of strings describing actions that are supported by the\
+ \ backend application for the table."
+ items:
+ type: "string"
+ type: "array"
+ deletePermission:
+ description: "Boolean to indicate if the user has delete permission for\
+ \ the table."
+ type: "boolean"
+ editPermission:
+ description: "Boolean to indicate if the user has edit permission for the\
+ \ table."
+ type: "boolean"
+ exposedJoins:
+ description: "Sections to organize fields on screens for this record"
+ items:
+ $ref: "#/components/schemas/ExposedJoin"
+ type: "array"
+ fields:
+ additionalProperties:
+ $ref: "#/components/schemas/FieldMetaData"
+ description: "Fields in this table"
+ type: "object"
+ helpContents:
+ description: "Help Contents for this table."
+ type: "object"
+ icon:
+ $ref: "#/components/schemas/Icon"
+ description: "Icon to display for the table"
+ insertPermission:
+ description: "Boolean to indicate if the user has insert permission for\
+ \ the table."
+ type: "boolean"
+ isHidden:
+ description: "Boolean indicator of whether the table should be shown to\
+ \ users or not"
+ type: "boolean"
+ label:
+ description: "User-facing name for this table"
+ type: "string"
+ name:
+ description: "Unique name for this table within the QQQ Instance"
+ type: "string"
+ primaryKeyField:
+ description: "Name of the primary key field in this table"
+ type: "string"
+ readPermission:
+ description: "Boolean to indicate if the user has read permission for the\
+ \ table."
+ type: "boolean"
+ sections:
+ description: "Sections to organize fields on screens for this record"
+ items:
+ $ref: "#/components/schemas/TableSection"
+ type: "array"
+ shareableTableMetaData:
+ description: "For tables that support the sharing feature, meta-data about\
+ \ the sharing setup."
+ properties:
+ assetIdFieldName:
+ type: "string"
+ audiencePossibleValueSourceName:
+ type: "string"
+ audienceTypes:
+ type: "object"
+ audienceTypesPossibleValueSourceName:
+ type: "string"
+ scopeFieldName:
+ type: "string"
+ sharedRecordTableName:
+ type: "string"
+ thisTableOwnerIdFieldName:
+ type: "string"
+ type: "object"
+ supplementalMetaData:
+ description: "Additional meta data about the table, not necessarily known\
+ \ to QQQ."
+ type: "object"
+ usesVariants:
+ description: "Whether or not this table's backend uses variants."
+ type: "boolean"
+ variantTableLabel:
+ description: "If the table uses variants, this is the user-facing label\
+ \ for the table that supplies variants for this table."
+ type: "string"
+ type: "object"
TableMetaDataLight:
properties:
capabilities:
@@ -765,6 +1019,157 @@ components:
\ for the table that supplies variants for this table."
type: "string"
type: "object"
+ TableMetaDataResponseV1:
+ properties:
+ capabilities:
+ description: "List of strings describing actions that are supported by the\
+ \ backend application for the table."
+ items:
+ type: "string"
+ type: "array"
+ deletePermission:
+ description: "Boolean to indicate if the user has delete permission for\
+ \ the table."
+ type: "boolean"
+ editPermission:
+ description: "Boolean to indicate if the user has edit permission for the\
+ \ table."
+ type: "boolean"
+ exposedJoins:
+ description: "Sections to organize fields on screens for this record"
+ items:
+ $ref: "#/components/schemas/ExposedJoin"
+ type: "array"
+ fields:
+ additionalProperties:
+ $ref: "#/components/schemas/FieldMetaData"
+ description: "Fields in this table"
+ type: "object"
+ helpContents:
+ description: "Help Contents for this table."
+ type: "object"
+ icon:
+ description: "Icon to display for the table"
+ properties:
+ color:
+ description: "A color code to use for displaying the icon"
+ type: "string"
+ name:
+ description: "A material UI icon name."
+ type: "string"
+ path:
+ description: "A path to an image file that can be requested from the\
+ \ server, to serve as the icon image instead of a material UI icon."
+ type: "string"
+ type: "object"
+ insertPermission:
+ description: "Boolean to indicate if the user has insert permission for\
+ \ the table."
+ type: "boolean"
+ isHidden:
+ description: "Boolean indicator of whether the table should be shown to\
+ \ users or not"
+ type: "boolean"
+ label:
+ description: "User-facing name for this table"
+ type: "string"
+ name:
+ description: "Unique name for this table within the QQQ Instance"
+ type: "string"
+ primaryKeyField:
+ description: "Name of the primary key field in this table"
+ type: "string"
+ readPermission:
+ description: "Boolean to indicate if the user has read permission for the\
+ \ table."
+ type: "boolean"
+ sections:
+ description: "Sections to organize fields on screens for this record"
+ items:
+ $ref: "#/components/schemas/TableSection"
+ type: "array"
+ shareableTableMetaData:
+ description: "For tables that support the sharing feature, meta-data about\
+ \ the sharing setup."
+ properties:
+ assetIdFieldName:
+ type: "string"
+ audiencePossibleValueSourceName:
+ type: "string"
+ audienceTypes:
+ type: "object"
+ audienceTypesPossibleValueSourceName:
+ type: "string"
+ scopeFieldName:
+ type: "string"
+ sharedRecordTableName:
+ type: "string"
+ thisTableOwnerIdFieldName:
+ type: "string"
+ type: "object"
+ supplementalMetaData:
+ description: "Additional meta data about the table, not necessarily known\
+ \ to QQQ."
+ type: "object"
+ usesVariants:
+ description: "Whether or not this table's backend uses variants."
+ type: "boolean"
+ variantTableLabel:
+ description: "If the table uses variants, this is the user-facing label\
+ \ for the table that supplies variants for this table."
+ type: "string"
+ type: "object"
+ TableQueryResponseV1:
+ properties:
+ records:
+ description: "List of records that satisfy the query request"
+ items:
+ $ref: "#/components/schemas/OutputRecord"
+ type: "array"
+ type: "object"
+ TableSection:
+ properties:
+ fieldNames:
+ description: "List of field names to include in this section."
+ type: "array"
+ gridColumns:
+ description: "Layout suggestion, for how many columns of a 12-grid this\
+ \ section should use."
+ type: "number"
+ helpContents:
+ description: "Help Contents for this section table."
+ type: "array"
+ icon:
+ $ref: "#/components/schemas/Icon"
+ description: "Icon to display for the table"
+ label:
+ description: "User-facing label to display for this section"
+ type: "string"
+ name:
+ description: "Unique identifier for this section within this table"
+ type: "string"
+ tier:
+ description: "Importance of this section (T1, T2, or T3)"
+ type: "string"
+ widgetName:
+ description: "Name of a widget in this QQQ instance to include in this section\
+ \ (instead of fields)."
+ type: "string"
+ type: "object"
+ TableVariant:
+ properties:
+ id:
+ description: "Identifier for the variant option (record) being used."
+ type: "string"
+ name:
+ description: "A user-facing name (or label) for the variant option being\
+ \ used."
+ type: "string"
+ type:
+ description: "Specification for the variant type (variantTypeKey from QQQ\
+ \ BackendVariantsConfig)"
+ type: "string"
+ type: "object"
WidgetBlock:
properties:
blockId:
@@ -1532,6 +1937,151 @@ paths:
summary: "Get instance metaData"
tags:
- "General"
+ /qqq/v1/metaData/table/{tableName}:
+ get:
+ description: "Load the full metadata for a single table, including all fields,\
+ \ which a frontend\nneeds to display to users."
+ parameters:
+ - description: "Name of the table to load."
+ example: "person"
+ in: "path"
+ name: "tableName"
+ required: true
+ schema:
+ type: "string"
+ responses:
+ 200:
+ content:
+ application/json:
+ examples:
+ TODO: {}
+ schema:
+ $ref: "#/components/schemas/TableMetaDataResponseV1"
+ description: "The full table metadata"
+ security:
+ - sessionUuidCookie:
+ - "N/A"
+ summary: "Get table metaData"
+ tags:
+ - "Tables"
+ /qqq/v1/table/{tableName}/query:
+ post:
+ description: "Execute a query against a table, returning records that match\
+ \ a filter."
+ parameters:
+ - description: "Name of the table to query."
+ example: "person"
+ in: "path"
+ name: "tableName"
+ required: true
+ schema:
+ type: "string"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ tableVariant:
+ $ref: "#/components/schemas/TableVariant"
+ description: "For tables that use variant backends, specification\
+ \ of which variant to use."
+ filter:
+ $ref: "#/components/schemas/QueryFilter"
+ description: "QueryFilter to specify matching records to be returned\
+ \ by the query"
+ joins:
+ description: "QueryJoin objects to specify tables to be joined into\
+ \ the query"
+ items:
+ $ref: "#/components/schemas/QueryJoin"
+ type: "array"
+ type: "object"
+ required: false
+ responses:
+ 200:
+ content:
+ application/json:
+ examples:
+ TODO:
+ value:
+ records:
+ - displayValues:
+ id: "17"
+ quantity: "1,000"
+ storeId: "QQQ-Mart"
+ recordLabel: "Item 17"
+ tableName: "item"
+ values:
+ id: 17
+ quantity: 1000
+ storeId: 42
+ schema:
+ $ref: "#/components/schemas/TableQueryResponseV1"
+ description: "The records matching query"
+ security:
+ - sessionUuidCookie:
+ - "N/A"
+ summary: "Query for records from a table"
+ tags:
+ - "Tables"
+ /qqq/v1/table/{tableName}/count:
+ post:
+ description: "Execute a query against a table, returning the number of records\
+ \ that match a filter."
+ parameters:
+ - description: "Name of the table to count."
+ example: "person"
+ in: "path"
+ name: "tableName"
+ required: true
+ schema:
+ type: "string"
+ - description: "Whether or not to also return the count distinct records from\
+ \ the main table (e.g., in case of a to-many join; by default, do not)."
+ example: "true"
+ in: "query"
+ name: "includeDistinct"
+ required: true
+ schema:
+ type: "boolean"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ tableVariant:
+ $ref: "#/components/schemas/TableVariant"
+ description: "For tables that use variant backends, specification\
+ \ of which variant to use."
+ filter:
+ $ref: "#/components/schemas/QueryFilter"
+ description: "QueryFilter to specify matching records to be returned\
+ \ by the query"
+ joins:
+ description: "QueryJoin objects to specify tables to be joined into\
+ \ the query"
+ items:
+ $ref: "#/components/schemas/QueryJoin"
+ type: "array"
+ type: "object"
+ required: false
+ responses:
+ 200:
+ content:
+ application/json:
+ examples:
+ TODO:
+ value:
+ count: 42
+ schema:
+ $ref: "#/components/schemas/TableCountResponseV1"
+ description: "The number (count) of records matching the query"
+ security:
+ - sessionUuidCookie:
+ - "N/A"
+ summary: "Count records in a table"
+ tags:
+ - "Tables"
/qqq/v1/metaData/process/{processName}:
get:
description: "Load the full metadata for a single process, including all screens\
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 59ae8177..61e4cdaa 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
@@ -87,11 +87,13 @@ 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.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantsConfig;
import com.kingsrook.qqq.backend.core.model.savedviews.SavedViewsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
+import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryModuleBackendVariantSetting;
import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
@@ -114,6 +116,10 @@ public class TestUtils
public static final String TABLE_NAME_PERSON = "person";
public static final String TABLE_NAME_PET = "pet";
+ public static final String MEMORY_BACKEND_WITH_VARIANTS_NAME = "memoryWithVariants";
+ public static final String TABLE_NAME_MEMORY_VARIANT_OPTIONS = "memoryVariantOptions";
+ public static final String TABLE_NAME_MEMORY_VARIANT_DATA = "memoryVariantData";
+
public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive";
public static final String PROCESS_NAME_SIMPLE_SLEEP = "simpleSleep";
public static final String PROCESS_NAME_SIMPLE_THROW = "simpleThrow";
@@ -191,6 +197,8 @@ public class TestUtils
qInstance.addPossibleValueSource(definePossibleValueSourcePerson());
defineWidgets(qInstance);
+ defineMemoryBackendVariantUseCases(qInstance);
+
List routeProviders = new ArrayList<>();
routeProviders.add(new JavalinRouteProviderMetaData()
.withHostedPath("/statically-served")
@@ -832,4 +840,39 @@ public class TestUtils
)));
}
}
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private static void defineMemoryBackendVariantUseCases(QInstance qInstance)
+ {
+ qInstance.addBackend(new QBackendMetaData()
+ .withName(MEMORY_BACKEND_WITH_VARIANTS_NAME)
+ .withBackendType(MemoryBackendModule.class)
+ .withUsesVariants(true)
+ .withBackendVariantsConfig(new BackendVariantsConfig()
+ .withVariantTypeKey(TABLE_NAME_MEMORY_VARIANT_OPTIONS)
+ .withOptionsTableName(TABLE_NAME_MEMORY_VARIANT_OPTIONS)
+ .withBackendSettingSourceFieldNameMap(Map.of(MemoryModuleBackendVariantSetting.PRIMARY_KEY, "id"))
+ ));
+
+ qInstance.addTable(new QTableMetaData()
+ .withName(TABLE_NAME_MEMORY_VARIANT_DATA)
+ .withBackendName(MEMORY_BACKEND_WITH_VARIANTS_NAME)
+ .withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER))
+ .withField(new QFieldMetaData("name", QFieldType.STRING))
+ );
+
+ qInstance.addTable(new QTableMetaData()
+ .withName(TABLE_NAME_MEMORY_VARIANT_OPTIONS)
+ .withBackendName(BACKEND_NAME_MEMORY) // note, the version without variants!
+ .withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER))
+ .withField(new QFieldMetaData("name", QFieldType.STRING))
+ );
+ }
+
}
diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/SpecTestBase.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/SpecTestBase.java
index a51e6b90..8425d3da 100644
--- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/SpecTestBase.java
+++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/SpecTestBase.java
@@ -109,6 +109,8 @@ public abstract class SpecTestBase
service = null;
}
+ TestMiddlewareVersion testMiddlewareVersion = new TestMiddlewareVersion();
+
if(service == null)
{
service = Javalin.create(config ->
@@ -117,12 +119,12 @@ public abstract class SpecTestBase
AbstractEndpointSpec, ?, ?> spec = getSpec();
spec.setQInstance(qInstance);
- config.router.apiBuilder(() -> spec.defineRoute(getVersion()));
+ config.router.apiBuilder(() -> spec.defineRoute(testMiddlewareVersion, getVersion()));
for(AbstractEndpointSpec, ?, ?> additionalSpec : getAdditionalSpecs())
{
additionalSpec.setQInstance(qInstance);
- config.router.apiBuilder(() -> additionalSpec.defineRoute(getVersion()));
+ config.router.apiBuilder(() -> additionalSpec.defineRoute(testMiddlewareVersion, getVersion()));
}
}
).start(PORT);
@@ -133,6 +135,35 @@ public abstract class SpecTestBase
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private static class TestMiddlewareVersion extends AbstractMiddlewareVersion
+ {
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public String getVersion()
+ {
+ return "test";
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public List> getEndpointSpecs()
+ {
+ return List.of();
+ }
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableCountSpecV1Test.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableCountSpecV1Test.java
new file mode 100644
index 00000000..9e9bfef8
--- /dev/null
+++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableCountSpecV1Test.java
@@ -0,0 +1,237 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1;
+
+
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+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.session.QSystemUserSession;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.backend.javalin.TestUtils;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
+import com.kingsrook.qqq.middleware.javalin.specs.SpecTestBase;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.TableVariant;
+import io.javalin.http.ContentType;
+import kong.unirest.HttpResponse;
+import kong.unirest.Unirest;
+import org.eclipse.jetty.http.HttpStatus;
+import org.json.JSONObject;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+
+/*******************************************************************************
+ ** Unit test for TableCountSpecV1
+ *******************************************************************************/
+class TableCountSpecV1Test extends SpecTestBase
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ protected AbstractEndpointSpec, ?, ?> getSpec()
+ {
+ return new TableCountSpecV1();
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ protected String getVersion()
+ {
+ return "v1";
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test()
+ {
+ HttpResponse response = Unirest.post(getBaseUrlAndPath() + "/table/person/count")
+ .contentType(ContentType.APPLICATION_JSON.getMimeType())
+ .body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, "Kelkhoff")))))
+ .asString();
+
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertEquals(2, jsonObject.getInt("count"));
+ assertFalse(jsonObject.has("distinctCount"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testIncludingDistinct()
+ {
+ HttpResponse response = Unirest.post(getBaseUrlAndPath() + "/table/person/count?includeDistinct=true")
+ .contentType(ContentType.APPLICATION_JSON.getMimeType())
+ .body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, "Kelkhoff")))))
+ .asString();
+
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertEquals(2, jsonObject.getInt("count"));
+ assertEquals(2, jsonObject.getInt("distinctCount"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testNoRecordsFound()
+ {
+ HttpResponse response = Unirest.post(getBaseUrlAndPath() + "/table/person/count")
+ .contentType(ContentType.APPLICATION_JSON.getMimeType())
+ .body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, "Unkelkhoff")))))
+ .asString();
+
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertEquals(0, jsonObject.getInt("count"));
+ }
+
+
+
+ /*******************************************************************************
+ ** test the for a non-real name
+ **
+ *******************************************************************************/
+ @Test
+ public void testTableNotFound()
+ {
+ HttpResponse response = Unirest.post(getBaseUrlAndPath() + "/table/notAnActualTable/count").asString();
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // expect a non-existing table to 403, the same as one that does exist but that you don't have permission to //
+ // to kinda hide from someone what is or isn't a real table (as a security thing i guess) //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ assertEquals(HttpStatus.FORBIDDEN_403, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
+ String error = jsonObject.getString("error");
+ assertThat(error).contains("Permission denied");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testPostingTableVariant() throws QException
+ {
+ ////////////////////////////////////
+ // insert our two variant options //
+ ////////////////////////////////////
+ QContext.init(TestUtils.defineInstance(), new QSystemUserSession());
+ new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_MEMORY_VARIANT_OPTIONS).withRecords(List.of(
+ new QRecord().withValue("id", 1).withValue("name", "People"),
+ new QRecord().withValue("id", 2).withValue("name", "Planets")
+ )));
+
+ ////////////////////////////////////////
+ // insert some data into each variant //
+ ////////////////////////////////////////
+ QContext.getQSession().setBackendVariants(Map.of(TestUtils.TABLE_NAME_MEMORY_VARIANT_OPTIONS, 1));
+ new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_MEMORY_VARIANT_DATA).withRecords(List.of(
+ new QRecord().withValue("name", "Tom"),
+ new QRecord().withValue("name", "Sally")
+ )));
+
+ QContext.getQSession().setBackendVariants(Map.of(TestUtils.TABLE_NAME_MEMORY_VARIANT_OPTIONS, 2));
+ new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_MEMORY_VARIANT_DATA).withRecords(List.of(
+ new QRecord().withValue("name", "Mars"),
+ new QRecord().withValue("name", "Jupiter"),
+ new QRecord().withValue("name", "Saturn")
+ )));
+
+ //////////////////////////////////////////
+ // count with no variant - expect error //
+ //////////////////////////////////////////
+ String url = getBaseUrlAndPath() + "/table/" + TestUtils.TABLE_NAME_MEMORY_VARIANT_DATA + "/count";
+ HttpResponse response = Unirest.post(url)
+ .contentType(ContentType.APPLICATION_JSON.getMimeType())
+ .body(JsonUtils.toJson(Map.of()))
+ .asString();
+ assertEquals(500, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertEquals("Could not find Backend Variant information in session under key 'memoryVariantOptions' for Backend 'memoryWithVariants'", jsonObject.getString("error"));
+
+ //////////////////////////////
+ // count for people variant //
+ //////////////////////////////
+ response = Unirest.post(url)
+ .contentType(ContentType.APPLICATION_JSON.getMimeType())
+ .body(JsonUtils.toJson(Map.of("tableVariant", new TableVariant().withType("memoryVariantOptions").withId("1"))))
+ .asString();
+
+ assertEquals(200, response.getStatus());
+ jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertEquals(2, jsonObject.getInt("count"));
+
+ ///////////////////////////////
+ // count for planets variant //
+ ///////////////////////////////
+ response = Unirest.post(url)
+ .contentType(ContentType.APPLICATION_JSON.getMimeType())
+ .body(JsonUtils.toJson(Map.of("tableVariant", new TableVariant().withType("memoryVariantOptions").withId("2"))))
+ .asString();
+
+ assertEquals(200, response.getStatus());
+ jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertEquals(3, jsonObject.getInt("count"));
+
+ ///////////////////////////////////////////////
+ // count with unknown variant - expect error //
+ ///////////////////////////////////////////////
+ response = Unirest.post(url)
+ .contentType(ContentType.APPLICATION_JSON.getMimeType())
+ .body(JsonUtils.toJson(Map.of("tableVariant", new TableVariant().withType("memoryVariantOptions").withId("3"))))
+ .asString();
+ assertEquals(500, response.getStatus());
+ jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertEquals("Could not find Backend Variant in table memoryVariantOptions with id '3'", jsonObject.getString("error"));
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableMetaDataSpecV1Test.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableMetaDataSpecV1Test.java
new file mode 100644
index 00000000..fa1d4e44
--- /dev/null
+++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableMetaDataSpecV1Test.java
@@ -0,0 +1,102 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1;
+
+
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
+import com.kingsrook.qqq.middleware.javalin.specs.SpecTestBase;
+import kong.unirest.HttpResponse;
+import kong.unirest.Unirest;
+import org.eclipse.jetty.http.HttpStatus;
+import org.json.JSONObject;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for TableMetaDataSpecV1
+ *******************************************************************************/
+class TableMetaDataSpecV1Test extends SpecTestBase
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ protected AbstractEndpointSpec, ?, ?> getSpec()
+ {
+ return new TableMetaDataSpecV1();
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ protected String getVersion()
+ {
+ return "v1";
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test()
+ {
+ HttpResponse response = Unirest.get(getBaseUrlAndPath() + "/metaData/table/person").asString();
+
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertEquals("person", jsonObject.getString("name"));
+ assertEquals("Person", jsonObject.getString("label"));
+
+ JSONObject fields = jsonObject.getJSONObject("fields");
+ JSONObject firstNameField = fields.getJSONObject("firstName");
+ assertEquals("firstName", firstNameField.getString("name"));
+ assertEquals("First Name", firstNameField.getString("label"));
+ }
+
+
+
+ /*******************************************************************************
+ ** test the table-level meta-data endpoint for a non-real name
+ **
+ *******************************************************************************/
+ @Test
+ public void testNotFound()
+ {
+ HttpResponse response = Unirest.get(getBaseUrlAndPath() + "/metaData/table/notAnActualTable").asString();
+
+ assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
+ String error = jsonObject.getString("error");
+ assertThat(error).contains("Table").contains("notAnActualTable").contains("not found");
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableQuerySpecV1Test.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableQuerySpecV1Test.java
new file mode 100644
index 00000000..07b120fb
--- /dev/null
+++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/specs/v1/TableQuerySpecV1Test.java
@@ -0,0 +1,232 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.specs.v1;
+
+
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+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.session.QSystemUserSession;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.backend.javalin.TestUtils;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
+import com.kingsrook.qqq.middleware.javalin.specs.SpecTestBase;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.components.TableVariant;
+import io.javalin.http.ContentType;
+import kong.unirest.HttpResponse;
+import kong.unirest.Unirest;
+import org.eclipse.jetty.http.HttpStatus;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for TableQuerySpecV1
+ *******************************************************************************/
+class TableQuerySpecV1Test extends SpecTestBase
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ protected AbstractEndpointSpec, ?, ?> getSpec()
+ {
+ return new TableQuerySpecV1();
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ protected String getVersion()
+ {
+ return "v1";
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test()
+ {
+ HttpResponse response = Unirest.post(getBaseUrlAndPath() + "/table/person/query")
+ .contentType(ContentType.APPLICATION_JSON.getMimeType())
+ .body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, "Kelkhoff")))))
+ .asString();
+
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ JSONArray records = jsonObject.getJSONArray("records");
+ assertThat(records.length()).isGreaterThanOrEqualTo(1);
+
+ JSONObject record = records.getJSONObject(0);
+ assertThat(record.getString("recordLabel")).contains("Kelkhoff");
+ assertThat(record.getString("tableName")).isEqualTo("person");
+ assertThat(record.getJSONObject("values").getString("lastName")).isEqualTo("Kelkhoff");
+ assertThat(record.getJSONObject("displayValues").getString("lastName")).isEqualTo("Kelkhoff");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testNoRecordsFound()
+ {
+ HttpResponse response = Unirest.post(getBaseUrlAndPath() + "/table/person/query")
+ .contentType(ContentType.APPLICATION_JSON.getMimeType())
+ .body(JsonUtils.toJson(Map.of("filter", new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, "Unkelkhoff")))))
+ .asString();
+
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ JSONArray records = jsonObject.getJSONArray("records");
+ assertThat(records.length()).isZero();
+ }
+
+
+
+ /*******************************************************************************
+ ** test the table-level meta-data endpoint for a non-real name
+ **
+ *******************************************************************************/
+ @Test
+ public void testTableNotFound()
+ {
+ HttpResponse response = Unirest.post(getBaseUrlAndPath() + "/table/notAnActualTable/query").asString();
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // expect a non-existing table to 403, the same as one that does exist but that you don't have permission to //
+ // to kinda hide from someone what is or isn't a real table (as a security thing i guess) //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ assertEquals(HttpStatus.FORBIDDEN_403, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
+ String error = jsonObject.getString("error");
+ assertThat(error).contains("Permission denied");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testPostingTableVariant() throws QException
+ {
+ ////////////////////////////////////
+ // insert our two variant options //
+ ////////////////////////////////////
+ QContext.init(TestUtils.defineInstance(), new QSystemUserSession());
+ new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_MEMORY_VARIANT_OPTIONS).withRecords(List.of(
+ new QRecord().withValue("id", 1).withValue("name", "People"),
+ new QRecord().withValue("id", 2).withValue("name", "Planets")
+ )));
+
+ ////////////////////////////////////////
+ // insert some data into each variant //
+ ////////////////////////////////////////
+ QContext.getQSession().setBackendVariants(Map.of(TestUtils.TABLE_NAME_MEMORY_VARIANT_OPTIONS, 1));
+ new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_MEMORY_VARIANT_DATA).withRecords(List.of(
+ new QRecord().withValue("name", "Tom"),
+ new QRecord().withValue("name", "Sally")
+ )));
+
+ QContext.getQSession().setBackendVariants(Map.of(TestUtils.TABLE_NAME_MEMORY_VARIANT_OPTIONS, 2));
+ new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_MEMORY_VARIANT_DATA).withRecords(List.of(
+ new QRecord().withValue("name", "Mars"),
+ new QRecord().withValue("name", "Jupiter"),
+ new QRecord().withValue("name", "Saturn")
+ )));
+
+ //////////////////////////////////////////
+ // query with no variant - expect error //
+ //////////////////////////////////////////
+ String url = getBaseUrlAndPath() + "/table/" + TestUtils.TABLE_NAME_MEMORY_VARIANT_DATA + "/query";
+ HttpResponse response = Unirest.post(url)
+ .contentType(ContentType.APPLICATION_JSON.getMimeType())
+ .body(JsonUtils.toJson(Map.of()))
+ .asString();
+ assertEquals(500, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertEquals("Could not find Backend Variant information in session under key 'memoryVariantOptions' for Backend 'memoryWithVariants'", jsonObject.getString("error"));
+
+ //////////////////////////////
+ // query for people variant //
+ //////////////////////////////
+ response = Unirest.post(url)
+ .contentType(ContentType.APPLICATION_JSON.getMimeType())
+ .body(JsonUtils.toJson(Map.of("tableVariant", new TableVariant().withType("memoryVariantOptions").withId("1"))))
+ .asString();
+
+ assertEquals(200, response.getStatus());
+ jsonObject = JsonUtils.toJSONObject(response.getBody());
+ JSONArray records = jsonObject.getJSONArray("records");
+ assertThat(records.length()).isEqualTo(2);
+ JSONObject record = records.getJSONObject(0);
+ assertThat(record.getJSONObject("values").getString("name")).isEqualTo("Tom");
+
+ ///////////////////////////////
+ // query for planets variant //
+ ///////////////////////////////
+ response = Unirest.post(url)
+ .contentType(ContentType.APPLICATION_JSON.getMimeType())
+ .body(JsonUtils.toJson(Map.of("tableVariant", new TableVariant().withType("memoryVariantOptions").withId("2"))))
+ .asString();
+
+ assertEquals(200, response.getStatus());
+ jsonObject = JsonUtils.toJSONObject(response.getBody());
+ records = jsonObject.getJSONArray("records");
+ assertThat(records.length()).isEqualTo(3);
+ record = records.getJSONObject(0);
+ assertThat(record.getJSONObject("values").getString("name")).isEqualTo("Mars");
+
+ ///////////////////////////////////////////////
+ // query with unknown variant - expect error //
+ ///////////////////////////////////////////////
+ response = Unirest.post(url)
+ .contentType(ContentType.APPLICATION_JSON.getMimeType())
+ .body(JsonUtils.toJson(Map.of("tableVariant", new TableVariant().withType("memoryVariantOptions").withId("3"))))
+ .asString();
+ assertEquals(500, response.getStatus());
+ jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertEquals("Could not find Backend Variant in table memoryVariantOptions with id '3'", jsonObject.getString("error"));
+
+ }
+
+}
\ No newline at end of file