feat: add rich mobile record fixtures and photo previews
Add synthetic grouped orders, related items and history, parent-scoped photo grants, and isolated native fixture support. Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
This commit is contained in:
15 files changed
+493
-12
No files matched your search
@@ -0,0 +1,19 @@
|
||||
# Mobile browsing test data
|
||||
|
||||
The composed Echo demo includes **Test Data → Test Orders** for testing the existing mobile app. These are synthetic records, separate from scanner captures and printer jobs.
|
||||
|
||||
| Table | Rows | Contents |
|
||||
| --- | ---: | --- |
|
||||
| Test Orders | 1,000 | 39 fields across Order overview, Customer, Shipping, Amounts, Warehouse, and Notes and audit |
|
||||
| Order Items | 12,000 | 12 related items per order, grouped item and handling fields |
|
||||
| Order History | 3,000 | Three related history entries per order |
|
||||
|
||||
Values exercise dates, timestamps, numbers, decimals, booleans, leading zeros, Unicode, multiline notes, long descriptions and empty optional fields. All customer information is fictional.
|
||||
|
||||
Open an order, scroll through its field groups, and open an item or history record from its related-record section. Table pages contain 25 records; related sections contain 10 records per page. Camera and gallery attachments use the selected record's normal Record Files permissions and upload lifecycle. A saved photo may remain awaiting server review.
|
||||
|
||||
The native preview uses standard QQQ metadata sections and direct association joins through the existing semantic query/get API. It supports read-only browsing; arbitrary widgets, nested editing and unsupported relationship definitions are not added by this fixture.
|
||||
|
||||
`MobileTestData` belongs only to `examples/echo/demo`. Startup adds missing H2 tables and missing synthetic rows without replacing existing rows or attachments. The scanner account can read these tables and attach photos; raw table edits remain refused. The basic Scan Capture demo does not install this dataset.
|
||||
|
||||
Verification uses separate databases, accounts and disposable application IDs. Updating the maintained demo must preserve its database, accounts, printer configuration and existing device enrollment.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Mobile record UX improvements
|
||||
|
||||
User direction: Add photo belongs in the record's Actions menu, alongside other actions. Address the full TC22 visual review in the existing app and maintained demo.
|
||||
|
||||
1. Preserve table page, scroll, search and filter state across record navigation and recreation.
|
||||
2. Keep record identity visible; add Details, related-record and Photos navigation, collapsible field groups, compact summaries and formatted values.
|
||||
3. Put capture/selection behind Actions → Add photo; retain pending-upload guards. Browse saved attachments with lifecycle status and authorized previews.
|
||||
4. Use standard semantic metadata, query/count/process and owner attachment/download APIs. Preserve existing permissions and quarantine.
|
||||
5. Verify unit/build/lint, isolated real native flows, and actual TC22 layout; update the same app/server with preserved data.
|
||||
|
||||
No source publication is included in this request.
|
||||
|
||||
## Progress
|
||||
|
||||
Implementation is complete. Android unit/lint/build checks and the composed server checks pass; the existing process-photo native regression passes. Final native acceptance for search/filter/back restoration, related tabs, Actions → Add photo and approved/quarantined photo presentation is running. Updating the maintained server and existing TC22 app, followed by an actual-device visual pass, remains pending.
|
||||
@@ -8,14 +8,16 @@
|
||||
| --- | --- |
|
||||
| Server source and development publication | `develop` at the merge of PR 13; PR 12 snapshot `0.1.0-20261103.233448-3645` read back anonymously and matches reviewed source. |
|
||||
| Android host libraries | `main` at `0ae2101`; SDK run 3634 passed and artifacts verified. Development libraries only. |
|
||||
| Current cohort tests | 198 tests pass (scanner 53, base demo 8, mobile Record Files 18, Printing bridge 16, Echo 68, Echo demo 35); Checkstyle passes; no private QQQ backend in the cohort. |
|
||||
| Current cohort tests | 189 tests pass (scanner 53, base demo 8, mobile Record Files 18, Echo 74, Echo demo 36); Checkstyle passes; no private QQQ backend in the cohort. |
|
||||
| Local runtime | One server on public QQQ RC.21 and Printing 1.0.0; API 1.10, mobile 1.5, printing 2.3, mobile-record-files 1.0; existing data preserved. |
|
||||
| Device and admin processes | Enrollment, approval and revocation work from a single record and from bulk selection (checked rows, select-all, first N), with confirmation and visible results, covered by HTTP and browser E2E tests. Earlier browser run: 15 cases. |
|
||||
| Hardware | TC22 manual tests: operator-reported pass on November 4 (no per-case artifacts). MC9400 configured and approved; trigger check pending. |
|
||||
|
||||
## Open gates
|
||||
|
||||
Local GS1-128 and native record/photo changes passed 188 server tests, Android unit/lint/build gates, and disposable native Echo/record-photo/process-photo acceptance. The server changes are merged and published as the Forge source prerelease `echo-gs1-source-20261104`; Android changes remain local. Neither is deployed to the maintained demo. Synthetic GS1 renderer/decoder evidence does not close physical print/rescan acceptance or the signed-build deployment gates below.
|
||||
November 5 local preview update: the maintained demo and existing TC22 app now include the rich browsing fixture and native grouped/related records. The composed server gate passed 189 tests; Android unit/lint/build checks and isolated MC9400 record/photo semantic E2E passed. Existing data was preserved with a backup. This supersedes the deployment status in the earlier checkpoint below; the new fixture/native source remains local and uncommitted. It does not qualify physical printing or a production rollout.
|
||||
|
||||
Local GS1-128 and native record/photo changes passed 188 server tests, Android unit/lint/build gates, and disposable native Echo/record-photo/process-photo acceptance. The server changes are merged and published as the Forge source prerelease `echo-gs1-source-20261104`; Android changes remain local. Both now run in the maintained local preview, as recorded above. Synthetic GS1 renderer/decoder evidence does not close physical print/rescan acceptance or the signed-build deployment gates below.
|
||||
|
||||
| Gate | What is still required |
|
||||
| --- | --- |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Session state
|
||||
|
||||
Current as of **2026-11-04**. This is the one place for present status; keep it short. Open work is in [TODO](TODO.md), release gates in [production readiness](PRODUCTION-READINESS.md), and all earlier logs in [history](history/README.md). Nothing here claims a production 1.0.
|
||||
Current as of **2026-11-05**. This is the one place for present status; keep it short. Open work is in [TODO](TODO.md), release gates in [production readiness](PRODUCTION-READINESS.md), and all earlier logs in [history](history/README.md). Nothing here claims a production 1.0.
|
||||
|
||||
## Source and publication
|
||||
|
||||
GS1-128 duplication (barcode plus HRI) is merged through [PR 15](https://git.kof22.com/QRun-IO/qbit-mobile-scanners/pulls/15), commit `f9b0eb25c737365618973455826740aeffe09ed3`, and published as [Forge source prerelease `echo-gs1-source-20261104`](https://git.kof22.com/QRun-IO/qbit-mobile-scanners/releases/tag/echo-gs1-source-20261104). It includes `examples/echo`; it is not a published Echo Maven dependency. Native table/record browsing and photo attachment remain separate local Android changes. See [implementation plan](GS1-RECORD-PHOTOS-PLAN.md). The GS1 profile covers the requested GTIN/production-date/serial structure and selected additional AIs. Original scans remain unchanged; canonical renderer input and HRI use additive nullable columns. Root/demo and the coherent Echo reactor passed; two synthetic outputs from the real renderer independently decoded with exact GS1 data and matching HRI. The scanner/demo/Echo reactor passed 135 tests. Disposable MC9400 native acceptance passed GS1 capture and saved-record browsing, plus camera/gallery attachment from record details with lifecycle cleanup. Android unit tests, lint and APK builds passed. The composed cohort also passed 188 tests (scanner 53, base demo 8, Record Files 18, Echo 74, Echo demo 35), with Checkstyle. No deployment, physical GS1 print/rescan or production qualification is claimed.
|
||||
GS1-128 duplication (barcode plus HRI) is merged through [PR 15](https://git.kof22.com/QRun-IO/qbit-mobile-scanners/pulls/15), commit `f9b0eb25c737365618973455826740aeffe09ed3`, and published as [Forge source prerelease `echo-gs1-source-20261104`](https://git.kof22.com/QRun-IO/qbit-mobile-scanners/releases/tag/echo-gs1-source-20261104). It includes `examples/echo`; it is not a published Echo Maven dependency. Native table/record browsing and photo attachment remain separate local Android changes. See [implementation plan](GS1-RECORD-PHOTOS-PLAN.md). The GS1 profile covers the requested GTIN/production-date/serial structure and selected additional AIs. Original scans remain unchanged; canonical renderer input and HRI use additive nullable columns. Root/demo and the coherent Echo reactor passed; two synthetic outputs from the real renderer independently decoded with exact GS1 data and matching HRI. The scanner/demo/Echo reactor passed 135 tests. Disposable MC9400 native acceptance passed GS1 capture and saved-record browsing, plus camera/gallery attachment from record details with lifecycle cleanup. Android unit tests, lint and APK builds passed. The composed cohort also passed 188 tests (scanner 53, base demo 8, Record Files 18, Echo 74, Echo demo 35), with Checkstyle. No physical GS1 print/rescan or production qualification is claimed. Local preview adoption is recorded below.
|
||||
|
||||
| Item | State |
|
||||
| --- | --- |
|
||||
@@ -16,6 +16,8 @@ GS1-128 duplication (barcode plus HRI) is merged through [PR 15](https://git.kof
|
||||
|
||||
## Local runtime
|
||||
|
||||
On November 5 the existing composed server was updated in place with the GS1 source changes and [rich browsing fixture](MOBILE-TEST-DATA.md). Live scanner-account reads verified 1,000 orders, 12,000 items, 3,000 history entries, 39 order fields, six sections and two associations. The database and attachment objects were backed up first; accounts, device enrollment and printer configuration were preserved. The fixture and native grouping/relationship changes remain local, uncommitted preview work.
|
||||
|
||||
One local server (the composed Echo launcher, see [local demo](LOCAL-DEMO.md)) runs Mobile Scanners/Echo, Collaboration/Record Files, Document Generation and Printing on loopback host port `57832`. It uses public QQQ `4.1.0-RC.21` and released Printing `1.0.0`, and preserves its existing database, accounts and printer configuration.
|
||||
|
||||
| Contract | Version |
|
||||
@@ -29,7 +31,7 @@ The Android client reaches it through a device-side forward from port 8080 to th
|
||||
|
||||
## Verification of the current cohort
|
||||
|
||||
- **198 tests pass** with no failures, errors or skips: scanner 53, base demo 8, mobile Record Files 18, Printing bridge 16, Echo 68, Echo demo 35. Checkstyle passes.
|
||||
- **189 tests pass** with no failures, errors or skips: scanner 53, base demo 8, mobile Record Files 18, Echo 74, Echo demo 36. Checkstyle passes. The prior 198-test cohort included the unchanged Printing bridge's 16 tests; that bridge was not rerun in this gate.
|
||||
- The 94 scanner Java source files match the reviewed source exactly.
|
||||
- No private QQQ backend artifact is in this cohort; all QQQ backend dependencies resolve to public RC.21 and Printing is the released `1.0.0`.
|
||||
- The earlier 377-test run plus 15 admin-browser E2E cases remains valid historical evidence. This session did not re-run the browser suite, so no new browser run is claimed.
|
||||
@@ -37,7 +39,8 @@ The Android client reaches it through a device-side forward from port 8080 to th
|
||||
|
||||
## Handhelds
|
||||
|
||||
- **Zebra TC22 (original, unchanged).** Development APK (SHA-256 `38A11155BF6A9C089A1EE6D9E44401E8FF39497B7150B51A85F1F916148BE60D`, version 0.1.0, code 1). On November 4 the operator reported that all outstanding manual tests pass; that is accepted as an operator report, not new per-case evidence, and the checks are not to be requested again. See [TC22 acceptance](TC22-ACCEPTANCE.md).
|
||||
- **Current TC22 rich-record preview:** updated in place on November 5, same signer and installed bytes verified, SHA-256 `65FB631F0CFF9B68A0F3FC01D37D656FD69D690F1BA63F8A34FB314A2B03B97F`. Uses the same local server and ordinary app; no data clear or separate manual-test app. Android unit tests, lint and builds pass. Disposable MC9400 native acceptance passed table paging, six field groups, filtered child paging/navigation, camera/gallery attachment to order 26, rotation, pending-photo navigation protection and remote logout cleanup; all requests matched the semantic API. This is automated evidence, not a new operator hardware report.
|
||||
- **TC22 operator acceptance:** the November 4 manual group pass remains accepted; do not request those tests again. The earlier November 5 preview installation is archived in [history](history/RICH-RECORDS-2026-11-05.md).
|
||||
- **Zebra MC9400 (Android 13, added).** A signed release 1.0 build was installed and launched, then at the user's request the device was configured like the TC22 for now: the same development APK, device port 8080 forwarded to the host server over paired wireless ADB, signed in as the scanner account, and approved through the ordinary device-approval process. A physical trigger scan and backend record check are still pending.
|
||||
- **Signed APK.** A signed 1.0 (code 2) APK exists, verified with the new QRun key and its backup (see the Android repository's `RELEASING` documentation). There is no production rollout and no automatic APK-signing CI yet.
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# TC22 visual and interaction review
|
||||
|
||||
Reviewed November 5 on the actual TC22 running the existing app and maintained demo, using synthetic Test Orders. Directly inspected the first and second table pages, all six order groups, photo controls, related items, an item detail, history links and Back navigation. This is a visual/interaction review, not a new camera, printer or hardware acceptance claim. No records or photos were changed.
|
||||
|
||||
## Assessment
|
||||
|
||||
Readable and functional, but the current presentation is inefficient for repeated handheld work. Large text and buttons help readability; hierarchy, navigation and access to frequent actions need improvement. The data volume exposes limitations that smaller records concealed.
|
||||
|
||||
## Prioritized changes
|
||||
|
||||
| Priority | Observed issue | Recommendation |
|
||||
| --- | --- | --- |
|
||||
| P1 | Page two → order 26 → Android Back returns to page one. | Preserve table page, scroll position and filters per navigation entry. Verify Back after multiple record visits. |
|
||||
| P1 | Reaching photo buttons required five long swipes from the top of order 1. Items/history follow all 39 fields. | Add direct Details, Items, Photos and History navigation, with a prominent Add photo action. Make detail groups collapsible and provide a section selector. |
|
||||
| P1 | Toolbar says only “Record”; order identity disappears while scrolling. Child records use the same title. | Keep a compact record reference and record type visible. Show a parent-order link on item details; keep record labels out of telemetry. |
|
||||
| P1 | A 1,000-row list has Previous/Next/Refresh but no visible search, filters, count or page indication. | Add server-backed search, relevant filters and “26–50 of 1,000.” Consider scan-to-find where declared metadata supports it. |
|
||||
| P2 | Table entries are centered blue links with uneven wrapping; child descriptions occupy three lines. History shows only event identifiers. | Use left-aligned rows with a strong reference, concise secondary text, status and a clear navigation affordance. Show item quantity/SKU and history timestamp/status through metadata-defined summaries. |
|
||||
| P2 | Item heading repeats the full description in very large type, then repeats it as a field. Persistent branding/header and diagnostic footer consume substantial space. | Use a short reference heading and secondary description. Consolidate the header and reduce connected status to a compact indicator; retain accessible text and touch targets. |
|
||||
| P2 | Dates/timestamps and money read like raw values; timestamp example includes `T` and `Z`. | Apply metadata-aware currency, units and localized date/time formatting with timezone context. Preserve leading zeros. Show an explicit empty-value indicator. |
|
||||
| P2 | Photo controls offer capture/select but this screen provides no visible saved-photo gallery or attachment count. | Add thumbnails/count, preview, upload progress and clear pending-review/retry states. Keep the destination record visible near photo actions. |
|
||||
|
||||
## What works
|
||||
|
||||
Text is legible, field labels are distinguishable, sections have clear headings, and the blue photo buttons are easy to identify. Leading zeros, Unicode and multiline values displayed correctly. Parent-to-item navigation worked. Table pagination is available without scrolling the whole page.
|
||||
|
||||
## Suggested order
|
||||
|
||||
Fix navigation state first. Then add persistent record identity and direct photo/related-record access. Next improve list summaries/search and reduce repeated content. Finish with formatting and attachment presentation. Implement generic metadata-driven controls rather than order-specific layouts. No UI fixes were made during this review.
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
# TODO
|
||||
|
||||
Open work only, as of **2026-11-04**. Present status is in [session state](SESSION-STATE.md); release gates are in [production readiness](PRODUCTION-READINESS.md). The earlier ledger, including every completed item, is archived in [history](history/TODO-through-2026-11-04.md).
|
||||
Open work only, as of **2026-11-05**. Present status is in [session state](SESSION-STATE.md); release gates are in [production readiness](PRODUCTION-READINESS.md). The earlier ledger, including every completed item, is archived in [history](history/TODO-through-2026-11-04.md).
|
||||
|
||||
## Needs an owner outside this repository
|
||||
|
||||
@@ -11,6 +11,7 @@ Note: redesign of the shared CI pipeline is outside this scanner/local Echo scop
|
||||
|
||||
## This repository
|
||||
|
||||
- [ ] Complete [mobile record UX improvements](MOBILE-UX-PLAN.md), including Actions → Add photo and all TC22 review findings, then verify and update the existing preview.
|
||||
- [ ] Finish GS1-128 duplication with printed HRI: native/HTTP acceptance, composed demo verification, GS1 template setup and physical print/rescan of the requested label profile.
|
||||
- [ ] Deliver native permitted-table browsing, record details and camera/gallery attachment from the selected record, with permission/lifecycle and native E2E coverage.
|
||||
- [ ] Complete the authorized readiness work in [the active plan](GS1-RECORD-PHOTOS-PLAN.md), retaining explicit external/hardware gates.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Documentation history
|
||||
|
||||
Latest checkpoint: [rich record preview, November 5](RICH-RECORDS-2026-11-05.md).
|
||||
|
||||
Superseded rolling logs, preserved verbatim as evidence. They are **not** current status: many entries describe work as pending that was later completed. Read the current entry points instead:
|
||||
|
||||
- [Project README](../../README.md), [session state](../SESSION-STATE.md), [TODO](../TODO.md), [production readiness](../PRODUCTION-READINESS.md)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Rich record preview — November 5
|
||||
|
||||
Before this update, the maintained demo ran the earlier 198-test public RC.21/Printing 1.0 cohort. The first TC22 records/photo preview was installed in place with SHA-256 `79E6CD0559419E059EC253DD0364C13BDDB3C27F566F0DA9A478FBB70D99A038`; the user then clarified that the existing app and existing server should be used, without a separate manual QA application.
|
||||
|
||||
The rich-record update added demo-only synthetic tables, ordinary metadata sections/associations, read-only permissions and explicit attachment grants. Native grouping and child queries use the versioned semantic API. Verification caught missing section tiers in metadata serialization and missing Origin headers in the composed-server test helper; both were corrected without weakening server guards. The final gate passed 189 tests and Android unit/lint/build checks. Isolated MC9400 native acceptance passed before adoption.
|
||||
|
||||
The maintained database and objects were backed up; the server restarted with the new application JARs and the same owner dependency cohort. Scanner-account reads verified all fixture counts and layout metadata. The existing TC22 app was updated with matching signer and installed bytes verified; its data and connection remained intact. Current details are in [session state](../SESSION-STATE.md).
|
||||
@@ -36,6 +36,7 @@ import com.kingsrook.qbits.printing.PrintingService;
|
||||
import com.kingsrook.qbits.recordfiles.RecordFilesQBitConfig;
|
||||
import com.kingsrook.qbits.recordfiles.RecordFilesQBitProducer;
|
||||
import com.kingsrook.qbits.recordfiles.RecordFilesSchema;
|
||||
import com.kingsrook.qbits.recordfiles.download.DownloadConfiguration;
|
||||
import com.kingsrook.qbits.recordfiles.lifecycle.RecordFilesAttachmentLifecycle;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
@@ -220,8 +221,11 @@ public class EchoDemo extends MobileScannersDemo implements AutoCloseable
|
||||
// Documents are the operator's published, Echo-compatible templates, discovered per run; the legacy
|
||||
// ECHO_TEMPLATE_* host pin no longer selects one.
|
||||
EchoHost.install(instance, new EchoHost.Deployment(BACKEND, BACKEND, BACKEND, dataSource, printing, renderer, maximumOutputBytes));
|
||||
RecordFilesQBitConfig recordFiles = new RecordFilesQBitConfig().withBackendName(BACKEND).withParentTables(List.of(EchoQBitProducer.SCAN_TABLE))
|
||||
MobileTestData.register(instance);
|
||||
RecordFilesQBitConfig recordFiles = new RecordFilesQBitConfig().withBackendName(BACKEND)
|
||||
.withParentTables(List.of(EchoQBitProducer.SCAN_TABLE, MobileTestData.ORDERS, MobileTestData.ITEMS, MobileTestData.EVENTS))
|
||||
.withQuarantineGeneratedFiles(settings.releaseGeneratedFiles() ? Boolean.FALSE : null)
|
||||
.withDownloadConfiguration(new DownloadConfiguration("http://127.0.0.1:" + settings.port() + "/qqq/v1", 20 * 1024 * 1024, 2))
|
||||
.withUploadConfiguration(EchoPhotoCapture.uploadConfiguration());
|
||||
new RecordFilesQBitProducer().withQBitConfig(recordFiles).produce(instance);
|
||||
// Configuring uploads makes the producer register the versioned record-files extension; the mobile widget
|
||||
@@ -268,6 +272,7 @@ public class EchoDemo extends MobileScannersDemo implements AutoCloseable
|
||||
collaborationTables(statement, instance);
|
||||
script(statement, UploadJournalTable.h2Script());
|
||||
script(statement, RecordFilesSchema.h2Script(config(instance, RecordFilesQBitConfig.class)));
|
||||
MobileTestData.initialize(connection, instance);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ public class EchoDemoAuthentication extends DemoAuthenticationModule
|
||||
*/
|
||||
static final List<String> SCANNER = List.of(EchoQBitProducer.PERMISSION, "printing_operator", "document-generation.view",
|
||||
"document-generation.generate", "document-generation.print", "record-files.view", "record-files.manage",
|
||||
EchoQBitProducer.SCAN_TABLE + ".attach");
|
||||
EchoQBitProducer.SCAN_TABLE + ".attach", MobileTestData.ORDERS + ".attach", MobileTestData.ITEMS + ".attach", MobileTestData.EVENTS + ".attach");
|
||||
|
||||
/**
|
||||
** Admin also gets Printing's own administrator permission, which its native
|
||||
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Kingsrook, LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.kingsrook.examples.echo.demo;
|
||||
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.examples.echo.EchoSourceReadPermissions;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
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.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Synthetic, read-only browsing fixtures for the local demo. Ordinary metadata
|
||||
** owns sections and relationships; no custom mobile endpoints are involved.
|
||||
*******************************************************************************/
|
||||
final class MobileTestData
|
||||
{
|
||||
static final String ORDERS = "testOrder";
|
||||
static final String ITEMS = "testOrderItem";
|
||||
static final String EVENTS = "testOrderEvent";
|
||||
static final List<String> TABLES = List.of(ORDERS, ITEMS, EVENTS);
|
||||
static final int ORDER_COUNT = 1000;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Keep all fixtures in the demo, visible to the ordinary Echo demo reader.
|
||||
***************************************************************************/
|
||||
static void register(QInstance instance)
|
||||
{
|
||||
QTableMetaData orders = table(ORDERS, "Test Orders");
|
||||
section(orders, "overview", "Order overview", "id:INTEGER", "reference:STRING", "status:STRING", "priority:STRING", "orderDate:DATE", "requestedDate:DATE", "expedited:BOOLEAN");
|
||||
section(orders, "customer", "Customer", "customerCode:STRING", "customerName:STRING", "contactName:STRING", "email:STRING", "phone:STRING", "purchaseOrder:STRING");
|
||||
section(orders, "shipping", "Shipping", "addressLine1:STRING", "addressLine2:STRING", "city:STRING", "region:STRING", "postalCode:STRING", "country:STRING", "carrier:STRING", "trackingNumber:STRING");
|
||||
section(orders, "amounts", "Amounts", "currency:STRING", "subtotal:DECIMAL", "tax:DECIMAL", "shippingCost:DECIMAL", "total:DECIMAL", "paid:BOOLEAN");
|
||||
section(orders, "warehouse", "Warehouse", "warehouse:STRING", "bin:STRING", "itemCount:INTEGER", "weightKg:DECIMAL", "fragile:BOOLEAN", "lotCode:STRING");
|
||||
section(orders, "notes", "Notes and audit", "instructions:STRING", "notes:STRING", "createdAt:DATE_TIME", "updatedAt:DATE_TIME", "externalReference:STRING", "optionalNote:STRING");
|
||||
orders.withRecordLabelFormatAndFields("%s — %s", "reference", "customerName");
|
||||
orders.withSearchFields(List.of("reference", "customerName"));
|
||||
for(String name : List.of("subtotal", "tax", "shippingCost", "total"))
|
||||
{
|
||||
orders.getField(name).withDisplayFormat("$%,.2f");
|
||||
}
|
||||
orders.getField("weightKg").withDisplayFormat("%,.2f kg");
|
||||
|
||||
QTableMetaData items = table(ITEMS, "Order Items");
|
||||
section(items, "item", "Item", "id:INTEGER", "reference:STRING", "sku:STRING", "quantity:INTEGER", "description:STRING", "orderId:INTEGER");
|
||||
section(items, "pricing", "Pricing and handling", "unitPrice:DECIMAL", "lineTotal:DECIMAL", "bin:STRING", "lotCode:STRING", "fragile:BOOLEAN", "notes:STRING");
|
||||
items.withRecordLabelFormatAndFields("%s · %s × %s", "reference", "quantity", "sku");
|
||||
items.withSearchFields(List.of("reference", "sku", "description"));
|
||||
items.getField("unitPrice").withDisplayFormat("$%,.2f");
|
||||
items.getField("lineTotal").withDisplayFormat("$%,.2f");
|
||||
|
||||
QTableMetaData events = table(EVENTS, "Order History");
|
||||
section(events, "event", "History entry", "id:INTEGER", "status:STRING", "createdAt:DATE_TIME", "notes:STRING", "reference:STRING", "orderId:INTEGER");
|
||||
events.withRecordLabelFormatAndFields("%s · %s", "reference", "status");
|
||||
events.withSearchFields(List.of("reference", "status", "notes"));
|
||||
instance.addTable(orders);
|
||||
instance.addTable(items);
|
||||
instance.addTable(events);
|
||||
associate(instance, orders, items, "Items");
|
||||
associate(instance, orders, events, "History");
|
||||
instance.addApp(new QAppMetaData().withName("mobileTestData").withLabel("Test Data").withPermissionRules(readRules())
|
||||
.withSection(new QAppSection().withName("browse").withLabel("Synthetic test records").withTables(TABLES)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Native read permission enforcement also protects related-record queries.
|
||||
***************************************************************************/
|
||||
private static QPermissionRules readRules()
|
||||
{
|
||||
return new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION)
|
||||
.withCustomPermissionChecker(new QCodeReference(EchoSourceReadPermissions.class));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Physical names deliberately match metadata names for the demo's H2 schema.
|
||||
***************************************************************************/
|
||||
private static QTableMetaData table(String name, String label)
|
||||
{
|
||||
return new QTableMetaData().withName(name).withLabel(label).withBackendName(EchoDemo.BACKEND)
|
||||
.withBackendDetails(new RDBMSTableBackendDetails().withTableName(name)).withPrimaryKeyField("id").withPermissionRules(readRules());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The ordinary metadata section controls order and grouping on every client.
|
||||
***************************************************************************/
|
||||
private static void section(QTableMetaData table, String name, String label, String... definitions)
|
||||
{
|
||||
List<String> names = new ArrayList<>();
|
||||
for(String definition : definitions)
|
||||
{
|
||||
String[] parts = definition.split(":");
|
||||
names.add(parts[0]);
|
||||
table.withField(new QFieldMetaData(parts[0], QFieldType.valueOf(parts[1])).withIsEditable(false));
|
||||
}
|
||||
table.withSection(new QFieldSection().withName(name).withLabel(label).withTier(Tier.T1).withFieldNames(names));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Associations use declared joins, never client-inferred foreign-key names.
|
||||
***************************************************************************/
|
||||
private static void associate(QInstance instance, QTableMetaData parent, QTableMetaData child, String label)
|
||||
{
|
||||
String join = parent.getName() + "_" + child.getName();
|
||||
instance.addJoin(new QJoinMetaData().withName(join).withLeftTable(parent.getName()).withRightTable(child.getName())
|
||||
.withType(JoinType.ONE_TO_MANY).withJoinOn(new JoinOn("id", "orderId")));
|
||||
parent.withAssociation(new Association().withName(label).withAssociatedTableName(child.getName()).withJoinName(join));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Add only missing fixture rows. Reapplying never resets photos or edits.
|
||||
** This metadata-derived schema is intentionally H2-only and demo-only.
|
||||
***************************************************************************/
|
||||
static void initialize(Connection connection, QInstance instance) throws SQLException
|
||||
{
|
||||
for(String name : TABLES)
|
||||
{
|
||||
QTableMetaData table = instance.getTable(name);
|
||||
List<QFieldMetaData> fields = new ArrayList<>(table.getFields().values());
|
||||
List<String> columns = new ArrayList<>();
|
||||
for(QFieldMetaData field : fields)
|
||||
{
|
||||
String type = switch(field.getType())
|
||||
{
|
||||
case INTEGER -> "INTEGER";
|
||||
case BOOLEAN -> "BOOLEAN";
|
||||
case DATE -> "DATE";
|
||||
case DATE_TIME -> "TIMESTAMP";
|
||||
case DECIMAL -> "DECIMAL(16,2)";
|
||||
default -> "VARCHAR(4000)";
|
||||
};
|
||||
columns.add(field.getName() + " " + type + (field.getName().equals("id") ? " PRIMARY KEY" : ""));
|
||||
}
|
||||
try(Statement statement = connection.createStatement())
|
||||
{
|
||||
statement.execute("CREATE TABLE IF NOT EXISTS " + name + " (" + String.join(",", columns) + ")");
|
||||
if(!name.equals(ORDERS))
|
||||
{
|
||||
statement.execute("CREATE INDEX IF NOT EXISTS idx_" + name + "_order ON " + name + " (orderId)");
|
||||
}
|
||||
}
|
||||
String names = String.join(",", fields.stream().map(QFieldMetaData::getName).toList());
|
||||
String placeholders = String.join(",", java.util.Collections.nCopies(fields.size(), "?"));
|
||||
String sql = "INSERT INTO " + name + " (" + names + ") SELECT " + placeholders + " WHERE NOT EXISTS (SELECT 1 FROM " + name + " WHERE id = ?)";
|
||||
int perOrder = name.equals(ORDERS) ? 1 : name.equals(ITEMS) ? 12 : 3;
|
||||
boolean autoCommit = connection.getAutoCommit();
|
||||
connection.setAutoCommit(false);
|
||||
try(PreparedStatement insert = connection.prepareStatement(sql))
|
||||
{
|
||||
for(int id = 1; id <= ORDER_COUNT * perOrder; id++)
|
||||
{
|
||||
int order = (id - 1) / perOrder + 1;
|
||||
for(int index = 0; index < fields.size(); index++)
|
||||
{
|
||||
insert.setObject(index + 1, value(name, fields.get(index), id, order));
|
||||
}
|
||||
insert.setInt(fields.size() + 1, id);
|
||||
insert.addBatch();
|
||||
if(id % 250 == 0)
|
||||
{
|
||||
insert.executeBatch();
|
||||
}
|
||||
}
|
||||
insert.executeBatch();
|
||||
connection.commit();
|
||||
}
|
||||
catch(SQLException failure)
|
||||
{
|
||||
connection.rollback();
|
||||
throw failure;
|
||||
}
|
||||
finally
|
||||
{
|
||||
connection.setAutoCommit(autoCommit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Deterministic, obviously synthetic values exercise long, empty, Unicode,
|
||||
** leading-zero, numeric, boolean, date and date-time display paths.
|
||||
***************************************************************************/
|
||||
private static Object value(String table, QFieldMetaData field, int id, int order)
|
||||
{
|
||||
return switch(field.getName())
|
||||
{
|
||||
case "id" -> id;
|
||||
case "orderId" -> order;
|
||||
case "reference" -> (table.equals(ORDERS) ? "TEST-" : table.equals(ITEMS) ? "ITEM-" : "EVENT-") + String.format(java.util.Locale.ROOT, "%06d", id);
|
||||
case "customerName" -> "Synthetic Customer " + order + (order % 3 == 0 ? " — Café & Supply" : "");
|
||||
case "customerCode", "sku", "lotCode", "externalReference", "postalCode" -> String.format(java.util.Locale.ROOT, "%08d", id);
|
||||
case "email" -> "customer" + order + "@example.invalid";
|
||||
case "phone" -> "Not a real telephone number";
|
||||
case "status" -> List.of("New", "Picking", "Packed", "Shipped", "On hold").get(id % 5);
|
||||
case "priority" -> id % 4 == 0 ? "High" : "Normal";
|
||||
case "currency" -> "USD";
|
||||
case "country" -> "US";
|
||||
case "description" -> "Synthetic product " + id + " — reusable storage container with a deliberately long description for wrapping";
|
||||
case "notes", "instructions" -> "Synthetic test data only.\nOrder " + order + ": inspect all sides and attach a photo.\nAccents: café, naïve; symbols: & / +.";
|
||||
case "optionalNote", "addressLine2" -> id % 2 == 0 ? null : "Optional example " + id;
|
||||
case "itemCount" -> 12;
|
||||
default -> switch(field.getType())
|
||||
{
|
||||
case BOOLEAN -> id % 2 == 0;
|
||||
case INTEGER -> id % 9 + 1;
|
||||
case DECIMAL -> BigDecimal.valueOf(1000L + id, 2);
|
||||
case DATE -> LocalDate.of(2026, 1, 1).plusDays(id % 300);
|
||||
case DATE_TIME -> LocalDateTime.of(2026, 1, 1, 10, 30).plusHours(id);
|
||||
default -> "Test " + field.getName() + " " + order;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -110,7 +110,7 @@ class EchoDemoSettingsTest
|
||||
void permissionSetsAreLeastPrivilege()
|
||||
{
|
||||
assertEquals(List.of("echo.use", "printing_operator", "document-generation.view", "document-generation.generate", "document-generation.print",
|
||||
"record-files.view", "record-files.manage", "echoScan.attach"), EchoDemoAuthentication.SCANNER);
|
||||
"record-files.view", "record-files.manage", "echoScan.attach", "testOrder.attach", "testOrderItem.attach", "testOrderEvent.attach"), EchoDemoAuthentication.SCANNER);
|
||||
assertEquals(List.of("printing_admin", "document-generation.configure", "document-generation.publish"), EchoDemoAuthentication.ADMIN_ADDITIONS);
|
||||
assertFalse(EchoDemoAuthentication.SCANNER.stream().anyMatch(name -> name.endsWith(".configure") || name.endsWith(".publish") || name.endsWith("admin")));
|
||||
}
|
||||
|
||||
+86
-1
@@ -6,6 +6,10 @@
|
||||
package com.kingsrook.examples.echo.demo;
|
||||
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
@@ -14,17 +18,25 @@ import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.sql.DriverManager;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import com.kingsrook.qbits.documentgeneration.GenerationPorts;
|
||||
import com.kingsrook.qbits.recordfiles.files.IntakeRequest;
|
||||
import com.kingsrook.qbits.recordfiles.files.RetainedFileService;
|
||||
import com.kingsrook.qbits.recordfiles.review.FileReviewService;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.TableBasedAuthenticationModule;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.extensions.MiddlewareSpecExtensionsMetaData;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -53,7 +65,8 @@ class EchoDemoStartupTest
|
||||
@TempDir
|
||||
Path root;
|
||||
|
||||
private final HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
|
||||
private final HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5))
|
||||
.cookieHandler(new java.net.CookieManager(null, java.net.CookiePolicy.ACCEPT_ALL)).build();
|
||||
|
||||
|
||||
|
||||
@@ -151,6 +164,7 @@ class EchoDemoStartupTest
|
||||
demo.start(port);
|
||||
|
||||
QInstance instance = demo.defineQInstance();
|
||||
seedPreviewPhotos(instance, ADMIN_PASSWORD);
|
||||
assertNotNull(instance.getApp("echo"));
|
||||
assertNotNull(instance.getProcess("echoPrint"));
|
||||
assertNotNull(instance.getTable("echoScan"));
|
||||
@@ -171,6 +185,19 @@ class EchoDemoStartupTest
|
||||
assertTrue(version.body().contains("protocolVersion"));
|
||||
assertNotEquals(200, send(port, "/qqq/v1/metaData", null, null).statusCode());
|
||||
assertEquals(200, login(port, "scanner", SCANNER_PASSWORD).statusCode());
|
||||
HttpResponse<String> metadata = send(port, "/qqq/v1/metaData/table/testOrder", null, null);
|
||||
assertEquals(200, metadata.statusCode());
|
||||
assertTrue(metadata.body().contains("Order overview") && metadata.body().contains("associatedTableName"));
|
||||
HttpResponse<String> page = send(port, "/qqq/v1/table/testOrder/query", null,
|
||||
"{\"filter\":{\"skip\":25,\"limit\":25,\"orderBys\":[{\"fieldName\":\"id\",\"isAscending\":true}]}}");
|
||||
assertEquals(200, page.statusCode());
|
||||
assertTrue(page.body().contains("TEST-000026"));
|
||||
assertFalse(page.body().contains("TEST-000001"));
|
||||
HttpResponse<String> children = send(port, "/qqq/v1/table/testOrderItem/query", null,
|
||||
"{\"filter\":{\"limit\":10,\"criteria\":[{\"fieldName\":\"orderId\",\"operator\":\"EQUALS\",\"values\":[26]}]}}");
|
||||
assertEquals(200, children.statusCode());
|
||||
assertTrue(children.body().contains("ITEM-000301"));
|
||||
assertFalse(children.body().contains("ITEM-000001"));
|
||||
assertNotEquals(200, login(port, "scanner", "not-the-password-1").statusCode());
|
||||
|
||||
assertEquals(0, renderer.calls.get());
|
||||
@@ -203,6 +230,64 @@ class EchoDemoStartupTest
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Disposable real-server fixture for native record browsing acceptance.
|
||||
** Requires a fresh temporary directory; never accepts a database to adopt.
|
||||
***************************************************************************/
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
if(!"rich-records-e2e".equals(System.getenv("ECHO_FIXTURE_TYPE")))
|
||||
{
|
||||
throw new IllegalArgumentException("An isolated record fixture is required.");
|
||||
}
|
||||
int port = Integer.parseInt(args[0]);
|
||||
if(port < 1024 || PROTECTED.contains(port))
|
||||
{
|
||||
throw new IllegalArgumentException("Protected runtime port.");
|
||||
}
|
||||
System.setProperty("qqq.logger.logSessionId.disabled", "true");
|
||||
Path directory = Files.createTempDirectory("qqq-rich-records-");
|
||||
EchoDemoSettings settings = new EchoDemoSettings(directory, port, "rich-records-test", TOKEN, false, null, null);
|
||||
EchoDemo demo = new EchoDemo(settings, new RefusingRenderer());
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(demo::close));
|
||||
demo.initializeDatabase(System.getenv("ECHO_FIXTURE_SCANNER_PASSWORD"), System.getenv("ECHO_FIXTURE_ADMIN_PASSWORD"));
|
||||
seedPreviewPhotos(demo.defineQInstance(), System.getenv("ECHO_FIXTURE_ADMIN_PASSWORD"));
|
||||
demo.start(port);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Synthetic approved images exist only in the disposable native fixture.
|
||||
** A second parent's image makes accidental unscoped gallery reads detectable.
|
||||
***************************************************************************/
|
||||
private static void seedPreviewPhotos(QInstance instance, String password) throws Exception
|
||||
{
|
||||
QContext.setQInstance(instance);
|
||||
String basic = Base64.getEncoder().encodeToString(("admin:" + password).getBytes(StandardCharsets.UTF_8));
|
||||
QSession session = new EchoDemoAuthentication().createSession(instance, Map.of(TableBasedAuthenticationModule.BASIC_AUTH_KEY, basic))
|
||||
.withPermission("record-files.release").withSecurityKeyValue("qqq.session.userId", "admin");
|
||||
QContext.init(instance, session);
|
||||
try(ByteArrayOutputStream output = new ByteArrayOutputStream())
|
||||
{
|
||||
ImageIO.write(new BufferedImage(16, 16, BufferedImage.TYPE_INT_RGB), "jpg", output);
|
||||
byte[] bytes = output.toByteArray();
|
||||
String hash = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
|
||||
for(int parent : new int[] {26, 27})
|
||||
{
|
||||
var file = new RetainedFileService().intake(new IntakeRequest("preview-" + parent, MobileTestData.ORDERS, parent,
|
||||
"Preview example " + parent + ".jpg", hash).withContentType("image/jpeg"), new ByteArrayInputStream(bytes));
|
||||
new FileReviewService().release(file.id(), "Synthetic native preview fixture");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** One loopback request; a body makes it a JSON POST.
|
||||
***************************************************************************/
|
||||
|
||||
+1
-1
@@ -169,7 +169,7 @@ class EchoPhotoCaptureTest
|
||||
|
||||
RecordFilesQBitConfig config = (RecordFilesQBitConfig) instance.getQBits().values().stream().map(bit -> bit.getConfig())
|
||||
.filter(RecordFilesQBitConfig.class::isInstance).findFirst().orElseThrow();
|
||||
assertEquals(List.of(EchoQBitProducer.SCAN_TABLE), config.getParentTables());
|
||||
assertEquals(List.of(EchoQBitProducer.SCAN_TABLE, MobileTestData.ORDERS, MobileTestData.ITEMS, MobileTestData.EVENTS), config.getParentTables());
|
||||
assertEquals(Set.of("image/jpeg", "image/png"), config.getUploadConfiguration().limits().acceptedMediaTypes());
|
||||
assertTrue(config.getUploadConfiguration().createIdentityProvider() instanceof MobileCaptureIdentityProvider);
|
||||
assertTrue(config.getUploadConfiguration().createStorage() instanceof LocalStagingChunkStorage);
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Kingsrook, LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.kingsrook.examples.echo.demo;
|
||||
|
||||
|
||||
import java.sql.DriverManager;
|
||||
import java.util.UUID;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fixture volume, relationships, variety and preserving reapplication on H2.
|
||||
*******************************************************************************/
|
||||
class MobileTestDataTest
|
||||
{
|
||||
/***************************************************************************
|
||||
** Every section uses real fields and every child belongs to one test order.
|
||||
** Existing fixture values survive repeated startup, just like attachments.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void seedsRichRelatedRecordsWithoutReplacingExistingRows() throws Exception
|
||||
{
|
||||
QInstance instance = new QInstance();
|
||||
MobileTestData.register(instance);
|
||||
var table = instance.getTable(MobileTestData.ORDERS);
|
||||
assertEquals(39, table.getFields().size());
|
||||
assertEquals(6, table.getSections().size());
|
||||
assertEquals(2, table.getAssociations().size());
|
||||
assertTrue(table.getSections().stream().flatMap(section -> section.getFieldNames().stream()).allMatch(table.getFields()::containsKey));
|
||||
try(var connection = DriverManager.getConnection("jdbc:h2:mem:" + UUID.randomUUID() + ";DATABASE_TO_UPPER=FALSE"); var statement = connection.createStatement())
|
||||
{
|
||||
MobileTestData.initialize(connection, instance);
|
||||
statement.executeUpdate("UPDATE testOrder SET notes = 'Retain this value' WHERE id = 1");
|
||||
MobileTestData.initialize(connection, instance);
|
||||
for(String name : MobileTestData.TABLES)
|
||||
{
|
||||
try(var rows = statement.executeQuery("SELECT COUNT(*) FROM " + name))
|
||||
{
|
||||
rows.next();
|
||||
assertEquals(name.equals(MobileTestData.ORDERS) ? 1000 : name.equals(MobileTestData.ITEMS) ? 12000 : 3000, rows.getInt(1));
|
||||
}
|
||||
}
|
||||
try(var rows = statement.executeQuery("SELECT notes, customerCode, optionalNote FROM testOrder WHERE id = 1"))
|
||||
{
|
||||
rows.next();
|
||||
assertEquals("Retain this value", rows.getString(1));
|
||||
assertEquals("00000001", rows.getString(2));
|
||||
assertEquals("Optional example 1", rows.getString(3));
|
||||
}
|
||||
try(var rows = statement.executeQuery("SELECT COUNT(*) FROM testOrderItem i JOIN testOrder o ON i.orderId = o.id WHERE o.id = 26"))
|
||||
{
|
||||
rows.next();
|
||||
assertEquals(12, rows.getInt(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user