Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70a72a54f4 | ||
|
|
4ff4ddbd9a | ||
|
|
9cb83d2b7f |
No files matched your search
@@ -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
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user