feat(release): migrate application template to QQQ 4.0 #4

Merged
KofTwentyTwo merged 4 commits from feature/qqq-4-polish into main 2026-09-24 18:32:07 +00:00
13 changed files with 112 additions and 191 deletions

No files matched your search

+13 -1
View File
@@ -3,7 +3,19 @@ version: 2.1
orbs:
qqq-orb: kingsrook/qqq-orb@0.5.0
commands:
configure-package-install:
steps:
- run:
name: Keep package installation unattended
command: |
# These build machines are discarded after the job.
sudo install -d /etc/needrestart/conf.d
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
workflows:
build:
jobs:
- qqq-orb/mvn_test
- qqq-orb/mvn_test_only:
pre-steps:
- configure-package-install
+4
View File
@@ -3,6 +3,10 @@
GitHub template repository for scaffolding Application QBits (complete mini-applications
with tables, processes, widgets, and UI navigation) on the QQQ platform.
## QQQ 4.0 baseline — 2026-09-24
The historical review below predates the 4.0 migration. This template targets Java 21 and the public QQQ 4.0.0 release from Maven Central. The corrected APIs, license references and actual template commands are in README.md and docs/00-getting-started.md. Preserve the documented multi-instance and scaffold limitations.
## Knowledge base
Reviewed dossier and platform knowledge live in the second-brain vault:
+8 -15
View File
@@ -12,30 +12,23 @@ Use this template when building:
## Quick Start
1. Click "Use this template" on GitHub
2. Clone your new repository
3. Run the customization script:
```bash
python scripts/customize_template.py --name my-app --package com.mycompany.qbits.myapp
```
4. Implement your tables, processes, and widgets
5. Add your QAppSection for UI navigation
Requires **Java 21**, **Maven 3.8+**, and QQQ **4.0.0**. Create a repository with **Use this template**, customize its Maven coordinates and Java package/classes, then run `mvn clean verify`. Follow the [Getting Started guide](docs/00-getting-started.md) for the full sequence.
## Structure
```
src/main/java/com/kingsrook/qbits/example/
├── ExampleAppQBitConfig.java # Configuration options
├── ExampleAppQBitProducer.java # Entry point, registers QAppSection
├── ExampleAppQBitConfig.java
├── ExampleAppQBitProducer.java
├── model/
│ ├── ExampleEntity.java
│ └── ExampleChildEntity.java
├── processes/
│ └── ExampleProcess.java
├── widgets/
│ └── ExampleDashboardWidget.java
└── api/
└── ExampleApiHandler.java
│ ├── ExampleProcessStep.java
│ └── ExampleProcessMetaDataProducer.java
└── widgets/
├── ExampleDashboardWidgetRenderer.java
└── ExampleDashboardWidgetMetaDataProducer.java
```
## Key Characteristics
+20 -37
View File
@@ -1,47 +1,30 @@
# Getting Started
This template helps you create Application QBits - complete mini-applications with UI navigation.
Targets Java 21, Maven 3.8 or later, and QQQ 4.0.0. Maven resolves the framework dependencies from Maven Central.
## Quick Start
1. Choose **Use this template** on GitHub, then clone the repository you created.
2. In `pom.xml`, set your own `groupId`, `artifactId`, name, description, and version.
3. Use your IDE's package refactoring to rename `com.kingsrook.qbits.example` throughout `src/`. Rename the `Example*` classes and update their imports and references with the IDE's rename refactoring.
4. Update the QBit producer's `GROUP_ID`, `ARTIFACT_ID`, and `VERSION` constants to match your project. Give the metadata names their own stable names before combining the QBit with other examples.
5. Build the generated project:
1. **Create from template:**
```bash
gh repo create my-company/qbit-my-app --template QRun-IO/qbit-template-application
```
```bash
mvn clean verify
```
2. **Run customization script:**
```bash
python scripts/customize_template.py --name my-app --package com.mycompany.qbits.myapp
```
The repository provides example Java sources; customization uses normal package/class refactoring. Keep example coordinates unpublished. After customizing, add tests for your QBit's behavior before setting up its publishing workflow.
3. **Build and test:**
```bash
mvn clean test
```
## Register with a host application
## Key Differences from Other QBit Types
| Feature | Extension | Data | Application |
|---------|-----------|------|-------------|
| QAppSection | No | Optional | **Required** |
| Tables | 0-few | Few (reference) | Many (business) |
| Processes | Customizers | Sync | Full business logic |
| Widgets | No | No | **Yes** |
## What to Customize
1. `ExampleAppQBitConfig.java` - Add your configuration options
2. `ExampleAppQBitProducer.java` - Update QAppSection with your tables
3. `model/` - Replace with your entities
4. `processes/` - Implement your business logic
5. `widgets/` - Create your dashboard components
## Usage in Host Application
The following uses the original example names; substitute the names chosen above:
```java
new MyAppQBitProducer()
.withConfig(new MyAppQBitConfig()
.withBackendName("rdbms")
.withTableNamePrefix("myapp"))
.produce(qInstance, "my-app");
new ExampleAppQBitProducer()
.withConfig(new ExampleAppQBitConfig()
.withBackendName("rdbms"))
.produce(qInstance, "my-application");
```
Register the configured backend in the host first. If you add table prefixes or multiple QBit instances, update related table references, possible-value-source names, and process/widget names consistently. A table prefix alone does not namespace all metadata.
[Tables and Relationships](01-tables-and-relationships.md) describes the model; the `processes/` and `widgets/` directories contain the remaining examples.
+2 -1
View File
@@ -42,8 +42,9 @@ Application QBits support optional table prefixing:
new MyAppQBitProducer()
.withConfig(new MyAppQBitConfig()
.withBackendName("rdbms")
.withTableNamePrefix("sales"))
.produce(qInstance, "sales-app");
```
The producer automatically applies the prefix to all table names.
The producer applies the prefix to table names. When extending this pattern, also update possible-value-source names, relationship targets, process tables, and navigation references; table renaming alone does not provide complete multi-instance support.
+3 -54
View File
@@ -1,58 +1,7 @@
# Processes
Application QBits include processes for business logic execution.
Start with [ExampleProcessMetaDataProducer](../src/main/java/com/kingsrook/qbits/example/processes/ExampleProcessMetaDataProducer.java) and [ExampleProcessStep](../src/main/java/com/kingsrook/qbits/example/processes/ExampleProcessStep.java). The producer defines the process, its table, inputs, and backend step; the step transforms `PENDING` records to `PROCESSED` and returns them in the output.
## Process Structure
In QQQ 4.0, import `MetaDataProducerInterface` from `com.kingsrook.qqq.backend.core.model.metadata`. Define process inputs through `withInputMetaData(new QFunctionInputMetaData().withField(...))`. An `AbstractTransformStep` must implement both `runOnePage(RunBackendStepInput, RunBackendStepOutput)` and `getProcessSummary(RunBackendStepOutput, boolean)`; keep the example's implementations when renaming it.
```java
public class MyProcessMetaDataProducer implements MetaDataProducerInterface<QProcessMetaData>
{
public static final String NAME = "myProcess";
@Override
public QProcessMetaData produce(QInstance qInstance)
{
return new QProcessMetaData()
.withName(NAME)
.withLabel("My Process")
.withTableName(MyEntity.TABLE_NAME)
.withStepList(List.of(
new QBackendStepMetaData()
.withName("execute")
.withCode(new QCodeReference(MyProcessStep.class))
));
}
}
```
## Process Steps
Implement `AbstractTransformStep` for record processing:
```java
public class MyProcessStep extends AbstractTransformStep
{
@Override
public void runOnePage(RunBackendStepInput input, RunBackendStepOutput output)
{
for(QRecord record : input.getRecords())
{
// Business logic here
output.addRecord(record);
}
}
}
```
## Including in QAppSection
Add processes to your app's navigation:
```java
return new QAppSection()
.withName("myApp")
.withTables(List.of(...))
.withProcesses(List.of(
config.applyPrefix(MyProcess.NAME)
));
```
To make the process discoverable in navigation, add its registered name to a `QAppSection` through `withProcesses(List.of(ExampleProcessMetaDataProducer.NAME))`, then include the section in a `QAppMetaData` registered with `qInstance.addApp(...)`. Use the actual registered name if you introduce a prefix. Add tests for record changes and failure cases when replacing the example business logic.
+15 -53
View File
@@ -1,61 +1,23 @@
# Widgets and UI
Application QBits include dashboard widgets and UI navigation via QAppSection.
## QAppSection (Required)
Every Application QBit must register a QAppSection:
The application producer registers a `QAppMetaData` containing a `QAppSection`. The section references metadata names already registered in the instance; it is not registered directly on `QInstance`.
```java
private QAppSection produceAppSection(MyAppConfig config, String qbitName)
{
return new QAppSection()
.withName(config.applyPrefix("myApp"))
.withLabel("My Application")
.withIcon(new QIcon().withName("dashboard"))
.withTables(List.of(
config.applyPrefix(Order.TABLE_NAME),
config.applyPrefix(OrderLine.TABLE_NAME)
))
.withProcesses(List.of(
config.applyPrefix(ProcessOrder.NAME)
))
.withWidgets(List.of(
config.applyPrefix(OrderDashboard.NAME)
))
.withSourceQBitName(qbitName);
}
QAppSection section = new QAppSection()
.withName("exampleSection")
.withLabel("Example Application")
.withTables(List.of(ExampleEntity.TABLE_NAME, ExampleChildEntity.TABLE_NAME))
.withProcesses(List.of(ExampleProcessMetaDataProducer.NAME))
.withWidgets(List.of(ExampleDashboardWidgetMetaDataProducer.NAME));
qInstance.addApp(new QAppMetaData()
.withName("exampleApp")
.withLabel("Example Application")
.withSection(section));
```
## Dashboard Widgets
Use [ExampleAppQBitProducer](../src/main/java/com/kingsrook/qbits/example/ExampleAppQBitProducer.java) as the starting point. Keep these references consistent when enabling/disabling tables or introducing prefixes.
Create widgets with metadata producer and renderer:
The widget has two parts: [its metadata producer](../src/main/java/com/kingsrook/qbits/example/widgets/ExampleDashboardWidgetMetaDataProducer.java) declares a statistics widget and its renderer, and [its renderer](../src/main/java/com/kingsrook/qbits/example/widgets/ExampleDashboardWidgetRenderer.java) returns a `RenderWidgetOutput` containing `StatisticsData`. Replace the example count with a query or calculation. Choose a widget type that matches the returned data model.
```java
// MetaData Producer
public class MyWidgetMetaDataProducer implements MetaDataProducerInterface<QWidgetMetaData>
{
@Override
public QWidgetMetaData produce(QInstance qInstance)
{
return new QWidgetMetaData()
.withName("myWidget")
.withLabel("My Dashboard")
.withType("chart")
.withCodeReference(new QCodeReference(MyWidgetRenderer.class));
}
}
// Renderer
public class MyWidgetRenderer extends AbstractWidgetRenderer
{
@Override
public RenderWidgetOutput render(RenderWidgetInput input)
{
Map<String, Object> data = new HashMap<>();
data.put("title", "Dashboard");
// Add chart data, counts, etc.
return new RenderWidgetOutput(data);
}
}
```
For QQQ 4.0, `AbstractWidgetRenderer` is in `com.kingsrook.qqq.backend.core.actions.dashboard.widgets`; `RenderWidgetInput` and `RenderWidgetOutput` are in `com.kingsrook.qqq.backend.core.model.actions.widgets`.
+1 -1
View File
@@ -16,7 +16,7 @@
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<qqq.version>0.35.0</qqq.version>
<qqq.version>4.0.0</qqq.version>
</properties>
<dependencies>
@@ -17,6 +17,7 @@ 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.QAppMetaData;
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;
@@ -104,20 +105,20 @@ public class ExampleAppQBitProducer implements QBitProducer
}
/////////////////////////////////////////////////////////////////////////
// Register QAppSection for UI navigation (REQUIRED for app QBits) //
// Register QAppMetaData for UI navigation (REQUIRED for app QBits) //
/////////////////////////////////////////////////////////////////////////
qInstance.addAppSection(produceAppSection(config, qBitMetaData.getName()));
qInstance.addApp(produceApp(config, qBitMetaData.getName()));
}
/*******************************************************************************
** Produce the QAppSection for UI navigation.
** Produce the QAppMetaData 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)
private QAppMetaData produceApp(ExampleAppQBitConfig config, String qbitName)
{
List<String> tables = new ArrayList<>();
tables.add(config.applyPrefix(ExampleEntity.TABLE_NAME));
@@ -127,12 +128,17 @@ public class ExampleAppQBitProducer implements QBitProducer
tables.add(config.applyPrefix(ExampleChildEntity.TABLE_NAME));
}
return new QAppSection()
QAppSection section = new QAppSection()
.withName(config.applyPrefix("exampleSection"))
.withLabel("Example Application")
.withIcon(new QIcon().withName("dashboard"))
.withTables(tables);
return new QAppMetaData()
.withName(config.applyPrefix("exampleApp"))
.withLabel("Example Application")
.withIcon(new QIcon().withName("dashboard"))
.withTables(tables)
.withSourceQBitName(qbitName);
.withSection(section);
}
@@ -7,12 +7,13 @@ package com.kingsrook.qbits.example.processes;
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.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
import com.kingsrook.qbits.example.model.ExampleEntity;
@@ -32,15 +33,14 @@ public class ExampleProcessMetaDataProducer implements MetaDataProducerInterface
.withName(NAME)
.withLabel("Example Process")
.withTableName(ExampleEntity.TABLE_NAME)
.withInputFields(List.of(
new QFieldMetaData("confirmAction", QFieldType.BOOLEAN)
.withLabel("Confirm Action")
.withDefaultValue(false)
))
.withStepList(List.of(
new QBackendStepMetaData()
.withName("process")
.withCode(new QCodeReference(ExampleProcessStep.class))
.withInputData(new QFunctionInputMetaData()
.withField(new QFieldMetaData("confirmAction", QFieldType.BOOLEAN)
.withLabel("Confirm Action")
.withDefaultValue(false)))
));
}
}
@@ -9,11 +9,13 @@
package com.kingsrook.qbits.example.processes;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@@ -50,4 +52,16 @@ public class ExampleProcessStep extends AbstractTransformStep
LOG.info("Processing complete", logPair("processed", records.size()));
}
/*******************************************************************************
** Return the process summary. Override this method in subclasses to provide
** meaningful summary lines for the result screen.
*******************************************************************************/
@Override
public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen)
{
return (new ArrayList<>());
}
}
@@ -7,10 +7,11 @@ package com.kingsrook.qbits.example.widgets;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
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.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
public class ExampleDashboardWidgetMetaDataProducer implements MetaDataProducerInterface<QWidgetMetaData>
@@ -28,7 +29,7 @@ public class ExampleDashboardWidgetMetaDataProducer implements MetaDataProducerI
return new QWidgetMetaData()
.withName(NAME)
.withLabel("Example Dashboard")
.withType("chart")
.withType(WidgetType.STATISTICS.getType())
.withCodeReference(new QCodeReference(ExampleDashboardWidgetRenderer.class));
}
}
@@ -6,12 +6,11 @@
package com.kingsrook.qbits.example.widgets;
import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.dashboard.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.dashboard.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.actions.dashboard.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.StatisticsData;
public class ExampleDashboardWidgetRenderer extends AbstractWidgetRenderer
@@ -22,16 +21,13 @@ public class ExampleDashboardWidgetRenderer extends AbstractWidgetRenderer
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
Map<String, Object> data = new HashMap<>();
data.put("title", "Example Dashboard");
data.put("description", "This is an example widget for the Application QBit template.");
/////////////////////////////////////////////////////////////////////////
// Add chart data, counts, or other dashboard information here //
/////////////////////////////////////////////////////////////////////////
data.put("totalCount", 0);
data.put("activeCount", 0);
StatisticsData data = new StatisticsData()
.withCount(0)
.withCountContext("Total items");
return new RenderWidgetOutput(data);
return (new RenderWidgetOutput(data));
}
}