diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..77a53a5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,31 @@
+# Maven
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+
+# IDE
+.idea/
+*.iml
+.project
+.classpath
+.settings/
+.vscode/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Build
+*.class
+*.jar
+*.war
+*.ear
+
+# Logs
+*.log
+logs/
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..3ed060e
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,90 @@
+
+
+ 4.0.0
+
+ com.kingsrook.qbits
+ qbit-example-app
+ 0.1.0-SNAPSHOT
+ jar
+
+ QBit Example Application
+ Template for Application QBits - complete mini-applications with UI navigation
+
+
+ 17
+ 17
+ UTF-8
+ 0.23.0
+
+
+
+
+
+ com.kingsrook.qqq
+ qqq-backend-core
+ ${qqq.version}
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.0
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.24.2
+ test
+
+
+ com.kingsrook.qqq
+ qqq-backend-module-rdbms
+ ${qqq.version}
+ test
+
+
+ com.h2database
+ h2
+ 2.2.224
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.1.2
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.10
+
+
+
+ prepare-agent
+
+
+
+ report
+ test
+
+ report
+
+
+
+
+
+
+
diff --git a/src/main/java/com/kingsrook/qbits/example/ExampleAppQBitConfig.java b/src/main/java/com/kingsrook/qbits/example/ExampleAppQBitConfig.java
new file mode 100644
index 0000000..30aea3e
--- /dev/null
+++ b/src/main/java/com/kingsrook/qbits/example/ExampleAppQBitConfig.java
@@ -0,0 +1,164 @@
+/*******************************************************************************
+ ** Configuration class for the Example Application QBit.
+ **
+ ** Application QBits support:
+ ** - Backend configuration for data storage
+ ** - Optional table prefixing for multi-instance deployment
+ ** - Feature toggles for optional modules
+ ** - Security key prefix for permission scoping
+ ** - Integration hooks for other QBits
+ *******************************************************************************/
+package com.kingsrook.qbits.example;
+
+
+import java.util.List;
+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.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+
+
+public class ExampleAppQBitConfig implements QBitConfig
+{
+ private String backendName;
+ private String tableNamePrefix;
+ private Boolean enableChildModule = true;
+ private String securityKeyPrefix;
+
+ /////////////////////////////////////////////////////////////////////////
+ // Integration hooks - allow host app to inject custom behavior //
+ /////////////////////////////////////////////////////////////////////////
+ private QCodeReference onEntityCreatedHandler;
+ private QCodeReference customValidationProvider;
+
+
+
+ /*******************************************************************************
+ ** Validate the configuration before the QBit is produced.
+ *******************************************************************************/
+ @Override
+ public void validate(QInstance qInstance, List errors)
+ {
+ if(!StringUtils.hasContent(backendName))
+ {
+ errors.add("backendName is required for ExampleAppQBit");
+ }
+
+ if(qInstance.getBackend(backendName) == null)
+ {
+ errors.add("Backend not found: " + backendName);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Apply prefix to a table name if configured.
+ *******************************************************************************/
+ public String applyPrefix(String tableName)
+ {
+ if(StringUtils.hasContent(tableNamePrefix))
+ {
+ return tableNamePrefix + "_" + tableName;
+ }
+ return tableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Get the security key for permission checks.
+ *******************************************************************************/
+ public String getSecurityKey(String baseName)
+ {
+ if(StringUtils.hasContent(securityKeyPrefix))
+ {
+ return securityKeyPrefix + "." + baseName;
+ }
+ return baseName;
+ }
+
+
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Getters and fluent setters //
+ //////////////////////////////////////////////////////////////////////////////
+
+ public String getBackendName()
+ {
+ return backendName;
+ }
+
+
+ public ExampleAppQBitConfig withBackendName(String backendName)
+ {
+ this.backendName = backendName;
+ return this;
+ }
+
+
+ public String getTableNamePrefix()
+ {
+ return tableNamePrefix;
+ }
+
+
+ public ExampleAppQBitConfig withTableNamePrefix(String tableNamePrefix)
+ {
+ this.tableNamePrefix = tableNamePrefix;
+ return this;
+ }
+
+
+ public Boolean getEnableChildModule()
+ {
+ return enableChildModule;
+ }
+
+
+ public ExampleAppQBitConfig withEnableChildModule(Boolean enableChildModule)
+ {
+ this.enableChildModule = enableChildModule;
+ return this;
+ }
+
+
+ public String getSecurityKeyPrefix()
+ {
+ return securityKeyPrefix;
+ }
+
+
+ public ExampleAppQBitConfig withSecurityKeyPrefix(String securityKeyPrefix)
+ {
+ this.securityKeyPrefix = securityKeyPrefix;
+ return this;
+ }
+
+
+ public QCodeReference getOnEntityCreatedHandler()
+ {
+ return onEntityCreatedHandler;
+ }
+
+
+ public ExampleAppQBitConfig withOnEntityCreatedHandler(QCodeReference onEntityCreatedHandler)
+ {
+ this.onEntityCreatedHandler = onEntityCreatedHandler;
+ return this;
+ }
+
+
+ public QCodeReference getCustomValidationProvider()
+ {
+ return customValidationProvider;
+ }
+
+
+ public ExampleAppQBitConfig withCustomValidationProvider(QCodeReference customValidationProvider)
+ {
+ this.customValidationProvider = customValidationProvider;
+ return this;
+ }
+}
diff --git a/src/main/java/com/kingsrook/qbits/example/ExampleAppQBitProducer.java b/src/main/java/com/kingsrook/qbits/example/ExampleAppQBitProducer.java
new file mode 100644
index 0000000..9dac58d
--- /dev/null
+++ b/src/main/java/com/kingsrook/qbits/example/ExampleAppQBitProducer.java
@@ -0,0 +1,169 @@
+/*******************************************************************************
+ ** Producer for the Example Application QBit.
+ **
+ ** Application QBits produce:
+ ** - QBitMetaData for identification
+ ** - QAppSection for UI navigation (REQUIRED)
+ ** - Tables, processes, widgets, and other metadata
+ ** - Security configuration
+ *******************************************************************************/
+package com.kingsrook.qbits.example;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
+import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProducer;
+import com.kingsrook.qqq.backend.core.model.metadata.qbits.SourceQBitAware;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qbits.example.model.ExampleEntity;
+import com.kingsrook.qbits.example.model.ExampleChildEntity;
+
+
+public class ExampleAppQBitProducer implements QBitProducer
+{
+ public static final String GROUP_ID = "com.kingsrook.qbits";
+ public static final String ARTIFACT_ID = "qbit-example-app";
+ public static final String VERSION = "0.1.0";
+
+ private ExampleAppQBitConfig config;
+
+
+
+ /*******************************************************************************
+ ** Produce the QBit metadata into the QInstance.
+ *******************************************************************************/
+ @Override
+ public void produce(QInstance qInstance, String namespace) throws QException
+ {
+ /////////////////////////////////////////////////////////////////////////
+ // Validate configuration //
+ /////////////////////////////////////////////////////////////////////////
+ List errors = new ArrayList<>();
+ config.validate(qInstance, errors);
+ if(!errors.isEmpty())
+ {
+ throw new QException("Configuration errors: " + String.join(", ", errors));
+ }
+
+ /////////////////////////////////////////////////////////////////////////
+ // Create and register QBit identity //
+ /////////////////////////////////////////////////////////////////////////
+ QBitMetaData qBitMetaData = new QBitMetaData()
+ .withGroupId(GROUP_ID)
+ .withArtifactId(ARTIFACT_ID)
+ .withVersion(VERSION)
+ .withNamespace(namespace)
+ .withConfig(config);
+ qInstance.addQBit(qBitMetaData);
+
+ /////////////////////////////////////////////////////////////////////////
+ // Discover and produce all component metadata //
+ /////////////////////////////////////////////////////////////////////////
+ List> producers =
+ MetaDataProducerHelper.findProducers(getClass().getPackageName());
+
+ for(MetaDataProducerInterface> producer : producers)
+ {
+ ///////////////////////////////////////////////////////////////////////
+ // Skip disabled components //
+ ///////////////////////////////////////////////////////////////////////
+ if(!isProducerEnabled(producer))
+ {
+ continue;
+ }
+
+ MetaDataProducerOutput output = producer.produce(qInstance);
+
+ ///////////////////////////////////////////////////////////////////////
+ // Apply table prefix if configured //
+ ///////////////////////////////////////////////////////////////////////
+ if(output instanceof QTableMetaData table)
+ {
+ String prefixedName = config.applyPrefix(table.getName());
+ table.setName(prefixedName);
+ table.setBackendName(config.getBackendName());
+ }
+
+ ///////////////////////////////////////////////////////////////////////
+ // Mark scope for all SourceQBitAware outputs //
+ ///////////////////////////////////////////////////////////////////////
+ if(output instanceof SourceQBitAware sqa)
+ {
+ sqa.setSourceQBitName(qBitMetaData.getName());
+ }
+
+ output.addSelfToInstance(qInstance);
+ }
+
+ /////////////////////////////////////////////////////////////////////////
+ // Register QAppSection for UI navigation (REQUIRED for app QBits) //
+ /////////////////////////////////////////////////////////////////////////
+ qInstance.addAppSection(produceAppSection(config, qBitMetaData.getName()));
+ }
+
+
+
+ /*******************************************************************************
+ ** Produce the QAppSection for UI navigation.
+ **
+ ** This is REQUIRED for Application QBits - it defines how users navigate
+ ** to the tables, processes, and widgets in the UI.
+ *******************************************************************************/
+ private QAppSection produceAppSection(ExampleAppQBitConfig config, String qbitName)
+ {
+ List tables = new ArrayList<>();
+ tables.add(config.applyPrefix(ExampleEntity.TABLE_NAME));
+
+ if(Boolean.TRUE.equals(config.getEnableChildModule()))
+ {
+ tables.add(config.applyPrefix(ExampleChildEntity.TABLE_NAME));
+ }
+
+ return new QAppSection()
+ .withName(config.applyPrefix("exampleApp"))
+ .withLabel("Example Application")
+ .withIcon(new QIcon().withName("dashboard"))
+ .withTables(tables)
+ .withSourceQBitName(qbitName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Check if a producer should be enabled based on configuration.
+ *******************************************************************************/
+ private boolean isProducerEnabled(MetaDataProducerInterface> producer)
+ {
+ /////////////////////////////////////////////////////////////////////////
+ // Skip child entity if module disabled //
+ /////////////////////////////////////////////////////////////////////////
+ if(!Boolean.TRUE.equals(config.getEnableChildModule()))
+ {
+ if(producer.getClass().getSimpleName().contains("ExampleChildEntity"))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Fluent setters //
+ //////////////////////////////////////////////////////////////////////////////
+
+ public ExampleAppQBitProducer withConfig(ExampleAppQBitConfig config)
+ {
+ this.config = config;
+ return this;
+ }
+}
diff --git a/src/main/java/com/kingsrook/qbits/example/model/ExampleChildEntity.java b/src/main/java/com/kingsrook/qbits/example/model/ExampleChildEntity.java
new file mode 100644
index 0000000..48eeb3e
--- /dev/null
+++ b/src/main/java/com/kingsrook/qbits/example/model/ExampleChildEntity.java
@@ -0,0 +1,159 @@
+/*******************************************************************************
+ ** Example child entity demonstrating parent-child relationships.
+ **
+ ** This entity shows:
+ ** - Foreign key relationship to parent entity
+ ** - Conditional inclusion via config toggle
+ *******************************************************************************/
+package com.kingsrook.qbits.example.model;
+
+
+import java.time.Instant;
+import com.kingsrook.qqq.backend.core.model.data.QField;
+import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
+import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
+
+
+@QMetaDataProducingEntity(producePossibleValueSource = true)
+public class ExampleChildEntity extends QRecordEntity
+{
+ public static final String TABLE_NAME = "exampleChildEntity";
+
+ @QField(isPrimaryKey = true)
+ private Integer id;
+
+ @QField(isRequired = true, possibleValueSourceName = "exampleEntity")
+ private Integer exampleEntityId;
+
+ @QField(isRequired = true, maxLength = 100)
+ private String name;
+
+ @QField(maxLength = 500)
+ private String description;
+
+ @QField
+ private Integer sortOrder;
+
+ @QField
+ private Boolean isActive = true;
+
+ @QField
+ private Instant createDate;
+
+ @QField
+ private Instant modifyDate;
+
+
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Fluent setters //
+ //////////////////////////////////////////////////////////////////////////////
+
+ public ExampleChildEntity withId(Integer id)
+ {
+ this.id = id;
+ return this;
+ }
+
+
+ public ExampleChildEntity withExampleEntityId(Integer exampleEntityId)
+ {
+ this.exampleEntityId = exampleEntityId;
+ return this;
+ }
+
+
+ public ExampleChildEntity withName(String name)
+ {
+ this.name = name;
+ return this;
+ }
+
+
+ public ExampleChildEntity withDescription(String description)
+ {
+ this.description = description;
+ return this;
+ }
+
+
+ public ExampleChildEntity withSortOrder(Integer sortOrder)
+ {
+ this.sortOrder = sortOrder;
+ return this;
+ }
+
+
+ public ExampleChildEntity withIsActive(Boolean isActive)
+ {
+ this.isActive = isActive;
+ return this;
+ }
+
+
+ public ExampleChildEntity withCreateDate(Instant createDate)
+ {
+ this.createDate = createDate;
+ return this;
+ }
+
+
+ public ExampleChildEntity withModifyDate(Instant modifyDate)
+ {
+ this.modifyDate = modifyDate;
+ return this;
+ }
+
+
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Getters //
+ //////////////////////////////////////////////////////////////////////////////
+
+ public Integer getId()
+ {
+ return id;
+ }
+
+
+ public Integer getExampleEntityId()
+ {
+ return exampleEntityId;
+ }
+
+
+ public String getName()
+ {
+ return name;
+ }
+
+
+ public String getDescription()
+ {
+ return description;
+ }
+
+
+ public Integer getSortOrder()
+ {
+ return sortOrder;
+ }
+
+
+ public Boolean getIsActive()
+ {
+ return isActive;
+ }
+
+
+ public Instant getCreateDate()
+ {
+ return createDate;
+ }
+
+
+ public Instant getModifyDate()
+ {
+ return modifyDate;
+ }
+}
diff --git a/src/main/java/com/kingsrook/qbits/example/model/ExampleEntity.java b/src/main/java/com/kingsrook/qbits/example/model/ExampleEntity.java
new file mode 100644
index 0000000..e7ebc52
--- /dev/null
+++ b/src/main/java/com/kingsrook/qbits/example/model/ExampleEntity.java
@@ -0,0 +1,145 @@
+/*******************************************************************************
+ ** Example entity for an Application QBit.
+ **
+ ** This entity demonstrates:
+ ** - Standard QQQ entity patterns
+ ** - PossibleValueSource generation
+ ** - Fields with various types and constraints
+ *******************************************************************************/
+package com.kingsrook.qbits.example.model;
+
+
+import java.time.Instant;
+import com.kingsrook.qqq.backend.core.model.data.QField;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
+import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
+
+
+@QMetaDataProducingEntity(producePossibleValueSource = true)
+public class ExampleEntity extends QRecordEntity
+{
+ public static final String TABLE_NAME = "exampleEntity";
+
+ @QField(isPrimaryKey = true)
+ private Integer id;
+
+ @QField(isRequired = true, maxLength = 100)
+ private String name;
+
+ @QField(maxLength = 500)
+ private String description;
+
+ @QField(label = "Status")
+ private String status;
+
+ @QField
+ private Boolean isActive = true;
+
+ @QField
+ private Instant createDate;
+
+ @QField
+ private Instant modifyDate;
+
+
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Fluent setters //
+ //////////////////////////////////////////////////////////////////////////////
+
+ public ExampleEntity withId(Integer id)
+ {
+ this.id = id;
+ return this;
+ }
+
+
+ public ExampleEntity withName(String name)
+ {
+ this.name = name;
+ return this;
+ }
+
+
+ public ExampleEntity withDescription(String description)
+ {
+ this.description = description;
+ return this;
+ }
+
+
+ public ExampleEntity withStatus(String status)
+ {
+ this.status = status;
+ return this;
+ }
+
+
+ public ExampleEntity withIsActive(Boolean isActive)
+ {
+ this.isActive = isActive;
+ return this;
+ }
+
+
+ public ExampleEntity withCreateDate(Instant createDate)
+ {
+ this.createDate = createDate;
+ return this;
+ }
+
+
+ public ExampleEntity withModifyDate(Instant modifyDate)
+ {
+ this.modifyDate = modifyDate;
+ return this;
+ }
+
+
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Getters //
+ //////////////////////////////////////////////////////////////////////////////
+
+ public Integer getId()
+ {
+ return id;
+ }
+
+
+ public String getName()
+ {
+ return name;
+ }
+
+
+ public String getDescription()
+ {
+ return description;
+ }
+
+
+ public String getStatus()
+ {
+ return status;
+ }
+
+
+ public Boolean getIsActive()
+ {
+ return isActive;
+ }
+
+
+ public Instant getCreateDate()
+ {
+ return createDate;
+ }
+
+
+ public Instant getModifyDate()
+ {
+ return modifyDate;
+ }
+}