diff --git a/CLAUDE.md b/CLAUDE.md index c57c577..51d71bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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: diff --git a/README.md b/README.md index 2684c8d..c47adaa 100644 --- a/README.md +++ b/README.md @@ -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 - - - - github-qqq-maven-registry - YOUR-GITHUB-USERNAME - YOUR-GITHUB-TOKEN - - - +```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 , 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). diff --git a/pom.xml b/pom.xml index 56feaa0..d943815 100644 --- a/pom.xml +++ b/pom.xml @@ -36,16 +36,16 @@ UTF-8 UTF-8 - 17 - 17 + 21 + 21 true true true true 0.80 - 0.24.0-20250106-145452 - 0.24.0-20241122.223138-1 + 4.0.0 + 0.41.0 @@ -101,30 +101,13 @@ - - org.liquibase - liquibase-core - 4.10.0 - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.1.2 - org.apache.logging.log4j log4j-api - 2.23.0 org.apache.logging.log4j log4j-core - 2.23.0 - - - org.apache.logging.log4j - log4j-slf4j2-impl - 2.23.0 org.junit.jupiter @@ -135,7 +118,6 @@ org.assertj assertj-core - 3.23.1 test @@ -222,6 +204,15 @@ 3.0.0-M5 @{jaCoCoArgLine} + true + + mysql + 127.0.0.1 + 3306 + qqq_starter_test + test + test + @@ -272,12 +263,7 @@ org.jacoco jacoco-maven-plugin - 0.8.8 - - - com/nutrifresh/**/* - - + 0.8.14 pre-unit-test @@ -324,53 +310,9 @@ exec-maven-plugin org.codehaus.mojo 3.0.0 - - - test-coverage-summary - verify - - exec - - - sh - - -c - - /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 - ]]> - - - - - - - - github-qqq-maven-registry - GitHub QQQ Maven Registry - https://maven.pkg.github.com/Kingsrook/qqq-maven-registry - - - - - - github-qqq-maven-registry - GitHub QQQ Maven Registry - https://maven.pkg.github.com/Kingsrook/qqq-maven-registry - - - diff --git a/src/main/java/com/kingsrook/qqq/starterapp/StarterAppJavalinServer.java b/src/main/java/com/kingsrook/qqq/starterapp/StarterAppJavalinServer.java index ea1d728..43d528d 100644 --- a/src/main/java/com/kingsrook/qqq/starterapp/StarterAppJavalinServer.java +++ b/src/main/java/com/kingsrook/qqq/starterapp/StarterAppJavalinServer.java @@ -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(); } - } diff --git a/src/main/java/com/kingsrook/qqq/starterapp/StarterAppMetaDataProvider.java b/src/main/java/com/kingsrook/qqq/starterapp/StarterAppMetaDataProvider.java index 16382df..bd1e89c 100644 --- a/src/main/java/com/kingsrook/qqq/starterapp/StarterAppMetaDataProvider.java +++ b/src/main/java/com/kingsrook/qqq/starterapp/StarterAppMetaDataProvider.java @@ -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)); diff --git a/src/test/java/com/kingsrook/qqq/starterapp/StarterAppTest.java b/src/test/java/com/kingsrook/qqq/starterapp/StarterAppTest.java new file mode 100644 index 0000000..e5d5ab5 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/starterapp/StarterAppTest.java @@ -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 . + */ + +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 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 dashboard = client.send(HttpRequest.newBuilder(baseUri.resolve("/")).timeout(Duration.ofSeconds(5)).build(), HttpResponse.BodyHandlers.ofString()); + assertEquals(200, dashboard.statusCode()); + assertTrue(dashboard.body().contains(" 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 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(); + } + } +}