diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTableCustomPossibleValueProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTableCustomPossibleValueProvider.java
new file mode 100644
index 00000000..af4b3791
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTableCustomPossibleValueProvider.java
@@ -0,0 +1,111 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.tables;
+
+
+import java.io.Serializable;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult;
+import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
+import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
+import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
+import com.kingsrook.qqq.backend.core.actions.values.BasicCustomPossibleValueProvider;
+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.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+
+
+/*******************************************************************************
+ ** possible-value source provider for the `QQQ Table` PVS - a list of all tables
+ ** in an application/qInstance (that you have permission to see)
+ *******************************************************************************/
+public class QQQTableCustomPossibleValueProvider extends BasicCustomPossibleValueProvider
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ protected QPossibleValue makePossibleValue(QRecord sourceObject)
+ {
+ return (new QPossibleValue<>(sourceObject.getValueInteger("id"), sourceObject.getValueString("label")));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ protected QRecord getSourceObject(Serializable id) throws QException
+ {
+ QRecord qqqTableRecord = GetAction.execute(QQQTable.TABLE_NAME, id);
+ if(qqqTableRecord == null)
+ {
+ return (null);
+ }
+
+ QTableMetaData table = QContext.getQInstance().getTable(qqqTableRecord.getValueString("name"));
+ return isTableAllowed(table) ? qqqTableRecord : null;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ protected List getAllSourceObjects() throws QException
+ {
+ return (QueryAction.execute(QQQTable.TABLE_NAME, null));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private boolean isTableAllowed(QTableMetaData table)
+ {
+ if(table == null)
+ {
+ return (false);
+ }
+
+ if(table.getIsHidden())
+ {
+ return (false);
+ }
+
+ PermissionCheckResult permissionCheckResult = PermissionsHelper.getPermissionCheckResult(new QueryInput(table.getName()), table);
+ if(!PermissionCheckResult.ALLOW.equals(permissionCheckResult))
+ {
+ return (false);
+ }
+
+ return (true);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTablesMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTablesMetaDataProvider.java
index e880a230..91712ab9 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTablesMetaDataProvider.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/tables/QQQTablesMetaDataProvider.java
@@ -27,6 +27,9 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
@@ -125,10 +128,11 @@ public class QQQTablesMetaDataProvider
public QPossibleValueSource defineQQQTablePossibleValueSource()
{
return (new QPossibleValueSource()
- .withType(QPossibleValueSourceType.TABLE)
.withName(QQQTable.TABLE_NAME)
- .withTableName(QQQTable.TABLE_NAME))
- .withOrderByField("label");
+ .withType(QPossibleValueSourceType.CUSTOM)
+ .withIdType(QFieldType.INTEGER)
+ .withCustomCodeReference(new QCodeReference(QQQTableCustomPossibleValueProvider.class))
+ .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY));
}
}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/tables/QQQTableCustomPossibleValueProviderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/tables/QQQTableCustomPossibleValueProviderTest.java
new file mode 100644
index 00000000..a573b2d6
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/tables/QQQTableCustomPossibleValueProviderTest.java
@@ -0,0 +1,172 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.tables;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
+import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesCustomPossibleValueProvider;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+
+/*******************************************************************************
+ ** Unit test for QQQTableCustomPossibleValueProvider
+ *******************************************************************************/
+class QQQTableCustomPossibleValueProviderTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeEach
+ void beforeEach() throws QException
+ {
+ QInstance qInstance = TestUtils.defineInstance();
+
+ qInstance.addTable(new QTableMetaData()
+ .withName("hidden")
+ .withIsHidden(true)
+ .withBackendName(TestUtils.MEMORY_BACKEND_NAME)
+ .withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER)));
+
+ qInstance.addTable(new QTableMetaData()
+ .withName("restricted")
+ .withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION))
+ .withBackendName(TestUtils.MEMORY_BACKEND_NAME)
+ .withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER)));
+
+ new QQQTablesMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null);
+
+ QContext.init(qInstance, newSession());
+
+ for(String tableName : qInstance.getTables().keySet())
+ {
+ QQQTableTableManager.getQQQTableId(qInstance, tableName);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testGetPossibleValue() throws QException
+ {
+ Integer personTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), TestUtils.TABLE_NAME_PERSON);
+
+ QQQTableCustomPossibleValueProvider provider = new QQQTableCustomPossibleValueProvider();
+
+ QPossibleValue possibleValue = provider.getPossibleValue(personTableId);
+ assertEquals(personTableId, possibleValue.getId());
+ assertEquals("Person", possibleValue.getLabel());
+
+ assertNull(provider.getPossibleValue(-1));
+
+ Integer hiddenTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), "hidden");
+ assertNull(provider.getPossibleValue(hiddenTableId));
+
+ Integer restrictedTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), "restricted");
+ assertNull(provider.getPossibleValue(restrictedTableId));
+
+ QContext.getQSession().withPermission("restricted.hasAccess");
+ assertNotNull(provider.getPossibleValue(restrictedTableId));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testSearchPossibleValue() throws QException
+ {
+ TablesCustomPossibleValueProvider provider = new TablesCustomPossibleValueProvider();
+
+ List> list = provider.search(new SearchPossibleValueSourceInput()
+ .withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME));
+ assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_PERSON));
+ assertThat(list).noneMatch(p -> p.getId().equals("no-such-table"));
+ assertThat(list).noneMatch(p -> p.getId().equals("hidden"));
+ assertThat(list).noneMatch(p -> p.getId().equals("restricted"));
+ assertNull(provider.getPossibleValue("restricted"));
+
+ list = provider.search(new SearchPossibleValueSourceInput()
+ .withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)
+ .withIdList(List.of(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_SHAPE, "hidden")));
+ assertEquals(2, list.size());
+ assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_PERSON));
+ assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_SHAPE));
+ assertThat(list).noneMatch(p -> p.getId().equals("hidden"));
+
+ list = provider.search(new SearchPossibleValueSourceInput()
+ .withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)
+ .withLabelList(List.of("Person", "Shape", "Restricted")));
+ assertEquals(2, list.size());
+ assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_PERSON));
+ assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_SHAPE));
+ assertThat(list).noneMatch(p -> p.getId().equals("restricted"));
+
+ list = provider.search(new SearchPossibleValueSourceInput()
+ .withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)
+ .withSearchTerm("restricted"));
+ assertEquals(0, list.size());
+
+ /////////////////////////////////////////
+ // add permission for restricted table //
+ /////////////////////////////////////////
+ QContext.getQSession().withPermission("restricted.hasAccess");
+ list = provider.search(new SearchPossibleValueSourceInput()
+ .withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)
+ .withSearchTerm("restricted"));
+ assertEquals(1, list.size());
+
+ list = provider.search(new SearchPossibleValueSourceInput()
+ .withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME)
+ .withLabelList(List.of("Person", "Shape", "Restricted")));
+ assertEquals(3, list.size());
+ assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_PERSON));
+ assertThat(list).anyMatch(p -> p.getId().equals(TestUtils.TABLE_NAME_SHAPE));
+ assertThat(list).anyMatch(p -> p.getId().equals("restricted"));
+
+ }
+
+}
\ No newline at end of file