Deliver QQQ 4.0 starter with Material Dashboard 0.41 #2

Merged
KofTwentyTwo merged 3 commits from feature/qqq-4-polish into main 2026-09-24 19:17:34 +00:00
6 changed files with 145 additions and 221 deletions

No files matched your search

+4
View File
@@ -3,6 +3,10 @@
Minimal example/learning QQQ application (one RDBMS table + Material Dashboard).
For production scaffolding use `new-qqq-application-template` instead (per README).
## QQQ 4.0 baseline — 2026-09-24
This learning starter targets Java 21, QQQ 4.0.0 and Material Dashboard 0.41.0. Its release dependencies resolve from Maven Central; use README.md for the database, environment and build commands. The historical review below predates this migration. The separate production scaffold is outside this release train's certification.
## Knowledge base
Deep-review dossiers live in the second-brain vault:
+28 -112
View File
@@ -1,140 +1,56 @@
# qqq-app-starter
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
A small QQQ 4.0 application for learning: one MySQL table, application navigation, and Material Dashboard 0.41.0. Requires **Java 21**, **Maven 3.8+**, and MySQL. The POM resolves QQQ 4.0.0 and the dashboard from Maven Central.
Example QQQ application for learning and experimentation.
## Database and configuration
**For:** Developers learning QQQ who want a working example to explore
**Status:** Stable
Create a dedicated development database and user, then run this SQL in that database:
## Why This Exists
Learning a framework is easier with a running example. This starter app connects to a MySQL database and exposes a table through the QQQ dashboard. Use it to understand how QQQ metadata, entities, and the dashboard work together.
For new production projects, use [new-qqq-application-template](https://github.com/QRun-IO/new-qqq-application-template) instead.
## What's Included
- **MetaDataProvider** - Example metadata configuration
- **Javalin Server** - HTTP server setup
- **Dashboard** - Material Dashboard at `/`
- **Sample Entity** - Configurable table definition
## Quick Start
### Prerequisites
- Java 17+
- Maven 3.8+
- MySQL database
- GitHub token for Maven registry access
### Maven Settings
Add to `~/.m2/settings.xml`:
```xml
<settings>
<servers>
<server>
<id>github-qqq-maven-registry</id>
<username>YOUR-GITHUB-USERNAME</username>
<password>YOUR-GITHUB-TOKEN</password>
</server>
</servers>
</settings>
```sql
CREATE TABLE sample (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255)
);
INSERT INTO sample (name) VALUES ('First example');
```
Generate a token at: GitHub > Settings > Developer Settings > Personal access tokens > Tokens (classic) with `read:packages` scope.
### Configuration
Create `.env` file:
Create a `.env` file in the project directory, using your database connection settings:
```properties
RDBMS_VENDOR=mysql
RDBMS_HOSTNAME=localhost
RDBMS_PORT=3306
RDBMS_DATABASE_NAME=your_database
RDBMS_USERNAME=your_user
RDBMS_PASSWORD=your_password
RDBMS_DATABASE_NAME=qqq_starter
RDBMS_USERNAME=qqq_starter
RDBMS_PASSWORD=your_local_database_password
```
### Run
QQQ reads this file automatically when launched from that directory. Process environment variables also work. The `.env` file is ignored by Git.
## Build and run
```bash
# Build
mvn package
# Run
mvn clean verify
java -jar target/qqq-app-starter-0.1-SNAPSHOT.jar
```
Open `http://localhost:8000/`
Open <http://localhost:8000/>, select **Sample App**, and open **Sample Table**. View the seeded row, create a row, and edit its name. The starter uses fully anonymous authentication for local learning; configure authentication before using real application data.
## Usage
## Customize
### Configure Your Table
Edit [StarterAppMetaDataProvider](src/main/java/com/kingsrook/qqq/starterapp/StarterAppMetaDataProvider.java) to define tables, backends, branding, and navigation. Register new tables in `defineQInstance()`, point them at `rdbmsBackend`, and use `RDBMSTableBackendDetails` for the actual SQL table name. Keep the metadata fields consistent with your database schema.
Edit `StarterAppMetaDataProvider.java`:
[StarterAppJavalinServer](src/main/java/com/kingsrook/qqq/starterapp/StarterAppJavalinServer.java) starts the HTTP server and scheduler. The bundled `StarterAppCli` is an empty placeholder, not a runnable CLI.
```java
private QTableMetaData defineSampleTable() {
return new QTableMetaData()
.withName("your_table")
.withBackendName("rdbms")
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("name", QFieldType.STRING));
}
```
For migration changes, see the [QQQ 4.0 migration guide](https://github.com/QRun-IO/qqq/blob/main/docs/migration/4.0.adoc). The separate [new-qqq-application-template](https://github.com/QRun-IO/new-qqq-application-template) is a production scaffolding project outside this release train.
### Add More Tables
## Troubleshooting
1. Create additional table definitions in the MetaDataProvider
2. Register them in the `defineInstance()` method
3. Restart the server
### Running in Docker
```dockerfile
FROM openjdk:17-alpine
EXPOSE 8000
ADD target/qqq-app-starter-0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
```
```bash
docker build -t qqq-app-starter .
docker run -p 8000:8000 --env-file .env qqq-app-starter
```
## Project Structure
| File | Purpose |
|------|---------|
| `StarterAppMetaDataProvider.java` | Defines tables, backends, apps |
| `StarterAppJavalinServer.java` | Starts HTTP server |
| `.env` | Database connection settings |
## Common Issues
**401 from Maven**: GitHub token missing or invalid. Check `~/.m2/settings.xml`.
**NullPointerException in defineSampleApp**: Table name mismatch between definition and reference.
**Connection refused**: Check database hostname, port, and credentials in `.env`.
## Project Status
Stable example project. For production apps, use [new-qqq-application-template](https://github.com/QRun-IO/new-qqq-application-template).
## Contributing
1. Fork the repository
2. Create a feature branch
3. Submit a pull request
- **Database connection refused:** verify MySQL is listening and check the hostname/port in `.env`.
- **Missing table:** run the SQL above in the database named by `RDBMS_DATABASE_NAME`.
- **Missing environment variables:** launch from the directory containing `.env`, or export those variables explicitly.
- **Wrong Java version:** both `java -version` and `mvn -version` must report Java 21 or later.
## License
Apache-2.0 - See [LICENSE](LICENSE) for details.
See [LICENSE](LICENSE) and [NOTICE](NOTICE).
+14 -72
View File
@@ -36,16 +36,16 @@
<!-- Common props for all qqq modules -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.showDeprecation>true</maven.compiler.showDeprecation>
<maven.compiler.showWarnings>true</maven.compiler.showWarnings>
<checkstyle.failOnViolation>true</checkstyle.failOnViolation>
<coverage.haltOnFailure>true</coverage.haltOnFailure>
<coverage.instructionCoveredRatioMinimum>0.80</coverage.instructionCoveredRatioMinimum>
<qqq.versions.bom>0.24.0-20250106-145452</qqq.versions.bom>
<qqq.versions.frontendMaterialDashboard>0.24.0-20241122.223138-1</qqq.versions.frontendMaterialDashboard>
<qqq.versions.bom>4.0.0</qqq.versions.bom>
<qqq.versions.frontendMaterialDashboard>0.41.0</qqq.versions.frontendMaterialDashboard>
</properties>
<dependencyManagement>
@@ -101,30 +101,13 @@
</dependency>
<!-- Other common 3rd party deps-->
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>4.10.0</version>
</dependency>
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.23.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.23.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.23.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
@@ -135,7 +118,6 @@
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.23.1</version>
<scope>test</scope>
</dependency>
</dependencies>
@@ -222,6 +204,15 @@
<version>3.0.0-M5</version>
<configuration>
<argLine>@{jaCoCoArgLine}</argLine>
<failIfNoTests>true</failIfNoTests>
<environmentVariables>
<RDBMS_VENDOR>mysql</RDBMS_VENDOR>
<RDBMS_HOSTNAME>127.0.0.1</RDBMS_HOSTNAME>
<RDBMS_PORT>3306</RDBMS_PORT>
<RDBMS_DATABASE_NAME>qqq_starter_test</RDBMS_DATABASE_NAME>
<RDBMS_USERNAME>test</RDBMS_USERNAME>
<RDBMS_PASSWORD>test</RDBMS_PASSWORD>
</environmentVariables>
</configuration>
</plugin>
<plugin>
@@ -272,12 +263,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
<configuration>
<includes>
<include>com/nutrifresh/**/*</include>
</includes>
</configuration>
<version>0.8.14</version>
<executions>
<execution>
<id>pre-unit-test</id>
@@ -324,53 +310,9 @@
<artifactId>exec-maven-plugin</artifactId>
<groupId>org.codehaus.mojo</groupId>
<version>3.0.0</version>
<executions>
<execution>
<id>test-coverage-summary</id>
<phase>verify</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>sh</executable>
<arguments>
<argument>-c</argument>
<argument>
<![CDATA[
echo "Element\nInstructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers
xpath -n -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
echo
echo "Jacoco coverage summary report:"
echo " See also target/site/jacoco/index.html"
echo " and https://www.jacoco.org/jacoco/trunk/doc/counters.html"
echo "------------------------------------------------------------"
paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}'
rm /tmp/$$.headers /tmp/$$.values
]]>
</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>github-qqq-maven-registry</id>
<name>GitHub QQQ Maven Registry</name>
<url>https://maven.pkg.github.com/Kingsrook/qqq-maven-registry</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>github-qqq-maven-registry</id>
<name>GitHub QQQ Maven Registry</name>
<url>https://maven.pkg.github.com/Kingsrook/qqq-maven-registry</url>
</repository>
</distributionManagement>
</project>
@@ -23,26 +23,23 @@ package com.kingsrook.qqq.starterapp;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer;
/*******************************************************************************
** Start a javalin (http) qqq server.
**
** Supported system properties:
**
** -Dqqq.scheduleManager.enabled=false - do not start the ScheduleManager
** (used inside that class).
**
** -Dqqq.javalin.hotSwapInstance=true - to cause the QInstance to be hot-swapped.
** Useful during development, to avoid needing as many server restarts.
** Starts the starter application's HTTP server and scheduled jobs.
*******************************************************************************/
public class StarterAppJavalinServer
public class StarterAppJavalinServer extends QApplicationJavalinServer
{
private static final QLogger LOG = QLogger.getLogger(StarterAppJavalinServer.class);
/*******************************************************************************
**
*******************************************************************************/
public StarterAppJavalinServer()
{
super(new StarterAppMetaDataProvider());
}
@@ -51,29 +48,8 @@ public class StarterAppJavalinServer
*******************************************************************************/
public static void main(String[] args) throws QException
{
try
{
/////////////////////////////
// define your application //
/////////////////////////////
StarterAppMetaDataProvider application = new StarterAppMetaDataProvider();
/////////////////////////////////////
// start the javalin (http) server //
/////////////////////////////////////
QApplicationJavalinServer javalinServer = new QApplicationJavalinServer(application);
javalinServer.start();
//////////////////////////////////////////////////////////////////////////
// if you want to use scheduled jobs, start the schedule manager module //
//////////////////////////////////////////////////////////////////////////
QScheduleManager scheduleManager = QScheduleManager.initInstance(application.defineQInstance(), () -> new QSystemUserSession());
scheduleManager.start();
}
catch(Exception e)
{
LOG.error("Error starting application server.", e);
}
new StarterAppJavalinServer().start();
QScheduleManager scheduleManager = QScheduleManager.initInstance(new StarterAppMetaDataProvider().defineQInstance(), () -> new QSystemUserSession());
scheduleManager.start();
}
}
@@ -29,6 +29,7 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.AuthScope;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
@@ -64,7 +65,7 @@ public class StarterAppMetaDataProvider extends AbstractQQQApplication
QInstance qInstance = new QInstance();
qInstance.setBranding(defineBranding());
qInstance.setAuthentication(defineAuthentication());
qInstance.registerAuthenticationProvider(AuthScope.instanceDefault(), defineAuthentication());
qInstance.addBackend(defineRDBMSBackend());
qInstance.addTable(defineSampleTable());
qInstance.addApp(defineSampleApp(qInstance));
@@ -0,0 +1,85 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.starterapp;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicReference;
import com.kingsrook.qqq.backend.core.context.QContext;
import io.javalin.Javalin;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Verifies the new-user HTTP entry point and its registered table metadata.
*******************************************************************************/
class StarterAppTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testServer_servesDashboardAndSampleTable() throws Exception
{
StarterAppJavalinServer server = new StarterAppJavalinServer();
AtomicReference<Javalin> service = new AtomicReference<>();
server.setPort(0);
server.withJavalinConfigurationCustomizer(service::set);
server.withJavalinConfigCustomizer(config -> config.routes.get("/starter-configuration-check",
context -> context.status(202).result("configured")));
try
{
server.start();
URI baseUri = URI.create("http://localhost:" + service.get().port());
try(HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build())
{
HttpResponse<String> dashboard = client.send(HttpRequest.newBuilder(baseUri.resolve("/")).timeout(Duration.ofSeconds(5)).build(), HttpResponse.BodyHandlers.ofString());
assertEquals(200, dashboard.statusCode());
assertTrue(dashboard.body().contains("<html"));
HttpResponse<String> metadata = client.send(HttpRequest.newBuilder(baseUri.resolve("/metaData")).timeout(Duration.ofSeconds(5)).build(), HttpResponse.BodyHandlers.ofString());
assertEquals(200, metadata.statusCode());
JSONObject metadataBody = new JSONObject(metadata.body());
assertTrue(metadataBody.getJSONObject("tables").has("sampleTable"));
assertTrue(metadataBody.getJSONObject("apps").has("sampleApp"));
HttpResponse<String> configured = client.send(HttpRequest.newBuilder(baseUri.resolve("/starter-configuration-check"))
.timeout(Duration.ofSeconds(5)).build(), HttpResponse.BodyHandlers.ofString());
assertEquals(202, configured.statusCode());
assertEquals("configured", configured.body());
}
}
finally
{
server.stop();
QContext.clear();
}
}
}