12 changed files with 252 additions and 93 deletions

No files matched your search

+18 -11
View File
@@ -1,14 +1,21 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.8
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:
setup:
jobs:
- qqq-orb/generate_pipeline:
orb_version: '0.8'
filters:
branches:
ignore: automation/qbit-rc-pending
tags:
ignore: /.*/
build:
jobs:
- qqq-orb/mvn_test_only:
pre-steps:
- configure-package-install
-10
View File
@@ -1,10 +0,0 @@
name: build
on:
push:
jobs:
maven:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
secrets: inherit
with:
publish: false
browsers: true
-23
View File
@@ -1,23 +0,0 @@
name: CodeQL
'on':
push:
branches:
- main
- develop
- release/**
- hotfix/**
pull_request: {}
schedule:
- cron: 41 16 * * 0
workflow_dispatch: {}
permissions:
contents: read
actions: read
security-events: write
packages: read
concurrency:
group: codeql-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
analyze:
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
-16
View File
@@ -1,16 +0,0 @@
profile: qbit
orb_version: '0.8'
default_branch: main
java_version: '21'
browsers: true
api_checks: false
docs_only: false
release_on_main: false
release_tag_policy: none
test_branch_policy: all
publish:
snapshot: false
release_candidate: false
release: false
asciidoc: false
release_completion: false
+9 -1
View File
@@ -7,7 +7,7 @@ Application QBits typically have multiple interconnected tables representing a b
Use `@QMetaDataProducingEntity` for automatic metadata generation:
```java
@QMetaDataProducingEntity(producePossibleValueSource = true)
@QMetaDataProducingEntity(produceTableMetaData = true, producePossibleValueSource = true)
public class Order extends QRecordEntity
{
public static final String TABLE_NAME = "order";
@@ -23,6 +23,12 @@ public class Order extends QRecordEntity
}
```
Provide JavaBean getters and setters for every persisted field, as in
[`ExampleEntity`](../src/main/java/com/kingsrook/qbits/example/model/ExampleEntity.java).
`producePossibleValueSource` alone does not create a table. Register the
referenced `customer` table and possible-value source before using this
relationship, or replace it with a registered name.
## Relationships
Define foreign keys using `possibleValueSourceName`:
@@ -48,3 +54,5 @@ new MyAppQBitProducer()
```
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.
The scaffold's child-module switch omits the child table, its possible-value
source, and its navigation entry when disabled.
+12 -2
View File
@@ -2,6 +2,16 @@
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.
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.
In QQQ 4.0 and 4.1, import `MetaDataProducerInterface` from
`com.kingsrook.qqq.backend.core.model.metadata`. Define backend-step inputs
with `withInputData(new QFunctionInputMetaData().withField(...))`. The scaffold
uses `BackendStep.run(RunBackendStepInput, RunBackendStepOutput)` and adds each
updated record to the output. Keep that contract when renaming the step.
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.
To make the process discoverable, register its metadata first, then pass the
registered process and tables as children to
`QAppMetaData.withSectionOfChildren(section, children)` before calling
`qInstance.addApp(...)`. This populates both the section references and the
app's child metadata required by validation. Use the actual registered names
if you introduce a prefix. Add tests for record changes and failure cases
when replacing the example business logic.
+12 -4
View File
@@ -6,18 +6,26 @@ The application producer registers a `QAppMetaData` containing a `QAppSection`.
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));
.withIcon(new QIcon().withName("dashboard"));
qInstance.addApp(new QAppMetaData()
.withName("exampleApp")
.withLabel("Example Application")
.withSection(section));
.withSectionOfChildren(section, List.of(
qInstance.getTable(ExampleEntity.TABLE_NAME),
qInstance.getTable(ExampleChildEntity.TABLE_NAME),
qInstance.getProcess(ExampleProcessMetaDataProducer.NAME))));
```
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.
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.
Widgets are registered on `QInstance` separately. `QAppSection` has no
`withWidgets` method in QQQ 4.0 or 4.1, so a widget cannot be added as a
section navigation child. `QAppMetaData.withWidgets(List.of(widgetName))`
associates a registered widget with the app, but rendering and widget
navigation depend on the host UI; the scaffold does not provide a widget
navigation route.
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`.
@@ -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.QAppChildMetaData;
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;
@@ -24,8 +25,9 @@ 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;
import com.kingsrook.qbits.example.model.ExampleEntity;
import com.kingsrook.qbits.example.processes.ExampleProcessMetaDataProducer;
public class ExampleAppQBitProducer implements QBitProducer
@@ -107,7 +109,7 @@ public class ExampleAppQBitProducer implements QBitProducer
/////////////////////////////////////////////////////////////////////////
// Register QAppMetaData for UI navigation (REQUIRED for app QBits) //
/////////////////////////////////////////////////////////////////////////
qInstance.addApp(produceApp(config, qBitMetaData.getName()));
qInstance.addApp(produceApp(qInstance, config));
}
@@ -118,27 +120,27 @@ public class ExampleAppQBitProducer implements QBitProducer
** This is REQUIRED for Application QBits - it defines how users navigate
** to the tables, processes, and widgets in the UI.
*******************************************************************************/
private QAppMetaData produceApp(ExampleAppQBitConfig config, String qbitName)
private QAppMetaData produceApp(QInstance qInstance, ExampleAppQBitConfig config)
{
List<String> tables = new ArrayList<>();
tables.add(config.applyPrefix(ExampleEntity.TABLE_NAME));
List<QAppChildMetaData> children = new ArrayList<>();
children.add(qInstance.getTable(config.applyPrefix(ExampleEntity.TABLE_NAME)));
if(Boolean.TRUE.equals(config.getEnableChildModule()))
{
tables.add(config.applyPrefix(ExampleChildEntity.TABLE_NAME));
children.add(qInstance.getTable(config.applyPrefix(ExampleChildEntity.TABLE_NAME)));
}
children.add(qInstance.getProcess(ExampleProcessMetaDataProducer.NAME));
QAppSection section = new QAppSection()
.withName(config.applyPrefix("exampleSection"))
.withLabel("Example Application")
.withIcon(new QIcon().withName("dashboard"))
.withTables(tables);
.withIcon(new QIcon().withName("dashboard"));
return new QAppMetaData()
.withName(config.applyPrefix("exampleApp"))
.withLabel("Example Application")
.withIcon(new QIcon().withName("dashboard"))
.withSection(section);
.withSectionOfChildren(section, children);
}
@@ -153,7 +155,7 @@ public class ExampleAppQBitProducer implements QBitProducer
/////////////////////////////////////////////////////////////////////////
if(!Boolean.TRUE.equals(config.getEnableChildModule()))
{
if(producer.getClass().getSimpleName().contains("ExampleChildEntity"))
if(ExampleChildEntity.class.equals(producer.getSourceClass()))
{
return false;
}
@@ -14,7 +14,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
@QMetaDataProducingEntity(producePossibleValueSource = true)
@QMetaDataProducingEntity(produceTableMetaData = true, producePossibleValueSource = true)
public class ExampleChildEntity extends QRecordEntity
{
public static final String TABLE_NAME = "exampleChildEntity";
@@ -156,4 +156,44 @@ public class ExampleChildEntity extends QRecordEntity
{
return modifyDate;
}
public void setId(Integer id)
{
this.id = id;
}
public void setExampleEntityId(Integer exampleEntityId)
{
this.exampleEntityId = exampleEntityId;
}
public void setName(String name)
{
this.name = name;
}
public void setDescription(String description)
{
this.description = description;
}
public void setSortOrder(Integer sortOrder)
{
this.sortOrder = sortOrder;
}
public void setIsActive(Boolean isActive)
{
this.isActive = isActive;
}
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
}
@@ -16,7 +16,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
@QMetaDataProducingEntity(producePossibleValueSource = true)
@QMetaDataProducingEntity(produceTableMetaData = true, producePossibleValueSource = true)
public class ExampleEntity extends QRecordEntity
{
public static final String TABLE_NAME = "exampleEntity";
@@ -142,4 +142,39 @@ public class ExampleEntity extends QRecordEntity
{
return modifyDate;
}
public void setId(Integer id)
{
this.id = id;
}
public void setName(String name)
{
this.name = name;
}
public void setDescription(String description)
{
this.description = description;
}
public void setStatus(String status)
{
this.status = status;
}
public void setIsActive(Boolean isActive)
{
this.isActive = isActive;
}
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
}
@@ -9,19 +9,17 @@
package com.kingsrook.qbits.example.processes;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
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;
public class ExampleProcessStep extends AbstractTransformStep
public class ExampleProcessStep implements BackendStep
{
private static final QLogger LOG = QLogger.getLogger(ExampleProcessStep.class);
@@ -31,7 +29,7 @@ public class ExampleProcessStep extends AbstractTransformStep
** Run the process step.
*******************************************************************************/
@Override
public void runOnePage(RunBackendStepInput input, RunBackendStepOutput output) throws QException
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
{
List<QRecord> records = input.getRecords();
LOG.info("Processing records", logPair("count", records.size()));
@@ -55,13 +53,4 @@ public class ExampleProcessStep extends AbstractTransformStep
/*******************************************************************************
** 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<>());
}
}
@@ -0,0 +1,109 @@
/*
* 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/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.example;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qbits.example.model.ExampleChildEntity;
import com.kingsrook.qbits.example.model.ExampleEntity;
import com.kingsrook.qbits.example.processes.ExampleProcessStep;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ExampleAppQBitProducerTest
{
@Test
void producesBothTablesAndTheirPossibleValueSources() throws Exception
{
QInstance instance = produce();
assertNotNull(instance.getTable(ExampleEntity.TABLE_NAME));
assertNotNull(instance.getTable(ExampleChildEntity.TABLE_NAME));
assertNotNull(instance.getPossibleValueSource(ExampleEntity.TABLE_NAME));
assertNotNull(instance.getPossibleValueSource(ExampleChildEntity.TABLE_NAME));
assertEquals(7, QRecordEntity.getFieldList(ExampleEntity.class).size());
assertEquals(8, QRecordEntity.getFieldList(ExampleChildEntity.class).size());
}
@Test
void exampleProcessStepMatchesRegisteredBackendStepContract()
{
assertTrue(BackendStep.class.isAssignableFrom(ExampleProcessStep.class));
}
@Test
void exampleProcessTransformsPendingRecord() throws Exception
{
QRecord record = new QRecord().withValue("status", "PENDING");
RunBackendStepOutput output = new RunBackendStepOutput();
new ExampleProcessStep().run(new RunBackendStepInput().withRecords(List.of(record)), output);
assertEquals("PROCESSED", output.getRecords().getFirst().getValueString("status"));
}
@Test
void producedAppOwnsNavigableTablesAndProcess() throws Exception
{
QInstance instance = produce();
var app = instance.getApp("exampleApp");
assertNotNull(app);
assertNotNull(app.getChildren());
assertTrue(app.getChildren().contains(instance.getTable(ExampleEntity.TABLE_NAME)));
assertTrue(app.getChildren().contains(instance.getTable(ExampleChildEntity.TABLE_NAME)));
assertTrue(app.getChildren().contains(instance.getProcess("exampleProcess")));
assertTrue(app.getSections().stream().anyMatch(section ->
section.getTables().contains(ExampleEntity.TABLE_NAME)
&& section.getProcesses().contains("exampleProcess")));
}
@Test
void disabledChildModuleOmitsChildTableSourceAndNavigation() throws Exception
{
QInstance instance = new QInstance();
instance.addBackend(new QBackendMetaData().withName("rdbms"));
new ExampleAppQBitProducer()
.withConfig(new ExampleAppQBitConfig().withBackendName("rdbms").withEnableChildModule(false))
.produce(instance, "no-child");
assertNotNull(instance.getTable(ExampleEntity.TABLE_NAME));
assertNull(instance.getTable(ExampleChildEntity.TABLE_NAME));
assertNull(instance.getPossibleValueSource(ExampleChildEntity.TABLE_NAME));
assertTrue(instance.getApp("exampleApp").getSections().stream()
.noneMatch(section -> section.getTables().contains(ExampleChildEntity.TABLE_NAME)));
}
private QInstance produce() throws Exception
{
QInstance instance = new QInstance();
instance.addBackend(new QBackendMetaData().withName("rdbms"));
new ExampleAppQBitProducer()
.withConfig(new ExampleAppQBitConfig().withBackendName("rdbms"))
.produce(instance, "regression");
return instance;
}
}