diff --git a/docs/01-tables-and-relationships.md b/docs/01-tables-and-relationships.md index c661ae3..e104b97 100644 --- a/docs/01-tables-and-relationships.md +++ b/docs/01-tables-and-relationships.md @@ -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. diff --git a/docs/02-processes.md b/docs/02-processes.md index ba48fa7..5d001a5 100644 --- a/docs/02-processes.md +++ b/docs/02-processes.md @@ -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. diff --git a/docs/03-widgets-and-ui.md b/docs/03-widgets-and-ui.md index 8dd11f8..e959cc7 100644 --- a/docs/03-widgets-and-ui.md +++ b/docs/03-widgets-and-ui.md @@ -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`. diff --git a/src/main/java/com/kingsrook/qbits/example/ExampleAppQBitProducer.java b/src/main/java/com/kingsrook/qbits/example/ExampleAppQBitProducer.java index e5fb536..f3a8e3a 100644 --- a/src/main/java/com/kingsrook/qbits/example/ExampleAppQBitProducer.java +++ b/src/main/java/com/kingsrook/qbits/example/ExampleAppQBitProducer.java @@ -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 tables = new ArrayList<>(); - tables.add(config.applyPrefix(ExampleEntity.TABLE_NAME)); + List 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; } diff --git a/src/main/java/com/kingsrook/qbits/example/model/ExampleChildEntity.java b/src/main/java/com/kingsrook/qbits/example/model/ExampleChildEntity.java index 48eeb3e..fd8cded 100644 --- a/src/main/java/com/kingsrook/qbits/example/model/ExampleChildEntity.java +++ b/src/main/java/com/kingsrook/qbits/example/model/ExampleChildEntity.java @@ -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; + } } diff --git a/src/main/java/com/kingsrook/qbits/example/model/ExampleEntity.java b/src/main/java/com/kingsrook/qbits/example/model/ExampleEntity.java index e7ebc52..2ad9a6f 100644 --- a/src/main/java/com/kingsrook/qbits/example/model/ExampleEntity.java +++ b/src/main/java/com/kingsrook/qbits/example/model/ExampleEntity.java @@ -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; + } } diff --git a/src/main/java/com/kingsrook/qbits/example/processes/ExampleProcessStep.java b/src/main/java/com/kingsrook/qbits/example/processes/ExampleProcessStep.java index 216d83d..b3ba0c7 100644 --- a/src/main/java/com/kingsrook/qbits/example/processes/ExampleProcessStep.java +++ b/src/main/java/com/kingsrook/qbits/example/processes/ExampleProcessStep.java @@ -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 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 getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen) - { - return (new ArrayList<>()); - } } diff --git a/src/test/java/com/kingsrook/qbits/example/ExampleAppQBitProducerTest.java b/src/test/java/com/kingsrook/qbits/example/ExampleAppQBitProducerTest.java new file mode 100644 index 0000000..a0984fd --- /dev/null +++ b/src/test/java/com/kingsrook/qbits/example/ExampleAppQBitProducerTest.java @@ -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; + } +}