From 33fe38bd74fb78a07fdc9e878c67125ebe181ea2 Mon Sep 17 00:00:00 2001 From: James Maes Date: Sat, 27 Dec 2025 15:09:02 -0600 Subject: [PATCH] feat: add core application QBit structure --- .gitignore | 31 ++++ pom.xml | 90 ++++++++++ .../qbits/example/ExampleAppQBitConfig.java | 164 +++++++++++++++++ .../qbits/example/ExampleAppQBitProducer.java | 169 ++++++++++++++++++ .../example/model/ExampleChildEntity.java | 159 ++++++++++++++++ .../qbits/example/model/ExampleEntity.java | 145 +++++++++++++++ 6 files changed, 758 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/com/kingsrook/qbits/example/ExampleAppQBitConfig.java create mode 100644 src/main/java/com/kingsrook/qbits/example/ExampleAppQBitProducer.java create mode 100644 src/main/java/com/kingsrook/qbits/example/model/ExampleChildEntity.java create mode 100644 src/main/java/com/kingsrook/qbits/example/model/ExampleEntity.java 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; + } +}