Deliver QQQ 4.0 starter with Material Dashboard 0.41 #2
No files matched your search
@@ -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:
|
||||
|
||||
@@ -1,140 +1,56 @@
|
||||
# qqq-app-starter
|
||||
|
||||
[](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).
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user