Compare commits

..

258 Commits

Author SHA1 Message Date
bb5c0f7e80 CE-1172 - finish a somewhat usable version of field-level locks - but note - only removing the field from the meta-data that goes to the frontend -- queries, for example, will still fetch the field and send it through.
In touching some code here, remove some un-used old ActionInput parameters.
2024-06-19 16:34:56 -05:00
3d7df30db4 Merged feature/CE-1113-single-carton-reqs into dev 2024-06-19 16:05:16 -05:00
a63d5d0c65 Merged feature/system-user-session into dev 2024-06-19 16:02:53 -05:00
e923ce1013 Merged feature/thread-pool-improvements into dev 2024-06-19 16:02:43 -05:00
231ca165a1 Merged feature/rdbms-connection-pool into dev 2024-06-19 16:02:26 -05:00
35c1150f80 Throw explicit exception if backend is missing a name 2024-06-19 15:59:02 -05:00
37f89110e7 Fix to escapeIdentifiers in doDeleteList 2024-06-17 11:00:17 -05:00
4766d2440d ListBuilder.of instead of List.of, for null-tolerance 2024-06-14 10:02:09 -05:00
ad07f3e1f8 CE-1113 Avoid an NPE in doSpecHtml, if branding doesn't have an accent color 2024-06-14 08:59:04 -05:00
7ab2f332e9 CE-1113 Add Map of HelpContent at instance and table levels 2024-06-14 08:57:24 -05:00
00a3f6632c CE-1113 Add ApiFieldCustomValueMapperBulkSupportInterface 2024-06-14 08:56:48 -05:00
9afbe7c3a9 Add formal concept of system-user-session, as a subclass of QSession. during construction, it is given all all-access keys, and its hasPermission method is hard-coded to return true. 2024-06-07 16:52:01 -05:00
27682870a1 Update API query & get with MAY_USE_READ_ONLY query hint (adding that option to Get's) 2024-06-07 16:22:56 -05:00
98031b53cb Give these tests a little room for timing-based instabilitiy 2024-06-07 12:53:26 -05:00
e1fd6d51c4 Update the processes to appear in the instance twice - on both jobs & triggers tables. 2024-06-07 12:46:32 -05:00
0fd2430866 Update to avoid NPE for null backend names (used by some non-standard flows) 2024-06-07 12:43:26 -05:00
9e9d1960c8 rather than exclusion for mchange-common (lib for c3p0), import the version that rdbms will want directly in backend-core 2024-06-06 19:45:12 -05:00
64380dd849 Exclude mchange-commons-java from backend-core -- prefer our own. 2024-06-06 16:41:55 -05:00
c21e9131a0 Update to use PrefixedDefaultThreadFactory, to give threads more unique names (based on service that owns thread pool) 2024-06-06 08:16:33 -05:00
ec04986434 Fix to use a static threadPool, rather than one per each instance! 2024-06-06 08:15:41 -05:00
f7d217a126 Initial checkin 2024-06-06 08:15:12 -05:00
f9cca885ed checkpoint - working version of c3p0 connection pooling, and read-only database meta-data connections (per query hint) 2024-06-05 15:23:02 -05:00
64f706a98e Merged dev into feature/rdbms-connection-pool 2024-06-04 20:06:50 -05:00
7ff7ae3a0c Merge pull request #95 from Kingsrook/feature/CE-938-order-release-automation
Feature/ce 938 order release automation
2024-06-04 20:04:42 -05:00
d7e295881f Merged dev into feature/CE-938-order-release-automation 2024-06-04 19:55:25 -05:00
9fd55746ca Merge pull request #94 from Kingsrook/feature/rename-run-to-runOnePage
Rename 'run' to 'runOnePage'
2024-06-04 19:54:57 -05:00
121f9aa477 Merge pull request #93 from Kingsrook/feature/sprint-43-cleanup
Feature/sprint 43 cleanup
2024-06-04 19:54:46 -05:00
c8edd14833 Merge pull request #92 from Kingsrook/feature/checkstyle-indentation-enhanced-switch
Upgrade checkstyle; remove supressed indentation markers for new-styl…
2024-06-04 19:54:24 -05:00
f7b6028ba1 CE-938: updated to get filter and column setup values from widget data for saved repoprts 2024-06-04 13:45:36 -05:00
bf2836b69f CE-938 - Add test on ProcessAlertWidget 2024-06-04 11:15:16 -05:00
d3f3e25ed5 Checkstyle 2024-06-04 11:07:05 -05:00
d0839dc93c CE-938 - Fix to enrich optional steps too 2024-06-04 11:02:40 -05:00
5540f85466 CE-938 - Initial checkin 2024-06-04 10:58:36 -05:00
1a66f45425 CE-938 - MORE propagation of updated steps from inside streamed-ETL pseudo-steps to top-level process state 2024-06-04 10:58:28 -05:00
90ac1bb9c3 CE-938 - Add existingLock to UnableToObtainProcessLockException 2024-06-04 10:57:52 -05:00
faafacc722 CE-938: fixed bug on deletion final associated child record was not working properly 2024-06-03 15:26:40 -05:00
4508dea767 CE-938 - avoid NPE in release if null input 2024-05-31 11:16:19 -05:00
7c6c02ab28 Add "convenience wrappers" 2024-05-31 11:16:07 -05:00
e1a63752ca (finally) downgrade some of the every-freaking-request session logging 2024-05-31 11:15:16 -05:00
059b746597 CE-938 Fix linkTableCreateWithDefaultValues - needed # not ? 2024-05-31 11:12:24 -05:00
11e1fb86b2 CE-938 move updatedFrontendStepList into state 2024-05-29 10:17:43 -05:00
eb8bf12047 CE-938 Adding cancel-process action, cancelStep meta-data 2024-05-28 16:59:09 -05:00
66b2b4ff4c CE-938 More flexible check in (can update message and control expires-timestamp) 2024-05-24 16:59:56 -05:00
a6e0741175 CE-938: added new general process utils 2024-05-24 16:15:30 -05:00
2b7432167d Actually add the @QIgnore annotation :) 2024-05-23 13:00:19 -05:00
0a12c76829 Add withRecordLabelFormatAndFields to continue to make table-meta-data setup slightly less verbose 2024-05-23 12:56:49 -05:00
3fe6828550 Apply @QIgnore annotation, to silence some debug logs done as part of entity annotaiton processing 2024-05-23 12:56:22 -05:00
27a17183ae CE-938: renamed reportSetup widget to filterAndColumns 2024-05-22 16:23:25 -05:00
94bf10fe6e CE-938 Adding some null-tolerance 2024-05-22 12:05:26 -05:00
610915bf94 CE-938 Committed too early last time 2024-05-21 12:18:59 -05:00
3e26ea94ee CE-938 Make session & user explicit fields, instead of packing into "holder" 2024-05-21 11:35:00 -05:00
e6190b4fe2 CE-938 Add getErrorsAsString; getWarningsAsString; withWarning 2024-05-20 11:35:18 -05:00
1c582621aa CE-938 Add releaseById; Remove throws from release methods (so you don't always have to try-catch yourself); more robust holder processing 2024-05-20 11:34:57 -05:00
b91da93858 CE-938 Add missing javadoc 2024-05-19 20:29:25 -05:00
522dafca69 CE-938 Initial checkin of ProcessLocks 2024-05-19 20:26:05 -05:00
82f0f177fb CE-938 add overload that takes a Duration 2024-05-19 20:24:55 -05:00
9c79ce3272 CE-938 add isPrimaryKey to @QField 2024-05-19 20:24:55 -05:00
85eae36c28 CE-938 Add concept of MetaDataProducerMultiOutput 2024-05-19 20:24:55 -05:00
485bc618e0 CE-938 update memoization to say if it should store null values or not 2024-05-19 20:21:03 -05:00
6f6f9af17d Updates for tests for min/max records 2024-05-17 16:40:00 -05:00
65fe6a002c Upgrade checkstyle; remove supressed indentation markers for new-style switches 2024-05-17 16:38:28 -05:00
ede497ee85 Add todos referencing lock-tree 2024-05-17 16:18:08 -05:00
5a56b5d9b4 Treat made-up primary keys as nulls... also, don't start them at -1 (which, idk, is maybe somewhat likely in some world? but instead of half of integer-min-value...) 2024-05-17 16:17:37 -05:00
e10a1e40da Implement min/max records enforcement 2024-05-17 16:16:45 -05:00
8816bc89c3 Add cases for merging an IN and IS_NOT_BLANK 2024-05-17 16:16:45 -05:00
759972b70c Fix chicken-egg session from repeating all-access key values 2024-05-17 16:16:45 -05:00
425629de52 Adding missing test 2024-05-17 16:16:45 -05:00
9dec3c517b Rename 'run' to 'runOnePage' 2024-05-16 12:49:28 -05:00
be69836b5b Merged wip/qqq-bom-pom into dev 2024-05-15 20:16:53 -05:00
d528f984d4 Add qqq-bom as child module; mark qqq-bom-pom as packaging:pom 2024-05-15 20:03:37 -05:00
3335e29535 Merged wip/qqq-bom-pom into dev 2024-05-15 19:51:09 -05:00
04547577f7 For CE-1280 - add helpContent to process steps 2024-05-15 19:31:40 -05:00
2d9ea8b73f Merge pull request #91 from Kingsrook/feature/CE-1240-out-of-stock-summary-page
CE-1240: added multi table widget type
2024-05-15 19:17:21 -05:00
1292c04040 Merge pull request #90 from Kingsrook/feature/CE-1180-order-address-validation
Feature/ce 1180 order address validation
2024-05-15 19:15:44 -05:00
7a1b99bab3 CE-1180 Add full QHttpResponse to BadResponse exception 2024-05-14 08:31:40 -05:00
e7735619c1 CE-1180 Add clone 2024-05-13 08:42:29 -05:00
a06db0b7a8 CE-1180 Add method getAllSteps 2024-05-13 08:42:09 -05:00
baac007c09 CE-1180 Add QBadHttpResponseStatusException, to make easier for implementations to see what status code there was in a failure 2024-05-13 08:41:38 -05:00
889697f86f CE-1180 Better built-in support for processes with dynamic flows - that is:
- in a backendStepOutput, you can set overrideLastStepName and call updateStepList
- those values then flow through RunProcessAction to the runProcessOutput, then out through the javalin to the frontend.
2024-05-10 15:16:59 -05:00
f009d50631 CE-1180 add option to setPrimaryKeyInInsertedRecords (on the INPUT records) 2024-05-10 12:51:20 -05:00
c9e9d62098 Add warnAndThrow and errorAndThrow methods, to slightly simplify catch-blocks that want to do both of those things; also add variant data in session portion of log, when available 2024-05-10 12:42:07 -05:00
196488ad6e stash in a static field, the list set of topLevel classes (performance gain for setups where this class is called multiple times (along with clearing that cache during a javalin instance hotswap) 2024-05-10 12:24:29 -05:00
81a5d868b6 Add a non-null filter to avoid an NPE if a null logPair ever gets in 2024-05-10 12:20:46 -05:00
0d3f886e5a Add overview & basic properties 2024-05-10 12:20:03 -05:00
f5f02a2234 Add maxPathLength performance workaround 2024-05-10 12:19:49 -05:00
00b55c583e Fix generic on withRecordEntity be '? extends' 2024-05-10 12:08:07 -05:00
35c079e1dd Set a default time zone (UTC) for tests (we have at least one that likes this) 2024-05-10 12:07:21 -05:00
df262e52e0 Fix to only look at process (not process schedule) to determine if variants are used 2024-05-06 18:34:28 -05:00
0e7c55e108 CE-1240: added multi table widget type 2024-05-03 20:29:50 -05:00
b8ac6d5d61 Initial attempt at a bom (bill of materials) pom 2024-05-03 20:24:27 -05:00
5f5a9db292 update to use NullValueBehaviorUtil.getEffectiveNullValueBehavior 2024-05-02 16:13:29 -05:00
c70f73d9cd Make QueryStat.add never throw; also, avoid where it was throwing, upon a null ActionStack 2024-05-02 15:01:09 -05:00
0651fa22af updated sender to validate a from address was provided and only adds a reply to if provided 2024-05-02 11:16:11 -05:00
9d669de989 Merge pull request #88 from Kingsrook/feature/CE-1068-add-basic-functionality-of
Feature/ce 1068 add basic functionality of
2024-05-02 08:39:46 -05:00
0cc6d7e618 CE-1068 - Add userId field 2024-05-01 16:54:51 -05:00
60e01a303a CE-1068 - Update isThisAnActionDirectlyOnThisTable for if running a process on the table (e.g., fixes bulk delete etc i think) 2024-05-01 16:54:42 -05:00
ae4248b5aa CE-1068 - relabel 'recipient' section to 'email' 2024-05-01 16:54:14 -05:00
4e0ccaa147 CE-1068 - Handle join fields; no message when running for process 2024-05-01 16:54:01 -05:00
e0bb0ef2de CE-1068 - Add line of output saying report has been sent; relabel email to field; 2024-05-01 16:53:34 -05:00
8ca59e0a5b CE-1068 - Move validateEmailAddresses to utils class; add validation when creating scheduled report 2024-05-01 16:53:12 -05:00
e022284ca7 CE-1068 - Check for variabale being not-empty 2024-05-01 16:49:30 -05:00
e853c67b67 CE-1068 - Add creating an automated-session for a user, to use those security keys while running a report 2024-05-01 16:46:59 -05:00
b3fb15e550 Checkstyle 2024-04-30 20:34:19 -05:00
c1f0615964 Merge remote-tracking branch 'origin/feature/CE-1068-add-basic-functionality-of' into feature/CE-1068-add-basic-functionality-of
# Conflicts:
#	qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportTableCustomizer.java
2024-04-30 20:30:38 -05:00
4e181d9932 CE-1068 - Save filter as JSON (not an object, which fails, lol) 2024-04-30 20:29:48 -05:00
77689dd75e CE-1068 - bringing together variable-criteria with running scheduled reports :allthethings: 2024-04-30 20:29:30 -05:00
478f539bee CE-1068: fixed test where filter json not being put back to string 2024-04-30 20:19:49 -05:00
a607f35805 Merged feature/CE-882-add-functionality-of-sharing into feature/CE-1068-add-basic-functionality-of 2024-04-30 20:04:56 -05:00
7ba4f142a6 CE-882 - Only allow owner to delete these records until sharing scopes work 2024-04-30 19:43:21 -05:00
60ce1d8c09 CE-1068: initial checkin of variables in filter handling 2024-04-30 19:22:30 -05:00
0b8549def3 CE-882 - Disable a failing test... 2024-04-30 15:40:53 -05:00
1f5129facc CE-882 - Turn off FAIL_ON_EMPTY_BEANS... seems like failing is bad, and {} is fine 2024-04-30 15:32:21 -05:00
fff1c2a131 CE-882 - Turn off the flipping to AND for UPDATE... e2e testing showed better this way 2024-04-30 15:31:47 -05:00
42ad9aec41 CE-882 - Add Shared views 2024-04-30 15:29:10 -05:00
dd3fa7fa69 CE-882 - Add toString 2024-04-30 15:28:49 -05:00
5c0b1ea2c3 CE-1068 - Add recordOfValues, with display values for PVSes 2024-04-30 14:43:17 -05:00
d417c2c93a Temp lower coverage.instructionCoveredRatioMinimum to 75 2024-04-30 10:42:06 -05:00
1d42b3eb16 Merged feature/CE-882-add-functionality-of-sharing into feature/CE-1068-add-basic-functionality-of 2024-04-30 10:36:24 -05:00
62d4c17aba CE-1068 - Capture recordId param, and pass it through to process via a Callback 2024-04-30 10:28:59 -05:00
76028ddcaa CE-1068 - Initial checkin 2024-04-30 10:28:36 -05:00
8829408e54 CE-1068 - Progress on scheduling reports, with variable inputs 2024-04-30 10:28:22 -05:00
0a35d02404 CE-1068 - add WIDGET type 2024-04-30 10:24:29 -05:00
8c882b8476 CE-1068 - add dynamicForm widget type 2024-04-30 10:24:19 -05:00
3d729d67e6 CE-1068 - Add timeZones PVS (if needed) 2024-04-29 12:31:27 -05:00
984e37bcd8 CE-1068 - Initial checkin 2024-04-29 12:23:42 -05:00
3f16f4c0c3 CE-1068 - Pass transaction throughout 2024-04-29 12:13:58 -05:00
29b8025c41 CE-1068 - Add fields foreignKeyType; foreignKeyValue 2024-04-29 12:13:48 -05:00
9f4cb02764 CE-1068 - Add scheduling 2024-04-29 12:12:55 -05:00
a32c4f4936 CE-1068 - pass transaction down to association query 2024-04-29 12:12:05 -05:00
20a13161c5 CE-1068 - add forPrimaryKeys 2024-04-29 12:11:54 -05:00
c37056f942 CE-1068 - wrap in try/catch/warn/throw 2024-04-29 12:11:43 -05:00
2b6d1ea468 CE-882 Add testPossibleValueWithoutTableOrProcess 2024-04-29 07:57:32 -05:00
7b2e9c4f55 CE-882 Fix test; more words in validation error 2024-04-29 07:50:45 -05:00
13c64de280 CE-882 Checkstyle? 2024-04-28 20:37:33 -05:00
2a6008f4dc CE-882 Add audiencePossibleValueSourceName 2024-04-28 20:32:44 -05:00
5623c6f6c2 CE-882 Add SHARED_SAVED_REPORT_JOIN_SAVED_REPORT 2024-04-28 20:32:36 -05:00
7ecc005232 CE-882 Re-label userId to Owner 2024-04-28 20:32:25 -05:00
bab994edbe CE-882 Add READ scope; fix filterForLockTree to make correct recursive call 2024-04-28 20:32:15 -05:00
9ebf07665a CE-882 Add possible values outside of table or process 2024-04-28 20:30:56 -05:00
bd947c9e16 CE-882 Add ShareableTableMetaData 2024-04-28 20:30:34 -05:00
ea28c007c1 CE-882 Better error on unique key violation 2024-04-28 20:30:21 -05:00
389e7a1515 CE-882 Sort results 2024-04-28 20:30:21 -05:00
b5ceb846e2 CE-882 add DuplicateKeyBadInputStatusMessage 2024-04-28 20:30:21 -05:00
e9cfb67101 CE-882 Add customizer.finalCustomizeSession, to do ... final ... customizaions if needed 2024-04-28 20:30:20 -05:00
c05a1d5812 CE-882 Change doesSelectClauseRequireDistinct to work with multilocks 2024-04-28 20:28:09 -05:00
a3ed016606 Checkstyle (from merger...) 2024-04-26 20:05:31 -05:00
4f0ded6bd6 Start adding overrideIdField to QPossibleValueSource 2024-04-26 19:54:45 -05:00
925afc95e3 Merged feature/join-enhancements into feature/CE-882-add-functionality-of-sharing 2024-04-26 19:52:51 -05:00
0e8e93bff9 CE-882 Checkstyle 2024-04-26 15:01:11 -05:00
bd8246ca5a CE-882 Add Disabled test that's failing... needs fixed! 2024-04-26 14:58:50 -05:00
c72e6ad23d CE-882 Add reversed join 2024-04-26 14:58:23 -05:00
52c1018d5e CE-882 Initial rewrite validateSecurityFields to handle MultiRecordSecurityLocks. 2024-04-26 14:57:56 -05:00
6911be1d52 CE-882 add Insert/Update/Delete tests 2024-04-26 10:55:37 -05:00
7e27c7a89a CE-882 add filterForWriteLockTree 2024-04-26 10:54:34 -05:00
3765f6351c CE-882 Add withPrimaryKey method 2024-04-26 10:53:21 -05:00
674b090ba3 CE-882 Checkstyle 2024-04-25 12:20:44 -05:00
c01a96d4c2 CE-882 Remove removed withHint calls 2024-04-25 12:15:57 -05:00
7fbe2e643e CE-882 change operator switch to new-style 2024-04-25 12:15:46 -05:00
96bd60e795 CE-882 Checkstyle 2024-04-25 12:14:41 -05:00
05b1c70a1b CE-882 Placeholder TODO re: multiRecordSecurityLocks 2024-04-25 12:08:22 -05:00
21657b918c CE-882 Updated test - passes real-world scenarios we need for story 2024-04-25 12:08:02 -05:00
bc47f8b80c CE-882 Handle processing MultiRecorSecurityLocks (which means building trees of securityFilters, with AND/OR logic) 2024-04-25 12:06:43 -05:00
5fe95ab0be CE-882 Add filterForReadLockTree (required cloning in RecordSecurityLock) 2024-04-25 12:03:34 -05:00
1e1b660979 CE-882 Better(?) toString 2024-04-25 12:03:03 -05:00
fec96c39cb CE-882 Add MultiRecordSecurityLocks 2024-04-25 12:02:51 -05:00
b1ba910ac7 CE-882 Update to use ReportDestination 2024-04-25 12:01:33 -05:00
ef2b08899b CE-882 Add TRUE and FALSE operators 2024-04-25 12:01:08 -05:00
c6e0389338 entities, meta-data, and test for sharing POC 2024-04-25 10:15:15 -05:00
9281d07e96 CE-1068: changes from review: allow multiple email address entry, fixed download urls, fixed localhost to use inbucket/filesystem 2024-04-24 17:11:48 -05:00
91526d67cf Merged dev into feature/join-enhancements 2024-04-24 14:59:32 -05:00
659c95a85b pin a static version of qfmd 2024-04-24 12:49:29 -05:00
b692f1a60d CE-882 Fixed expected field name (for the id...) 2024-04-24 08:58:01 -05:00
68687a5d3b CE-882 Add shared reports table, e.g., sharing of reports. 2024-04-24 08:50:05 -05:00
97b17085be CE-882 in case field doesn't have label, use fieldName in some cases (helps w/ tests that don't enrich) 2024-04-24 08:50:05 -05:00
8a74609c51 CE-882 Initial checkin - Meta-data for making a table shareable 2024-04-24 08:50:05 -05:00
97132665a8 CE-882 Initial build of processes to manage shared records 2024-04-24 08:43:53 -05:00
e9bcec4c07 CE-882 Add methods to make newSession (either with default user id, or specified user id) 2024-04-24 08:34:12 -05:00
570d1a80b5 CE-1068: disabled email test in circle ci 2024-04-23 21:04:28 -05:00
5e4305d1d5 CE-1068: checkpoint commit of SES support 2024-04-23 20:55:58 -05:00
a591f57591 QQQ Messaging WIP/POC - Initial checkin 2024-04-19 07:59:21 -05:00
57bb39319f pin a static version of qfmd 2024-04-18 21:06:06 -05:00
974b90f0b7 make sure join-name-chain for record locks ends at the expected join table 2024-04-18 20:44:38 -05:00
09fb9a9502 stop showing scripts repos; show up to 20 builds; always show running builds in menu (i think) 2024-04-18 18:30:12 -05:00
9f142a4909 Change sort tie-breaker to be done with map rather than if/else-ifs; adding Widget after Joins (for child lists built of a join) 2024-04-18 18:30:00 -05:00
5d8cfa5935 Cleanup comments 2024-04-18 18:29:50 -05:00
8c27bb7a49 Don't try to compute aggregates on virtual fields - that is, try-catch around the lookup of such fields 2024-04-18 18:21:32 -05:00
66cf485c41 Avoid NPE; just noop 2024-04-18 15:43:44 -05:00
d40a466d16 Merge pull request #84 from Kingsrook/feature/quartz-schedule-new-jobs
Feature/quartz schedule new jobs
2024-04-18 09:46:42 -05:00
6d0f421efa Merge branch 'feature/CE-1107-add-day-by-day-views-for-the' into dev 2024-04-18 09:20:38 -05:00
bab6cbec8e Merge pull request #83 from Kingsrook/feature/CE-978-crashing-nodes
CE-978 - Moving from 6 hours to 5 hours for clean hours in addition t…
2024-04-18 09:09:55 -05:00
ecf7b463d1 Merged dev into feature/quartz-schedule-new-jobs 2024-04-18 09:09:09 -05:00
a90f244c7f Merge pull request #85 from Kingsrook/feature/backend-module-self-registration
Feature/backend module self registration
2024-04-18 09:07:30 -05:00
dc9a2f8698 CE-1107: added ability to use replaceAction specifying that null key values be treated as equal 2024-04-17 22:26:30 -05:00
51ba4367d1 Remove very loud log 2024-04-17 18:58:21 -05:00
98e84e86e3 Merge pull request #79 from Kingsrook/feature/CE-881-create-basic-saved-reports
Feature/ce 881 create basic saved reports
2024-04-17 18:55:53 -05:00
904b1e9362 Add getStartTimeMillis 2024-04-17 17:04:32 -05:00
34b5da78eb No logs if null zone id in other-field; smaller logs if invalid zone (shouldn't really need stack) 2024-04-17 17:04:20 -05:00
af3c0cd041 Merge pull request #86 from Kingsrook/feature/CE-1123-expose-access-lo-data-in
CE-1123 Add valuesForFrontend to session; pass them to frontend in ma…
2024-04-17 09:18:07 -05:00
1461ddef49 CE-1115 Re-label saved views & reports w/o the "saved" part. 2024-04-16 20:40:08 -05:00
3d38ae20af Update toalways deploy on integration branches, same as dev 2024-04-16 07:57:56 -05:00
a8212ed9e5 CE-1123 Add valuesForFrontend to session; pass them to frontend in manageSesssion javalin endpoint 2024-04-15 12:46:57 -05:00
c89fe958c3 CE-1115 pre-QA commit on saved report UI, including:
- adding pre-insert/update validation
- move json field value formatting from post-query customizer to a FieldDisplayBehavior instead (works for audits this way :)
2024-04-14 20:14:32 -05:00
32021fc36e Merged dev into feature/CE-881-create-basic-saved-reports 2024-04-12 19:55:15 -05:00
0641ac41d6 CE-1107: added alert widget type and datepicker dropdown type 2024-04-12 15:28:33 -05:00
27bbaf8afa Boosting some quartz test coverage 2024-04-12 09:21:25 -05:00
eb47a4f3ab Fix (?) for usage within maven, then checks for xpath and html2text for jacoco reporting 2024-04-12 09:19:45 -05:00
bd01bed62f Fix to check if html2text is installed, before trying to report on un-covered classes (since that's the script we use, and we do install it in circleci) 2024-04-12 09:04:55 -05:00
d839869172 Merge branch 'dev' into feature/join-enhancements 2024-04-12 08:57:54 -05:00
efa195cdee CE-1115 add simple-human-facing versions of display values for json fields 2024-04-11 19:56:52 -05:00
3c6ffbbd73 CE-1115 Moving widget helpContent to be map of list, to support multiple roles per slot (e.g., view screen vs edit screen) 2024-04-11 10:15:04 -05:00
731d1e266a Remove withBackendType(String), to encourage usage instead of with/set method that takes the module class, so the module will be init'ed and registered 2024-04-10 09:05:43 -05:00
1eae1d6dc4 Remove hard-coded registration of known backend-modules from QBackendModuleDispatcher, in favor of all modules self-registering in static-init block 2024-04-10 08:51:44 -05:00
cf0c905dc6 Move publish_asciidoc call into deploy workflow (i think) 2024-04-09 19:01:01 -05:00
3440d45060 Remove usages of log4j's placeholders-based log formatting calls (some of which were accidental and wrong anyway) 2024-04-09 18:52:43 -05:00
c32d9110ce Remove usages of log4j's placeholders-based log formatting calls (some of which were accidental and wrong anyway) 2024-04-09 18:51:43 -05:00
696887254c Use disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) on all JsonUtils.toObject calls 2024-04-09 18:47:51 -05:00
5630d57cba More and better test cleanup try 2 2024-04-09 08:48:07 -05:00
1b1d066131 More and better test cleanup 2024-04-09 08:43:47 -05:00
4dd4d1256a Boosting some quartz test coverage 2024-04-09 08:27:34 -05:00
2a0d5686e7 Merged dev into feature/quartz-schedule-new-jobs 2024-04-09 07:57:39 -05:00
b55631a767 Add liquibase as level INFO 2024-04-08 20:20:50 -05:00
5dc4b26f9e Add ScheduleAllNewJobsProcess and supporting methods 2024-04-08 20:18:23 -05:00
d7b2efdfb2 CE-1115 - Adding widgets for editing query, columns, and pivot table; udpate labels, required fields 2024-04-08 20:09:30 -05:00
482b0ca546 Merged feature/more-state-provider-cleanup into feature/CE-978-crashing-nodes 2024-04-08 14:26:34 -05:00
b0a4ea39d0 CE-978 - Moving from 6 hours to 5 hours for clean hours in addition to running every 30min. Also added status to interface. 2024-04-08 12:22:25 -05:00
5b6260dd1e Try again to make predicatbly sorted content map 2024-04-07 17:24:57 -05:00
5520111836 Use a NonPersistedAsyncJobCallback for cases where it doesn't need to be persisted to state provider 2024-04-06 20:23:58 -05:00
4d65fe8014 LinkedHashMap for content types, for stable builds 2024-04-05 16:17:15 -05:00
9ab5a8b305 Wrap DateTimeDisplayValueBehavior's better 2024-04-05 15:39:00 -05:00
ff9add437d Update publish-asciidoc to only run on dev 2024-04-05 15:35:42 -05:00
a6bf474448 Add publishing of docs 2024-04-05 15:27:23 -05:00
f409d0bd19 Add field behaviors 2024-04-05 15:21:50 -05:00
b608bda83a CE-1072 feedback from code review & testing 2024-04-05 15:21:32 -05:00
8f503945cd CE-1072 Add fallbackZoneId in case zoneIdFromFieldName isn't set or valid; More tests & validation 2024-04-04 20:39:38 -05:00
512d73af34 checkstyle 2024-04-04 20:07:28 -05:00
e8b4368acc CE-1072 Add DateTimeDisplayValueBehavior as first version of FieldDisplayBehavior, sub-interface of FieldBehavior, called through ValueBehaviorApplier by QValueFormatter; 2024-04-04 20:02:42 -05:00
1e7e7cdd64 Merge pull request #81 from Kingsrook/feature/CE-978-crashing-nodes
Feature/ce 978 crashing nodes
2024-04-04 13:51:45 -05:00
a784e59c50 Change to run once an hour, after 6 hours, and to clean 6-hour jobs 2024-04-04 13:50:20 -05:00
4e6460c469 Merge pull request #80 from Kingsrook/hotfix/large-result-query-hint-exports
Hotfix/large result query hint exports
2024-04-04 10:53:00 -05:00
8da05b9ab9 CE-881 - Feedback from code review & continued QA testing 2024-04-04 08:56:13 -05:00
98fc34fb27 Turn on QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS 2024-04-03 14:53:52 -05:00
df6bed2453 CE-881 - Add QueryHints enum & set to QueryInput; do mysql result set streaming based on the POTENTIALLY_LARGE_NUMBER_OF_RESULTS hint being present 2024-04-03 14:51:28 -05:00
0759085431 CE-978 - Initial commit of a clean thread/method to our InMemoryStateProvider to reduce memory leak 2024-04-02 16:52:22 -05:00
71ecde74df jdbc Connections in try-with-resources (so they close and return to connection pool) 2024-03-12 14:28:46 -05:00
7e34b97998 jdbc Connections in try-with-resources (so they close and return to connection pool) 2024-03-12 13:53:47 -05:00
1062f00ed4 Add c3p0 connection pooling to RDBMS module (ConnectionManager) 2024-03-12 12:02:36 -05:00
923872dab7 Merged feature/CE-680-push-tracking-data-to-ship-station into feature/join-enhancements 2023-09-27 20:00:42 -05:00
bf0a554c6a Instead of returning 1=1 if no clauses, make that return an optional, and handle smarter (avoid making a 1=1 OR <some clause>, which borke some tests!) 2023-09-27 14:14:35 -05:00
c3d69d812a Add test: testWriteLockOnJoinTableDoesntLimitQuery 2023-09-27 13:55:19 -05:00
f0d59895f0 Avoid NPE (in StringBuilder constructor?) if fieldName is null. 2023-09-27 13:55:06 -05:00
02cd335b95 Only consider read-locks when looking at join tables 2023-09-27 13:54:35 -05:00
34a1cd80f4 in getSqlWhereStringAndPopulateParamsList... - skip a criteria with null fieldName or operator - and then if there were no valid criteria, return 1=1 2023-09-27 13:54:05 -05:00
635807c525 Avoid NPE in toString if orderBys is null 2023-09-27 11:09:47 -05:00
623f3aca93 Merged dev into feature/join-enhancements 2023-09-27 09:16:08 -05:00
831ac3bc07 Avoid NPE in hasAnyCriteria if a sub-filter in the list is null 2023-09-08 10:33:20 -05:00
a85c06a407 Set tableName if null before filtering (as BackendQueryFilterUtils uses it for some cases now) 2023-09-08 10:32:58 -05:00
6c7621a2f7 Update handling of criteria in format "table.field" when the "table" portion equals the record's tableName; fix applyBooleanOperator to always update the accumulator; 2023-09-08 10:30:39 -05:00
73e826f81d Join Enhancements:
- Moving responsibility for adding security clauses out of AbstractRDBMSAction, into JoinsContext
- Adding QueryJoin securityClauses (helps outer-join security filtering work as expected)
- Add security clauses for all joined tables
- Improved inferring of joinMetaData, especially from ExposedJoins
- Fix processes use of selectDistinct when ordering by a field from a joinTable (by doing the Distinct in the record pipe)
2023-09-07 12:23:12 -05:00
390 changed files with 27061 additions and 2913 deletions

View File

@ -98,6 +98,31 @@ commands:
- ~/.m2
key: v1-dependencies-{{ checksum "pom.xml" }}
install_asciidoctor:
steps:
- checkout
- run:
name: Install asciidoctor
command: |
sudo apt-get update
sudo apt install -y asciidoctor
run_asciidoctor:
steps:
- run:
name: Run asciidoctor
command: |
cd docs
asciidoctor -a docinfo=shared index.adoc
upload_docs_site:
steps:
- run:
name: scp html to justinsgotskinnylegs.com
command: |
cd docs
scp index.html dkelkhoff@45.79.44.221:/mnt/first-volume/dkelkhoff/nginx/html/justinsgotskinnylegs.com/qqq-docs.html
jobs:
mvn_test:
executor: localstack/default
@ -114,6 +139,13 @@ jobs:
- mvn_verify
- mvn_jar_deploy
publish_asciidoc:
executor: localstack/default
steps:
- install_asciidoctor
- run_asciidoctor
- upload_docs_site
workflows:
test_only:
jobs:
@ -121,7 +153,7 @@ workflows:
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
filters:
branches:
ignore: /dev/
ignore: /(dev|integration.*)/
tags:
ignore: /(version|snapshot)-.*/
@ -131,7 +163,10 @@ workflows:
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
filters:
branches:
only: /dev/
only: /(dev|integration.*)/
tags:
only: /(version|snapshot)-.*/
- publish_asciidoc:
filters:
branches:
only: /dev/

View File

@ -21,5 +21,99 @@ Used to set values in the `displayValues` map within a `QRecord`.
* `possibleValueSourceName` - *String* - Reference to a {link-pvs} to be used for this field.
Values in this field should correspond to ids from the referenced Possible Value Source.
* `maxLength` - *Integer* - Maximum length (number of characters) allowed for values in this field.
Only applicable for fields with `type=STRING`.
* `
Only applicable for fields with `type=STRING`. Needs to be used with a `FieldBehavior` of type `ValueTooLongBehavior`.
==== Field Behaviors
Additional behaviors can be attached to fields through the use of the `behaviors` attribute,
which is a `Set` of 0 or more instances of implementations of the `FieldBehavior` interface.
Note that in some cases, these instances may be `enum` constants,
but other times may be regular Objects.
QQQ provides a set of common field behaviors.
Applications can also define their own field behaviors by implementing the `FieldBehavior` interface,
and attaching instances of their custom behavior classes to fields.
===== ValueTooLongBehavior
Used on String fields. Requires the field to have a `maxLength` set.
Depending on the chosen instance of this enum, before a record is Inserted or Updated,
if the value in the field is longer than the `maxLength`, then one of the following actions can occur:
* `TRUNCATE` - The value will be simply truncated to the `maxLength`.
* `TRUNCATE_ELLIPSIS` - The value will be truncated to 3 characters less than the `maxLength`, and three periods (an ellipsis) will be placed at the end.
* `ERROR` - An error will be reported, and the record will not be inserted or updated.
* `PASS_THROUGH` - Nothing will happen. This is the same as not having a `ValueTooLongBehavior` on the field.
[source,java]
.Examples of using ValueTooLongBehavior
----
new QFieldMetaData("sku", QFieldType.STRING)
.withMaxLength(40),
.withBehavior(ValueTooLongBehavior.ERROR),
new QFieldMetaData("reason", QFieldType.STRING)
.withMaxLength(250),
.withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS),
----
===== DynamicDefaultValueBehavior
Used to set a dynamic default value to a field when it is being inserted or updated.
For example, instead of having a hard-coded `defaultValue` specified in the field meta-data,
and instead of having to add, for example, a pre-insert custom action.
* `CREATE_DATE` - On inserts, sets the field's value to the current time.
* `MODIFY_DATE` - On inserts and updates, sets the field's value to the current time.
* `USER_ID` - On inserts and updates, sets the field's value to the current user's id (but only if the value is currently null).
_Note that the `QInstanceEnricher` will, by default, add the `CREATE_DATE` and `MODIFY_DATE` `DynamicDefaultValueBehavior`
options to any fields named `"createDate"` or `"modifyDate"`.
This behavior can be disabled by setting the `configAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate` property
on the `QInstanceEnricher` instance used by the application to `false`._
[source,java]
.Examples of using DynamicDefaultValueBehavior
----
new QFieldMetaData("createDate", QFieldType.DATE_TIME)
.withBehavior(DynamicDefaultValueBehavior.CREATE_DATE),
new QFieldMetaData("modifyDate", QFieldType.DATE_TIME)
.withBehavior(DynamicDefaultValueBehavior.MODIFY_DATE),
new QFieldMetaData("createdByUserId", QFieldType.STRING)
.withBehavior(DynamicDefaultValueBehavior.USER_ID),
----
===== DateTimeDisplayValueBehavior
By default, in QQQ, fields of type `DATE_TIME` are stored in UTC,
and their values in a QRecord is a java `Instant` instance, which is always UTC.
However, frontends will prefer to display date-time values in the user's local Time Zone whenever possible.
Using `DateTimeDisplayValueBehavior` allows a `DATE_TIME` field to be displayed in a different Time Zone.
An example use-case for this would be displaying airplane flight times,
where you would want a flight from California to New York to display Pacific Time for its departure time,
and Eastern Time for its arrival.
An instance of `DateTimeDisplayValueBehavior` can be configured to either use a hard-coded time `ZoneId`
(for example, to always show users UTC, or a business's home-office time zone).
Or, it can be set up to get the time zone to use from another field in the table.
[source,java]
.Examples of using DateTimeDisplayValueBehavior
----
new QTableMetaData().withName("flights").withFields(List.of(
...
new QFieldMetaData("departureTimeZoneId", QFieldType.STRING),
new QFieldMetaData("arrivaTimeZoneId", QFieldType.STRING),
new QFieldMetaData("departureTime", QFieldType.DATE_TIME)
.withBehavior(new DateTimeDisplayValueBehavior()
.withZoneIdFromFieldName("departureTimeZoneId")),
new QFieldMetaData("arrivalTime", QFieldType.DATE_TIME)
.withBehavior(new DateTimeDisplayValueBehavior()
.withZoneIdFromFieldName("arrivalTimeZoneId"))
new QFieldMetaData("ticketSaleStartDateTime", QFieldType.DATE_TIME)
.withBehavior(new DateTimeDisplayValueBehavior()
.withDefaultZoneId("UTC"))
----

View File

@ -2,16 +2,57 @@
== Joins
include::../variables.adoc[]
#TODO#
A `QJoinMetaData` is a meta-data object that tells QQQ, essentially “it is possible for these 2 tables to join, heres how to do it”.
Joins can be used then, in an application, in a number of possible ways:
* In a {link-table}, we can specify joins to be “exposed”, e.g., made available to users on a query screen
* Also in a Table, as part of an “Association”, which sets up one table as a “parent” of another,
such that you can store (and fetch) the child-records at the same time as the parent
** A common use-case here may be an order & lineItem table -
such that QQQ can generate an API uses to allow you to post an order and its lines in a single request, and they get stored all together
* In defining the security field (record lock) on a table,
sometimes, it isnt a field directly on the table, but instead comes from a joined table (possibly even more than just 1 table away).
** For example, maybe a lineItem table, doesn't have a clientId, but needs secured by that field
- so its recordLock can specify a “joinNameChain” that describes how to get from lineItem to order.clientId
* The `QueryAction` can take (through its QueryInput object) zero or more QueryJoin objects,
which must make a reference (implicitly or explicitly) to a QJoinMetaData.
See the section on <<QueryJoin,QueryJoins>> for more details.
=== QJoinMetaData
Joins are defined in a QQQ Instance in a `*QJoinMetaData*` object.
#TODO#
In this object, we have the concept of a "leftTable" and a "rightTable".
There isn't generally anything special about which table is on the "left" and which is on the "right".
But the remaining pieces of the QJoinMetaData do all need to line-up with these sides.
For example:
* The Type (one-to-one, one-to-many, many-to-one) - where the leftTable comes first, and rightTable comes second
(e.g., a one-to-many means 1-row in leftTable has many-rows in rightTable associated with it)
* In a JoinOn object, the 1st field name given is from the leftTable;
the second fieldName from the rightTable.
*QJoinMetaData Properties:*
* `name` - *String, Required* - Unique name for the join within the QQQ Instance. #todo infererences or conventions?#
#TODO#
* `name` - *String, Required* - Unique name for the join within the QQQ Instance.
** One convention is to name joins based on (leftTable + "Join" + rightTable).
** If you do not wish to define join names yourself, the method `withInferredName()`
can be called (which defers to
`public static String makeInferredJoinName(String leftTable, String rightTable)`),
to create a name for the join following the (leftTable + "Join" + rightTable) convention.
* `leftTable` - *String, Required* - Name of a {link-table} in the {link-instance}.
* `rightTable` - *String, Required* - Name of a {link-table} in the {link-instance}.
* `type` - *enum, Required* - cardinality between the two tables in the join.
** e.g., `ONE_TO_ONE`, `ONE_TO_MANY` (indicating 1 record in the left table may join
to many records in the right table), or `MANY_TO_ONE` (vice-versa).
** Note that there is no MANY_TO_MANY option, as a many-to-many is built as multiple QJoinMetaData's
going through the intermediary (intersection) table.
* `joinOns` - *List<JoinOn>, Required* - fields used to join the tables.
Note: In the 2-arg JoinOn constructor, the leftTable's field comes first.
Alternatively, the no-arg constructor can be used along with `.withLeftField().withRightField()`
* `orderBys` - *List<QFilterOrderBy>* - Optional list of order-by objects,
used in some framework-generated queries using the join.
The field names are assumed to come from the rightTable.
#TODO# what else do we need here?

View File

@ -16,6 +16,7 @@ Processes are defined in a QQQ Instance in a `*QProcessMetaData*` object.
In addition to directly building a `QProcessMetaData` object setting its properties directly, there are a few common process patterns that provide *Builder* objects for ease-of-use.
See StreamedETLWithFrontendProcess below for a common example
[#_QProcessMetaData_Properties]
*QProcessMetaData Properties:*
* `name` - *String, Required* - Unique name for the process within the QQQ Instance.
@ -30,12 +31,13 @@ See below for details.
* `permissionRules` - *QPermissionRules object* - define the permission/access rules for the process.
See {link-permissionRules} for details.
* `steps` and `stepList` - *Map of String → <<QStepMetaData>>* and *List of QStepMetaData* - Defines the <<QFrontendStepMetaData,screens>> and <<QBackendStepMetaData,backend code>> that makes up the process.
** `stepList` is the list of steps in the order that they will by default be executed.
** `steps` is a map, including all steps from `stepList`, but which may also include steps which can used by the process if its backend steps make the decision to do so, at run-time.
** `stepList` is the list of steps in the order that they will be executed
(that is to say - this is the _default_ order of execution - but it can be customized - see <<_custom_process_flow>> for details).
** `steps` is a map, including all steps from `stepList`, but which may also include steps which can used by the process if its backend steps make the decision to do so, at run-time (e.g., using <<_custom_process_flow>>).
** A process's steps are normally defined in one of two was:
*** 1) by a single call to `.withStepList(List<QStepMetaData>)`, which internally adds each step into the `steps` map.
*** 2) by multiple calls to `.addStep(QStepMetaData)`, which adds a step to both the `stepList` and `steps` map.
** If a process also needs optional steps, they should be added by a call to `.addOptionalStep(QStepMetaData)`, which only places them in the `steps` map.
** If a process also needs optional steps (for a <<_custom_process_flow>>), they should be added by a call to `.addOptionalStep(QStepMetaData)`, which only places them in the `steps` map.
* `schedule` - *<<QScheduleMetaData>>* - set up the process to run automatically on the specified schedule.
See below for details.
* `minInputRecords` - *Integer* - #not used...#
@ -214,3 +216,112 @@ But for some cases, doing page-level transactions can reduce long-transactions a
* `withFields(List<QFieldMetaData> fieldList)` - Adds additional input fields to the preview step of the process.
* `withBasepullConfiguration(BasepullConfiguration basepullConfiguration)` - Add a <<BasepullConfiguration>> to the process.
* `withSchedule(QScheduleMetaData schedule)` - Add a <<QScheduleMetaData>> to the process.
[#_custom_process_flow]
==== Custom Process Flow
As referenced in the definition of the <<_QProcessMetaData_Properties,QProcessMetaData Properties>>, by default, a process
will execute each of its steps in-order, as defined in the `stepList` property.
However, a Backend Step can customize this flow #todo - write more clearly here...
There are generally 2 method to call (in a `BackendStep`) to do a dynamic flow:
* `RunBackendStepOutput.setOverrideLastStepName(String stepName)`
** QQQ's `RunProcessAction` keeps track of which step it "last" ran, e.g., to tell it which one to run next.
However, if a step sets the `OverrideLastStepName` property in its output object,
then the step named in that property becomes the effective "last" step,
thus determining which step comes next.
* `RunBackendStepOutput.updateStepList(List<String> stepNameList)`
** Calling this method changes the process's runtime definition of steps to be executed.
Thus allowing a completely custom flow.
It should be noted, that the "last" step name (as tracked by QQQ within `RunProcessAction`)
does need to be found in the new `stepNameList` - otherwise, the framework will not know where you were,
for figuring out where to go next.
[source,java]
.Example of a defining process that can use a flexible flow:
----
// for a case like this, it would be recommended to define all step names in constants:
public final static String STEP_START = "start";
public final static String STEP_A = "a";
public final static String STEP_B = "b";
public final static String STEP_C = "c";
public final static String STEP_1 = "1";
public final static String STEP_2 = "2";
public final static String STEP_3 = "3";
public final static String STEP_END = "end";
// also, to define the possible flows (lists of steps) in constants as well:
public final static List<String> LETTERS_STEP_LIST = List.of(
STEP_START, STEP_A, STEP_B, STEP_C, STEP_END);
public final static List<String> NUMBERS_STEP_LIST = List.of(
STEP_START, STEP_1, STEP_2, STEP_3, STEP_END);
// when we define the process's meta-data, we only give a "skeleton" stepList -
// we must at least have our starting step, and we may want at least one frontend step
// for the UI to show some placeholder(s):
QProcessMetaData process = new QProcessMetaData()
.withName(PROCESS_NAME)
.withStepList(List.of(
new QBackendStepMetaData().withName(STEP_START)
.withCode(new QCodeReference(/*...*/)),
new QFrontendStepMetaData()
.withName(STEP_END)
));
// the additional steps get added via `addOptionalStep`, which only puts them in
// the process's stepMap, not its stepList!
process.addOptionalStep(new QFrontendStepMetaData().withName(STEP_A));
process.addOptionalStep(new QBackendStepMetaData().withName(STEP_B)
.withCode(new QCodeReference(/*...*/)));
process.addOptionalStep(new QFrontendStepMetaData().withName(STEP_C));
process.addOptionalStep(new QBackendStepMetaData().withName(STEP_1)
.withCode(new QCodeReference(/*...*/)));
process.addOptionalStep(new QFrontendStepMetaData().withName(STEP_2));
process.addOptionalStep(new QBackendStepMetaData().withName(STEP_3)
.withCode(new QCodeReference(/*...*/)));
----
[source,java]
.Example of a process backend step adjusting the process's runtime flow:
----
/***************************************************************************
** look at the value named "which". if it's "letters", then make the process
** go through the stepList consisting of letters; else, update the step list
** to be the "numbers" steps.
**
** Also - if the "skipSomeSteps" value is give as true, then set the
** overrideLastStepName to skip again (in the letters case, skip past A, B
** and C; in the numbers case, skip past 1 and 2).
***************************************************************************/
public static class StartStep implements BackendStep
{
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
Boolean skipSomeSteps = runBackendStepInput.getValueBoolean("skipSomeSteps");
if(runBackendStepInput.getValueString("which").equals("letters"))
{
runBackendStepOutput.updateStepList(LETTERS_STEP_LIST);
if(BooleanUtils.isTrue(skipSomeSteps))
{
runBackendStepOutput.setOverrideLastStepName(STEP_C);
}
}
else
{
runBackendStepOutput.updateStepList(NUMBERS_STEP_LIST);
if(BooleanUtils.isTrue(skipSomeSteps))
{
runBackendStepOutput.setOverrideLastStepName(STEP_2);
}
}
}
}
----

11
pom.xml
View File

@ -29,6 +29,7 @@
<packaging>pom</packaging>
<modules>
<module>qqq-bom</module>
<module>qqq-backend-core</module>
<module>qqq-backend-module-api</module>
<module>qqq-backend-module-filesystem</module>
@ -54,7 +55,7 @@
<maven.compiler.showDeprecation>true</maven.compiler.showDeprecation>
<maven.compiler.showWarnings>true</maven.compiler.showWarnings>
<coverage.haltOnFailure>true</coverage.haltOnFailure>
<coverage.instructionCoveredRatioMinimum>0.80</coverage.instructionCoveredRatioMinimum>
<coverage.instructionCoveredRatioMinimum>0.75</coverage.instructionCoveredRatioMinimum>
<coverage.classCoveredRatioMinimum>0.95</coverage.classCoveredRatioMinimum>
<plugin.shade.phase>none</plugin.shade.phase>
</properties>
@ -149,7 +150,7 @@
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>9.0</version>
<version>10.16.0</version>
</dependency>
</dependencies>
<executions>
@ -245,8 +246,7 @@ echo " See also target/site/jacoco/index.html"
echo " and https://www.jacoco.org/jacoco/trunk/doc/counters.html"
echo "------------------------------------------------------------"
which xpath > /dev/null 2>&1
if [ "$?" == "0" ]; then
if which xpath > /dev/null 2>&1; then
echo "Element\nInstructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers
xpath -n -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}'
@ -255,8 +255,7 @@ else
echo "xpath is not installed. Jacoco coverage summary will not be produced here...";
fi
which xpath > /dev/null 2>&1
if [ "$?" == "0" ]; then
if which html2text > /dev/null 2>&1; then
echo "Untested classes, per Jacoco:"
echo "-----------------------------"
for i in target/site/jacoco/*/index.html; do

View File

@ -173,12 +173,32 @@
<version>1.12.321</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-ses</artifactId>
<version>1.12.705</version>
</dependency>
<dependency>
<groupId>cloud.localstack</groupId>
<artifactId>localstack-utils</artifactId>
<version>0.2.20</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
<!-- bring in a newer version of this lib, which quartz transitively loads through c3p0 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>mchange-commons-java</artifactId>
<version>0.3.0</version>
</dependency>
<!-- Many of the deps we bring in use slf4j. This dep maps slf4j to our logger, log4j -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
@ -186,6 +206,12 @@
<version>2.23.0</version>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.1</version>
</dependency>
<!-- Common deps for all qqq modules -->
<dependency>
<groupId>org.apache.maven.plugins</groupId>

View File

@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.utils.PrefixedDefaultThreadFactory;
/*******************************************************************************
@ -55,7 +56,7 @@ public class ActionHelper
/////////////////////////////////////////////////////////////////////////////
private static Integer CORE_THREADS = 8;
private static Integer MAX_THREADS = 500;
private static ExecutorService executorService = new ThreadPoolExecutor(CORE_THREADS, MAX_THREADS, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
private static ExecutorService executorService = new ThreadPoolExecutor(CORE_THREADS, MAX_THREADS, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), new PrefixedDefaultThreadFactory(ActionHelper.class));

View File

@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
import com.kingsrook.qqq.backend.core.state.StateType;
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
import com.kingsrook.qqq.backend.core.utils.PrefixedDefaultThreadFactory;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.Level;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -65,7 +66,7 @@ public class AsyncJobManager
/////////////////////////////////////////////////////////////////////////////
private static Integer CORE_THREADS = 8;
private static Integer MAX_THREADS = 500;
private static ExecutorService executorService = new ThreadPoolExecutor(CORE_THREADS, MAX_THREADS, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
private static ExecutorService executorService = new ThreadPoolExecutor(CORE_THREADS, MAX_THREADS, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), new PrefixedDefaultThreadFactory(AsyncJobManager.class));
private String forcedJobUUID = null;

View File

@ -31,6 +31,7 @@ import java.io.Serializable;
*******************************************************************************/
public class AsyncJobStatus implements Serializable
{
private String jobName;
private AsyncJobState state;
private String message;
private Integer current;
@ -187,4 +188,36 @@ public class AsyncJobStatus implements Serializable
{
this.cancelRequested = cancelRequested;
}
/*******************************************************************************
** Getter for jobName
*******************************************************************************/
public String getJobName()
{
return (this.jobName);
}
/*******************************************************************************
** Setter for jobName
*******************************************************************************/
public void setJobName(String jobName)
{
this.jobName = jobName;
}
/*******************************************************************************
** Fluent setter for jobName
*******************************************************************************/
public AsyncJobStatus withJobName(String jobName)
{
this.jobName = jobName;
return (this);
}
}

View File

@ -0,0 +1,62 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.async;
import java.util.UUID;
/*******************************************************************************
** subclass designed to be used when we want there to be an instance (so code
** doesn't have to all be null-tolerant), but there's no one who will ever be
** reading the status data, so we don't need to store the object in a
** state provider.
*******************************************************************************/
public class NonPersistedAsyncJobCallback extends AsyncJobCallback
{
private final AsyncJobStatus asyncJobStatus;
/*******************************************************************************
**
*******************************************************************************/
public NonPersistedAsyncJobCallback(UUID jobUUID, AsyncJobStatus asyncJobStatus)
{
super(jobUUID, asyncJobStatus);
this.asyncJobStatus = asyncJobStatus;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
protected void storeUpdatedStatus()
{
///////////////////////////////////////////////////////////////////////////////////////
// noop - cf. base class, which writes to persistence here (our point is, we do not) //
///////////////////////////////////////////////////////////////////////////////////////
}
}

View File

@ -149,7 +149,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
// sort the field names by their labels //
//////////////////////////////////////////
List<String> sortedFieldNames = table.getFields().keySet().stream()
.sorted(Comparator.comparing(fieldName -> table.getFields().get(fieldName).getLabel()))
.sorted(Comparator.comparing(fieldName -> Objects.requireNonNullElse(table.getFields().get(fieldName).getLabel(), fieldName)))
.toList();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
@ -303,7 +303,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
}
else
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
String formattedValue = getFormattedValueForAuditDetail(table, record, fieldName, field, value);
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formattedValue);
detailRecord.withValue("newValue", formattedValue);
}
@ -329,8 +329,8 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
}
else
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
String formattedOldValue = getFormattedValueForAuditDetail(oldRecord, fieldName, field, oldValue);
String formattedValue = getFormattedValueForAuditDetail(table, record, fieldName, field, value);
String formattedOldValue = getFormattedValueForAuditDetail(table, oldRecord, fieldName, field, oldValue);
if(oldValue == null)
{
@ -464,7 +464,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
/*******************************************************************************
**
*******************************************************************************/
private static String getFormattedValueForAuditDetail(QRecord record, String fieldName, QFieldMetaData field, Serializable value)
private static String getFormattedValueForAuditDetail(QTableMetaData table, QRecord record, String fieldName, QFieldMetaData field, Serializable value)
{
String formattedValue = null;
if(value != null)
@ -479,7 +479,8 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
}
else
{
formattedValue = QValueFormatter.formatValue(field, value);
QValueFormatter.setDisplayValuesInRecord(table, table.getFields(), record);
formattedValue = record.getDisplayValue(fieldName);
}
}

View File

@ -127,7 +127,6 @@ public enum AutomationStatus implements PossibleValueEnum<Integer>
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public String getInsertOrUpdate()
{
return switch(this)

View File

@ -526,7 +526,7 @@ public class PollingAutomationPerTableRunner implements Runnable
// note - this method - will re-query the objects, so we should have confidence that their data is fresh... //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> matchingQRecords = getRecordsMatchingActionFilter(table, records, action);
LOG.debug("Of the {} records that were pending automations, {} of them match the filter on the action {}", records.size(), matchingQRecords.size(), action);
LOG.debug("Of the [" + records.size() + "] records that were pending automations, [" + matchingQRecords.size() + "] of them match the filter on the action:" + action);
if(CollectionUtils.nullSafeHasContents(matchingQRecords))
{
LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
@ -601,7 +601,7 @@ public class PollingAutomationPerTableRunner implements Runnable
/*******************************************************************************
** Finally, actually run action code against a list of known matching records.
** todo not commit - move to somewhere genericer
**
*******************************************************************************/
public static void applyActionToMatchingRecords(QTableMetaData table, List<QRecord> records, TableAutomationAction action) throws Exception
{

View File

@ -96,7 +96,7 @@ public class QCodeLoader
}
catch(Exception e)
{
LOG.error("Error initializing customizer", logPair("codeReference", codeReference), e);
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
@ -135,7 +135,7 @@ public class QCodeLoader
}
catch(Exception e)
{
LOG.error("Error initializing customizer", logPair("codeReference", codeReference), e);
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
@ -187,7 +187,7 @@ public class QCodeLoader
}
catch(Exception e)
{
LOG.error("Error initializing customizer", logPair("codeReference", codeReference), e);
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //

View File

@ -50,10 +50,7 @@ public interface RecordCustomizerUtilityInterface
/*******************************************************************************
** Container for an old value and a new value.
*******************************************************************************/
@SuppressWarnings("checkstyle:MethodName")
record Change(Serializable oldValue, Serializable newValue)
{
}
record Change(Serializable oldValue, Serializable newValue) {}
/*******************************************************************************

View File

@ -161,7 +161,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
public static String linkTableCreateWithDefaultValues(RenderWidgetInput input, String tableName, Map<String, Serializable> defaultValues) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/create?defaultValues=" + URLEncoder.encode(JsonUtils.toJson(defaultValues), Charset.defaultCharset()));
return (tablePath + "/create#defaultValues=" + URLEncoder.encode(JsonUtils.toJson(defaultValues), Charset.defaultCharset()));
}
@ -183,7 +183,6 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
/*******************************************************************************
**
*******************************************************************************/

View File

@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.WidgetDropdownData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.WidgetDropdownType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -72,80 +73,104 @@ public abstract class AbstractWidgetRenderer
*******************************************************************************/
protected boolean setupDropdowns(RenderWidgetInput input, QWidgetMetaData metaData, QWidgetData widgetData) throws QException
{
List<List<Map<String, String>>> pvsData = new ArrayList<>();
List<String> pvsLabels = new ArrayList<>();
List<String> pvsNames = new ArrayList<>();
List<List<Map<String, String>>> dataList = new ArrayList<>();
List<String> labelList = new ArrayList<>();
List<String> nameList = new ArrayList<>();
List<String> missingRequiredSelections = new ArrayList<>();
for(WidgetDropdownData dropdownData : CollectionUtils.nonNullList(metaData.getDropdowns()))
{
String possibleValueSourceName = dropdownData.getPossibleValueSourceName();
QPossibleValueSource possibleValueSource = input.getInstance().getPossibleValueSource(possibleValueSourceName);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this looks complicated, but is just look for a label in the dropdown data and if found use it, //
// otherwise look for label in PVS and if found use that, otherwise just use the PVS name //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
String pvsLabel = dropdownData.getLabel() != null ? dropdownData.getLabel() : (possibleValueSource.getLabel() != null ? possibleValueSource.getLabel() : possibleValueSourceName);
pvsLabels.add(pvsLabel);
pvsNames.add(possibleValueSourceName);
SearchPossibleValueSourceInput pvsInput = new SearchPossibleValueSourceInput();
pvsInput.setPossibleValueSourceName(possibleValueSourceName);
if(dropdownData.getForeignKeyFieldName() != null)
if(WidgetDropdownType.DATE_PICKER.equals(dropdownData.getType()))
{
////////////////////////////////////////
// look for an id in the query params //
////////////////////////////////////////
Integer id = null;
if(input.getQueryParams() != null && input.getQueryParams().containsKey("id") && StringUtils.hasContent(input.getQueryParams().get("id")))
String name = dropdownData.getName();
nameList.add(name);
labelList.add(dropdownData.getLabel());
dataList.add(new ArrayList<>());
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// sure that something has been selected, and if not, display a message that a selection needs made //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(dropdownData.getIsRequired())
{
id = Integer.parseInt(input.getQueryParams().get("id"));
}
if(id != null)
{
pvsInput.setDefaultQueryFilter(new QQueryFilter().withCriteria(
new QFilterCriteria(
dropdownData.getForeignKeyFieldName(),
QCriteriaOperator.EQUALS,
id)));
if(!input.getQueryParams().containsKey(name) || !StringUtils.hasContent(input.getQueryParams().get(name)))
{
missingRequiredSelections.add(dropdownData.getLabel());
}
}
}
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(pvsInput);
List<Map<String, String>> dropdownOptionList = new ArrayList<>();
pvsData.add(dropdownOptionList);
//////////////////////////////////////////
// sort results, dedupe, and add to map //
//////////////////////////////////////////
Set<String> exists = new HashSet<>();
output.getResults().removeIf(pvs -> !exists.add(pvs.getLabel()));
for(QPossibleValue<?> possibleValue : output.getResults())
else
{
dropdownOptionList.add(MapBuilder.of(
"id", String.valueOf(possibleValue.getId()),
"label", possibleValue.getLabel()
));
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// because we know the dropdowns and what the field names will be when something is selected, we can make //
// sure that something has been selected, and if not, display a message that a selection needs made //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(dropdownData.getIsRequired())
{
if(!input.getQueryParams().containsKey(possibleValueSourceName) || !StringUtils.hasContent(input.getQueryParams().get(possibleValueSourceName)))
String possibleValueSourceName = dropdownData.getPossibleValueSourceName();
if(possibleValueSourceName != null)
{
missingRequiredSelections.add(pvsLabel);
QPossibleValueSource possibleValueSource = input.getInstance().getPossibleValueSource(possibleValueSourceName);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this looks complicated, but is just look for a label in the dropdown data and if found use it, //
// otherwise look for label in PVS and if found use that, otherwise just use the PVS name //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
String pvsLabel = dropdownData.getLabel() != null ? dropdownData.getLabel() : (possibleValueSource.getLabel() != null ? possibleValueSource.getLabel() : possibleValueSourceName);
labelList.add(pvsLabel);
nameList.add(possibleValueSourceName);
SearchPossibleValueSourceInput pvsInput = new SearchPossibleValueSourceInput();
pvsInput.setPossibleValueSourceName(possibleValueSourceName);
if(dropdownData.getForeignKeyFieldName() != null)
{
////////////////////////////////////////
// look for an id in the query params //
////////////////////////////////////////
Integer id = null;
if(input.getQueryParams() != null && input.getQueryParams().containsKey("id") && StringUtils.hasContent(input.getQueryParams().get("id")))
{
id = Integer.parseInt(input.getQueryParams().get("id"));
}
if(id != null)
{
pvsInput.setDefaultQueryFilter(new QQueryFilter().withCriteria(
new QFilterCriteria(
dropdownData.getForeignKeyFieldName(),
QCriteriaOperator.EQUALS,
id)));
}
}
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(pvsInput);
List<Map<String, String>> dropdownOptionList = new ArrayList<>();
dataList.add(dropdownOptionList);
//////////////////////////////////////////
// sort results, dedupe, and add to map //
//////////////////////////////////////////
Set<String> exists = new HashSet<>();
output.getResults().removeIf(pvs -> !exists.add(pvs.getLabel()));
for(QPossibleValue<?> possibleValue : output.getResults())
{
dropdownOptionList.add(MapBuilder.of(
"id", String.valueOf(possibleValue.getId()),
"label", possibleValue.getLabel()
));
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// because we know the dropdowns and what the field names will be when something is selected, we can make //
// sure that something has been selected, and if not, display a message that a selection needs made //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(dropdownData.getIsRequired())
{
if(!input.getQueryParams().containsKey(possibleValueSourceName) || !StringUtils.hasContent(input.getQueryParams().get(possibleValueSourceName)))
{
missingRequiredSelections.add(pvsLabel);
}
}
}
}
}
widgetData.setDropdownNameList(pvsNames);
widgetData.setDropdownLabelList(pvsLabels);
widgetData.setDropdownDataList(pvsData);
widgetData.setDropdownNameList(nameList);
widgetData.setDropdownLabelList(labelList);
widgetData.setDropdownDataList(dataList);
////////////////////////////////////////////////////////////////////////////////
// if there are any missing required dropdowns, build up a message to display //

View File

@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
@ -59,6 +60,7 @@ import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.BooleanUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -66,6 +68,9 @@ import org.apache.commons.lang.BooleanUtils;
*******************************************************************************/
public class ChildRecordListRenderer extends AbstractWidgetRenderer
{
private static final QLogger LOG = QLogger.getLogger(ChildRecordListRenderer.class);
/*******************************************************************************
**
@ -172,126 +177,134 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
String widgetLabel = input.getQueryParams().get("widgetLabel");
String joinName = input.getQueryParams().get("joinName");
QJoinMetaData join = input.getInstance().getJoin(joinName);
String id = input.getQueryParams().get("id");
QTableMetaData leftTable = input.getInstance().getTable(join.getLeftTable());
QTableMetaData rightTable = input.getInstance().getTable(join.getRightTable());
Integer maxRows = null;
if(StringUtils.hasContent(input.getQueryParams().get("maxRows")))
try
{
maxRows = ValueUtils.getValueAsInteger(input.getQueryParams().get("maxRows"));
}
else if(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"))
{
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"));
}
String widgetLabel = input.getQueryParams().get("widgetLabel");
String joinName = input.getQueryParams().get("joinName");
QJoinMetaData join = input.getInstance().getJoin(joinName);
String id = input.getQueryParams().get("id");
QTableMetaData leftTable = input.getInstance().getTable(join.getLeftTable());
QTableMetaData rightTable = input.getInstance().getTable(join.getRightTable());
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// fetch the record that we're getting children for. e.g., the left-side of the join, with the input id //
// but - only try this if we were given an id. note, this widget could be called for on an INSERT screen, where we don't have a record yet //
// but we still want to be able to return all the other data in here that otherwise comes from the widget meta data, join, etc. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int totalRows = 0;
QRecord primaryRecord = null;
QQueryFilter filter = null;
QueryOutput queryOutput = new QueryOutput(new QueryInput());
if(StringUtils.hasContent(id))
{
GetInput getInput = new GetInput();
getInput.setTableName(join.getLeftTable());
getInput.setPrimaryKey(id);
GetOutput getOutput = new GetAction().execute(getInput);
primaryRecord = getOutput.getRecord();
if(primaryRecord == null)
Integer maxRows = null;
if(StringUtils.hasContent(input.getQueryParams().get("maxRows")))
{
throw (new QNotFoundException("Could not find " + (leftTable == null ? "" : leftTable.getLabel()) + " with primary key " + id));
maxRows = ValueUtils.getValueAsInteger(input.getQueryParams().get("maxRows"));
}
else if(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"))
{
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"));
}
////////////////////////////////////////////////////////////////////
// set up the query - for the table on the right side of the join //
////////////////////////////////////////////////////////////////////
filter = new QQueryFilter();
for(JoinOn joinOn : join.getJoinOns())
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// fetch the record that we're getting children for. e.g., the left-side of the join, with the input id //
// but - only try this if we were given an id. note, this widget could be called for on an INSERT screen, where we don't have a record yet //
// but we still want to be able to return all the other data in here that otherwise comes from the widget meta data, join, etc. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int totalRows = 0;
QRecord primaryRecord = null;
QQueryFilter filter = null;
QueryOutput queryOutput = new QueryOutput(new QueryInput());
if(StringUtils.hasContent(id))
{
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, List.of(primaryRecord.getValue(joinOn.getLeftField()))));
}
filter.setOrderBys(join.getOrderBys());
filter.setLimit(maxRows);
GetInput getInput = new GetInput();
getInput.setTableName(join.getLeftTable());
getInput.setPrimaryKey(id);
GetOutput getOutput = new GetAction().execute(getInput);
primaryRecord = getOutput.getRecord();
QueryInput queryInput = new QueryInput();
queryInput.setTableName(join.getRightTable());
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setShouldGenerateDisplayValues(true);
queryInput.setFilter(filter);
queryOutput = new QueryAction().execute(queryInput);
if(primaryRecord == null)
{
throw (new QNotFoundException("Could not find " + (leftTable == null ? "" : leftTable.getLabel()) + " with primary key " + id));
}
QValueFormatter.setBlobValuesToDownloadUrls(rightTable, queryOutput.getRecords());
totalRows = queryOutput.getRecords().size();
if(maxRows != null && (queryOutput.getRecords().size() == maxRows))
{
/////////////////////////////////////////////////////////////////////////////////////
// if the input said to only do some max, and the # of results we got is that max, //
// then do a count query, for displaying 1-n of <count> //
/////////////////////////////////////////////////////////////////////////////////////
CountInput countInput = new CountInput();
countInput.setTableName(join.getRightTable());
countInput.setFilter(filter);
totalRows = new CountAction().execute(countInput).getCount();
}
}
String tablePath = input.getInstance().getTablePath(rightTable.getName());
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, rightTable, tablePath, viewAllLink, totalRows);
if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("canAddChildRecord"))))
{
widgetData.setCanAddChildRecord(true);
//////////////////////////////////////////////////////////
// new child records must have values from the join-ons //
//////////////////////////////////////////////////////////
Map<String, Serializable> defaultValuesForNewChildRecords = new HashMap<>();
if(primaryRecord != null)
{
////////////////////////////////////////////////////////////////////
// set up the query - for the table on the right side of the join //
////////////////////////////////////////////////////////////////////
filter = new QQueryFilter();
for(JoinOn joinOn : join.getJoinOns())
{
defaultValuesForNewChildRecords.put(joinOn.getRightField(), primaryRecord.getValue(joinOn.getLeftField()));
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, List.of(primaryRecord.getValue(joinOn.getLeftField()))));
}
filter.setOrderBys(join.getOrderBys());
filter.setLimit(maxRows);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(join.getRightTable());
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setShouldGenerateDisplayValues(true);
queryInput.setFilter(filter);
queryOutput = new QueryAction().execute(queryInput);
QValueFormatter.setBlobValuesToDownloadUrls(rightTable, queryOutput.getRecords());
totalRows = queryOutput.getRecords().size();
if(maxRows != null && (queryOutput.getRecords().size() == maxRows))
{
/////////////////////////////////////////////////////////////////////////////////////
// if the input said to only do some max, and the # of results we got is that max, //
// then do a count query, for displaying 1-n of <count> //
/////////////////////////////////////////////////////////////////////////////////////
CountInput countInput = new CountInput();
countInput.setTableName(join.getRightTable());
countInput.setFilter(filter);
totalRows = new CountAction().execute(countInput).getCount();
}
}
widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords);
String tablePath = input.getInstance().getTablePath(rightTable.getName());
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
Map<String, Serializable> widgetValues = input.getWidgetMetaData().getDefaultValues();
if(widgetValues.containsKey("disabledFieldsForNewChildRecords"))
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, rightTable, tablePath, viewAllLink, totalRows);
if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("canAddChildRecord"))))
{
widgetData.setDisabledFieldsForNewChildRecords((Set<String>) widgetValues.get("disabledFieldsForNewChildRecords"));
}
else
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there are no disabled fields specified - then normally any fields w/ a default value get implicitly disabled //
// but - if we didn't look-up the primary record, then we'll want to explicit disable fields from joins //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(primaryRecord == null)
widgetData.setCanAddChildRecord(true);
//////////////////////////////////////////////////////////
// new child records must have values from the join-ons //
//////////////////////////////////////////////////////////
Map<String, Serializable> defaultValuesForNewChildRecords = new HashMap<>();
if(primaryRecord != null)
{
Set<String> implicitlyDisabledFields = new HashSet<>();
widgetData.setDisabledFieldsForNewChildRecords(implicitlyDisabledFields);
for(JoinOn joinOn : join.getJoinOns())
{
implicitlyDisabledFields.add(joinOn.getRightField());
defaultValuesForNewChildRecords.put(joinOn.getRightField(), primaryRecord.getValue(joinOn.getLeftField()));
}
}
widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords);
Map<String, Serializable> widgetValues = input.getWidgetMetaData().getDefaultValues();
if(widgetValues.containsKey("disabledFieldsForNewChildRecords"))
{
widgetData.setDisabledFieldsForNewChildRecords((Set<String>) widgetValues.get("disabledFieldsForNewChildRecords"));
}
else
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there are no disabled fields specified - then normally any fields w/ a default value get implicitly disabled //
// but - if we didn't look-up the primary record, then we'll want to explicit disable fields from joins //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(primaryRecord == null)
{
Set<String> implicitlyDisabledFields = new HashSet<>();
widgetData.setDisabledFieldsForNewChildRecords(implicitlyDisabledFields);
for(JoinOn joinOn : join.getJoinOns())
{
implicitlyDisabledFields.add(joinOn.getRightField());
}
}
}
}
}
return (new RenderWidgetOutput(widgetData));
return (new RenderWidgetOutput(widgetData));
}
catch(Exception e)
{
LOG.warn("Error rendering child record list", e, logPair("widgetName", () -> input.getWidgetMetaData().getName()));
throw (e);
}
}
}

View File

@ -256,7 +256,6 @@ public enum DateTimeGroupBy
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public Instant roundDown(Instant instant, ZoneId zoneId)
{
ZonedDateTime zoned = instant.atZone(zoneId);

View File

@ -0,0 +1,86 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.AlertData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
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.dashboard.QWidgetMetaData;
/*******************************************************************************
** Widget that can add an Alert to a process screen.
**
** In the process, you'll want values:
** - alertType - name of entry in AlertType enum (ERROR, WARNING, SUCCESS)
** - alertHtml - html to display inside the alert (other than its icon)
*******************************************************************************/
public class ProcessAlertWidget extends AbstractWidgetRenderer implements MetaDataProducerInterface<QWidgetMetaData>
{
public static final String NAME = "ProcessAlertWidget";
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
AlertData.AlertType alertType = AlertData.AlertType.WARNING;
if(input.getQueryParams().containsKey("alertType"))
{
alertType = AlertData.AlertType.valueOf(input.getQueryParams().get("alertType"));
}
String html = "Warning";
if(input.getQueryParams().containsKey("alertHtml"))
{
html = input.getQueryParams().get("alertHtml");
}
return (new RenderWidgetOutput(new AlertData(alertType, html)));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QWidgetMetaData produce(QInstance qInstance) throws QException
{
return new QWidgetMetaData()
.withType(WidgetType.ALERT.getType())
.withGridColumns(12)
.withName(NAME)
.withIsCard(false)
.withShowReloadButton(false)
.withCodeReference(new QCodeReference(getClass()));
}
}

View File

@ -30,7 +30,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
/*******************************************************************************
** Interface for actions that a backend can perform, based on streaming data
** into the backend's storage.
** into the backend's storage.
*******************************************************************************/
public interface QStorageInterface
{
@ -46,4 +46,24 @@ public interface QStorageInterface
*******************************************************************************/
InputStream getInputStream(StorageInput storageInput) throws QException;
/*******************************************************************************
**
*******************************************************************************/
default void makePublic(StorageInput storageInput) throws QException
{
//////////
// noop //
//////////
}
/*******************************************************************************
**
*******************************************************************************/
default String getDownloadURL(StorageInput storageInput) throws QException
{
return (null);
}
}

View File

@ -0,0 +1,64 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.messaging;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.messaging.SendMessageInput;
import com.kingsrook.qqq.backend.core.model.actions.messaging.SendMessageOutput;
import com.kingsrook.qqq.backend.core.model.metadata.messaging.QMessagingProviderMetaData;
import com.kingsrook.qqq.backend.core.modules.messaging.MessagingProviderInterface;
import com.kingsrook.qqq.backend.core.modules.messaging.QMessagingProviderDispatcher;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
**
*******************************************************************************/
public class SendMessageAction extends AbstractQActionFunction<SendMessageInput, SendMessageOutput>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public SendMessageOutput execute(SendMessageInput input) throws QException
{
if(!StringUtils.hasContent(input.getMessagingProviderName()))
{
throw (new QException("Messaging provider name was not given in SendMessageInput."));
}
QMessagingProviderMetaData messagingProvider = QContext.getQInstance().getMessagingProvider(input.getMessagingProviderName());
if(messagingProvider == null)
{
throw (new QException("Messaging provider named [" + input.getMessagingProviderName() + "] was not found in this QInstance."));
}
MessagingProviderInterface messagingProviderInterface = new QMessagingProviderDispatcher().getMessagingProviderInterface(messagingProvider.getType());
return (messagingProviderInterface.sendMessage(input));
}
}

View File

@ -29,6 +29,7 @@ import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -41,9 +42,19 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
*******************************************************************************/
public class JoinGraph
{
private Set<Edge> edges = new HashSet<>();
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// as an instance grows, with the number of joins (say, more than 50?), especially as they may have a lot of connections, //
// it can become very very slow to process a full join graph (e.g., 10 seconds, maybe much worse, per Big-O...) //
// also, it's not frequently useful to look at a join path that's more than a handful of tables long. //
// thus - this property exists - to limit the max length of a join path. Keeping it small keeps instance enrichment //
// and validation reasonably performant, at the possible cost of, some join-path that's longer than this limit may not //
// be found - but - chances are, you don't want some 12-element join path to be used anyway, thus, this makes sense. //
// but - it can be adjusted, per system property or ENV var. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private int maxPathLength = new QMetaDataVariableInterpreter().getIntegerFromPropertyOrEnvironment("qqq.instance.joinGraph.maxPathLength", "QQQ_INSTANCE_JOIN_GRAPH_MAX_PATH_LENGTH", 3);
/*******************************************************************************
@ -303,6 +314,13 @@ public class JoinGraph
if(otherTableName != null)
{
if(newPath.size() > maxPathLength)
{
////////////////////////////////////////////////////////////////
// performance hack. see comment at maxPathLength definition //
////////////////////////////////////////////////////////////////
continue;
}
JoinConnectionList newConnectionList = connectionList.copy();
JoinConnection joinConnection = new JoinConnection(otherTableName, edge.joinName);

View File

@ -84,7 +84,7 @@ public class MetaDataAction
}
QBackendMetaData backendForTable = metaDataInput.getInstance().getBackendForTable(tableName);
tables.put(tableName, new QFrontendTableMetaData(metaDataInput, backendForTable, table, false, false));
tables.put(tableName, new QFrontendTableMetaData(backendForTable, table, false, false));
treeNodes.put(tableName, new AppTreeNode(table));
}
metaDataOutput.setTables(tables);
@ -218,6 +218,8 @@ public class MetaDataAction
metaDataOutput.setEnvironmentValues(metaDataInput.getInstance().getEnvironmentValues());
metaDataOutput.setHelpContents(metaDataInput.getInstance().getHelpContent());
// todo post-customization - can do whatever w/ the result if you want?
return metaDataOutput;

View File

@ -54,7 +54,7 @@ public class TableMetaDataAction
throw (new QNotFoundException("Table [" + tableMetaDataInput.getTableName() + "] was not found."));
}
QBackendMetaData backendForTable = tableMetaDataInput.getInstance().getBackendForTable(table.getName());
tableMetaDataOutput.setTable(new QFrontendTableMetaData(tableMetaDataInput, backendForTable, table, true, true));
tableMetaDataOutput.setTable(new QFrontendTableMetaData(backendForTable, table, true, true));
// todo post-customization - can do whatever w/ the result if you want

View File

@ -65,7 +65,7 @@ public class PermissionsHelper
*******************************************************************************/
public static void checkTablePermissionThrowing(AbstractTableActionInput tableActionInput, TablePermissionSubType permissionSubType) throws QPermissionDeniedException
{
checkTablePermissionThrowing(tableActionInput, tableActionInput.getTableName(), permissionSubType);
checkTablePermissionThrowing(tableActionInput.getTableName(), permissionSubType);
}
@ -73,7 +73,7 @@ public class PermissionsHelper
/*******************************************************************************
**
*******************************************************************************/
private static void checkTablePermissionThrowing(AbstractActionInput actionInput, String tableName, TablePermissionSubType permissionSubType) throws QPermissionDeniedException
private static void checkTablePermissionThrowing(String tableName, TablePermissionSubType permissionSubType) throws QPermissionDeniedException
{
warnAboutPermissionSubTypeForTables(permissionSubType);
QTableMetaData table = QContext.getQInstance().getTable(tableName);
@ -99,11 +99,11 @@ public class PermissionsHelper
/*******************************************************************************
**
*******************************************************************************/
public static boolean hasTablePermission(AbstractActionInput actionInput, String tableName, TablePermissionSubType permissionSubType)
public static boolean hasTablePermission(String tableName, TablePermissionSubType permissionSubType)
{
try
{
checkTablePermissionThrowing(actionInput, tableName, permissionSubType);
checkTablePermissionThrowing(tableName, permissionSubType);
return (true);
}
catch(QPermissionDeniedException e)
@ -500,7 +500,6 @@ public class PermissionsHelper
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
static PermissionSubType getEffectivePermissionSubType(QPermissionRules rules, PermissionSubType originalPermissionSubType)
{
if(rules == null || rules.getLevel() == null)
@ -515,10 +514,10 @@ public class PermissionsHelper
if(PrivatePermissionSubType.HAS_ACCESS.equals(originalPermissionSubType))
{
return switch(rules.getLevel())
{
case NOT_PROTECTED -> null;
default -> PrivatePermissionSubType.HAS_ACCESS;
};
{
case NOT_PROTECTED -> null;
default -> PrivatePermissionSubType.HAS_ACCESS;
};
}
else
{
@ -527,30 +526,30 @@ public class PermissionsHelper
// permission sub-type to what we expect to be set for the table //
////////////////////////////////////////////////////////////////////////////////////////////////////////
return switch(rules.getLevel())
{
case NOT_PROTECTED -> null;
case HAS_ACCESS_PERMISSION -> PrivatePermissionSubType.HAS_ACCESS;
case READ_WRITE_PERMISSIONS ->
{
case NOT_PROTECTED -> null;
case HAS_ACCESS_PERMISSION -> PrivatePermissionSubType.HAS_ACCESS;
case READ_WRITE_PERMISSIONS ->
if(PrivatePermissionSubType.READ.equals(originalPermissionSubType) || PrivatePermissionSubType.WRITE.equals(originalPermissionSubType))
{
if(PrivatePermissionSubType.READ.equals(originalPermissionSubType) || PrivatePermissionSubType.WRITE.equals(originalPermissionSubType))
{
yield (originalPermissionSubType);
}
else if(TablePermissionSubType.INSERT.equals(originalPermissionSubType) || TablePermissionSubType.EDIT.equals(originalPermissionSubType) || TablePermissionSubType.DELETE.equals(originalPermissionSubType))
{
yield (PrivatePermissionSubType.WRITE);
}
else if(TablePermissionSubType.READ.equals(originalPermissionSubType))
{
yield (PrivatePermissionSubType.READ);
}
else
{
throw new IllegalStateException("Unexpected permissionSubType: " + originalPermissionSubType);
}
yield (originalPermissionSubType);
}
case READ_INSERT_EDIT_DELETE_PERMISSIONS -> originalPermissionSubType;
};
else if(TablePermissionSubType.INSERT.equals(originalPermissionSubType) || TablePermissionSubType.EDIT.equals(originalPermissionSubType) || TablePermissionSubType.DELETE.equals(originalPermissionSubType))
{
yield (PrivatePermissionSubType.WRITE);
}
else if(TablePermissionSubType.READ.equals(originalPermissionSubType))
{
yield (PrivatePermissionSubType.READ);
}
else
{
throw new IllegalStateException("Unexpected permissionSubType: " + originalPermissionSubType);
}
}
case READ_INSERT_EDIT_DELETE_PERMISSIONS -> originalPermissionSubType;
};
}
}

View File

@ -0,0 +1,110 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.processes;
import java.util.Optional;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
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.ProcessState;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.state.StateType;
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Action handler for running the cancel step of a qqq process
*
*******************************************************************************/
public class CancelProcessAction extends RunProcessAction
{
private static final QLogger LOG = QLogger.getLogger(CancelProcessAction.class);
/*******************************************************************************
**
*******************************************************************************/
public RunProcessOutput execute(RunProcessInput runProcessInput) throws QException
{
ActionHelper.validateSession(runProcessInput);
QProcessMetaData process = runProcessInput.getInstance().getProcess(runProcessInput.getProcessName());
if(process == null)
{
throw new QBadRequestException("Process [" + runProcessInput.getProcessName() + "] is not defined in this instance.");
}
if(runProcessInput.getProcessUUID() == null)
{
throw (new QBadRequestException("Cannot cancel process - processUUID was not given."));
}
UUIDAndTypeStateKey stateKey = new UUIDAndTypeStateKey(UUID.fromString(runProcessInput.getProcessUUID()), StateType.PROCESS_STATUS);
Optional<ProcessState> processState = getState(runProcessInput.getProcessUUID());
if(processState.isEmpty())
{
throw (new QBadRequestException("Cannot cancel process - State for process UUID [" + runProcessInput.getProcessUUID() + "] was not found."));
}
RunProcessOutput runProcessOutput = new RunProcessOutput();
try
{
if(process.getCancelStep() != null)
{
LOG.info("Running cancel step for process", logPair("processName", process.getName()));
runBackendStep(runProcessInput, process, runProcessOutput, stateKey, process.getCancelStep(), process, processState.get());
}
else
{
LOG.debug("Process does not have a custom cancel step to run.", logPair("processName", process.getName()));
}
}
catch(QException qe)
{
////////////////////////////////////////////////////////////
// upon exception (e.g., one thrown by a step), throw it. //
////////////////////////////////////////////////////////////
throw (qe);
}
catch(Exception e)
{
throw (new QException("Error cancelling process", e));
}
finally
{
//////////////////////////////////////////////////////
// always put the final state in the process result //
//////////////////////////////////////////////////////
runProcessOutput.setProcessState(processState.get());
}
return (runProcessOutput);
}
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.processes;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -114,4 +115,14 @@ public class QProcessCallbackFactory
return (forFilter(new QQueryFilter().withCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, value))));
}
/*******************************************************************************
**
*******************************************************************************/
public static QProcessCallback forPrimaryKeys(String fieldName, Collection<? extends Serializable> values)
{
return (forFilter(new QQueryFilter().withCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.IN, values))));
}
}

View File

@ -26,6 +26,7 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
@ -34,6 +35,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
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.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
@ -71,7 +73,17 @@ public class RunBackendStepAction
QStepMetaData stepMetaData = process.getStep(runBackendStepInput.getStepName());
if(stepMetaData == null)
{
throw new QException("Step [" + runBackendStepInput.getStepName() + "] is not defined in the process [" + process.getName() + "]");
if(process.getCancelStep() != null && Objects.equals(process.getCancelStep().getName(), runBackendStepInput.getStepName()))
{
/////////////////////////////////////
// special case for cancel step... //
/////////////////////////////////////
stepMetaData = process.getCancelStep();
}
else
{
throw new QException("Step [" + runBackendStepInput.getStepName() + "] is not defined in the process [" + process.getName() + "]");
}
}
if(!(stepMetaData instanceof QBackendStepMetaData backendStepMetaData))
@ -82,7 +94,7 @@ public class RunBackendStepAction
//////////////////////////////////////////////////////////////////////////////////////
// ensure input data is set as needed - use callback object to get anything missing //
//////////////////////////////////////////////////////////////////////////////////////
ensureRecordsAreInRequest(runBackendStepInput, backendStepMetaData);
ensureRecordsAreInRequest(runBackendStepInput, backendStepMetaData, process);
ensureInputFieldsAreInRequest(runBackendStepInput, backendStepMetaData);
////////////////////////////////////////////////////////////////////
@ -167,7 +179,7 @@ public class RunBackendStepAction
** check if this step uses a record list - and if so, if we need to get one
** via the callback
*******************************************************************************/
private void ensureRecordsAreInRequest(RunBackendStepInput runBackendStepInput, QBackendStepMetaData step) throws QException
private void ensureRecordsAreInRequest(RunBackendStepInput runBackendStepInput, QBackendStepMetaData step, QProcessMetaData process) throws QException
{
QFunctionInputMetaData inputMetaData = step.getInputMetaData();
if(inputMetaData != null && inputMetaData.getRecordListMetaData() != null)
@ -190,9 +202,44 @@ public class RunBackendStepAction
queryInput.setFilter(callback.getQueryFilter());
//////////////////////////////////////////////////////////////////////////////////////////
// if process has a max-no of records, set a limit on the process of that number plus 1 //
// (the plus 1 being so we can see "oh, you selected more than that many; error!" //
//////////////////////////////////////////////////////////////////////////////////////////
if(process.getMaxInputRecords() != null)
{
if(callback.getQueryFilter() == null)
{
queryInput.setFilter(new QQueryFilter());
}
queryInput.getFilter().setLimit(process.getMaxInputRecords() + 1);
}
QueryOutput queryOutput = new QueryAction().execute(queryInput);
runBackendStepInput.setRecords(queryOutput.getRecords());
// todo - handle 0 results found?
////////////////////////////////////////////////////////////////////////////////
// if process defines a max, and more than the max were found, throw an error //
////////////////////////////////////////////////////////////////////////////////
if(process.getMaxInputRecords() != null)
{
if(queryOutput.getRecords().size() > process.getMaxInputRecords())
{
throw (new QUserFacingException("Too many records were selected for this process. At most, only " + process.getMaxInputRecords() + " can be selected."));
}
}
/////////////////////////////////////////////////////////////////////////////////
// if process defines a min, and fewer than the min were found, throw an error //
/////////////////////////////////////////////////////////////////////////////////
if(process.getMinInputRecords() != null)
{
if(queryOutput.getRecords().size() < process.getMinInputRecords())
{
throw (new QUserFacingException("Too few records were selected for this process. At least " + process.getMinInputRecords() + " must be selected."));
}
}
}
}
}

View File

@ -82,7 +82,7 @@ public class RunProcessAction
public static final String BASEPULL_THIS_RUNTIME_KEY = "basepullThisRuntimeKey";
public static final String BASEPULL_LAST_RUNTIME_KEY = "basepullLastRuntimeKey";
public static final String BASEPULL_TIMESTAMP_FIELD = "basepullTimestampField";
public static final String BASEPULL_CONFIGURATION = "basepullConfiguration";
public static final String BASEPULL_CONFIGURATION = "basepullConfiguration";
////////////////////////////////////////////////////////////////////////////////////////////////
// indicator that the timestamp field should be updated - e.g., the execute step is finished. //
@ -190,7 +190,25 @@ public class RunProcessAction
// Run backend steps //
///////////////////////
LOG.debug("Running backend step [" + step.getName() + "] in process [" + process.getName() + "]");
runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
RunBackendStepOutput runBackendStepOutput = runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
/////////////////////////////////////////////////////////////////////////////////////////
// if the step returned an override lastStepName, use that to determine how we proceed //
/////////////////////////////////////////////////////////////////////////////////////////
if(runBackendStepOutput.getOverrideLastStepName() != null)
{
LOG.debug("Process step [" + lastStepName + "] returned an overrideLastStepName [" + runBackendStepOutput.getOverrideLastStepName() + "]!");
lastStepName = runBackendStepOutput.getOverrideLastStepName();
}
/////////////////////////////////////////////////////////////////////////////////////////////
// similarly, if the step produced an updatedFrontendStepList, propagate that data outward //
/////////////////////////////////////////////////////////////////////////////////////////////
if(runBackendStepOutput.getUpdatedFrontendStepList() != null)
{
LOG.debug("Process step [" + lastStepName + "] generated an updatedFrontendStepList [" + runBackendStepOutput.getUpdatedFrontendStepList().stream().map(s -> s.getName()).toList() + "]!");
runProcessOutput.setUpdatedFrontendStepList(runBackendStepOutput.getUpdatedFrontendStepList());
}
}
else
{
@ -317,6 +335,13 @@ public class RunProcessAction
///////////////////////////////////////////////////
runProcessInput.seedFromProcessState(optionalProcessState.get());
///////////////////////////////////////////////////////////////////////////////////////////////////
// if we're restoring an old state, we can discard a previously stored updatedFrontendStepList - //
// it is only needed on the transitional edge from a backend-step to a frontend step, but not //
// in the other directly //
///////////////////////////////////////////////////////////////////////////////////////////////////
optionalProcessState.get().setUpdatedFrontendStepList(null);
///////////////////////////////////////////////////////////////////////////
// if there were values from the caller, put those (back) in the request //
///////////////////////////////////////////////////////////////////////////
@ -339,7 +364,7 @@ public class RunProcessAction
/*******************************************************************************
** Run a single backend step.
*******************************************************************************/
private void runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
protected RunBackendStepOutput runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
{
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(processState);
runBackendStepInput.setProcessName(process.getName());
@ -368,14 +393,16 @@ public class RunProcessAction
runBackendStepInput.setBasepullLastRunTime((Instant) runProcessInput.getValues().get(BASEPULL_LAST_RUNTIME_KEY));
}
RunBackendStepOutput lastFunctionResult = new RunBackendStepAction().execute(runBackendStepInput);
storeState(stateKey, lastFunctionResult.getProcessState());
RunBackendStepOutput runBackendStepOutput = new RunBackendStepAction().execute(runBackendStepInput);
storeState(stateKey, runBackendStepOutput.getProcessState());
if(lastFunctionResult.getException() != null)
if(runBackendStepOutput.getException() != null)
{
runProcessOutput.setException(lastFunctionResult.getException());
throw (lastFunctionResult.getException());
runProcessOutput.setException(runBackendStepOutput.getException());
throw (runBackendStepOutput.getException());
}
return (runBackendStepOutput);
}
@ -495,15 +522,15 @@ public class RunProcessAction
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if backend specifies that it uses variants, look for that data in the session and append to our basepull key //
// if process specifies that it uses variants, look for that data in the session and append to our basepull key //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(process.getSchedule() != null && process.getVariantBackend() != null)
if(process.getVariantBackend() != null)
{
QSession session = QContext.getQSession();
QBackendMetaData backendMetaData = QContext.getQInstance().getBackend(process.getVariantBackend());
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(backendMetaData.getVariantOptionsTableTypeValue()))
{
LOG.info("Could not find Backend Variant information for Backend '" + backendMetaData.getName() + "'");
LOG.warn("Could not find Backend Variant information for Backend '" + backendMetaData.getName() + "'");
}
else
{

View File

@ -0,0 +1,146 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
/*******************************************************************************
** Subclass of record pipe that ony allows through distinct records, based on
** the set of fields specified in the constructor as a uniqueKey.
*******************************************************************************/
public class DistinctFilteringRecordPipe extends RecordPipe
{
private UniqueKey uniqueKey;
private Set<Serializable> seenValues = new HashSet<>();
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public DistinctFilteringRecordPipe(UniqueKey uniqueKey)
{
this.uniqueKey = uniqueKey;
}
/*******************************************************************************
** Constructor that accepts pipe's overrideCapacity (allowed to be null)
**
*******************************************************************************/
public DistinctFilteringRecordPipe(UniqueKey uniqueKey, Integer overrideCapacity)
{
super(overrideCapacity);
this.uniqueKey = uniqueKey;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addRecords(List<QRecord> records) throws QException
{
List<QRecord> recordsToAdd = new ArrayList<>();
for(QRecord record : records)
{
if(!seenBefore(record))
{
recordsToAdd.add(record);
}
}
if(recordsToAdd.isEmpty())
{
return;
}
super.addRecords(recordsToAdd);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addRecord(QRecord record) throws QException
{
if(seenBefore(record))
{
return;
}
super.addRecord(record);
}
/*******************************************************************************
** return true if we've seen this record before (based on the unique key) -
** also - update the set of seen values!
*******************************************************************************/
private boolean seenBefore(QRecord record)
{
Serializable ukValues = extractUKValues(record);
if(seenValues.contains(ukValues))
{
return true;
}
seenValues.add(ukValues);
return false;
}
/*******************************************************************************
**
*******************************************************************************/
private Serializable extractUKValues(QRecord record)
{
if(uniqueKey.getFieldNames().size() == 1)
{
return (record.getValue(uniqueKey.getFieldNames().get(0)));
}
else
{
ArrayList<Serializable> rs = new ArrayList<>();
for(String fieldName : uniqueKey.getFieldNames())
{
rs.add(record.getValue(fieldName));
}
return (rs);
}
}
}

View File

@ -44,6 +44,7 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -190,6 +191,9 @@ public class ExportAction
Set<String> addedJoinNames = new HashSet<>();
if(CollectionUtils.nullSafeHasContents(exportInput.getFieldNames()))
{
/////////////////////////////////////////////////////////////////////////////////////////////
// make sure that any tables being selected from are included as (LEFT) joins in the query //
/////////////////////////////////////////////////////////////////////////////////////////////
for(String fieldName : exportInput.getFieldNames())
{
if(fieldName.contains("."))
@ -198,27 +202,7 @@ public class ExportAction
String joinTableName = parts[0];
if(!addedJoinNames.contains(joinTableName))
{
QueryJoin queryJoin = new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true);
queryJoins.add(queryJoin);
/////////////////////////////////////////////////////////////////////////////////////////////
// in at least some cases, we need to let the queryJoin know what join-meta-data to use... //
// This code basically mirrors what QFMD is doing right now, so it's better - //
// but shouldn't all of this just be in JoinsContext? it does some of this... //
/////////////////////////////////////////////////////////////////////////////////////////////
QTableMetaData table = exportInput.getTable();
Optional<ExposedJoin> exposedJoinOptional = CollectionUtils.nonNullList(table.getExposedJoins()).stream().filter(ej -> ej.getJoinTable().equals(joinTableName)).findFirst();
if(exposedJoinOptional.isEmpty())
{
throw (new QException("Could not find exposed join between base table " + table.getName() + " and requested join table " + joinTableName));
}
ExposedJoin exposedJoin = exposedJoinOptional.get();
if(exposedJoin.getJoinPath().size() == 1)
{
queryJoin.setJoinMetaData(QContext.getQInstance().getJoin(exposedJoin.getJoinPath().get(exposedJoin.getJoinPath().size() - 1)));
}
queryJoins.add(new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true));
addedJoinNames.add(joinTableName);
}
}
@ -233,7 +217,8 @@ public class ExportAction
}
queryInput.getFilter().setLimit(exportInput.getLimit());
queryInput.setShouldTranslatePossibleValues(true);
queryInput.withQueryHint(QueryInput.QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS);
queryInput.withQueryHint(QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS);
queryInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND);
/////////////////////////////////////////////////////////////////
// tell this query that it needs to put its output into a pipe //

View File

@ -59,6 +59,7 @@ import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext;
@ -136,8 +137,8 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
*******************************************************************************/
public ReportOutput execute(ReportInput reportInput) throws QException
{
ReportOutput reportOutput = new ReportOutput();
QReportMetaData report = getReportMetaData(reportInput);
ReportOutput reportOutput = new ReportOutput();
QReportMetaData report = getReportMetaData(reportInput);
this.views = report.getViews();
this.dataSources = report.getDataSources();
@ -398,7 +399,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
RunBackendStepInput finalTransformStepInput = transformStepInput;
RunBackendStepOutput finalTransformStepOutput = transformStepOutput;
String tableLabel = ObjectUtils.tryElse(() -> QContext.getQInstance().getTable(dataSource.getSourceTable()).getLabel(), Objects.requireNonNullElse(dataSource.getSourceTable(), ""));
String tableLabel = ObjectUtils.tryElse(() -> QContext.getQInstance().getTable(dataSource.getSourceTable()).getLabel(), Objects.requireNonNullElse(dataSource.getSourceTable(), ""));
AtomicInteger consumedCount = new AtomicInteger(0);
/////////////////////////////////////////////////////////////////
@ -417,7 +418,8 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
queryInput.setTableName(dataSource.getSourceTable());
queryInput.setFilter(queryFilter);
queryInput.setQueryJoins(dataSource.getQueryJoins());
queryInput.withQueryHint(QueryInput.QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS);
queryInput.withQueryHint(QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS);
queryInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND);
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setFieldsToTranslatePossibleValues(setupFieldsToTranslatePossibleValues(reportInput, dataSource, new JoinsContext(reportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), queryInput.getFilter())));
@ -457,7 +459,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
if(finalTransformStep != null)
{
finalTransformStepInput.setRecords(records);
finalTransformStep.run(finalTransformStepInput, finalTransformStepOutput);
finalTransformStep.runOnePage(finalTransformStepInput, finalTransformStepOutput);
records = finalTransformStepOutput.getRecords();
}
@ -531,7 +533,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/*******************************************************************************
**
*******************************************************************************/
private FieldAndJoinTable getFieldAndJoinTable(QTableMetaData mainTable, String fieldName) throws QException
public static FieldAndJoinTable getFieldAndJoinTable(QTableMetaData mainTable, String fieldName) throws QException
{
if(fieldName.indexOf('.') > -1)
{
@ -557,7 +559,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/*******************************************************************************
**
*******************************************************************************/
private void setInputValuesInQueryFilter(ReportInput reportInput, QQueryFilter queryFilter)
private void setInputValuesInQueryFilter(ReportInput reportInput, QQueryFilter queryFilter) throws QException
{
if(queryFilter == null || queryFilter.getCriteria() == null)
{
@ -704,8 +706,24 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
//////////////////////////////////////////////////////////////////////////////////////
for(String fieldName : record.getValues().keySet())
{
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, fieldName);
QFieldMetaData field = fieldAndJoinTable.field();
QFieldMetaData field = null;
try
{
//////////////////////////////////////////////////////
// todo - memoize this, if we ever need to optimize //
//////////////////////////////////////////////////////
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, fieldName);
field = fieldAndJoinTable.field();
}
catch(Exception e)
{
//////////////////////////////////////////////////////////////////////////////////////
// for non-real-fields... let's skip for now - but maybe treat as string in future? //
//////////////////////////////////////////////////////////////////////////////////////
LOG.debug("Couldn't find field in table qInstance - won't compute aggregates for it", logPair("fieldName", fieldName));
continue;
}
if(StringUtils.hasContent(field.getPossibleValueSourceName()))
{
@SuppressWarnings("unchecked")
@ -1097,5 +1115,22 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/*******************************************************************************
**
*******************************************************************************/
private record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable) {}
public record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable)
{
/*******************************************************************************
**
*******************************************************************************/
public String getLabel(QTableMetaData mainTable)
{
if(mainTable.getName().equals(joinTable.getName()))
{
return (field.getLabel());
}
else
{
return (joinTable.getLabel() + ": " + field.getLabel());
}
}
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@ -43,8 +44,9 @@ public class RecordPipe
private static final long BLOCKING_SLEEP_MILLIS = 100;
private static final long MAX_SLEEP_LOOP_MILLIS = 300_000; // 5 minutes
private static final int DEFAULT_CAPACITY = 1_000;
private int capacity = 1_000;
private int capacity = DEFAULT_CAPACITY;
private ArrayBlockingQueue<QRecord> queue = new ArrayBlockingQueue<>(capacity);
private boolean isTerminated = false;
@ -70,11 +72,13 @@ public class RecordPipe
/*******************************************************************************
** Construct a record pipe, with an alternative capacity for the internal queue.
**
** overrideCapacity is allowed to be null - in which case, DEFAULT_CAPACITY is used.
*******************************************************************************/
public RecordPipe(Integer overrideCapacity)
{
this.capacity = overrideCapacity;
queue = new ArrayBlockingQueue<>(overrideCapacity);
this.capacity = Objects.requireNonNullElse(overrideCapacity, DEFAULT_CAPACITY);
queue = new ArrayBlockingQueue<>(this.capacity);
}
@ -138,7 +142,7 @@ public class RecordPipe
{
if(now - sleepLoopStartTime > MAX_SLEEP_LOOP_MILLIS)
{
LOG.warn("Giving up adding record to pipe, due to pipe being full for more than {} millis", MAX_SLEEP_LOOP_MILLIS);
LOG.warn("Giving up adding record to pipe, due to pipe being full for more than " + MAX_SLEEP_LOOP_MILLIS + " millis");
throw (new IllegalStateException("Giving up adding record to pipe, due to pipe staying full too long."));
}
LOG.trace("Record pipe.add failed (due to full pipe). Blocking.");

View File

@ -82,12 +82,26 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook;
/*******************************************************************************
** Excel export format implementation using POI library, but with modifications
** to actually stream output rather than use any temp files.
**
** For a rough outline:
** - create a basically empty Excel workbook using POI - empty meaning, without
** data rows.
** - have POI write that workbook out into a byte[] - which will be a zip full
** of xml (e.g., xlsx).
** - then open a new ZipOutputStream wrapper around the OutputStream we took in
** as the report destination (e.g., streamed into storage or http output)
** - Copy over all entries from the xlsx into our new zip-output-stream, other than
** ones that are the actual sheets that we want to put data into.
** - For the sheet entries, use the StreamedPoiSheetWriter class to write the
** report's data directly out as Excel XML (not using POI).
** - Pivot tables require a bit of an additional hack - to write a "pivot cache
** definition", which, while we won't put all the data in it (because we'll tell
** it refreshOnLoad="true"), we will at least need to know how many cols & rows
** are in the data-sheet (which we wouldn't know until we streamed that sheet!)
*******************************************************************************/
public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInterface
{
private static final QLogger LOG = QLogger.getLogger(ExcelPoiBasedStreamingExportStreamer.class);
public static final String EXCEL_DATE_FORMAT = "yyyy-MM-dd";
public static final String EXCEL_DATE_TIME_FORMAT = "yyyy-MM-dd H:mm:ss";
private List<QReportView> views;
private ExportInput exportInput;
@ -95,17 +109,20 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
private OutputStream outputStream;
private ZipOutputStream zipOutputStream;
private PoiExcelStylerInterface poiExcelStylerInterface = getStylerInterface();
private Map<String, String> excelCellFormats;
public static final String EXCEL_DATE_FORMAT = "yyyy-MM-dd";
public static final String EXCEL_DATE_TIME_FORMAT = "yyyy-MM-dd H:mm:ss";
private PoiExcelStylerInterface poiExcelStylerInterface = getStylerInterface();
private Map<String, String> excelCellFormats;
private Map<String, XSSFCellStyle> styles = new HashMap<>();
private int rowNo = 0;
private int sheetIndex = 1;
private Map<String, String> pivotViewToCacheDefinitionReferenceMap = new HashMap<>();
private Map<String, XSSFCellStyle> styles = new HashMap<>();
private Map<String, String> pivotViewToCacheDefinitionReferenceMap = new HashMap<>();
private Writer activeSheetWriter = null;
private StreamedPoiSheetWriter sheetWriter = null;
private Writer activeSheetWriter = null;
private StreamedSheetWriter sheetWriter = null;
private QReportView currentView = null;
private Map<String, List<QFieldMetaData>> fieldsPerView = new HashMap<>();
@ -194,6 +211,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
/////////////////////////////////////////////////////////////////////////////////////////////
// open up a zipOutputStream around the output stream that the report is to be written to. //
// note: this stream is closed in the finish method - see more comments there. //
/////////////////////////////////////////////////////////////////////////////////////////////
this.zipOutputStream = new ZipOutputStream(this.outputStream);
@ -271,7 +289,6 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
for(QReportField column : dataView.getColumns())
{
XSSFCell cell = headerRow.createCell(columnNo++);
// todo ... not like this
cell.setCellValue(QInstanceEnricher.nameToLabel(column.getName()));
}
@ -313,10 +330,6 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
{
int columnLabelColumnIndex = getColumnIndex(dataView.getColumns(), value.getFieldName());
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - some bug where, if use a group-by field here, then ... it doesn't get used for the grouping. //
// g-sheets does let me do this, so, maybe, download their file and see how it's different? //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
String labelPrefix = value.getFunction().name() + " of ";
String label = labelPrefix + QInstanceEnricher.nameToLabel(value.getFieldName());
String valueFormat = null;
@ -435,7 +448,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
//////////////////////////////////////////
zipOutputStream.putNextEntry(new ZipEntry("xl/worksheets/sheet" + this.sheetIndex++ + ".xml"));
activeSheetWriter = new OutputStreamWriter(zipOutputStream);
sheetWriter = new StreamedPoiSheetWriter(activeSheetWriter);
sheetWriter = new StreamedSheetWriter(activeSheetWriter);
if(ReportType.PIVOT.equals(view.getType()))
{
@ -582,7 +595,13 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
String format = excelCellFormats.get(field.getName());
if(format != null)
{
// todo - formats...
////////////////////////////////////////////////////////////////////////////////////////////
// todo - so - for this streamed/zip approach, we need to know all styles before we start //
// any sheets. but, right now Report action only calls us with per-sheet styles when //
// it's starting individual sheets. so, we can't quite support this at this time. //
// "just" need to change GenerateReportAction to look up all cell formats for all sheets //
// before preRun is called... and change all existing streamer classes to handle that too //
////////////////////////////////////////////////////////////////////////////////////////////
// worksheet.style(rowNo, col).format(format).set();
}
}
@ -609,7 +628,6 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
}
else if(value instanceof Instant i)
{
// todo - what would be a better zone to use here?
sheetWriter.createCell(col, DateUtil.getExcelDate(i.atZone(ZoneId.systemDefault()).toLocalDateTime()), dateTimeStyleIndex);
}
else
@ -656,10 +674,11 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
//////////////////////////////////////////////
closeLastSheetIfOpen();
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// note - leave the zipOutputStream open. It is a wrapper around the OutputStream we were given by the caller, //
// so it is their responsibility to close that stream (which implicitly closes the zip, it appears) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
// so, we DO need a close here, on the zipOutputStream, to finish its "zippiness". //
// even though, doing so also closes the outputStream from the caller that this //
// zipOutputStream is wrapping (and the caller will likely call close on too)... //
/////////////////////////////////////////////////////////////////////////////////////
zipOutputStream.close();
}
catch(Exception e)
@ -748,7 +767,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
zipOutputStream.putNextEntry(new ZipEntry(pivotViewToCacheDefinitionReferenceMap.get(pivotTableView.getName())));
/////////////////////////////////////////////////////////
// prepare the xml for each field (e.g., w/ its labelO //
// prepare the xml for each field (e.g., w/ its label) //
/////////////////////////////////////////////////////////
List<String> cachedFieldElements = new ArrayList<>();
for(QFieldMetaData column : this.fieldsPerView.get(dataView.getName()))
@ -766,7 +785,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
activeSheetWriter = new OutputStreamWriter(zipOutputStream);
activeSheetWriter.write(String.format("""
<?xml version="1.0" encoding="UTF-8"?>
<pivotCacheDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" createdVersion="3" minRefreshableVersion="3" refreshedVersion="3" refreshedBy="Apache POI" refreshedDate="1.7113> 95767702E12" refreshOnLoad="true" r:id="rId1">
<pivotCacheDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" createdVersion="3" minRefreshableVersion="3" refreshedVersion="3" refreshedBy="Apache POI" refreshedDate="1.711395767702E12" refreshOnLoad="true" r:id="rId1">
<cacheSource type="worksheet">
<worksheetSource sheet="%s" ref="A1:%s%d"/>
</cacheSource>
@ -775,7 +794,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
</cacheFields>
</pivotCacheDefinition>
""",
StreamedPoiSheetWriter.cleanseValue(labelViewsByName.get(dataView.getName())),
StreamedSheetWriter.cleanseValue(labelViewsByName.get(dataView.getName())),
CellReference.convertNumToColString(dataView.getColumns().size() - 1),
rowsPerView.get(dataView.getName()),
dataView.getColumns().size(),

View File

@ -24,6 +24,8 @@ package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import org.apache.poi.ss.util.CellReference;
@ -31,7 +33,7 @@ import org.apache.poi.ss.util.CellReference;
** Write excel formatted XML to a Writer.
** Originally from https://coderanch.com/t/548897/java/Generate-large-excel-POI
*******************************************************************************/
public class StreamedPoiSheetWriter
public class StreamedSheetWriter
{
private final Writer writer;
private int rowNo;
@ -41,7 +43,7 @@ public class StreamedPoiSheetWriter
/*******************************************************************************
**
*******************************************************************************/
public StreamedPoiSheetWriter(Writer writer)
public StreamedSheetWriter(Writer writer)
{
this.writer = writer;
}
@ -125,14 +127,44 @@ public class StreamedPoiSheetWriter
{
if(value != null)
{
if(value.indexOf('&') > -1 || value.indexOf('<') > -1 || value.indexOf('>') > -1 || value.indexOf('\'') > -1 || value.indexOf('"') > -1)
StringBuilder rs = new StringBuilder();
for(int i = 0; i < value.length(); i++)
{
value = value.replace("&", "&amp;");
value = value.replace("<", "&lt;");
value = value.replace(">", "&gt;");
value = value.replace("'", "&apos;");
value = value.replace("\"", "&quot;");
char c = value.charAt(i);
if(c == '&')
{
rs.append("&amp;");
}
else if(c == '<')
{
rs.append("&lt;");
}
else if(c == '>')
{
rs.append("&gt;");
}
else if(c == '\'')
{
rs.append("&apos;");
}
else if(c == '"')
{
rs.append("&quot;");
}
else if (c < 32 && c != '\t' && c != '\n')
{
rs.append(' ');
}
else
{
rs.append(c);
}
}
Map<String, Integer> m = new HashMap();
m.computeIfAbsent("s", (s) -> 3);
value = rs.toString();
}
return (value);
@ -207,8 +239,4 @@ public class StreamedPoiSheetWriter
writer.write("</c>");
}
// todo? public void createCell(int columnIndex, Calendar value, int styleIndex) throws IOException
// todo? {
// todo? createCell(columnIndex, DateUtil.getExcelDate(value, false), styleIndex);
// todo? }
}

View File

@ -77,7 +77,6 @@ public class ExecuteCodeAction
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public void run(ExecuteCodeInput input, ExecuteCodeOutput output) throws QException, QCodeException
{
QCodeReference codeReference = input.getCodeReference();

View File

@ -470,7 +470,7 @@ public class DeleteAction
QRecord recordWithError = new QRecord();
recordsWithErrors.add(recordWithError);
recordWithError.setValue(primaryKeyField.getName(), primaryKeyValue);
recordWithError.addError(new NotFoundStatusMessage("No record was found to delete for " + primaryKeyField.getLabel() + " = " + primaryKeyValue));
recordWithError.addError(new NotFoundStatusMessage("No record was found to delete for " + Objects.requireNonNullElse(primaryKeyField.getLabel(), primaryKeyField.getName()) + " = " + primaryKeyValue));
}
}
}

View File

@ -65,16 +65,6 @@ public class GetAction
/*******************************************************************************
**
*******************************************************************************/
public QRecord executeForRecord(GetInput getInput) throws QException
{
return (execute(getInput).getRecord());
}
/*******************************************************************************
**
*******************************************************************************/
@ -108,7 +98,7 @@ public class GetAction
}
GetOutput getOutput;
boolean usingDefaultGetInterface = false;
boolean usingDefaultGetInterface = false;
if(getInterface == null)
{
getInterface = new DefaultGetInterface();
@ -140,6 +130,43 @@ public class GetAction
/*******************************************************************************
** shorthand way to call for the most common use-case, when you just want the
** output record to be returned.
*******************************************************************************/
public QRecord executeForRecord(GetInput getInput) throws QException
{
return (execute(getInput).getRecord());
}
/*******************************************************************************
** more shorthand way to call for the most common use-case, when you just want the
** output record to be returned, and you just want to pass in a table name and primary key.
*******************************************************************************/
public static QRecord execute(String tableName, Serializable primaryKey) throws QException
{
GetAction getAction = new GetAction();
GetInput getInput = new GetInput(tableName).withPrimaryKey(primaryKey);
return getAction.executeForRecord(getInput);
}
/*******************************************************************************
** more shorthand way to call for the most common use-case, when you just want the
** output record to be returned, and you just want to pass in a table name and unique key
*******************************************************************************/
public static QRecord execute(String tableName, Map<String, Serializable> uniqueKey) throws QException
{
GetAction getAction = new GetAction();
GetInput getInput = new GetInput(tableName).withUniqueKey(uniqueKey);
return getAction.executeForRecord(getInput);
}
/*******************************************************************************
** Run a GetAction by using the QueryAction instead (e.g., with a filter made
** from the pkey/ukey, and returning the single record if found).

View File

@ -62,6 +62,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.DuplicateKeyBadInputStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@ -414,7 +415,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record);
if(keyValues.isPresent() && (existingKeys.get(uniqueKey).contains(keyValues.get()) || keysInThisList.get(uniqueKey).contains(keyValues.get())))
{
record.addError(new BadInputStatusMessage("Another record already exists with this " + uniqueKey.getDescription(table)));
record.addError(new DuplicateKeyBadInputStatusMessage("Another record already exists with this " + uniqueKey.getDescription(table)));
foundDupe = true;
break;
}

View File

@ -151,6 +151,22 @@ public class QueryAction
/*******************************************************************************
** shorthand way to call for the most common use-case, when you just want the
** records to be returned, and you just want to pass in a table name and filter.
*******************************************************************************/
public static List<QRecord> execute(String tableName, QQueryFilter filter) throws QException
{
QueryAction queryAction = new QueryAction();
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setFilter(filter);
QueryOutput queryOutput = queryAction.execute(queryInput);
return (queryOutput.getRecords());
}
/*******************************************************************************
**
*******************************************************************************/
@ -169,6 +185,7 @@ public class QueryAction
nextLevelQueryInput.setTableName(association.getAssociatedTableName());
nextLevelQueryInput.setIncludeAssociations(true);
nextLevelQueryInput.setAssociationNamesToInclude(buildNextLevelAssociationNamesToInclude(association.getName(), queryInput.getAssociationNamesToInclude()));
nextLevelQueryInput.setTransaction(queryInput.getTransaction());
QQueryFilter filter = new QQueryFilter();
nextLevelQueryInput.setFilter(filter);

View File

@ -47,6 +47,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.commons.lang.BooleanUtils;
/*******************************************************************************
@ -79,9 +80,11 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
try
{
QTableMetaData table = input.getTable();
UniqueKey uniqueKey = input.getKey();
String primaryKeyField = table.getPrimaryKeyField();
QTableMetaData table = input.getTable();
UniqueKey uniqueKey = input.getKey();
String primaryKeyField = table.getPrimaryKeyField();
boolean allowNullKeyValuesToEqual = BooleanUtils.isTrue(input.getAllowNullKeyValuesToEqual());
if(transaction == null)
{
transaction = QBackendTransaction.openFor(new InsertInput(input.getTableName()));
@ -98,10 +101,11 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
// originally it was thought that we'd need to pass the filter in here //
// but, it's been decided not to. the filter only applies to what we can delete //
///////////////////////////////////////////////////////////////////////////////////
Map<List<Serializable>, Serializable> existingKeys = UniqueKeyHelper.getExistingKeys(transaction, table, page, uniqueKey);
Map<List<Serializable>, Serializable> existingKeys = UniqueKeyHelper.getExistingKeys(transaction, table, page, uniqueKey, allowNullKeyValuesToEqual);
for(QRecord record : page)
{
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record);
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record, allowNullKeyValuesToEqual);
if(keyValues.isPresent())
{
if(existingKeys.containsKey(keyValues.get()))
@ -153,6 +157,17 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
output.setDeleteOutput(deleteOutput);
}
if(input.getSetPrimaryKeyInInsertedRecords())
{
for(int i = 0; i < insertList.size(); i++)
{
if(i < insertOutput.getRecords().size())
{
insertList.get(i).setValue(primaryKeyField, insertOutput.getRecords().get(i).getValue(primaryKeyField));
}
}
}
if(weOwnTheTransaction)
{
transaction.commit();

View File

@ -79,7 +79,7 @@ public class StorageAction
if(storageInput.getTableName() == null)
{
throw (new QException("Table name was not specified in query input"));
throw (new QException("Table name was not specified in storage input"));
}
QTableMetaData table = storageInput.getTable();
@ -93,4 +93,28 @@ public class StorageAction
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(backend);
return (qModule);
}
/*******************************************************************************
**
*******************************************************************************/
public void makePublic(StorageInput storageInput) throws QException
{
QBackendModuleInterface qBackendModuleInterface = preAction(storageInput);
QStorageInterface storageInterface = qBackendModuleInterface.getStorageInterface();
storageInterface.makePublic(storageInput);
}
/*******************************************************************************
**
*******************************************************************************/
public String getDownloadURL(StorageInput storageInput) throws QException
{
QBackendModuleInterface qBackendModuleInterface = preAction(storageInput);
QStorageInterface storageInterface = qBackendModuleInterface.getStorageInterface();
return (storageInterface.getDownloadURL(storageInput));
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -68,6 +69,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.NotFoundStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@ -334,6 +336,9 @@ public class UpdateAction
QTableMetaData table = updateInput.getTable();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
/////////////////////////////////////////////////////////////
// todo - evolve to use lock tree (e.g., from multi-locks) //
/////////////////////////////////////////////////////////////
List<RecordSecurityLock> onlyWriteLocks = RecordSecurityLockFilters.filterForOnlyWriteLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks()));
for(List<QRecord> page : CollectionUtils.getPages(updateInput.getRecords(), 1000))
@ -393,7 +398,12 @@ public class UpdateAction
QRecord oldRecord = lookedUpRecords.get(value);
QFieldType fieldType = table.getField(lock.getFieldName()).getType();
Serializable lockValue = ValueUtils.getValueAsFieldType(fieldType, oldRecord.getValue(lock.getFieldName()));
ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, record, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE);
List<QErrorMessage> errors = ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE, Collections.emptyMap());
if(CollectionUtils.nullSafeHasContents(errors))
{
errors.forEach(e -> record.addError(e));
}
}
}
}

View File

@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.utils.PrefixedDefaultThreadFactory;
/*******************************************************************************
@ -50,6 +52,9 @@ public class ActionTimeoutHelper
private boolean didTimeout = false;
private static Integer CORE_THREADS = 10;
private static ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(CORE_THREADS, new PrefixedDefaultThreadFactory(ActionTimeoutHelper.class));
/*******************************************************************************
@ -75,7 +80,7 @@ public class ActionTimeoutHelper
return;
}
future = Executors.newSingleThreadScheduledExecutor().schedule(() ->
future = scheduledExecutorService.schedule(() ->
{
didTimeout = true;
runnable.run();

View File

@ -57,6 +57,7 @@ import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
import com.kingsrook.qqq.backend.core.model.tables.QQQTablesMetaDataProvider;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.PrefixedDefaultThreadFactory;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -176,7 +177,7 @@ public class QueryStatManager
active = true;
queryStats = new ArrayList<>();
executorService = Executors.newSingleThreadScheduledExecutor();
executorService = Executors.newSingleThreadScheduledExecutor(new PrefixedDefaultThreadFactory(this));
executorService.scheduleAtFixedRate(new QueryStatManagerInsertJob(), jobInitialDelay, jobPeriodSeconds, TimeUnit.SECONDS);
}
@ -214,71 +215,78 @@ public class QueryStatManager
*******************************************************************************/
public void add(QueryStat queryStat)
{
if(queryStat == null)
try
{
return;
}
if(active)
{
////////////////////////////////////////////////////////////////////////////////////////
// set fields that we need to capture now (rather than when the thread to store runs) //
////////////////////////////////////////////////////////////////////////////////////////
if(queryStat.getFirstResultTimestamp() == null)
if(queryStat == null)
{
queryStat.setFirstResultTimestamp(Instant.now());
}
if(queryStat.getStartTimestamp() != null && queryStat.getFirstResultTimestamp() != null && queryStat.getFirstResultMillis() == null)
{
long millis = queryStat.getFirstResultTimestamp().toEpochMilli() - queryStat.getStartTimestamp().toEpochMilli();
queryStat.setFirstResultMillis((int) millis);
}
if(queryStat.getFirstResultMillis() != null && queryStat.getFirstResultMillis() < minMillisToStore)
{
//////////////////////////////////////////////////////////////
// discard this record if it's under the min millis setting //
//////////////////////////////////////////////////////////////
return;
}
if(queryStat.getSessionId() == null && QContext.getQSession() != null)
if(active)
{
queryStat.setSessionId(QContext.getQSession().getUuid());
}
if(queryStat.getAction() == null)
{
if(!QContext.getActionStack().isEmpty())
////////////////////////////////////////////////////////////////////////////////////////
// set fields that we need to capture now (rather than when the thread to store runs) //
////////////////////////////////////////////////////////////////////////////////////////
if(queryStat.getFirstResultTimestamp() == null)
{
queryStat.setAction(QContext.getActionStack().peek().getActionIdentity());
queryStat.setFirstResultTimestamp(Instant.now());
}
else
if(queryStat.getStartTimestamp() != null && queryStat.getFirstResultTimestamp() != null && queryStat.getFirstResultMillis() == null)
{
boolean expected = false;
Exception e = new Exception("Unexpected empty action stack");
for(StackTraceElement stackTraceElement : e.getStackTrace())
long millis = queryStat.getFirstResultTimestamp().toEpochMilli() - queryStat.getStartTimestamp().toEpochMilli();
queryStat.setFirstResultMillis((int) millis);
}
if(queryStat.getFirstResultMillis() != null && queryStat.getFirstResultMillis() < minMillisToStore)
{
//////////////////////////////////////////////////////////////
// discard this record if it's under the min millis setting //
//////////////////////////////////////////////////////////////
return;
}
if(queryStat.getSessionId() == null && QContext.getQSession() != null)
{
queryStat.setSessionId(QContext.getQSession().getUuid());
}
if(queryStat.getAction() == null)
{
if(QContext.getActionStack() != null && !QContext.getActionStack().isEmpty())
{
String className = stackTraceElement.getClassName();
if(className.contains(QueryStatManagerInsertJob.class.getName()))
queryStat.setAction(QContext.getActionStack().peek().getActionIdentity());
}
else
{
boolean expected = false;
Exception e = new Exception("Unexpected empty action stack");
for(StackTraceElement stackTraceElement : e.getStackTrace())
{
expected = true;
break;
String className = stackTraceElement.getClassName();
if(className.contains(QueryStatManagerInsertJob.class.getName()))
{
expected = true;
break;
}
}
if(!expected)
{
LOG.debug(e);
}
}
}
if(!expected)
{
LOG.debug(e);
}
synchronized(this)
{
queryStats.add(queryStat);
}
}
synchronized(this)
{
queryStats.add(queryStat);
}
}
catch(Exception e)
{
LOG.debug("Error adding query stat", e);
}
}

View File

@ -54,7 +54,7 @@ public class UniqueKeyHelper
/*******************************************************************************
**
*******************************************************************************/
public static Map<List<Serializable>, Serializable> getExistingKeys(QBackendTransaction transaction, QTableMetaData table, List<QRecord> recordList, UniqueKey uniqueKey) throws QException
public static Map<List<Serializable>, Serializable> getExistingKeys(QBackendTransaction transaction, QTableMetaData table, List<QRecord> recordList, UniqueKey uniqueKey, boolean allowNullKeyValuesToEqual) throws QException
{
List<String> ukFieldNames = uniqueKey.getFieldNames();
Map<List<Serializable>, Serializable> existingRecords = new HashMap<>();
@ -112,7 +112,7 @@ public class UniqueKeyHelper
QueryOutput queryOutput = new QueryAction().execute(queryInput);
for(QRecord record : queryOutput.getRecords())
{
Optional<List<Serializable>> keyValues = getKeyValues(table, uniqueKey, record);
Optional<List<Serializable>> keyValues = getKeyValues(table, uniqueKey, record, allowNullKeyValuesToEqual);
if(keyValues.isPresent())
{
existingRecords.put(keyValues.get(), record.getValue(table.getPrimaryKeyField()));
@ -128,7 +128,17 @@ public class UniqueKeyHelper
/*******************************************************************************
**
*******************************************************************************/
public static Optional<List<Serializable>> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record)
public static Map<List<Serializable>, Serializable> getExistingKeys(QBackendTransaction transaction, QTableMetaData table, List<QRecord> recordList, UniqueKey uniqueKey) throws QException
{
return (getExistingKeys(transaction, table, recordList, uniqueKey, false));
}
/*******************************************************************************
**
*******************************************************************************/
public static Optional<List<Serializable>> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record, boolean allowNullKeyValuesToEqual)
{
try
{
@ -138,7 +148,19 @@ public class UniqueKeyHelper
QFieldMetaData field = table.getField(fieldName);
Serializable value = record.getValue(fieldName);
Serializable typedValue = ValueUtils.getValueAsFieldType(field.getType(), value);
keyValues.add(typedValue == null ? new NullUniqueKeyValue() : typedValue);
///////////////////////////////////////////////////////////////////////////////////
// if null value, look at flag to determine if a null should be used (which will //
// allow keys to match), or a NullUniqueKeyValue, (which will never match) //
///////////////////////////////////////////////////////////////////////////////////
if(typedValue == null)
{
keyValues.add(allowNullKeyValuesToEqual ? null : new NullUniqueKeyValue());
}
else
{
keyValues.add(typedValue);
}
}
return (Optional.of(keyValues));
}
@ -150,6 +172,16 @@ public class UniqueKeyHelper
/*******************************************************************************
**
*******************************************************************************/
public static Optional<List<Serializable>> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record)
{
return (getKeyValues(table, uniqueKey, record, false));
}
/*******************************************************************************
** To make a list of unique key values here behave like they do in an RDBMS
** (which is what we're trying to mimic - which is - 2 null values in a field

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -42,13 +43,16 @@ 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.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.NullValueBehaviorUtil;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -81,160 +85,273 @@ public class ValidateRecordSecurityLockHelper
*******************************************************************************/
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action) throws QException
{
List<RecordSecurityLock> locksToCheck = getRecordSecurityLocks(table, action);
if(CollectionUtils.nullSafeIsEmpty(locksToCheck))
MultiRecordSecurityLock locksToCheck = getRecordSecurityLocks(table, action);
if(locksToCheck == null || CollectionUtils.nullSafeIsEmpty(locksToCheck.getLocks()))
{
return;
}
//////////////////////////////////////////////////////////////////////////////////////////
// we will be relying on primary keys being set in records - but (at least for inserts) //
// we might not have pkeys - so make them up (and clear them out at the end) //
//////////////////////////////////////////////////////////////////////////////////////////
Map<Serializable, QRecord> madeUpPrimaryKeys = makeUpPrimaryKeysIfNeeded(records, table);
////////////////////////////////
// actually check lock values //
////////////////////////////////
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>(), madeUpPrimaryKeys);
/////////////////////////////////
// propagate errors to records //
/////////////////////////////////
for(RecordWithErrors recordWithErrors : errorRecords.values())
{
recordWithErrors.propagateErrorsToRecord(locksToCheck);
}
/////////////////////////////////
// remove made-up primary keys //
/////////////////////////////////
String primaryKeyField = table.getPrimaryKeyField();
for(QRecord record : madeUpPrimaryKeys.values())
{
record.setValue(primaryKeyField, null);
}
}
/*******************************************************************************
** For a list of `records` from a `table`, and a given `action`, evaluate a
** `recordSecurityLock` (which may be a multi-lock) - populating the input map
** of `errorRecords` - key'ed by primary key value (real or made up), with
** error messages existing in a tree, with positions matching the multi-lock
** tree that we're navigating, as tracked by `treePosition`.
**
** Recursively processes multi-locks (and the top-level call is always with a
** multi-lock - as the table's recordLocks list converted to an AND-multi-lock).
**
** Of note - for the case of READ_WRITE locks, we're only evaluating the values
** on the record, to see if they're allowed for us to store (because if we didn't
** have the key, we wouldn't have been able to read the value (which is verified
** outside of here, in UpdateAction/DeleteAction).
**
** BUT - WRITE locks - in their case, we read the record no matter what, and in
** here we need to verify we have a key that allows us to WRITE the record.
*******************************************************************************/
private static void evaluateRecordLocks(QTableMetaData table, List<QRecord> records, Action action, RecordSecurityLock recordSecurityLock, Map<Serializable, RecordWithErrors> errorRecords, List<Integer> treePosition, Map<Serializable, QRecord> madeUpPrimaryKeys) throws QException
{
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
{
/////////////////////////////////////////////
// for multi-locks, make recursive descent //
/////////////////////////////////////////////
int i = 0;
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
{
treePosition.add(i);
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys);
treePosition.remove(treePosition.size() - 1);
i++;
}
return;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if this lock has an all-access key, and the user has that key, then there can't be any errors here, so return early //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
{
return;
}
////////////////////////////////
// actually check lock values //
// proceed w/ non-multi locks //
////////////////////////////////
for(RecordSecurityLock recordSecurityLock : locksToCheck)
String primaryKeyField = table.getPrimaryKeyField();
if(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()))
{
if(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()))
//////////////////////////////////////////////////////////////////////////////////
// handle the value being in the table we're inserting/updating (e.g., no join) //
//////////////////////////////////////////////////////////////////////////////////
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
for(QRecord record : records)
{
//////////////////////////////////////////////////////////////////////////////////
// handle the value being in the table we're inserting/updating (e.g., no join) //
//////////////////////////////////////////////////////////////////////////////////
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
for(QRecord record : records)
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()) && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
{
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()) && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// if this is a read-write lock, then if we have the record, it means we were able to read the record. //
// So if we're not updating the security field, then no error can come from it! //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
continue;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// if this is a read-write lock, then if we have the record, it means we were able to read the record. //
// So if we're not updating the security field, then no error can come from it! //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
continue;
}
Serializable recordSecurityValue = record.getValue(field.getName());
validateRecordSecurityValue(table, record, recordSecurityLock, recordSecurityValue, field.getType(), action);
Serializable recordSecurityValue = record.getValue(field.getName());
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys);
if(CollectionUtils.nullSafeHasContents(recordErrors))
{
errorRecords.computeIfAbsent(record.getValue(primaryKeyField), (k) -> new RecordWithErrors(record)).addAll(recordErrors, treePosition);
}
}
else
}
else
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else look for the joined record - if it isn't found, assume a fail - else validate security value if found //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QJoinMetaData leftMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(0));
QJoinMetaData rightMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(recordSecurityLock.getJoinNameChain().size() - 1));
////////////////////////////////
// todo probably, but more... //
////////////////////////////////
// if(leftMostJoin.getLeftTable().equals(table.getName()))
// {
// leftMostJoin = leftMostJoin.flip();
// }
QTableMetaData rightMostJoinTable = QContext.getQInstance().getTable(rightMostJoin.getRightTable());
QTableMetaData leftMostJoinTable = QContext.getQInstance().getTable(leftMostJoin.getLeftTable());
for(List<QRecord> inputRecordPage : CollectionUtils.getPages(records, 500))
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else look for the joined record - if it isn't found, assume a fail - else validate security value if found //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QJoinMetaData leftMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(0));
QJoinMetaData rightMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(recordSecurityLock.getJoinNameChain().size() - 1));
QTableMetaData rightMostJoinTable = QContext.getQInstance().getTable(rightMostJoin.getRightTable());
QTableMetaData leftMostJoinTable = QContext.getQInstance().getTable(leftMostJoin.getLeftTable());
////////////////////////////////////////////////////////////////////////////////////////////////
// set up a query for joined records //
// query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) //
////////////////////////////////////////////////////////////////////////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTableName(leftMostJoin.getLeftTable());
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
queryInput.setFilter(filter);
for(List<QRecord> inputRecordPage : CollectionUtils.getPages(records, 500))
for(String joinName : recordSecurityLock.getJoinNameChain())
{
////////////////////////////////////////////////////////////////////////////////////////////////
// set up a query for joined records //
// query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) //
////////////////////////////////////////////////////////////////////////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTableName(leftMostJoin.getLeftTable());
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
queryInput.setFilter(filter);
for(String joinName : recordSecurityLock.getJoinNameChain())
///////////////////////////////////////
// we don't need the right-most join //
///////////////////////////////////////
if(!joinName.equals(rightMostJoin.getName()))
{
///////////////////////////////////////
// we don't need the right-most join //
///////////////////////////////////////
if(!joinName.equals(rightMostJoin.getName()))
queryInput.withQueryJoin(new QueryJoin().withJoinMetaData(QContext.getQInstance().getJoin(joinName)).withSelect(true));
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// foreach input record (in this page), put it in a listing hash, with key = list of join-values //
// e.g., (17,47)=(QRecord1), (18,48)=(QRecord2,QRecord3) //
// also build up the query's sub-filters here (only adding them if they're unique). //
// e.g., 2 order-lines referencing the same orderId don't need to be added to the query twice //
///////////////////////////////////////////////////////////////////////////////////////////////////
ListingHash<List<Serializable>, QRecord> inputRecordMapByJoinFields = new ListingHash<>();
for(QRecord inputRecord : inputRecordPage)
{
List<Serializable> inputRecordJoinValues = new ArrayList<>();
QQueryFilter subFilter = new QQueryFilter();
boolean updatingAnyLockJoinFields = false;
for(JoinOn joinOn : rightMostJoin.getJoinOns())
{
QFieldType type = rightMostJoinTable.getField(joinOn.getRightField()).getType();
Serializable inputRecordValue = ValueUtils.getValueAsFieldType(type, inputRecord.getValue(joinOn.getRightField()));
inputRecordJoinValues.add(inputRecordValue);
// if we have a value in this field (and it's not the primary key), then it means we're updating part of the lock
if(inputRecordValue != null && !joinOn.getRightField().equals(table.getPrimaryKeyField()))
{
queryInput.withQueryJoin(new QueryJoin().withJoinMetaData(QContext.getQInstance().getJoin(joinName)).withSelect(true));
updatingAnyLockJoinFields = true;
}
subFilter.addCriteria(inputRecordValue == null
? new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.IS_BLANK)
: new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue));
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// foreach input record (in this page), put it in a listing hash, with key = list of join-values //
// e.g., (17,47)=(QRecord1), (18,48)=(QRecord2,QRecord3) //
// also build up the query's sub-filters here (only adding them if they're unique). //
// e.g., 2 order-lines referencing the same orderId don't need to be added to the query twice //
///////////////////////////////////////////////////////////////////////////////////////////////////
ListingHash<List<Serializable>, QRecord> inputRecordMapByJoinFields = new ListingHash<>();
for(QRecord inputRecord : inputRecordPage)
//////////////////////////////////
// todo maybe, some version of? //
//////////////////////////////////
// if(action.equals(Action.UPDATE) && !updatingAnyLockJoinFields && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
// {
// /////////////////////////////////////////////////////////////////////////////////////////////////////////
// // if this is a read-write lock, then if we have the record, it means we were able to read the record. //
// // So if we're not updating the security field, then no error can come from it! //
// /////////////////////////////////////////////////////////////////////////////////////////////////////////
// continue;
// }
if(!inputRecordMapByJoinFields.containsKey(inputRecordJoinValues))
{
List<Serializable> inputRecordJoinValues = new ArrayList<>();
QQueryFilter subFilter = new QQueryFilter();
for(JoinOn joinOn : rightMostJoin.getJoinOns())
{
QFieldType type = rightMostJoinTable.getField(joinOn.getRightField()).getType();
Serializable inputRecordValue = ValueUtils.getValueAsFieldType(type, inputRecord.getValue(joinOn.getRightField()));
inputRecordJoinValues.add(inputRecordValue);
subFilter.addCriteria(inputRecordValue == null
? new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.IS_BLANK)
: new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue));
}
if(!inputRecordMapByJoinFields.containsKey(inputRecordJoinValues))
{
////////////////////////////////////////////////////////////////////////////////
// only add this sub-filter if it's for a list of keys we haven't seen before //
////////////////////////////////////////////////////////////////////////////////
filter.addSubFilter(subFilter);
}
inputRecordMapByJoinFields.add(inputRecordJoinValues, inputRecord);
////////////////////////////////////////////////////////////////////////////////
// only add this sub-filter if it's for a list of keys we haven't seen before //
////////////////////////////////////////////////////////////////////////////////
filter.addSubFilter(subFilter);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// execute the query for joined records - then put them in a map with keys corresponding to the join values //
// e.g., (17,47)=(JoinRecord), (18,48)=(JoinRecord) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
QueryOutput queryOutput = new QueryAction().execute(queryInput);
Map<List<Serializable>, QRecord> joinRecordMapByJoinFields = new HashMap<>();
for(QRecord joinRecord : queryOutput.getRecords())
{
List<Serializable> joinRecordValues = new ArrayList<>();
for(JoinOn joinOn : rightMostJoin.getJoinOns())
{
Serializable joinValue = joinRecord.getValue(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField());
if(joinValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> !n.contains(".")))
{
joinValue = joinRecord.getValue(joinOn.getLeftField());
}
joinRecordValues.add(joinValue);
}
inputRecordMapByJoinFields.add(inputRecordJoinValues, inputRecord);
}
joinRecordMapByJoinFields.put(joinRecordValues, joinRecord);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// execute the query for joined records - then put them in a map with keys corresponding to the join values //
// e.g., (17,47)=(JoinRecord), (18,48)=(JoinRecord) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
QueryOutput queryOutput = new QueryAction().execute(queryInput);
Map<List<Serializable>, QRecord> joinRecordMapByJoinFields = new HashMap<>();
for(QRecord joinRecord : queryOutput.getRecords())
{
List<Serializable> joinRecordValues = new ArrayList<>();
for(JoinOn joinOn : rightMostJoin.getJoinOns())
{
Serializable joinValue = joinRecord.getValue(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField());
if(joinValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> !n.contains(".")))
{
joinValue = joinRecord.getValue(joinOn.getLeftField());
}
joinRecordValues.add(joinValue);
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// now for each input record, look for its joinRecord - if it isn't found, then this insert //
// isn't allowed. if it is found, then validate its value matches this session's security keys //
//////////////////////////////////////////////////////////////////////////////////////////////////
for(Map.Entry<List<Serializable>, List<QRecord>> entry : inputRecordMapByJoinFields.entrySet())
joinRecordMapByJoinFields.put(joinRecordValues, joinRecord);
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// now for each input record, look for its joinRecord - if it isn't found, then this insert //
// isn't allowed. if it is found, then validate its value matches this session's security keys //
//////////////////////////////////////////////////////////////////////////////////////////////////
for(Map.Entry<List<Serializable>, List<QRecord>> entry : inputRecordMapByJoinFields.entrySet())
{
List<Serializable> inputRecordJoinValues = entry.getKey();
List<QRecord> inputRecords = entry.getValue();
if(joinRecordMapByJoinFields.containsKey(inputRecordJoinValues))
{
List<Serializable> inputRecordJoinValues = entry.getKey();
List<QRecord> inputRecords = entry.getValue();
if(joinRecordMapByJoinFields.containsKey(inputRecordJoinValues))
QRecord joinRecord = joinRecordMapByJoinFields.get(inputRecordJoinValues);
String fieldName = recordSecurityLock.getFieldName().replaceFirst(".*\\.", "");
QFieldMetaData field = leftMostJoinTable.getField(fieldName);
Serializable recordSecurityValue = joinRecord.getValue(fieldName);
if(recordSecurityValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> n.contains(".")))
{
QRecord joinRecord = joinRecordMapByJoinFields.get(inputRecordJoinValues);
recordSecurityValue = joinRecord.getValue(recordSecurityLock.getFieldName());
}
String fieldName = recordSecurityLock.getFieldName().replaceFirst(".*\\.", "");
QFieldMetaData field = leftMostJoinTable.getField(fieldName);
Serializable recordSecurityValue = joinRecord.getValue(fieldName);
if(recordSecurityValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> n.contains(".")))
for(QRecord inputRecord : inputRecords)
{
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys);
if(CollectionUtils.nullSafeHasContents(recordErrors))
{
recordSecurityValue = joinRecord.getValue(recordSecurityLock.getFieldName());
}
for(QRecord inputRecord : inputRecords)
{
validateRecordSecurityValue(table, inputRecord, recordSecurityLock, recordSecurityValue, field.getType(), action);
errorRecords.computeIfAbsent(inputRecord.getValue(primaryKeyField), (k) -> new RecordWithErrors(inputRecord)).addAll(recordErrors, treePosition);
}
}
else
}
else
{
for(QRecord inputRecord : inputRecords)
{
for(QRecord inputRecord : inputRecords)
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
inputRecord.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record - the referenced " + leftMostJoinTable.getLabel() + " was not found."));
}
PermissionDeniedMessage error = new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record - the referenced " + leftMostJoinTable.getLabel() + " was not found.");
errorRecords.computeIfAbsent(inputRecord.getValue(primaryKeyField), (k) -> new RecordWithErrors(inputRecord)).add(error, treePosition);
}
}
}
@ -246,45 +363,80 @@ public class ValidateRecordSecurityLockHelper
/*******************************************************************************
**
** for tracking errors, we use primary keys. add "made up" ones to records
** if needed (e.g., insert use-case).
*******************************************************************************/
private static List<RecordSecurityLock> getRecordSecurityLocks(QTableMetaData table, Action action)
private static Map<Serializable, QRecord> makeUpPrimaryKeysIfNeeded(List<QRecord> records, QTableMetaData table)
{
List<RecordSecurityLock> recordSecurityLocks = CollectionUtils.nonNullList(table.getRecordSecurityLocks());
List<RecordSecurityLock> locksToCheck = new ArrayList<>();
recordSecurityLocks = switch(action)
String primaryKeyField = table.getPrimaryKeyField();
Map<Serializable, QRecord> madeUpPrimaryKeys = new HashMap<>();
Integer madeUpPrimaryKey = Integer.MIN_VALUE / 2;
for(QRecord record : records)
{
case INSERT, UPDATE, DELETE -> RecordSecurityLockFilters.filterForWriteLocks(recordSecurityLocks);
case SELECT -> RecordSecurityLockFilters.filterForReadLocks(recordSecurityLocks);
if(record.getValue(primaryKeyField) == null)
{
madeUpPrimaryKeys.put(madeUpPrimaryKey, record);
record.setValue(primaryKeyField, madeUpPrimaryKey);
madeUpPrimaryKey++;
}
}
return madeUpPrimaryKeys;
}
/*******************************************************************************
** For a given table & action type, convert the table's record locks to a
** MultiRecordSecurityLock, with only the appropriate lock-scopes being included
** (e.g., read-locks for selects, write-locks for insert/update/delete).
*******************************************************************************/
static MultiRecordSecurityLock getRecordSecurityLocks(QTableMetaData table, Action action)
{
List<RecordSecurityLock> allLocksOnTable = CollectionUtils.nonNullList(table.getRecordSecurityLocks());
MultiRecordSecurityLock locksOfType = switch(action)
{
case INSERT, UPDATE, DELETE -> RecordSecurityLockFilters.filterForWriteLockTree(allLocksOnTable);
case SELECT -> RecordSecurityLockFilters.filterForReadLockTree(allLocksOnTable);
default -> throw (new IllegalArgumentException("Unsupported action: " + action));
};
if(action.equals(Action.UPDATE))
{
////////////////////////////////////////////////////////////////////////////
// todo at some point this seemed right, but now it doesn't - needs work. //
////////////////////////////////////////////////////////////////////////////
// ////////////////////////////////////////////////////////
// // when doing an update, convert all OR's to AND's... //
// ////////////////////////////////////////////////////////
// updateOperators(locksOfType, MultiRecordSecurityLock.BooleanOperator.AND);
}
////////////////////////////////////////
// if there are no locks, just return //
////////////////////////////////////////
if(CollectionUtils.nullSafeIsEmpty(recordSecurityLocks))
if(locksOfType == null || CollectionUtils.nullSafeIsEmpty(locksOfType.getLocks()))
{
return (null);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// decide if any locks need checked - where one may not need checked if it has an all-access key, and the user has all-access //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(RecordSecurityLock recordSecurityLock : recordSecurityLocks)
return (locksOfType);
}
/*******************************************************************************
** for a full multi-lock tree, set all of the boolean operators to the specified one.
*******************************************************************************/
private static void updateOperators(MultiRecordSecurityLock multiLock, MultiRecordSecurityLock.BooleanOperator operator)
{
multiLock.setOperator(operator);
for(RecordSecurityLock childLock : multiLock.getLocks())
{
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
if(childLock instanceof MultiRecordSecurityLock childMultiLock)
{
LOG.trace("Session has " + securityKeyType.getAllAccessKeyName() + " - not checking this lock.");
}
else
{
locksToCheck.add(recordSecurityLock);
updateOperators(childMultiLock, operator);
}
}
return (locksToCheck);
}
@ -292,9 +444,9 @@ public class ValidateRecordSecurityLockHelper
/*******************************************************************************
**
*******************************************************************************/
public static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
public static List<QErrorMessage> validateRecordSecurityValue(QTableMetaData table, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action, Map<Serializable, QRecord> madeUpPrimaryKeys)
{
if(recordSecurityValue == null)
if(recordSecurityValue == null || (madeUpPrimaryKeys != null && madeUpPrimaryKeys.containsKey(recordSecurityValue)))
{
/////////////////////////////////////////////////////////////////
// handle null values - error if the NullValueBehavior is DENY //
@ -302,7 +454,7 @@ public class ValidateRecordSecurityLockHelper
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
String lockLabel = CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()) ? recordSecurityLock.getSecurityKeyType() : table.getField(recordSecurityLock.getFieldName()).getLabel();
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel));
return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel)));
}
}
else
@ -314,15 +466,305 @@ public class ValidateRecordSecurityLockHelper
///////////////////////////////////////////////////////////////////////////////////////////////
// avoid telling the user a value from a foreign record that they didn't pass in themselves. //
///////////////////////////////////////////////////////////////////////////////////////////////
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record."));
return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record.")));
}
else
{
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record with a value of " + recordSecurityValue + " in the field: " + field.getLabel()));
return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record with a value of " + recordSecurityValue + " in the field: " + field.getLabel())));
}
}
}
return (Collections.emptyList());
}
/*******************************************************************************
** Class to track errors that we're associating with a record.
**
** More complex than it first seems to be needed, because as we're evaluating
** locks, we might find some, but based on the boolean condition associated with
** them, they might not actually be record-level errors.
**
** e.g., two locks with an OR relationship - as long as one passes, the record
** should have no errors. And so-on through the tree of locks/multi-locks.
**
** Stores the errors in a tree of ErrorTreeNode objects.
**
** References into that tree are achieved via a List of Integer called "tree positions"
** where each entry in the list denotes the index of the tree node at that level.
**
** e.g., given this tree:
** <pre>
** A B
** / \ /|\
** C D E F G
** |
** H
** </pre>
**
** The positions of each node would be:
** <pre>
** A: [0]
** B: [1]
** C: [0,0]
** D: [0,1]
** E: [1,0]
** F: [1,1]
** G: [1,2]
** H: [0,1,0]
** </pre>
*******************************************************************************/
static class RecordWithErrors
{
private QRecord record;
private ErrorTreeNode errorTree;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public RecordWithErrors(QRecord record)
{
this.record = record;
}
/*******************************************************************************
** add a list of errors, for a given list of tree positions
*******************************************************************************/
public void addAll(List<QErrorMessage> recordErrors, List<Integer> treePositions)
{
if(errorTree == null)
{
errorTree = new ErrorTreeNode();
}
ErrorTreeNode node = errorTree;
for(Integer treePosition : treePositions)
{
if(node.children == null)
{
node.children = new ArrayList<>(treePosition);
}
while(treePosition >= node.children.size())
{
node.children.add(null);
}
if(node.children.get(treePosition) == null)
{
node.children.set(treePosition, new ErrorTreeNode());
}
node = node.children.get(treePosition);
}
if(node.errors == null)
{
node.errors = new ArrayList<>();
}
node.errors.addAll(recordErrors);
}
/*******************************************************************************
** add a single error to a given tree-position
*******************************************************************************/
public void add(QErrorMessage error, List<Integer> treePositions)
{
addAll(List.of(error), treePositions);
}
/*******************************************************************************
** after the tree of errors has been built - walk a lock-tree (locksToCheck)
** and resolve boolean operations, to get a final list of errors (possibly empty)
** to put on the record.
*******************************************************************************/
public void propagateErrorsToRecord(MultiRecordSecurityLock locksToCheck)
{
List<QErrorMessage> errors = recursivePropagation(locksToCheck, new ArrayList<>());
if(CollectionUtils.nullSafeHasContents(errors))
{
errors.forEach(e -> record.addError(e));
}
}
/*******************************************************************************
** recursive implementation of the propagation method - e.g., walk tree applying
** boolean logic.
*******************************************************************************/
private List<QErrorMessage> recursivePropagation(MultiRecordSecurityLock locksToCheck, List<Integer> treePositions)
{
//////////////////////////////////////////////////////////////////
// build a list of errors at this level (and deeper levels too) //
//////////////////////////////////////////////////////////////////
List<QErrorMessage> errorsFromThisLevel = new ArrayList<>();
int i = 0;
for(RecordSecurityLock lock : locksToCheck.getLocks())
{
List<QErrorMessage> errorsFromThisLock;
treePositions.add(i);
if(lock instanceof MultiRecordSecurityLock childMultiLock)
{
errorsFromThisLock = recursivePropagation(childMultiLock, treePositions);
}
else
{
errorsFromThisLock = getErrorsFromTree(treePositions);
}
errorsFromThisLevel.addAll(errorsFromThisLock);
treePositions.remove(treePositions.size() - 1);
i++;
}
if(MultiRecordSecurityLock.BooleanOperator.AND.equals(locksToCheck.getOperator()))
{
//////////////////////////////////////////////////////////////
// for an AND - if there were ANY errors, then return them. //
//////////////////////////////////////////////////////////////
if(!errorsFromThisLevel.isEmpty())
{
return (errorsFromThisLevel);
}
}
else // OR
{
//////////////////////////////////////////////////////////
// for an OR - only return if ALL conditions had errors //
//////////////////////////////////////////////////////////
if(errorsFromThisLevel.size() == locksToCheck.getLocks().size())
{
return (errorsFromThisLevel); // todo something smarter?
}
}
///////////////////////////////////
// else - no errors - empty list //
///////////////////////////////////
return Collections.emptyList();
}
/*******************************************************************************
**
*******************************************************************************/
private List<QErrorMessage> getErrorsFromTree(List<Integer> treePositions)
{
ErrorTreeNode node = errorTree;
for(Integer treePosition : treePositions)
{
if(node.children == null)
{
return Collections.emptyList();
}
if(treePosition >= node.children.size())
{
return Collections.emptyList();
}
if(node.children.get(treePosition) == null)
{
return Collections.emptyList();
}
node = node.children.get(treePosition);
}
if(node.errors == null)
{
return Collections.emptyList();
}
return node.errors;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
try
{
return JsonUtils.toPrettyJson(this);
}
catch(Exception e)
{
return "error in toString";
}
}
}
/*******************************************************************************
** tree node used by RecordWithErrors
*******************************************************************************/
static class ErrorTreeNode
{
private List<QErrorMessage> errors;
private ArrayList<ErrorTreeNode> children;
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
try
{
return JsonUtils.toPrettyJson(this);
}
catch(Exception e)
{
return "error in toString";
}
}
/*******************************************************************************
** Getter for errors - only here for Jackson/toString
**
*******************************************************************************/
public List<QErrorMessage> getErrors()
{
return errors;
}
/*******************************************************************************
** Getter for children - only here for Jackson/toString
**
*******************************************************************************/
public ArrayList<ErrorTreeNode> getChildren()
{
return children;
}
}
}

View File

@ -32,13 +32,12 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -78,44 +77,6 @@ public class QPossibleValueTranslator
private int maxSizePerPvsCache = 50_000;
private Map<String, QBackendTransaction> transactionsPerTable = new HashMap<>();
// todo not commit - remove instance & session - use Context
boolean useTransactionsAsConnectionPool = false;
/*******************************************************************************
**
*******************************************************************************/
private QBackendTransaction getTransaction(String tableName)
{
/////////////////////////////////////////////////////////////
// mmm, this does cut down on connections used - //
// especially seems helpful in big exports. //
// but, let's just start using connection pools instead... //
/////////////////////////////////////////////////////////////
if(useTransactionsAsConnectionPool)
{
try
{
if(!transactionsPerTable.containsKey(tableName))
{
transactionsPerTable.put(tableName, QBackendTransaction.openFor(new InsertInput(tableName)));
}
return (transactionsPerTable.get(tableName));
}
catch(Exception e)
{
LOG.warn("Error opening transaction for table", logPair("tableName", tableName));
}
}
return null;
}
/*******************************************************************************
@ -421,7 +382,6 @@ public class QPossibleValueTranslator
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:Indentation")
private String doFormatPossibleValue(String formatString, List<String> valueFields, Object id, String label)
{
List<Object> values = new ArrayList<>();
@ -560,21 +520,48 @@ public class QPossibleValueTranslator
*******************************************************************************/
private void primePvsCache(String tableName, List<QPossibleValueSource> possibleValueSources, Collection<Serializable> values)
{
String idField = null;
for(QPossibleValueSource possibleValueSource : possibleValueSources)
{
possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>());
String thisPvsIdField;
if(StringUtils.hasContent(possibleValueSource.getOverrideIdField()))
{
thisPvsIdField = possibleValueSource.getOverrideIdField();
}
else
{
thisPvsIdField = QContext.getQInstance().getTable(tableName).getPrimaryKeyField();
}
if(idField == null)
{
idField = thisPvsIdField;
}
else
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// does this ever happen? maybe not... because, like, the list of values probably wouldn't make sense for //
// more than one field in the table... //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(!idField.equals(thisPvsIdField))
{
for(QPossibleValueSource valueSource : possibleValueSources)
{
primePvsCache(tableName, List.of(valueSource), values);
}
}
}
}
try
{
String primaryKeyField = QContext.getQInstance().getTable(tableName).getPrimaryKeyField();
for(List<Serializable> page : CollectionUtils.getPages(values, 1000))
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, page)));
queryInput.setTransaction(getTransaction(tableName));
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(idField, QCriteriaOperator.IN, page)));
queryInput.hasQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// when querying for possible values, we do want to generate their display values, which makes record labels, which are usually used as PVS labels //
@ -618,7 +605,7 @@ public class QPossibleValueTranslator
///////////////////////////////////////////////////////////////////////////////////
for(QRecord record : queryOutput.getRecords())
{
Serializable pkeyValue = record.getValue(primaryKeyField);
Serializable pkeyValue = record.getValue(idField);
for(QPossibleValueSource possibleValueSource : possibleValueSources)
{
QPossibleValue<?> possibleValue = new QPossibleValue<>(pkeyValue, record.getRecordLabel());

View File

@ -28,7 +28,6 @@ import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -364,7 +363,9 @@ public class QValueFormatter
}
}
setDisplayValuesInRecord(fieldMap, record);
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.FORMATTING, QContext.getQInstance(), table, records, null);
setDisplayValuesInRecord(table, fieldMap, record, true);
record.setRecordLabel(formatRecordLabel(table, record));
}
}
@ -374,61 +375,49 @@ public class QValueFormatter
/*******************************************************************************
** For a list of records, set their recordLabels and display values
*******************************************************************************/
public static void setDisplayValuesInRecords(Collection<QFieldMetaData> fields, List<QRecord> records)
public static void setDisplayValuesInRecords(QTableMetaData table, Map<String, QFieldMetaData> fields, List<QRecord> records)
{
if(records == null)
{
return;
}
if(table != null)
{
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.FORMATTING, QContext.getQInstance(), table, records, null);
}
for(QRecord record : records)
{
setDisplayValuesInRecord(fields, record);
setDisplayValuesInRecord(table, fields, record, true);
}
}
/*******************************************************************************
** For a list of records, set their recordLabels and display values
** For a single record, set its display values - public version of this.
*******************************************************************************/
public static void setDisplayValuesInRecords(Map<String, QFieldMetaData> fields, List<QRecord> records)
public static void setDisplayValuesInRecord(QTableMetaData table, Map<String, QFieldMetaData> fields, QRecord record)
{
if(records == null)
{
return;
}
for(QRecord record : records)
{
setDisplayValuesInRecord(fields, record);
}
setDisplayValuesInRecord(table, fields, record, false);
}
/*******************************************************************************
** For a list of records, set their display values
** For a single record, set its display values - where caller (meant to stay private)
** can specify if they've already done fieldBehaviors (to avoid re-doing).
*******************************************************************************/
public static void setDisplayValuesInRecord(Collection<QFieldMetaData> fields, QRecord record)
private static void setDisplayValuesInRecord(QTableMetaData table, Map<String, QFieldMetaData> fields, QRecord record, boolean alreadyAppliedFieldDisplayBehaviors)
{
for(QFieldMetaData field : fields)
if(!alreadyAppliedFieldDisplayBehaviors)
{
if(record.getDisplayValue(field.getName()) == null)
if(table != null)
{
String formattedValue = formatValue(field, record.getValue(field.getName()));
record.setDisplayValue(field.getName(), formattedValue);
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.FORMATTING, QContext.getQInstance(), table, List.of(record), null);
}
}
}
/*******************************************************************************
** For a list of records, set their display values
*******************************************************************************/
public static void setDisplayValuesInRecord(Map<String, QFieldMetaData> fields, QRecord record)
{
for(Map.Entry<String, QFieldMetaData> entry : fields.entrySet())
{
String fieldName = entry.getKey();

View File

@ -275,8 +275,19 @@ public class SearchPossibleValueSourceAction
queryInput.setFilter(queryFilter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(table.getPrimaryKeyField())).toList();
QueryOutput queryOutput = new QueryAction().execute(queryInput);
String fieldName;
if(StringUtils.hasContent(possibleValueSource.getOverrideIdField()))
{
fieldName = possibleValueSource.getOverrideIdField();
}
else
{
fieldName = table.getPrimaryKeyField();
}
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(fieldName)).toList();
List<QPossibleValue<?>> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, ids);
output.setResults(qPossibleValues);

View File

@ -27,6 +27,7 @@ import java.util.Set;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldDisplayBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -44,7 +45,8 @@ public class ValueBehaviorApplier
public enum Action
{
INSERT,
UPDATE
UPDATE,
FORMATTING
}
@ -63,7 +65,34 @@ public class ValueBehaviorApplier
{
for(FieldBehavior<?> fieldBehavior : CollectionUtils.nonNullCollection(field.getBehaviors()))
{
fieldBehavior.apply(action, recordList, instance, table, field, behaviorsToOmit);
boolean applyBehavior = true;
if(behaviorsToOmit != null && behaviorsToOmit.contains(fieldBehavior))
{
/////////////////////////////////////////////////////////////////////////////////////////
// if we're given a set of behaviors to omit, and this behavior is in there, then skip //
/////////////////////////////////////////////////////////////////////////////////////////
applyBehavior = false;
}
if(Action.FORMATTING == action && !(fieldBehavior instanceof FieldDisplayBehavior<?>))
{
////////////////////////////////////////////////////////////////////////////////////////////////
// for the formatting action, do not apply the behavior unless it is a field-display-behavior //
////////////////////////////////////////////////////////////////////////////////////////////////
applyBehavior = false;
}
else if(Action.FORMATTING != action && fieldBehavior instanceof FieldDisplayBehavior<?>)
{
/////////////////////////////////////////////////////////////////////////////////////////////
// for non-formatting actions, do not apply the behavior IF it is a field-display-behavior //
/////////////////////////////////////////////////////////////////////////////////////////////
applyBehavior = false;
}
if(applyBehavior)
{
fieldBehavior.apply(action, recordList, instance, table, field);
}
}
}
}

View File

@ -384,9 +384,9 @@ public class QInstanceEnricher
process.setLabel(nameToLabel(process.getName()));
}
if(process.getStepList() != null)
for(QStepMetaData step : CollectionUtils.nonNullMap(process.getAllSteps()).values())
{
process.getStepList().forEach(this::enrichStep);
enrichStep(step);
}
for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values())

View File

@ -43,6 +43,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.help.HelpFormat;
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpRole;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@ -104,13 +105,21 @@ public class QInstanceHelpContentManager
for(String part : key.split(";"))
{
String[] parts = part.split(":");
nameValuePairs.put(parts[0], parts[1]);
if(parts.length > 1)
{
nameValuePairs.put(parts[0], parts[1]);
}
else
{
LOG.info("Discarding help content with key that does not contain name:value format", logPair("key", key), logPair("id", record.getValue("id")));
}
}
String tableName = nameValuePairs.get("table");
String processName = nameValuePairs.get("process");
String fieldName = nameValuePairs.get("field");
String sectionName = nameValuePairs.get("section");
String stepName = nameValuePairs.get("step");
String widgetName = nameValuePairs.get("widget");
String slotName = nameValuePairs.get("slot");
@ -141,16 +150,19 @@ public class QInstanceHelpContentManager
///////////////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(tableName))
{
processHelpContentForTable(key, tableName, sectionName, fieldName, roles, helpContent);
processHelpContentForTable(key, tableName, sectionName, fieldName, slotName, roles, helpContent);
}
else if(StringUtils.hasContent(processName))
{
processHelpContentForProcess(key, processName, fieldName, roles, helpContent);
processHelpContentForProcess(key, processName, fieldName, stepName, roles, helpContent);
}
else if(StringUtils.hasContent(widgetName))
{
processHelpContentForWidget(key, widgetName, slotName, helpContent);
processHelpContentForWidget(key, widgetName, slotName, roles, helpContent);
}
else if(nameValuePairs.containsKey("instanceLevel"))
{
processHelpContentForInstance(key, slotName, roles, helpContent);
}
}
catch(Exception e)
@ -164,7 +176,7 @@ public class QInstanceHelpContentManager
/*******************************************************************************
**
*******************************************************************************/
private static void processHelpContentForTable(String key, String tableName, String sectionName, String fieldName, Set<HelpRole> roles, QHelpContent helpContent)
private static void processHelpContentForTable(String key, String tableName, String sectionName, String fieldName, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
{
QTableMetaData table = QContext.getQInstance().getTable(tableName);
if(table == null)
@ -209,6 +221,24 @@ public class QInstanceHelpContentManager
optionalSection.get().removeHelpContent(roles);
}
}
else
{
if(!StringUtils.hasContent(slotName))
{
LOG.info("Missing slot name in table-level help content", logPair("key", key));
}
else
{
if(helpContent != null)
{
table.withHelpContent(slotName, helpContent);
}
else
{
table.removeHelpContent(slotName, roles);
}
}
}
}
@ -216,7 +246,7 @@ public class QInstanceHelpContentManager
/*******************************************************************************
**
*******************************************************************************/
private static void processHelpContentForProcess(String key, String processName, String fieldName, Set<HelpRole> roles, QHelpContent helpContent)
private static void processHelpContentForProcess(String key, String processName, String fieldName, String stepName, Set<HelpRole> roles, QHelpContent helpContent)
{
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
if(process == null)
@ -245,6 +275,30 @@ public class QInstanceHelpContentManager
optionalField.get().removeHelpContent(roles);
}
}
else if(StringUtils.hasContent(stepName))
{
/////////////////////////////
// handle a process screen //
/////////////////////////////
QFrontendStepMetaData frontendStep = process.getFrontendStep(stepName);
if(frontendStep == null)
{
LOG.info("Unrecognized process step in help content", logPair("key", key));
}
else if(helpContent != null)
{
frontendStep.withHelpContent(helpContent);
}
else
{
frontendStep.removeHelpContent(roles);
}
}
else
{
LOG.info("Unrecognized key format for process help content", logPair("key", key));
}
}
@ -252,7 +306,7 @@ public class QInstanceHelpContentManager
/*******************************************************************************
**
*******************************************************************************/
private static void processHelpContentForWidget(String key, String widgetName, String slotName, QHelpContent helpContent)
private static void processHelpContentForWidget(String key, String widgetName, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
{
QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
if(!StringUtils.hasContent(slotName))
@ -265,22 +319,38 @@ public class QInstanceHelpContentManager
}
else
{
Map<String, QHelpContent> widgetHelpContent = widget.getHelpContent();
if(widgetHelpContent == null)
{
widgetHelpContent = new HashMap<>();
}
if(helpContent != null)
{
widgetHelpContent.put(slotName, helpContent);
widget.withHelpContent(slotName, helpContent);
}
else
{
widgetHelpContent.remove(slotName);
widget.removeHelpContent(slotName, roles);
}
}
}
widget.setHelpContent(widgetHelpContent);
/*******************************************************************************
**
*******************************************************************************/
private static void processHelpContentForInstance(String key, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
{
if(!StringUtils.hasContent(slotName))
{
LOG.info("Missing slot name in instance-level help content", logPair("key", key));
}
else
{
if(helpContent != null)
{
QContext.getQInstance().withHelpContent(slotName, helpContent);
}
else
{
QContext.getQInstance().removeHelpContent(slotName, roles);
}
}
}

View File

@ -64,6 +64,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.dashboard.ParentWidgetMetaD
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
@ -85,6 +86,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
@ -107,6 +109,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeLambda;
import org.quartz.CronExpression;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -149,6 +152,7 @@ public class QInstanceValidator
// once, during the enrichment/validation work, so, capture it, and store it back in the instance. //
/////////////////////////////////////////////////////////////////////////////////////////////////////
JoinGraph joinGraph = null;
long start = System.currentTimeMillis();
try
{
/////////////////////////////////////////////////////////////////////////////////////////////////
@ -189,6 +193,9 @@ public class QInstanceValidator
validateUniqueTopLevelNames(qInstance);
runPlugins(QInstance.class, qInstance, qInstance);
long end = System.currentTimeMillis();
LOG.info("Validation (and enrichment) performance", logPair("millis", (end - start)));
}
catch(Exception e)
{
@ -207,6 +214,17 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
public void revalidate(QInstance qInstance) throws QInstanceValidationException
{
qInstance.setHasBeenValidated(null);
validate(qInstance);
}
/*******************************************************************************
**
*******************************************************************************/
@ -623,6 +641,11 @@ public class QInstanceValidator
supplementalTableMetaData.validate(qInstance, table, this);
}
if(table.getShareableTableMetaData() != null)
{
table.getShareableTableMetaData().validate(qInstance, table, this);
}
runPlugins(QTableMetaData.class, table, qInstance);
});
}
@ -650,17 +673,20 @@ public class QInstanceValidator
{
if(assertCondition(CollectionUtils.nullSafeHasContents(exposedJoin.getJoinPath()), joinPrefix + "is missing a joinPath."))
{
joinConnectionsForTable = Objects.requireNonNullElseGet(joinConnectionsForTable, () -> joinGraph.getJoinConnections(table.getName()));
boolean foundJoinConnection = false;
for(JoinGraph.JoinConnectionList joinConnectionList : joinConnectionsForTable)
if(joinGraph != null)
{
if(joinConnectionList.matchesJoinPath(exposedJoin.getJoinPath()))
joinConnectionsForTable = Objects.requireNonNullElseGet(joinConnectionsForTable, () -> joinGraph.getJoinConnections(table.getName()));
boolean foundJoinConnection = false;
for(JoinGraph.JoinConnectionList joinConnectionList : joinConnectionsForTable)
{
foundJoinConnection = true;
if(joinConnectionList.matchesJoinPath(exposedJoin.getJoinPath()))
{
foundJoinConnection = true;
}
}
assertCondition(foundJoinConnection, joinPrefix + "specified a joinPath [" + exposedJoin.getJoinPath() + "] which does not match a valid join connection in the instance.");
}
assertCondition(foundJoinConnection, joinPrefix + "specified a joinPath [" + exposedJoin.getJoinPath() + "] which does not match a valid join connection in the instance.");
assertCondition(!usedJoinPaths.contains(exposedJoin.getJoinPath()), tablePrefix + "has more than one join with the joinPath: " + exposedJoin.getJoinPath());
usedJoinPaths.add(exposedJoin.getJoinPath());
@ -710,7 +736,6 @@ public class QInstanceValidator
{
String prefix = "Table " + table.getName() + " ";
RECORD_SECURITY_LOCKS_LOOP:
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
{
if(!assertCondition(recordSecurityLock != null, prefix + "has a null recordSecurityLock (did you mean to give it a null list of locks?)"))
@ -718,90 +743,14 @@ public class QInstanceValidator
continue;
}
String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType"))
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
{
assertCondition(qInstance.getSecurityKeyType(securityKeyTypeName) != null, prefix + "has a recordSecurityLock with an unrecognized securityKeyType: " + securityKeyTypeName);
validateMultiRecordSecurityLock(qInstance, table, multiRecordSecurityLock, prefix);
}
prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
assertCondition(recordSecurityLock.getLockScope() != null, prefix + " is missing its lockScope");
boolean hasAnyBadJoins = false;
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
else
{
if(!assertCondition(qInstance.getJoin(joinName) != null, prefix + "has an unrecognized joinName: " + joinName))
{
hasAnyBadJoins = true;
}
validateRecordSecurityLock(qInstance, table, recordSecurityLock, prefix);
}
String fieldName = recordSecurityLock.getFieldName();
////////////////////////////////////////////////////////////////////////////////
// don't bother trying to validate field names if we know we have a bad join. //
////////////////////////////////////////////////////////////////////////////////
if(assertCondition(StringUtils.hasContent(fieldName), prefix + "is missing a fieldName") && !hasAnyBadJoins)
{
if(fieldName.contains("."))
{
if(assertCondition(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " looks like a join (has a dot), but no joinNameChain was given."))
{
List<QueryJoin> joins = new ArrayList<>();
///////////////////////////////////////////////////////////////////////////////////////////////////
// ok - so - the join name chain is going to be like this: //
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
// - securityFieldName = order.clientId //
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
// and step (via tmpTable variable) back to the securityField //
///////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
Collections.reverse(joinNameChain);
QTableMetaData tmpTable = table;
for(String joinName : joinNameChain)
{
QJoinMetaData join = qInstance.getJoin(joinName);
if(join == null)
{
errors.add(prefix + "joinNameChain contained an unrecognized join: " + joinName);
continue RECORD_SECURITY_LOCKS_LOOP;
}
if(join.getLeftTable().equals(tmpTable.getName()))
{
joins.add(new QueryJoin(join));
tmpTable = qInstance.getTable(join.getRightTable());
}
else if(join.getRightTable().equals(tmpTable.getName()))
{
joins.add(new QueryJoin(join.flip()));
tmpTable = qInstance.getTable(join.getLeftTable());
}
else
{
errors.add(prefix + "joinNameChain could not be followed through join: " + joinName);
continue RECORD_SECURITY_LOCKS_LOOP;
}
}
assertCondition(findField(qInstance, table, joins, fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
}
}
else
{
if(assertCondition(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " does not look like a join (does not have a dot), but a joinNameChain was given."))
{
assertNoException(() -> table.getField(fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
}
}
}
assertCondition(recordSecurityLock.getNullValueBehavior() != null, prefix + "is missing a nullValueBehavior");
}
}
@ -810,7 +759,121 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateTableField(QInstance qInstance, String tableName, String fieldName, QTableMetaData table, QFieldMetaData field)
private void validateMultiRecordSecurityLock(QInstance qInstance, QTableMetaData table, MultiRecordSecurityLock multiRecordSecurityLock, String prefix)
{
assertCondition(multiRecordSecurityLock.getOperator() != null, prefix + "has a MultiRecordSecurityLock that is missing an operator");
for(RecordSecurityLock lock : multiRecordSecurityLock.getLocks())
{
validateRecordSecurityLock(qInstance, table, lock, prefix);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void validateRecordSecurityLock(QInstance qInstance, QTableMetaData table, RecordSecurityLock recordSecurityLock, String prefix)
{
String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType"))
{
assertCondition(qInstance.getSecurityKeyType(securityKeyTypeName) != null, prefix + "has a recordSecurityLock with an unrecognized securityKeyType: " + securityKeyTypeName);
}
prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
assertCondition(recordSecurityLock.getLockScope() != null, prefix + " is missing its lockScope");
boolean hasAnyBadJoins = false;
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
{
if(!assertCondition(qInstance.getJoin(joinName) != null, prefix + "has an unrecognized joinName: " + joinName))
{
hasAnyBadJoins = true;
}
}
String fieldName = recordSecurityLock.getFieldName();
////////////////////////////////////////////////////////////////////////////////
// don't bother trying to validate field names if we know we have a bad join. //
////////////////////////////////////////////////////////////////////////////////
if(assertCondition(StringUtils.hasContent(fieldName), prefix + "is missing a fieldName") && !hasAnyBadJoins)
{
if(fieldName.contains("."))
{
if(assertCondition(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " looks like a join (has a dot), but no joinNameChain was given."))
{
String[] split = fieldName.split("\\.");
String joinTableName = split[0];
String joinFieldName = split[1];
List<QueryJoin> joins = new ArrayList<>();
///////////////////////////////////////////////////////////////////////////////////////////////////
// ok - so - the join name chain is going to be like this: //
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
// - securityFieldName = order.clientId //
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
// and step (via tmpTable variable) back to the securityField //
///////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
Collections.reverse(joinNameChain);
QTableMetaData tmpTable = table;
for(String joinName : joinNameChain)
{
QJoinMetaData join = qInstance.getJoin(joinName);
if(join == null)
{
errors.add(prefix + "joinNameChain contained an unrecognized join: " + joinName);
return;
}
if(join.getLeftTable().equals(tmpTable.getName()))
{
joins.add(new QueryJoin(join));
tmpTable = qInstance.getTable(join.getRightTable());
}
else if(join.getRightTable().equals(tmpTable.getName()))
{
joins.add(new QueryJoin(join.flip()));
tmpTable = qInstance.getTable(join.getLeftTable());
}
else
{
errors.add(prefix + "joinNameChain could not be followed through join: " + joinName);
return;
}
}
assertCondition(Objects.equals(tmpTable.getName(), joinTableName), prefix + "has a joinNameChain doesn't end in the expected table [" + joinTableName + "] (was: " + tmpTable.getName() + ")");
assertCondition(findField(qInstance, table, joins, fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
}
}
else
{
if(assertCondition(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " does not look like a join (does not have a dot), but a joinNameChain was given."))
{
assertNoException(() -> table.getField(fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
}
}
}
assertCondition(recordSecurityLock.getNullValueBehavior() != null, prefix + "is missing a nullValueBehavior");
}
/*******************************************************************************
**
*******************************************************************************/
private <T extends FieldBehavior<T>> void validateTableField(QInstance qInstance, String tableName, String fieldName, QTableMetaData table, QFieldMetaData field)
{
assertCondition(Objects.equals(fieldName, field.getName()),
"Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + ".");
@ -823,12 +886,32 @@ public class QInstanceValidator
String prefix = "Field " + fieldName + " in table " + tableName + " ";
///////////////////////////////////////////////////
// validate things we know about field behaviors //
///////////////////////////////////////////////////
ValueTooLongBehavior behavior = field.getBehaviorOrDefault(qInstance, ValueTooLongBehavior.class);
if(behavior != null && !behavior.equals(ValueTooLongBehavior.PASS_THROUGH))
{
assertCondition(field.getMaxLength() != null, prefix + "specifies a ValueTooLongBehavior, but not a maxLength.");
}
Set<Class<FieldBehavior<T>>> usedFieldBehaviorTypes = new HashSet<>();
if(field.getBehaviors() != null)
{
for(FieldBehavior<?> fieldBehavior : field.getBehaviors())
{
Class<FieldBehavior<T>> behaviorClass = (Class<FieldBehavior<T>>) fieldBehavior.getClass();
errors.addAll(fieldBehavior.validateBehaviorConfiguration(table, field));
if(!fieldBehavior.allowMultipleBehaviorsOfThisType())
{
assertCondition(!usedFieldBehaviorTypes.contains(behaviorClass), prefix + "has more than 1 fieldBehavior of type " + behaviorClass.getSimpleName() + ", which is not allowed for this type");
}
usedFieldBehaviorTypes.add(behaviorClass);
}
}
if(field.getMaxLength() != null)
{
assertCondition(field.getMaxLength() > 0, prefix + "has an invalid maxLength (" + field.getMaxLength() + ") - must be greater than 0.");
@ -850,7 +933,7 @@ public class QInstanceValidator
}
assertCondition(fieldSecurityLock.getDefaultBehavior() != null, prefix + "has a fieldSecurityLock that is missing a defaultBehavior");
assertCondition(CollectionUtils.nullSafeHasContents(fieldSecurityLock.getOverrideValues()), prefix + "has a fieldSecurityLock that is missing overrideValues");
assertCondition(CollectionUtils.nullSafeHasContents(fieldSecurityLock.getKeyValueBehaviors()), prefix + "has a fieldSecurityLock that is missing keyValueBehaviors");
}
for(FieldAdornment adornment : CollectionUtils.nonNullList(field.getAdornments()))
@ -1404,7 +1487,7 @@ public class QInstanceValidator
warn("Error loading expectedType for field [" + fieldMetaData.getName() + "] in process [" + processName + "]: " + e.getMessage());
}
validateSimpleCodeReference("Process " + processName + " code reference: ", codeReference, expectedClass);
validateSimpleCodeReference("Process " + processName + " code reference:", codeReference, expectedClass);
}
}
}
@ -1412,6 +1495,14 @@ public class QInstanceValidator
}
}
if(process.getCancelStep() != null)
{
if(assertCondition(process.getCancelStep().getCode() != null, "Cancel step is missing a code reference, in process " + processName))
{
validateSimpleCodeReference("Process " + processName + " cancel step code reference: ", process.getCancelStep().getCode(), BackendStep.class);
}
}
///////////////////////////////////////////////////////////////////////////////
// if the process has a schedule, make sure required schedule data populated //
///////////////////////////////////////////////////////////////////////////////
@ -1423,7 +1514,11 @@ public class QInstanceValidator
if(process.getVariantBackend() != null)
{
assertCondition(qInstance.getBackend(process.getVariantBackend()) != null, "Process " + processName + ", a variant backend was not found named " + process.getVariantBackend());
if(qInstance.getBackends() != null)
{
assertCondition(qInstance.getBackend(process.getVariantBackend()) != null, "Process " + processName + ", a variant backend was not found named " + process.getVariantBackend());
}
assertCondition(process.getVariantRunStrategy() != null, "A variant run strategy was not set for process " + processName + " (which does specify a variant backend)");
}
else
@ -1449,7 +1544,7 @@ public class QInstanceValidator
private void validateScheduleMetaData(QScheduleMetaData schedule, QInstance qInstance, String prefix)
{
boolean isRepeat = schedule.getRepeatMillis() != null || schedule.getRepeatSeconds() != null;
boolean isCron = StringUtils.hasContent(schedule.getCronExpression());
boolean isCron = StringUtils.hasContent(schedule.getCronExpression());
assertCondition(isRepeat || isCron, prefix + " either repeatMillis or repeatSeconds or cronExpression must be set");
assertCondition(!(isRepeat && isCron), prefix + " both a repeat time and cronExpression may not be set");
@ -1469,8 +1564,8 @@ public class QInstanceValidator
if(assertCondition(StringUtils.hasContent(schedule.getCronTimeZoneId()), prefix + " a cron schedule must specify a cronTimeZoneId"))
{
String[] availableIDs = TimeZone.getAvailableIDs();
Optional<String> first = Arrays.stream(availableIDs).filter(id -> id.equals(schedule.getCronTimeZoneId())).findFirst();
String[] availableIDs = TimeZone.getAvailableIDs();
Optional<String> first = Arrays.stream(availableIDs).filter(id -> id.equals(schedule.getCronTimeZoneId())).findFirst();
assertCondition(first.isPresent(), prefix + " unrecognized cronTimeZoneId: " + schedule.getCronTimeZoneId());
}
}

View File

@ -79,7 +79,7 @@ public class LogPair
}
else if(value instanceof LogPair[] subLogPairs)
{
String subLogPairsString = Arrays.stream(subLogPairs).map(LogPair::toString).collect(Collectors.joining(","));
String subLogPairsString = Arrays.stream(subLogPairs).filter(Objects::nonNull).map(LogPair::toString).collect(Collectors.joining(","));
valueString = '{' + subLogPairsString + '}';
}
else if(value instanceof UnsafeSupplier<?, ?> us)

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.logging;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -147,6 +148,28 @@ public class QLogger
/*******************************************************************************
**
*******************************************************************************/
public <T extends Throwable> T warnAndThrow(T t, LogPair... logPairs) throws T
{
warn(t.getMessage(), t, logPairs);
throw (t);
}
/*******************************************************************************
**
*******************************************************************************/
public <T extends Throwable> T errorAndThrow(T t, LogPair... logPairs) throws T
{
error(t.getMessage(), t, logPairs);
throw (t);
}
/*******************************************************************************
**
*******************************************************************************/
@ -595,7 +618,10 @@ public class QLogger
{
user = session.getUser().getIdReference();
}
sessionLogPair = logPair("session", logPair("id", session.getUuid()), logPair("user", user));
LogPair variantsLogPair = getVariantsLogPair(session);
sessionLogPair = logPair("session", logPair("id", session.getUuid()), logPair("user", user), variantsLogPair);
}
try
@ -615,6 +641,38 @@ public class QLogger
/*******************************************************************************
**
*******************************************************************************/
private static LogPair getVariantsLogPair(QSession session)
{
LogPair variantsLogPair = null;
try
{
if(session.getBackendVariants() != null)
{
LogPair[] variants = new LogPair[session.getBackendVariants().size()];
int i = 0;
for(Map.Entry<String, Serializable> entry : session.getBackendVariants().entrySet())
{
variants[i] = new LogPair(entry.getKey(), entry.getValue());
}
variantsLogPair = new LogPair("variants", variants);
}
}
catch(Exception e)
{
////////////////
// leave null //
////////////////
}
return variantsLogPair;
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -23,8 +23,8 @@ package com.kingsrook.qqq.backend.core.model;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.TopLevelMetaDataInterface;
/*******************************************************************************
@ -42,7 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
** implement this interface. or, same idea for a QRecordEntity that provides
** its own TableMetaData.
*******************************************************************************/
public interface MetaDataProducerInterface<T extends TopLevelMetaDataInterface>
public interface MetaDataProducerInterface<T extends MetaDataProducerOutput>
{
int DEFAULT_SORT_ORDER = 500;

View File

@ -26,6 +26,7 @@ import java.util.UUID;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
import com.kingsrook.qqq.backend.core.actions.async.NonPersistedAsyncJobCallback;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
@ -139,7 +140,7 @@ public class AbstractActionInput
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// don't return null here (too easy to NPE). instead, if someone wants one of these, create one and give it to them. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
asyncJobCallback = new AsyncJobCallback(UUID.randomUUID(), new AsyncJobStatus());
asyncJobCallback = new NonPersistedAsyncJobCallback(UUID.randomUUID(), new AsyncJobStatus().withJobName(getClass().getSimpleName()));
}
return asyncJobCallback;
}

View File

@ -0,0 +1,95 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.messaging;
/*******************************************************************************
**
*******************************************************************************/
public class Attachment
{
private byte[] contents;
private String name;
/*******************************************************************************
** Getter for contents
*******************************************************************************/
public byte[] getContents()
{
return (this.contents);
}
/*******************************************************************************
** Setter for contents
*******************************************************************************/
public void setContents(byte[] contents)
{
this.contents = contents;
}
/*******************************************************************************
** Fluent setter for contents
*******************************************************************************/
public Attachment withContents(byte[] contents)
{
this.contents = contents;
return (this);
}
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public Attachment withName(String name)
{
this.name = name;
return (this);
}
}

View File

@ -0,0 +1,95 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.messaging;
/*******************************************************************************
**
*******************************************************************************/
public class Content
{
private String body;
private ContentRole contentRole;
/*******************************************************************************
** Getter for body
*******************************************************************************/
public String getBody()
{
return (this.body);
}
/*******************************************************************************
** Setter for body
*******************************************************************************/
public void setBody(String body)
{
this.body = body;
}
/*******************************************************************************
** Fluent setter for body
*******************************************************************************/
public Content withBody(String body)
{
this.body = body;
return (this);
}
/*******************************************************************************
** Getter for contentRole
*******************************************************************************/
public ContentRole getContentRole()
{
return (this.contentRole);
}
/*******************************************************************************
** Setter for contentRole
*******************************************************************************/
public void setContentRole(ContentRole contentRole)
{
this.contentRole = contentRole;
}
/*******************************************************************************
** Fluent setter for contentRole
*******************************************************************************/
public Content withContentRole(ContentRole contentRole)
{
this.contentRole = contentRole;
return (this);
}
}

View File

@ -0,0 +1,39 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.messaging;
/*******************************************************************************
**
*******************************************************************************/
public interface ContentRole
{
/*******************************************************************************
**
*******************************************************************************/
enum Default implements ContentRole
{
DEFAULT
}
}

View File

@ -0,0 +1,92 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.messaging;
import java.util.ArrayList;
import java.util.List;
/*******************************************************************************
**
*******************************************************************************/
public class MultiParty extends Party
{
private List<Party> partyList;
/*******************************************************************************
** Getter for partyList
*******************************************************************************/
public List<Party> getPartyList()
{
return (this.partyList);
}
/*******************************************************************************
** Setter for partyList
*******************************************************************************/
public void setPartyList(List<Party> partyList)
{
this.partyList = partyList;
}
/*******************************************************************************
** Fluent setter for partyList
*******************************************************************************/
public MultiParty withPartyList(List<Party> partyList)
{
this.partyList = partyList;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public MultiParty withParty(Party party)
{
addParty(party);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void addParty(Party party)
{
if(this.partyList == null)
{
this.partyList = new ArrayList<>();
}
this.partyList.add(party);
}
}

View File

@ -0,0 +1,127 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.messaging;
/*******************************************************************************
**
*******************************************************************************/
public class Party
{
private String label;
private String address;
private PartyRole role;
/*******************************************************************************
** Getter for label
*******************************************************************************/
public String getLabel()
{
return (this.label);
}
/*******************************************************************************
** Setter for label
*******************************************************************************/
public void setLabel(String label)
{
this.label = label;
}
/*******************************************************************************
** Fluent setter for label
*******************************************************************************/
public Party withLabel(String label)
{
this.label = label;
return (this);
}
/*******************************************************************************
** Getter for address
*******************************************************************************/
public String getAddress()
{
return (this.address);
}
/*******************************************************************************
** Setter for address
*******************************************************************************/
public void setAddress(String address)
{
this.address = address;
}
/*******************************************************************************
** Fluent setter for address
*******************************************************************************/
public Party withAddress(String address)
{
this.address = address;
return (this);
}
/*******************************************************************************
** Getter for role
*******************************************************************************/
public PartyRole getRole()
{
return (this.role);
}
/*******************************************************************************
** Setter for role
*******************************************************************************/
public void setRole(PartyRole role)
{
this.role = role;
}
/*******************************************************************************
** Fluent setter for role
*******************************************************************************/
public Party withRole(PartyRole role)
{
this.role = role;
return (this);
}
}

View File

@ -0,0 +1,39 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.messaging;
/*******************************************************************************
**
*******************************************************************************/
public interface PartyRole
{
/*******************************************************************************
**
*******************************************************************************/
enum Default implements PartyRole
{
DEFAULT
}
}

View File

@ -0,0 +1,278 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.messaging;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
/*******************************************************************************
**
*******************************************************************************/
public class SendMessageInput extends AbstractActionInput
{
private String messagingProviderName;
private Party to;
private Party from;
private String subject;
private List<Content> contentList;
private List<Attachment> attachmentList;
/*******************************************************************************
** Getter for to
*******************************************************************************/
public Party getTo()
{
return (this.to);
}
/*******************************************************************************
** Setter for to
*******************************************************************************/
public void setTo(Party to)
{
this.to = to;
}
/*******************************************************************************
** Fluent setter for to
*******************************************************************************/
public SendMessageInput withTo(Party to)
{
this.to = to;
return (this);
}
/*******************************************************************************
** Getter for from
*******************************************************************************/
public Party getFrom()
{
return (this.from);
}
/*******************************************************************************
** Setter for from
*******************************************************************************/
public void setFrom(Party from)
{
this.from = from;
}
/*******************************************************************************
** Fluent setter for from
*******************************************************************************/
public SendMessageInput withFrom(Party from)
{
this.from = from;
return (this);
}
/*******************************************************************************
** Getter for subject
*******************************************************************************/
public String getSubject()
{
return (this.subject);
}
/*******************************************************************************
** Setter for subject
*******************************************************************************/
public void setSubject(String subject)
{
this.subject = subject;
}
/*******************************************************************************
** Fluent setter for subject
*******************************************************************************/
public SendMessageInput withSubject(String subject)
{
this.subject = subject;
return (this);
}
/*******************************************************************************
** Getter for contentList
*******************************************************************************/
public List<Content> getContentList()
{
return (this.contentList);
}
/*******************************************************************************
** Setter for contentList
*******************************************************************************/
public void setContentList(List<Content> contentList)
{
this.contentList = contentList;
}
/*******************************************************************************
** Fluent setter for contentList
*******************************************************************************/
public SendMessageInput withContentList(List<Content> contentList)
{
this.contentList = contentList;
return (this);
}
/*******************************************************************************
** Getter for attachmentList
*******************************************************************************/
public List<Attachment> getAttachmentList()
{
return (this.attachmentList);
}
/*******************************************************************************
** Setter for attachmentList
*******************************************************************************/
public void setAttachmentList(List<Attachment> attachmentList)
{
this.attachmentList = attachmentList;
}
/*******************************************************************************
** Fluent setter for attachmentList
*******************************************************************************/
public SendMessageInput withAttachmentList(List<Attachment> attachmentList)
{
this.attachmentList = attachmentList;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public SendMessageInput withContent(Content content)
{
addContent(content);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void addContent(Content content)
{
if(this.contentList == null)
{
this.contentList = new ArrayList<>();
}
this.contentList.add(content);
}
/*******************************************************************************
**
*******************************************************************************/
public SendMessageInput withAttachment(Attachment attachment)
{
addAttachment(attachment);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void addAttachment(Attachment attachment)
{
if(this.attachmentList == null)
{
this.attachmentList = new ArrayList<>();
}
this.attachmentList.add(attachment);
}
/*******************************************************************************
** Getter for messagingProviderName
*******************************************************************************/
public String getMessagingProviderName()
{
return (this.messagingProviderName);
}
/*******************************************************************************
** Setter for messagingProviderName
*******************************************************************************/
public void setMessagingProviderName(String messagingProviderName)
{
this.messagingProviderName = messagingProviderName;
}
/*******************************************************************************
** Fluent setter for messagingProviderName
*******************************************************************************/
public SendMessageInput withMessagingProviderName(String messagingProviderName)
{
this.messagingProviderName = messagingProviderName;
return (this);
}
}

View File

@ -0,0 +1,33 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.messaging;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
/*******************************************************************************
**
*******************************************************************************/
public class SendMessageOutput extends AbstractActionOutput
{
}

View File

@ -0,0 +1,35 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.messaging.email;
import com.kingsrook.qqq.backend.core.model.actions.messaging.ContentRole;
/*******************************************************************************
**
*******************************************************************************/
public enum EmailContentRole implements ContentRole
{
TEXT,
HTML
}

View File

@ -0,0 +1,38 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.messaging.email;
import com.kingsrook.qqq.backend.core.model.actions.messaging.PartyRole;
/*******************************************************************************
**
*******************************************************************************/
public enum EmailPartyRole implements PartyRole
{
TO,
CC,
BCC,
FROM,
REPLY_TO
}

View File

@ -32,6 +32,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMe
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
/*******************************************************************************
@ -47,8 +48,9 @@ public class MetaDataOutput extends AbstractActionOutput
private Map<String, QFrontendWidgetMetaData> widgets;
private Map<String, String> environmentValues;
private List<AppTreeNode> appTree;
private QBrandingMetaData branding;
private List<AppTreeNode> appTree;
private QBrandingMetaData branding;
private Map<String, List<QHelpContent>> helpContents;
@ -226,4 +228,25 @@ public class MetaDataOutput extends AbstractActionOutput
this.environmentValues = environmentValues;
}
/*******************************************************************************
** Setter for helpContents
**
*******************************************************************************/
public void setHelpContents(Map<String, List<QHelpContent>> helpContents)
{
this.helpContents = helpContents;
}
/*******************************************************************************
** Getter for helpContents
**
*******************************************************************************/
public Map<String, List<QHelpContent>> getHelpContents()
{
return helpContents;
}
}

View File

@ -29,6 +29,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
/*******************************************************************************
@ -41,6 +42,11 @@ public class ProcessState implements Serializable
private List<String> stepList = new ArrayList<>();
private Optional<String> nextStepName = Optional.empty();
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// maybe, remove this altogether - just let the frontend compute & send if needed... but how does it know last version...? //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private List<QFrontendStepMetaData> updatedFrontendStepList = null;
/*******************************************************************************
@ -139,4 +145,36 @@ public class ProcessState implements Serializable
{
this.stepList = stepList;
}
/*******************************************************************************
** Getter for updatedFrontendStepList
*******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
{
return (this.updatedFrontendStepList);
}
/*******************************************************************************
** Setter for updatedFrontendStepList
*******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
}
/*******************************************************************************
** Fluent setter for updatedFrontendStepList
*******************************************************************************/
public ProcessState withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
return (this);
}
}

View File

@ -30,6 +30,7 @@ import java.util.Map;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
import com.kingsrook.qqq.backend.core.actions.async.NonPersistedAsyncJobCallback;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
@ -450,7 +451,7 @@ public class RunBackendStepInput extends AbstractActionInput
/////////////////////////////////////////////////////////////////////////
// avoid NPE in case we didn't have one of these! create a new one... //
/////////////////////////////////////////////////////////////////////////
asyncJobCallback = new AsyncJobCallback(UUID.randomUUID(), new AsyncJobStatus());
asyncJobCallback = new NonPersistedAsyncJobCallback(UUID.randomUUID(), new AsyncJobStatus().withJobName(processName + "." + stepName));
}
return (asyncJobCallback);
}

View File

@ -27,10 +27,14 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditSingleInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -40,9 +44,13 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
*******************************************************************************/
public class RunBackendStepOutput extends AbstractActionOutput implements Serializable
{
private String processName;
private ProcessState processState;
private Exception exception; // todo - make optional
private String overrideLastStepName; // todo - does this need to go into state too??
private List<AuditInput> auditInputList = new ArrayList<>();
@ -78,6 +86,7 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
public void seedFromRequest(RunBackendStepInput runBackendStepInput)
{
this.processState = runBackendStepInput.getProcessState();
this.processName = runBackendStepInput.getProcessName();
}
@ -312,4 +321,111 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
auditInput.addAuditSingleInput(auditSingleInput);
}
/*******************************************************************************
** Getter for overrideLastStepName
*******************************************************************************/
public String getOverrideLastStepName()
{
return (this.overrideLastStepName);
}
/*******************************************************************************
** Setter for overrideLastStepName
*******************************************************************************/
public void setOverrideLastStepName(String overrideLastStepName)
{
this.overrideLastStepName = overrideLastStepName;
}
/*******************************************************************************
** Fluent setter for overrideLastStepName
*******************************************************************************/
public RunBackendStepOutput withOverrideLastStepName(String overrideLastStepName)
{
this.overrideLastStepName = overrideLastStepName;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void updateStepList(List<String> stepList)
{
getProcessState().setStepList(stepList);
if(processName == null)
{
throw (new QRuntimeException("ProcessName was not set in this object, therefore updateStepList cannot complete successfully. Try to manually call setProcessName as a work around."));
}
QProcessMetaData processMetaData = QContext.getQInstance().getProcess(processName);
ArrayList<QFrontendStepMetaData> updatedFrontendStepList = new ArrayList<>(stepList.stream()
.map(name -> processMetaData.getStep(name))
.filter(step -> step instanceof QFrontendStepMetaData)
.map(step -> (QFrontendStepMetaData) step)
.toList());
setUpdatedFrontendStepList(updatedFrontendStepList);
}
/*******************************************************************************
** Getter for processName
*******************************************************************************/
public String getProcessName()
{
return (this.processName);
}
/*******************************************************************************
** Setter for processName
*******************************************************************************/
public void setProcessName(String processName)
{
this.processName = processName;
}
/*******************************************************************************
** Fluent setter for processName
*******************************************************************************/
public RunBackendStepOutput withProcessName(String processName)
{
this.processName = processName;
return (this);
}
/*******************************************************************************
** Getter for updatedFrontendStepList
*******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
{
return (this.processState.getUpdatedFrontendStepList());
}
/*******************************************************************************
** Setter for updatedFrontendStepList
*******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.processState.setUpdatedFrontendStepList(updatedFrontendStepList);
}
}

View File

@ -32,6 +32,7 @@ import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -327,4 +328,25 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
{
return exception;
}
/*******************************************************************************
**
*******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.processState.setUpdatedFrontendStepList(updatedFrontendStepList);
}
/*******************************************************************************
**
*******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
{
return this.processState.getUpdatedFrontendStepList();
}
}

View File

@ -26,7 +26,8 @@ import java.io.OutputStream;
/*******************************************************************************
**
** Member of report & export Inputs, that wraps details about the destination of
** where & how the report (or export) is being written.
*******************************************************************************/
public class ReportDestination
{

View File

@ -31,8 +31,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValu
public enum ReportFormatPossibleValueEnum implements PossibleValueEnum<String>
{
XLSX,
JSON,
CSV;
CSV,
JSON;
public static final String NAME = "reportFormat";

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@ -29,7 +30,7 @@ import java.util.List;
/*******************************************************************************
** Full definition of a pivot table - its rows, columns, and values.
*******************************************************************************/
public class PivotTableDefinition implements Cloneable
public class PivotTableDefinition implements Cloneable, Serializable
{
private List<PivotTableGroupBy> rows;
private List<PivotTableGroupBy> columns;

View File

@ -22,11 +22,14 @@
package com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable;
import java.io.Serializable;
/*******************************************************************************
** Either a row or column grouping in a pivot table. e.g., a field plus
** sorting details, plus showTotals boolean.
*******************************************************************************/
public class PivotTableGroupBy implements Cloneable
public class PivotTableGroupBy implements Cloneable, Serializable
{
private String fieldName;
private PivotTableOrderBy orderBy;

View File

@ -22,10 +22,13 @@
package com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable;
import java.io.Serializable;
/*******************************************************************************
** How a group-by (rows or columns) should be sorted.
*******************************************************************************/
public class PivotTableOrderBy
public class PivotTableOrderBy implements Serializable
{
// todo - implement, but only if POI supports (or we build our own support...)
}

View File

@ -22,10 +22,13 @@
package com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable;
import java.io.Serializable;
/*******************************************************************************
** a value (e.g., field name + function) used in a pivot table
*******************************************************************************/
public class PivotTableValue implements Cloneable
public class PivotTableValue implements Cloneable, Serializable
{
private String fieldName;
private PivotTableFunction function;

View File

@ -0,0 +1,38 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables;
/*******************************************************************************
** Information about the query that an application (or qqq service) may know and
** want to tell the backend, that can help influence how the backend processes
** query.
**
** For example, a query with potentially a large result set, for MySQL backend,
** we may want to configure the result set to stream results rather than do its
** default in-memory thing. See RDBMSQueryAction for usage.
*******************************************************************************/
public enum QueryHint
{
POTENTIALLY_LARGE_NUMBER_OF_RESULTS,
MAY_USE_READ_ONLY_BACKEND
}

View File

@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.aggregate;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
@ -44,6 +46,8 @@ public class AggregateInput extends AbstractTableActionInput
private List<QueryJoin> queryJoins = null;
private EnumSet<QueryHint> queryHints = EnumSet.noneOf(QueryHint.class);
/*******************************************************************************
@ -302,4 +306,78 @@ public class AggregateInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for queryHints
*******************************************************************************/
public EnumSet<QueryHint> getQueryHints()
{
return (this.queryHints);
}
/*******************************************************************************
** Setter for queryHints
*******************************************************************************/
public void setQueryHints(EnumSet<QueryHint> queryHints)
{
this.queryHints = queryHints;
}
/*******************************************************************************
** Fluent setter for queryHints
*******************************************************************************/
public AggregateInput withQueryHints(EnumSet<QueryHint> queryHints)
{
this.queryHints = queryHints;
return (this);
}
/*******************************************************************************
** Fluent setter for queryHints
*******************************************************************************/
public AggregateInput withQueryHint(QueryHint queryHint)
{
if(this.queryHints == null)
{
this.queryHints = EnumSet.noneOf(QueryHint.class);
}
this.queryHints.add(queryHint);
return (this);
}
/*******************************************************************************
** Fluent setter for queryHints
*******************************************************************************/
public AggregateInput withoutQueryHint(QueryHint queryHint)
{
if(this.queryHints != null)
{
this.queryHints.remove(queryHint);
}
return (this);
}
/*******************************************************************************
** null-safely check if query hints map contains the specified hint
*******************************************************************************/
public boolean hasQueryHint(QueryHint queryHint)
{
if(this.queryHints == null)
{
return (false);
}
return (queryHints.contains(queryHint));
}
}

View File

@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.count;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
@ -42,6 +44,8 @@ public class CountInput extends AbstractTableActionInput
private List<QueryJoin> queryJoins = null;
private Boolean includeDistinctCount = false;
private EnumSet<QueryHint> queryHints = EnumSet.noneOf(QueryHint.class);
/*******************************************************************************
@ -207,4 +211,78 @@ public class CountInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for queryHints
*******************************************************************************/
public EnumSet<QueryHint> getQueryHints()
{
return (this.queryHints);
}
/*******************************************************************************
** Setter for queryHints
*******************************************************************************/
public void setQueryHints(EnumSet<QueryHint> queryHints)
{
this.queryHints = queryHints;
}
/*******************************************************************************
** Fluent setter for queryHints
*******************************************************************************/
public CountInput withQueryHints(EnumSet<QueryHint> queryHints)
{
this.queryHints = queryHints;
return (this);
}
/*******************************************************************************
** Fluent setter for queryHints
*******************************************************************************/
public CountInput withQueryHint(QueryHint queryHint)
{
if(this.queryHints == null)
{
this.queryHints = EnumSet.noneOf(QueryHint.class);
}
this.queryHints.add(queryHint);
return (this);
}
/*******************************************************************************
** Fluent setter for queryHints
*******************************************************************************/
public CountInput withoutQueryHint(QueryHint queryHint)
{
if(this.queryHints != null)
{
this.queryHints.remove(queryHint);
}
return (this);
}
/*******************************************************************************
** null-safely check if query hints map contains the specified hint
*******************************************************************************/
public boolean hasQueryHint(QueryHint queryHint)
{
if(this.queryHints == null)
{
return (false);
}
return (queryHints.contains(queryHint));
}
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.delete;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
@ -139,6 +140,24 @@ public class DeleteInput extends AbstractTableActionInput
/*******************************************************************************
** Fluently add 1 primary key to the delete input
**
*******************************************************************************/
public DeleteInput withPrimaryKey(Serializable primaryKey)
{
if(primaryKeys == null)
{
primaryKeys = new ArrayList<>();
}
primaryKeys.add(primaryKey);
return (this);
}
/*******************************************************************************
** Setter for ids
**

View File

@ -25,10 +25,12 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.get;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
@ -60,6 +62,8 @@ public class GetInput extends AbstractTableActionInput implements QueryOrGetInpu
private boolean includeAssociations = false;
private Collection<String> associationNamesToInclude = null;
private EnumSet<QueryHint> queryHints = EnumSet.noneOf(QueryHint.class);
/*******************************************************************************
@ -462,4 +466,79 @@ public class GetInput extends AbstractTableActionInput implements QueryOrGetInpu
return (this);
}
/*******************************************************************************
** Getter for queryHints
*******************************************************************************/
public EnumSet<QueryHint> getQueryHints()
{
return (this.queryHints);
}
/*******************************************************************************
** Setter for queryHints
*******************************************************************************/
public void setQueryHints(EnumSet<QueryHint> queryHints)
{
this.queryHints = queryHints;
}
/*******************************************************************************
** Fluent setter for queryHints
*******************************************************************************/
public GetInput withQueryHints(EnumSet<QueryHint> queryHints)
{
this.queryHints = queryHints;
return (this);
}
/*******************************************************************************
** Fluent setter for queryHints
*******************************************************************************/
public GetInput withQueryHint(QueryHint queryHint)
{
if(this.queryHints == null)
{
this.queryHints = EnumSet.noneOf(QueryHint.class);
}
this.queryHints.add(queryHint);
return (this);
}
/*******************************************************************************
** Fluent setter for queryHints
*******************************************************************************/
public GetInput withoutQueryHint(QueryHint queryHint)
{
if(this.queryHints != null)
{
this.queryHints.remove(queryHint);
}
return (this);
}
/*******************************************************************************
** null-safely check if query hints map contains the specified hint
*******************************************************************************/
public boolean hasQueryHint(QueryHint queryHint)
{
if(this.queryHints == null)
{
return (false);
}
return (queryHints.contains(queryHint));
}
}

View File

@ -112,7 +112,7 @@ public class InsertInput extends AbstractTableActionInput
/*******************************************************************************
**
*******************************************************************************/
public InsertInput withRecordEntities(List<QRecordEntity> recordEntityList)
public InsertInput withRecordEntities(List<? extends QRecordEntity> recordEntityList)
{
for(QRecordEntity recordEntity : CollectionUtils.nonNullList(recordEntityList))
{

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@ -37,12 +38,18 @@ import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
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.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.NullValueBehaviorUtil;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
import org.apache.logging.log4j.Level;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -60,11 +67,23 @@ public class JoinsContext
private final String mainTableName;
private final List<QueryJoin> queryJoins;
private final QQueryFilter securityFilter;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// pointer either at securityFilter, or at a sub-filter within it, for when we're doing a recursive build-out of multi-locks //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private QQueryFilter securityFilterCursor;
////////////////////////////////////////////////////////////////
// note - will have entries for all tables, not just aliases. //
////////////////////////////////////////////////////////////////
private final Map<String, String> aliasToTableNameMap = new HashMap<>();
private Level logLevel = Level.OFF;
/////////////////////////////////////////////////////////////////////////////
// we will get a TON of more output if this gets turned up, so be cautious //
/////////////////////////////////////////////////////////////////////////////
private Level logLevel = Level.OFF;
private Level logLevelForFilter = Level.OFF;
@ -74,54 +93,225 @@ public class JoinsContext
*******************************************************************************/
public JoinsContext(QInstance instance, String tableName, List<QueryJoin> queryJoins, QQueryFilter filter) throws QException
{
log("--- START ----------------------------------------------------------------------", logPair("mainTable", tableName));
this.instance = instance;
this.mainTableName = tableName;
this.queryJoins = new MutableList<>(queryJoins);
this.securityFilter = new QQueryFilter();
this.securityFilterCursor = this.securityFilter;
// log("--- START ----------------------------------------------------------------------", logPair("mainTable", tableName));
dumpDebug(true, false);
for(QueryJoin queryJoin : this.queryJoins)
{
log("Processing input query join", logPair("joinTable", queryJoin.getJoinTable()), logPair("alias", queryJoin.getAlias()), logPair("baseTableOrAlias", queryJoin.getBaseTableOrAlias()), logPair("joinMetaDataName", () -> queryJoin.getJoinMetaData().getName()));
processQueryJoin(queryJoin);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
// make sure that all tables specified in filter columns are being brought into the query as joins //
/////////////////////////////////////////////////////////////////////////////////////////////////////
ensureFilterIsRepresented(filter);
logFilter("After ensureFilterIsRepresented:", securityFilter);
///////////////////////////////////////////////////////////////////////////////////////
// ensure that any record locks on the main table, which require a join, are present //
///////////////////////////////////////////////////////////////////////////////////////
MultiRecordSecurityLock multiRecordSecurityLock = RecordSecurityLockFilters.filterForReadLockTree(CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks()));
for(RecordSecurityLock lock : multiRecordSecurityLock.getLocks())
{
ensureRecordSecurityLockIsRepresented(tableName, tableName, lock, null);
logFilter("After ensureRecordSecurityLockIsRepresented[fieldName=" + lock.getFieldName() + "]:", securityFilter);
}
///////////////////////////////////////////////////////////////////////////////////
// make sure that all joins in the query have meta data specified //
// e.g., a user-added join may just specify the join-table //
// or a join implicitly added from a filter may also not have its join meta data //
///////////////////////////////////////////////////////////////////////////////////
fillInMissingJoinMetaData();
logFilter("After fillInMissingJoinMetaData:", securityFilter);
///////////////////////////////////////////////////////////////
// ensure any joins that contribute a recordLock are present //
///////////////////////////////////////////////////////////////
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks())))
{
ensureRecordSecurityLockIsRepresented(instance, tableName, recordSecurityLock);
}
ensureAllJoinRecordSecurityLocksAreRepresented(instance);
logFilter("After ensureAllJoinRecordSecurityLocksAreRepresented:", securityFilter);
ensureFilterIsRepresented(filter);
addJoinsFromExposedJoinPaths();
/* todo!!
for(QueryJoin queryJoin : queryJoins)
{
QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks()))
{
// addCriteriaForRecordSecurityLock(instance, session, joinTable, securityCriteria, recordSecurityLock, joinsContext, queryJoin.getJoinTableOrItsAlias());
}
}
*/
////////////////////////////////////////////////////////////////////////////////////
// if there were any security filters built, then put those into the input filter //
////////////////////////////////////////////////////////////////////////////////////
addSecurityFiltersToInputFilter(filter);
log("Constructed JoinsContext", logPair("mainTableName", this.mainTableName), logPair("queryJoins", this.queryJoins.stream().map(qj -> qj.getJoinTable()).collect(Collectors.joining(","))));
log("--- END ------------------------------------------------------------------------");
log("", logPair("securityFilter", securityFilter));
log("", logPair("fullFilter", filter));
dumpDebug(false, true);
// log("--- END ------------------------------------------------------------------------");
}
/*******************************************************************************
**
** Update the input filter with any security filters that were built.
*******************************************************************************/
private void ensureRecordSecurityLockIsRepresented(QInstance instance, String tableName, RecordSecurityLock recordSecurityLock) throws QException
private void addSecurityFiltersToInputFilter(QQueryFilter filter)
{
////////////////////////////////////////////////////////////////////////////////////
// if there's no security filter criteria (including sub-filters), return w/ noop //
////////////////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeIsEmpty(securityFilter.getSubFilters()))
{
return;
}
///////////////////////////////////////////////////////////////////////
// if the input filter is an OR we need to replace it with a new AND //
///////////////////////////////////////////////////////////////////////
if(filter.getBooleanOperator().equals(QQueryFilter.BooleanOperator.OR))
{
List<QFilterCriteria> originalCriteria = filter.getCriteria();
List<QQueryFilter> originalSubFilters = filter.getSubFilters();
QQueryFilter replacementFilter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
replacementFilter.setCriteria(originalCriteria);
replacementFilter.setSubFilters(originalSubFilters);
filter.setCriteria(new ArrayList<>());
filter.setSubFilters(new ArrayList<>());
filter.setBooleanOperator(QQueryFilter.BooleanOperator.AND);
filter.addSubFilter(replacementFilter);
}
filter.addSubFilter(securityFilter);
}
/*******************************************************************************
** In case we've added any joins to the query that have security locks which
** weren't previously added to the query, add them now. basically, this is
** calling ensureRecordSecurityLockIsRepresented for each queryJoin.
*******************************************************************************/
private void ensureAllJoinRecordSecurityLocksAreRepresented(QInstance instance) throws QException
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// avoid concurrent modification exceptions by doing a double-loop and breaking the inner any time anything gets added //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Set<QueryJoin> processedQueryJoins = new HashSet<>();
boolean addedAnyThisIteration = true;
while(addedAnyThisIteration)
{
addedAnyThisIteration = false;
for(QueryJoin queryJoin : this.queryJoins)
{
boolean addedAnyForThisJoin = false;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// avoid double-processing the same query join //
// or adding security filters for a join who was only added to the query so that we could add locks (an ImplicitQueryJoinForSecurityLock) //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(processedQueryJoins.contains(queryJoin) || queryJoin instanceof ImplicitQueryJoinForSecurityLock)
{
continue;
}
processedQueryJoins.add(queryJoin);
//////////////////////////////////////////////////////////////////////////////////////////
// process all locks on this join's join-table. keep track if any new joins were added //
//////////////////////////////////////////////////////////////////////////////////////////
QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
MultiRecordSecurityLock multiRecordSecurityLock = RecordSecurityLockFilters.filterForReadLockTree(CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks()));
for(RecordSecurityLock lock : multiRecordSecurityLock.getLocks())
{
List<QueryJoin> addedQueryJoins = ensureRecordSecurityLockIsRepresented(joinTable.getName(), queryJoin.getJoinTableOrItsAlias(), lock, queryJoin);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if any joins were added by this call, add them to the set of processed ones, so they don't get re-processed. //
// also mark the flag that any were added for this join, to manage the double-looping //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(addedQueryJoins))
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// make all new joins added in that method be of the same type (inner/left/etc) as the query join they are connected to //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QueryJoin addedQueryJoin : addedQueryJoins)
{
addedQueryJoin.setType(queryJoin.getType());
}
processedQueryJoins.addAll(addedQueryJoins);
addedAnyForThisJoin = true;
}
}
///////////////////////////////////////////////////////////////////////////////////////////////
// if any new joins were added, we need to break the inner-loop, and continue the outer loop //
// e.g., to process the next query join (but we can't just go back to the foreach queryJoin, //
// because it would fail with concurrent modification) //
///////////////////////////////////////////////////////////////////////////////////////////////
if(addedAnyForThisJoin)
{
addedAnyThisIteration = true;
break;
}
}
}
}
/*******************************************************************************
** For a given recordSecurityLock on a given table (with a possible alias),
** make sure that if any joins are needed to get to the lock, that they are in the query.
**
** returns the list of query joins that were added, if any were added
*******************************************************************************/
private List<QueryJoin> ensureRecordSecurityLockIsRepresented(String tableName, String tableNameOrAlias, RecordSecurityLock recordSecurityLock, QueryJoin sourceQueryJoin) throws QException
{
List<QueryJoin> addedQueryJoins = new ArrayList<>();
////////////////////////////////////////////////////////////////////////////
// if this lock is a multi-lock, then recursively process its child-locks //
////////////////////////////////////////////////////////////////////////////
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
{
log("Processing MultiRecordSecurityLock...");
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// make a new level in the filter-tree - storing old cursor, and updating cursor to point at new level //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
QQueryFilter oldSecurityFilterCursor = this.securityFilterCursor;
QQueryFilter nextLevelSecurityFilter = new QQueryFilter();
this.securityFilterCursor.addSubFilter(nextLevelSecurityFilter);
this.securityFilterCursor = nextLevelSecurityFilter;
///////////////////////////////////////
// set the boolean operator to match //
///////////////////////////////////////
nextLevelSecurityFilter.setBooleanOperator(multiRecordSecurityLock.getOperator().toFilterOperator());
//////////////////////
// process children //
//////////////////////
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
{
log(" - Recursive call for childLock: " + childLock);
addedQueryJoins.addAll(ensureRecordSecurityLockIsRepresented(tableName, tableNameOrAlias, childLock, sourceQueryJoin));
}
////////////////////
// restore cursor //
////////////////////
this.securityFilterCursor = oldSecurityFilterCursor;
return addedQueryJoins;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// ok - so - the join name chain is going to be like this: //
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
// A join name chain is going to look like this: //
// for a table: orderLineItemExtrinsic (that's 2 away from order, where its security field is): //
// - securityFieldName = order.clientId //
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
@ -129,30 +319,30 @@ public class JoinsContext
///////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
Collections.reverse(joinNameChain);
log("Evaluating recordSecurityLock", logPair("recordSecurityLock", recordSecurityLock.getFieldName()), logPair("joinNameChain", joinNameChain));
log("Evaluating recordSecurityLock. Join name chain is of length: " + joinNameChain.size(), logPair("tableNameOrAlias", tableNameOrAlias), logPair("recordSecurityLock", recordSecurityLock.getFieldName()), logPair("joinNameChain", joinNameChain));
QTableMetaData tmpTable = instance.getTable(mainTableName);
QTableMetaData tmpTable = instance.getTable(tableName);
String securityFieldTableAlias = tableNameOrAlias;
String baseTableOrAlias = tableNameOrAlias;
boolean chainIsInner = true;
if(sourceQueryJoin != null && QueryJoin.Type.isOuter(sourceQueryJoin.getType()))
{
chainIsInner = false;
}
for(String joinName : joinNameChain)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////
// check the joins currently in the query - if any are for this table, then we don't need to add one //
///////////////////////////////////////////////////////////////////////////////////////////////////////
List<QueryJoin> matchingJoins = this.queryJoins.stream().filter(queryJoin ->
//////////////////////////////////////////////////////////////////////////////////////////////////
// check the joins currently in the query - if any are THIS join, then we don't need to add one //
//////////////////////////////////////////////////////////////////////////////////////////////////
List<QueryJoin> matchingQueryJoins = this.queryJoins.stream().filter(queryJoin ->
{
QJoinMetaData joinMetaData = null;
if(queryJoin.getJoinMetaData() != null)
{
joinMetaData = queryJoin.getJoinMetaData();
}
else
{
joinMetaData = findJoinMetaData(instance, tableName, queryJoin.getJoinTable());
}
QJoinMetaData joinMetaData = queryJoin.getJoinMetaData();
return (joinMetaData != null && Objects.equals(joinMetaData.getName(), joinName));
}).toList();
if(CollectionUtils.nullSafeHasContents(matchingJoins))
if(CollectionUtils.nullSafeHasContents(matchingQueryJoins))
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// note - if a user added a join as an outer type, we need to change it to be inner, for the security purpose. //
@ -160,11 +350,40 @@ public class JoinsContext
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
log("- skipping join already in the query", logPair("joinName", joinName));
if(matchingJoins.get(0).getType().equals(QueryJoin.Type.LEFT) || matchingJoins.get(0).getType().equals(QueryJoin.Type.RIGHT))
QueryJoin matchedQueryJoin = matchingQueryJoins.get(0);
if(matchedQueryJoin.getType().equals(QueryJoin.Type.LEFT) || matchedQueryJoin.getType().equals(QueryJoin.Type.RIGHT))
{
chainIsInner = false;
}
/* ?? todo ??
if(matchedQueryJoin.getType().equals(QueryJoin.Type.LEFT) || matchedQueryJoin.getType().equals(QueryJoin.Type.RIGHT))
{
log("- - although... it was here as an outer - so switching it to INNER", logPair("joinName", joinName));
matchingJoins.get(0).setType(QueryJoin.Type.INNER);
matchedQueryJoin.setType(QueryJoin.Type.INNER);
}
*/
//////////////////////////////////////////////////////////////////////////////////////////////////////
// as we're walking from tmpTable to the table which ultimately has the security key field, //
// if the queryJoin we just found is joining out to tmpTable, then we need to advance tmpTable back //
// to the queryJoin's base table - else, tmpTable advances to the matched queryJoin's joinTable //
//////////////////////////////////////////////////////////////////////////////////////////////////////
if(tmpTable.getName().equals(matchedQueryJoin.getJoinTable()))
{
securityFieldTableAlias = Objects.requireNonNullElse(matchedQueryJoin.getBaseTableOrAlias(), mainTableName);
}
else
{
securityFieldTableAlias = matchedQueryJoin.getJoinTableOrItsAlias();
}
tmpTable = instance.getTable(securityFieldTableAlias);
////////////////////////////////////////////////////////////////////////////////////////
// set the baseTableOrAlias for the next iteration to be this join's joinTableOrAlias //
////////////////////////////////////////////////////////////////////////////////////////
baseTableOrAlias = securityFieldTableAlias;
continue;
}
@ -172,20 +391,233 @@ public class JoinsContext
QJoinMetaData join = instance.getJoin(joinName);
if(join.getLeftTable().equals(tmpTable.getName()))
{
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin, "forRecordSecurityLock (non-flipped)");
securityFieldTableAlias = join.getRightTable() + "_forSecurityJoin_" + join.getName();
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock()
.withJoinMetaData(join)
.withType(chainIsInner ? QueryJoin.Type.INNER : QueryJoin.Type.LEFT)
.withBaseTableOrAlias(baseTableOrAlias)
.withAlias(securityFieldTableAlias);
if(securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.OR)
{
queryJoin.withType(QueryJoin.Type.LEFT);
chainIsInner = false;
}
addQueryJoin(queryJoin, "forRecordSecurityLock (non-flipped)", "- ");
addedQueryJoins.add(queryJoin);
tmpTable = instance.getTable(join.getRightTable());
}
else if(join.getRightTable().equals(tmpTable.getName()))
{
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join.flip()).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin, "forRecordSecurityLock (flipped)");
securityFieldTableAlias = join.getLeftTable() + "_forSecurityJoin_" + join.getName();
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock()
.withJoinMetaData(join.flip())
.withType(chainIsInner ? QueryJoin.Type.INNER : QueryJoin.Type.LEFT)
.withBaseTableOrAlias(baseTableOrAlias)
.withAlias(securityFieldTableAlias);
if(securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.OR)
{
queryJoin.withType(QueryJoin.Type.LEFT);
chainIsInner = false;
}
addQueryJoin(queryJoin, "forRecordSecurityLock (flipped)", "- ");
addedQueryJoins.add(queryJoin);
tmpTable = instance.getTable(join.getLeftTable());
}
else
{
dumpDebug(false, true);
throw (new QException("Error adding security lock joins to query - table name [" + tmpTable.getName() + "] not found in join [" + joinName + "]"));
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// for the next iteration of the loop, set the next join's baseTableOrAlias to be the alias we just created //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
baseTableOrAlias = securityFieldTableAlias;
}
////////////////////////////////////////////////////////////////////////////////////
// now that we know the joins/tables are in the query, add to the security filter //
////////////////////////////////////////////////////////////////////////////////////
QueryJoin lastAddedQueryJoin = addedQueryJoins.isEmpty() ? null : addedQueryJoins.get(addedQueryJoins.size() - 1);
if(sourceQueryJoin != null && lastAddedQueryJoin == null)
{
lastAddedQueryJoin = sourceQueryJoin;
}
addSubFilterForRecordSecurityLock(recordSecurityLock, tmpTable, securityFieldTableAlias, !chainIsInner, lastAddedQueryJoin);
log("Finished evaluating recordSecurityLock");
return (addedQueryJoins);
}
/*******************************************************************************
**
*******************************************************************************/
private void addSubFilterForRecordSecurityLock(RecordSecurityLock recordSecurityLock, QTableMetaData table, String tableNameOrAlias, boolean isOuter, QueryJoin sourceQueryJoin)
{
QSession session = QContext.getQSession();
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// check if the key type has an all-access key, and if so, if it's set to true for the current user/session //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
QSecurityKeyType securityKeyType = instance.getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
boolean haveAllAccessKey = false;
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if we have all-access on this key, then we don't need a criterion for it (as long as we're in an AND filter) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(session.hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
{
haveAllAccessKey = true;
if(sourceQueryJoin != null)
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// in case the queryJoin object is re-used between queries, and its security criteria need to be different (!!), reset it //
// this can be exposed in tests - maybe not entirely expected in real-world, but seems safe enough //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
sourceQueryJoin.withSecurityCriteria(new ArrayList<>());
}
////////////////////////////////////////////////////////////////////////////////////////
// if we're in an AND filter, then we don't need a criteria for this lock, so return. //
////////////////////////////////////////////////////////////////////////////////////////
boolean inAnAndFilter = securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.AND;
if(inAnAndFilter)
{
return;
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////
// for locks w/o a join chain, the lock fieldName will simply be a field on the table. //
// so just prepend that with the tableNameOrAlias. //
/////////////////////////////////////////////////////////////////////////////////////////
String fieldName = tableNameOrAlias + "." + recordSecurityLock.getFieldName();
if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()))
{
/////////////////////////////////////////////////////////////////////////////////
// else, expect a "table.field" in the lock fieldName - but we want to replace //
// the table name part with a possible alias that we took in. //
/////////////////////////////////////////////////////////////////////////////////
String[] parts = recordSecurityLock.getFieldName().split("\\.");
if(parts.length != 2)
{
dumpDebug(false, true);
throw new IllegalArgumentException("Mal-formatted recordSecurityLock fieldName for lock with joinNameChain in query: " + fieldName);
}
fieldName = tableNameOrAlias + "." + parts[1];
}
///////////////////////////////////////////////////////////////////////////////////////////
// else - get the key values from the session and decide what kind of criterion to build //
///////////////////////////////////////////////////////////////////////////////////////////
QQueryFilter lockFilter = new QQueryFilter();
List<QFilterCriteria> lockCriteria = new ArrayList<>();
lockFilter.setCriteria(lockCriteria);
QFieldType type = QFieldType.INTEGER;
try
{
JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = getFieldAndTableNameOrAlias(fieldName);
type = fieldAndTableNameOrAlias.field().getType();
}
catch(Exception e)
{
LOG.debug("Error getting field type... Trying Integer", e);
}
if(haveAllAccessKey)
{
////////////////////////////////////////////////////////////////////////////////////////////
// if we have an all access key (but we got here because we're part of an OR query), then //
// write a criterion that will always be true - e.g., field=field //
////////////////////////////////////////////////////////////////////////////////////////////
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.TRUE));
}
else
{
List<Serializable> securityKeyValues = session.getSecurityKeyValues(recordSecurityLock.getSecurityKeyType(), type);
if(CollectionUtils.nullSafeIsEmpty(securityKeyValues))
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// handle user with no values -- they can only see null values, and only iff the lock's null-value behavior is ALLOW //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK));
}
else
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else, if no user/session values, and null-value behavior is deny, then setup a FALSE condition, to allow no rows. //
// todo - maybe avoid running the whole query - as you're not allowed ANY records (based on boolean tree down to this point) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.FALSE));
}
}
else
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else, if user/session has some values, build an IN rule - //
// noting that if the lock's null-value behavior is ALLOW, then we actually want IS_NULL_OR_IN, not just IN //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_NULL_OR_IN, securityKeyValues));
}
else
{
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IN, securityKeyValues));
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there's a sourceQueryJoin, then set the lockCriteria on that join - so it gets written into the JOIN ... ON clause //
// ... unless we're writing an OR filter. then we need the condition in the WHERE clause //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
boolean doNotPutCriteriaInJoinOn = securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.OR;
if(sourceQueryJoin != null && !doNotPutCriteriaInJoinOn)
{
sourceQueryJoin.withSecurityCriteria(lockCriteria);
}
else
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// we used to add an OR IS NULL for cases of an outer-join - but instead, this is now handled by putting the lockCriteria //
// into the join (see above) - so this check is probably deprecated. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if this field is on the outer side of an outer join, then if we do a straight filter on it, then we're basically //
// nullifying the outer join... so for an outer join use-case, OR the security field criteria with a primary-key IS NULL //
// which will make missing rows from the join be found. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(isOuter)
{
if(table == null)
{
table = QContext.getQInstance().getTable(aliasToTableNameMap.get(tableNameOrAlias));
}
lockFilter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
lockFilter.addCriteria(new QFilterCriteria(tableNameOrAlias + "." + table.getPrimaryKeyField(), QCriteriaOperator.IS_BLANK));
}
*/
/////////////////////////////////////////////////////////////////////////////////////////////////////
// If this filter isn't for a queryJoin, then just add it to the main list of security sub-filters //
/////////////////////////////////////////////////////////////////////////////////////////////////////
this.securityFilterCursor.addSubFilter(lockFilter);
}
}
@ -197,9 +629,9 @@ public class JoinsContext
** use this method to add to the list, instead of ever adding directly, as it's
** important do to that process step (and we've had bugs when it wasn't done).
*******************************************************************************/
private void addQueryJoin(QueryJoin queryJoin, String reason) throws QException
private void addQueryJoin(QueryJoin queryJoin, String reason, String logPrefix) throws QException
{
log("Adding query join to context",
log(Objects.requireNonNullElse(logPrefix, "") + "Adding query join to context",
logPair("reason", reason),
logPair("joinTable", queryJoin.getJoinTable()),
logPair("joinMetaData.name", () -> queryJoin.getJoinMetaData().getName()),
@ -208,34 +640,46 @@ public class JoinsContext
);
this.queryJoins.add(queryJoin);
processQueryJoin(queryJoin);
dumpDebug(false, false);
}
/*******************************************************************************
** If there are any joins in the context that don't have a join meta data, see
** if we can find the JoinMetaData to use for them by looking at the main table's
** exposed joins, and using their join paths.
** if we can find the JoinMetaData to use for them by looking at all joins in the
** instance, or at the main table's exposed joins, and using their join paths.
*******************************************************************************/
private void addJoinsFromExposedJoinPaths() throws QException
private void fillInMissingJoinMetaData() throws QException
{
log("Begin adding missing join meta data");
////////////////////////////////////////////////////////////////////////////////
// do a double-loop, to avoid concurrent modification on the queryJoins list. //
// that is to say, we'll loop over that list, but possibly add things to it, //
// in which case we'll set this flag, and break the inner loop, to go again. //
////////////////////////////////////////////////////////////////////////////////
boolean addedJoin;
Set<QueryJoin> processedQueryJoins = new HashSet<>();
boolean addedJoin;
do
{
addedJoin = false;
for(QueryJoin queryJoin : queryJoins)
{
if(processedQueryJoins.contains(queryJoin))
{
continue;
}
processedQueryJoins.add(queryJoin);
///////////////////////////////////////////////////////////////////////////////////////////////
// if the join has joinMetaData, then we don't need to process it... unless it needs flipped //
///////////////////////////////////////////////////////////////////////////////////////////////
QJoinMetaData joinMetaData = queryJoin.getJoinMetaData();
if(joinMetaData != null)
{
log("- QueryJoin already has joinMetaData", logPair("joinMetaDataName", joinMetaData.getName()));
boolean isJoinLeftTableInQuery = false;
String joinMetaDataLeftTable = joinMetaData.getLeftTable();
if(joinMetaDataLeftTable.equals(mainTableName))
@ -265,7 +709,7 @@ public class JoinsContext
/////////////////////////////////////////////////////////////////////////////////
if(!isJoinLeftTableInQuery)
{
log("Flipping queryJoin because its leftTable wasn't found in the query", logPair("joinMetaDataName", joinMetaData.getName()), logPair("leftTable", joinMetaDataLeftTable));
log("- - Flipping queryJoin because its leftTable wasn't found in the query", logPair("joinMetaDataName", joinMetaData.getName()), logPair("leftTable", joinMetaDataLeftTable));
queryJoin.setJoinMetaData(joinMetaData.flip());
}
}
@ -275,11 +719,13 @@ public class JoinsContext
// try to find a direct join between the main table and this table. //
// if one is found, then put it (the meta data) on the query join. //
//////////////////////////////////////////////////////////////////////
log("- QueryJoin doesn't have metaData - looking for it", logPair("joinTableOrItsAlias", queryJoin.getJoinTableOrItsAlias()));
String baseTableName = Objects.requireNonNullElse(resolveTableNameOrAliasToTableName(queryJoin.getBaseTableOrAlias()), mainTableName);
QJoinMetaData found = findJoinMetaData(instance, baseTableName, queryJoin.getJoinTable());
QJoinMetaData found = findJoinMetaData(baseTableName, queryJoin.getJoinTable(), true);
if(found != null)
{
log("Found joinMetaData - setting it in queryJoin", logPair("joinMetaDataName", found.getName()), logPair("baseTableName", baseTableName), logPair("joinTable", queryJoin.getJoinTable()));
log("- - Found joinMetaData - setting it in queryJoin", logPair("joinMetaDataName", found.getName()), logPair("baseTableName", baseTableName), logPair("joinTable", queryJoin.getJoinTable()));
queryJoin.setJoinMetaData(found);
}
else
@ -293,7 +739,7 @@ public class JoinsContext
{
if(queryJoin.getJoinTable().equals(exposedJoin.getJoinTable()))
{
log("Found an exposed join", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()), logPair("joinPath", exposedJoin.getJoinPath()));
log("- - Found an exposed join", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()), logPair("joinPath", exposedJoin.getJoinPath()));
/////////////////////////////////////////////////////////////////////////////////////
// loop backward through the join path (from the joinTable back to the main table) //
@ -304,6 +750,7 @@ public class JoinsContext
{
String joinName = exposedJoin.getJoinPath().get(i);
QJoinMetaData joinToAdd = instance.getJoin(joinName);
log("- - - evaluating joinPath element", logPair("i", i), logPair("joinName", joinName));
/////////////////////////////////////////////////////////////////////////////
// get the name from the opposite side of the join (flipping it if needed) //
@ -332,15 +779,22 @@ public class JoinsContext
queryJoin.setBaseTableOrAlias(nextTable);
}
queryJoin.setJoinMetaData(joinToAdd);
log("- - - - this is the last element in the join path, so setting this joinMetaData on the original queryJoin");
}
else
{
QueryJoin queryJoinToAdd = makeQueryJoinFromJoinAndTableNames(nextTable, tmpTable, joinToAdd);
queryJoinToAdd.setType(queryJoin.getType());
addedAnyQueryJoins = true;
this.addQueryJoin(queryJoinToAdd, "forExposedJoin");
log("- - - - this is not the last element in the join path, so adding a new query join:");
addQueryJoin(queryJoinToAdd, "forExposedJoin", "- - - - - - ");
dumpDebug(false, false);
}
}
else
{
log("- - - - join doesn't need added to the query");
}
tmpTable = nextTable;
}
@ -361,6 +815,7 @@ public class JoinsContext
}
while(addedJoin);
log("Done adding missing join meta data");
}
@ -370,12 +825,12 @@ public class JoinsContext
*******************************************************************************/
private boolean doesJoinNeedAddedToQuery(String joinName)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// look at all queryJoins already in context - if any have this join's name, then we don't need this join... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// look at all queryJoins already in context - if any have this join's name, and aren't implicit-security joins, then we don't need this join... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QueryJoin queryJoin : queryJoins)
{
if(queryJoin.getJoinMetaData() != null && queryJoin.getJoinMetaData().getName().equals(joinName))
if(queryJoin.getJoinMetaData() != null && queryJoin.getJoinMetaData().getName().equals(joinName) && !(queryJoin instanceof ImplicitQueryJoinForSecurityLock))
{
return (false);
}
@ -395,6 +850,7 @@ public class JoinsContext
String tableNameOrAlias = queryJoin.getJoinTableOrItsAlias();
if(aliasToTableNameMap.containsKey(tableNameOrAlias))
{
dumpDebug(false, true);
throw (new QException("Duplicate table name or alias: " + tableNameOrAlias));
}
aliasToTableNameMap.put(tableNameOrAlias, joinTable.getName());
@ -439,6 +895,7 @@ public class JoinsContext
String[] parts = fieldName.split("\\.");
if(parts.length != 2)
{
dumpDebug(false, true);
throw new IllegalArgumentException("Mal-formatted field name in query: " + fieldName);
}
@ -449,6 +906,7 @@ public class JoinsContext
QTableMetaData table = instance.getTable(tableName);
if(table == null)
{
dumpDebug(false, true);
throw new IllegalArgumentException("Could not find table [" + tableName + "] in instance for query");
}
return new FieldAndTableNameOrAlias(table.getField(baseFieldName), tableOrAlias);
@ -503,17 +961,17 @@ public class JoinsContext
for(String filterTable : filterTables)
{
log("Evaluating filterTable", logPair("filterTable", filterTable));
log("Evaluating filter", logPair("filterTable", filterTable));
if(!aliasToTableNameMap.containsKey(filterTable) && !Objects.equals(mainTableName, filterTable))
{
log("- table not in query - adding it", logPair("filterTable", filterTable));
log("- table not in query - adding a join for it", logPair("filterTable", filterTable));
boolean found = false;
for(QJoinMetaData join : CollectionUtils.nonNullMap(QContext.getQInstance().getJoins()).values())
{
QueryJoin queryJoin = makeQueryJoinFromJoinAndTableNames(mainTableName, filterTable, join);
if(queryJoin != null)
{
this.addQueryJoin(queryJoin, "forFilter (join found in instance)");
addQueryJoin(queryJoin, "forFilter (join found in instance)", "- - ");
found = true;
break;
}
@ -522,9 +980,13 @@ public class JoinsContext
if(!found)
{
QueryJoin queryJoin = new QueryJoin().withJoinTable(filterTable).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin, "forFilter (join not found in instance)");
addQueryJoin(queryJoin, "forFilter (join not found in instance)", "- - ");
}
}
else
{
log("- table is already in query - not adding any joins", logPair("filterTable", filterTable));
}
}
}
@ -566,6 +1028,11 @@ public class JoinsContext
getTableNameFromFieldNameAndAddToSet(criteria.getOtherFieldName(), filterTables);
}
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(filter.getOrderBys()))
{
getTableNameFromFieldNameAndAddToSet(orderBy.getFieldName(), filterTables);
}
for(QQueryFilter subFilter : CollectionUtils.nonNullList(filter.getSubFilters()))
{
populateFilterTablesSet(subFilter, filterTables);
@ -592,7 +1059,7 @@ public class JoinsContext
/*******************************************************************************
**
*******************************************************************************/
public QJoinMetaData findJoinMetaData(QInstance instance, String baseTableName, String joinTableName)
public QJoinMetaData findJoinMetaData(String baseTableName, String joinTableName, boolean useExposedJoins)
{
List<QJoinMetaData> matches = new ArrayList<>();
if(baseTableName != null)
@ -644,7 +1111,29 @@ public class JoinsContext
}
else if(matches.size() > 1)
{
throw (new RuntimeException("More than 1 join was found between [" + baseTableName + "] and [" + joinTableName + "]. Specify which one in your QueryJoin."));
////////////////////////////////////////////////////////////////////////////////
// if we found more than one join, but we're allowed to useExposedJoins, then //
// see if we can tell which match to used based on the table's exposed joins //
////////////////////////////////////////////////////////////////////////////////
if(useExposedJoins)
{
QTableMetaData mainTable = QContext.getQInstance().getTable(mainTableName);
for(ExposedJoin exposedJoin : mainTable.getExposedJoins())
{
if(exposedJoin.getJoinTable().equals(joinTableName))
{
// todo ... is it wrong to always use 0??
return instance.getJoin(exposedJoin.getJoinPath().get(0));
}
}
}
///////////////////////////////////////////////
// if we couldn't figure it out, then throw. //
///////////////////////////////////////////////
dumpDebug(false, true);
throw (new RuntimeException("More than 1 join was found between [" + baseTableName + "] and [" + joinTableName + "] "
+ (useExposedJoins ? "(and exposed joins didn't clarify which one to use). " : "") + "Specify which one in your QueryJoin."));
}
return (null);
@ -669,4 +1158,79 @@ public class JoinsContext
LOG.log(logLevel, message, null, logPairs);
}
/*******************************************************************************
**
*******************************************************************************/
private void logFilter(String message, QQueryFilter filter)
{
if(logLevelForFilter.equals(Level.OFF))
{
return;
}
System.out.println(message + "\n" + filter);
}
/*******************************************************************************
** Print (to stdout, for easier reading) the object in a big table format for
** debugging. Happens any time logLevel is > OFF. Not meant for loggly.
*******************************************************************************/
private void dumpDebug(boolean isStart, boolean isEnd)
{
if(logLevel.equals(Level.OFF))
{
return;
}
int sm = 8;
int md = 30;
int lg = 50;
int overhead = 14;
int full = sm + 3 * md + lg + overhead;
if(isStart)
{
System.out.println("\n" + StringUtils.safeTruncate("--- Start [main table: " + this.mainTableName + "] " + "-".repeat(full), full));
}
StringBuilder rs = new StringBuilder();
String formatString = "| %-" + md + "s | %-" + md + "s %-" + md + "s | %-" + lg + "s | %-" + sm + "s |\n";
rs.append(String.format(formatString, "Base Table", "Join Table", "(Alias)", "Join Meta Data", "Type"));
String dashesLg = "-".repeat(lg);
String dashesMd = "-".repeat(md);
String dashesSm = "-".repeat(sm);
rs.append(String.format(formatString, dashesMd, dashesMd, dashesMd, dashesLg, dashesSm));
if(CollectionUtils.nullSafeHasContents(queryJoins))
{
for(QueryJoin queryJoin : queryJoins)
{
rs.append(String.format(
formatString,
StringUtils.hasContent(queryJoin.getBaseTableOrAlias()) ? StringUtils.safeTruncate(queryJoin.getBaseTableOrAlias(), md) : "--",
StringUtils.safeTruncate(queryJoin.getJoinTable(), md),
(StringUtils.hasContent(queryJoin.getAlias()) ? "(" + StringUtils.safeTruncate(queryJoin.getAlias(), md - 2) + ")" : ""),
queryJoin.getJoinMetaData() == null ? "--" : StringUtils.safeTruncate(queryJoin.getJoinMetaData().getName(), lg),
queryJoin.getType()));
}
}
else
{
rs.append(String.format(formatString, "-empty-", "", "", "", ""));
}
System.out.print(rs);
System.out.println(securityFilter);
if(isEnd)
{
System.out.println(StringUtils.safeTruncate("--- End " + "-".repeat(full), full) + "\n");
}
else
{
System.out.println(StringUtils.safeTruncate("-".repeat(full), full));
}
}
}

View File

@ -49,5 +49,7 @@ public enum QCriteriaOperator
IS_BLANK,
IS_NOT_BLANK,
BETWEEN,
NOT_BETWEEN
NOT_BETWEEN,
TRUE,
FALSE
}

View File

@ -306,6 +306,11 @@ public class QFilterCriteria implements Serializable, Cloneable
@Override
public String toString()
{
if(fieldName == null)
{
return ("<null-field-criteria>");
}
StringBuilder rs = new StringBuilder(fieldName);
try
{

View File

@ -25,12 +25,18 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.FilterVariableExpression;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -137,7 +143,7 @@ public class QQueryFilter implements Serializable, Cloneable
/*******************************************************************************
**
** recursively look at both this filter, and any sub-filters it may have.
*******************************************************************************/
public boolean hasAnyCriteria()
{
@ -150,7 +156,7 @@ public class QQueryFilter implements Serializable, Cloneable
{
for(QQueryFilter subFilter : subFilters)
{
if(subFilter.hasAnyCriteria())
if(subFilter != null && subFilter.hasAnyCriteria())
{
return (true);
}
@ -360,23 +366,44 @@ public class QQueryFilter implements Serializable, Cloneable
StringBuilder rs = new StringBuilder("(");
try
{
int criteriaIndex = 0;
for(QFilterCriteria criterion : CollectionUtils.nonNullList(criteria))
{
rs.append(criterion).append(" ").append(getBooleanOperator()).append(" ");
if(criteriaIndex > 0)
{
rs.append(" ").append(getBooleanOperator()).append(" ");
}
rs.append(criterion);
criteriaIndex++;
}
for(QQueryFilter subFilter : CollectionUtils.nonNullList(subFilters))
if(CollectionUtils.nullSafeHasContents(subFilters))
{
rs.append(subFilter);
rs.append("Sub:{");
int subIndex = 0;
for(QQueryFilter subFilter : CollectionUtils.nonNullList(subFilters))
{
if(subIndex > 0)
{
rs.append(" ").append(getBooleanOperator()).append(" ");
}
rs.append(subFilter);
subIndex++;
}
rs.append("}");
}
rs.append(")");
rs.append("OrderBy[");
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(orderBys))
if(CollectionUtils.nullSafeHasContents(orderBys))
{
rs.append(orderBy).append(",");
rs.append("OrderBy[");
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(orderBys))
{
rs.append(orderBy).append(",");
}
rs.append("]");
}
rs.append("]");
}
catch(Exception e)
{
@ -389,6 +416,88 @@ public class QQueryFilter implements Serializable, Cloneable
/*******************************************************************************
** Replaces any FilterVariables' variableNames with one constructed from the field
** name, criteria, and index, camel style
**
*******************************************************************************/
public void prepForBackend()
{
Map<String, Integer> fieldOperatorMap = new HashMap<>();
for(QFilterCriteria criterion : getCriteria())
{
if(criterion.getValues() != null)
{
int criteriaIndex = 1;
int valueIndex = 0;
for(Serializable value : criterion.getValues())
{
///////////////////////////////////////////////////////////////////////////////
// keep track of what the index is for this criterion, this way if there are //
// more than one with the same id/operator values, we can differentiate //
///////////////////////////////////////////////////////////////////////////////
String backendName = getBackendName(criterion, valueIndex);
if(!fieldOperatorMap.containsKey(backendName))
{
fieldOperatorMap.put(backendName, criteriaIndex);
}
else
{
criteriaIndex = fieldOperatorMap.get(backendName) + 1;
fieldOperatorMap.put(backendName, criteriaIndex);
}
if(value instanceof FilterVariableExpression fve)
{
if(criteriaIndex > 1)
{
backendName += criteriaIndex;
}
fve.setVariableName(backendName);
}
valueIndex++;
}
}
}
}
/*******************************************************************************
** builds up a backend name for a field variable expression
**
*******************************************************************************/
private String getBackendName(QFilterCriteria criterion, int valueIndex)
{
StringBuilder backendName = new StringBuilder();
for(String fieldNameParts : criterion.getFieldName().split("\\."))
{
backendName.append(StringUtils.ucFirst(fieldNameParts));
}
for(String operatorParts : criterion.getOperator().name().split("_"))
{
backendName.append(StringUtils.ucFirst(operatorParts.toLowerCase()));
}
if(criterion.getOperator().equals(QCriteriaOperator.BETWEEN) || criterion.getOperator().equals(QCriteriaOperator.NOT_BETWEEN))
{
if(valueIndex == 0)
{
backendName.append("From");
}
else
{
backendName.append("To");
}
}
return (StringUtils.lcFirst(backendName.toString()));
}
/*******************************************************************************
** Replace any criteria values that look like ${input.XXX} with the value of XXX
** from the supplied inputValues map.
@ -397,8 +506,10 @@ public class QQueryFilter implements Serializable, Cloneable
** QQueryFilter - e.g., if it's one that defined in metaData, and that we don't
** want to be (permanently) changed!!
*******************************************************************************/
public void interpretValues(Map<String, Serializable> inputValues)
public void interpretValues(Map<String, Serializable> inputValues) throws QException
{
List<Exception> caughtExceptions = new ArrayList<>();
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
variableInterpreter.addValueMap("input", inputValues);
for(QFilterCriteria criterion : getCriteria())
@ -409,13 +520,45 @@ public class QQueryFilter implements Serializable, Cloneable
for(Serializable value : criterion.getValues())
{
String valueAsString = ValueUtils.getValueAsString(value);
Serializable interpretedValue = variableInterpreter.interpretForObject(valueAsString);
newValues.add(interpretedValue);
try
{
if(value instanceof AbstractFilterExpression<?>)
{
///////////////////////////////////////////////////////////////////////
// if a filter variable expression, evaluate the input values, which //
// will replace the variables with the corresponding actual values //
///////////////////////////////////////////////////////////////////////
if(value instanceof FilterVariableExpression filterVariableExpression)
{
newValues.add(filterVariableExpression.evaluateInputValues(inputValues));
}
else
{
newValues.add(value);
}
}
else
{
String valueAsString = ValueUtils.getValueAsString(value);
Serializable interpretedValue = variableInterpreter.interpretForObject(valueAsString);
newValues.add(interpretedValue);
}
}
catch(Exception e)
{
caughtExceptions.add(e);
}
}
criterion.setValues(newValues);
}
}
if(!caughtExceptions.isEmpty())
{
String message = "Error interpreting filter values: " + StringUtils.joinWithCommasAndAnd(caughtExceptions.stream().map(e -> e.getMessage()).toList());
boolean allUserFacing = caughtExceptions.stream().allMatch(QUserFacingException.class::isInstance);
throw (allUserFacing ? new QUserFacingException(message) : new QException(message));
}
}

View File

@ -25,19 +25,24 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
/*******************************************************************************
** Input data for the Query action
**
** Todo - maybe make a class between AbstractTableActionInput and {QueryInput,
** CountInput, and AggregateInput}, with common attributes for all of these
** "read" operations (like, queryHints,
*******************************************************************************/
public class QueryInput extends AbstractTableActionInput implements QueryOrGetInputInterface
public class QueryInput extends AbstractTableActionInput implements QueryOrGetInputInterface, Cloneable
{
private QBackendTransaction transaction;
private QQueryFilter filter;
@ -73,22 +78,6 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn
/*******************************************************************************
** Information about the query that an application (or qqq service) may know and
** want to tell the backend, that can help influence how the backend processes
** query.
**
** For example, a query with potentially a large result set, for MySQL backend,
** we may want to configure the result set to stream results rather than do its
** default in-memory thing. See RDBMSQueryAction for usage.
*******************************************************************************/
public enum QueryHint
{
POTENTIALLY_LARGE_NUMBER_OF_RESULTS
}
/*******************************************************************************
**
*******************************************************************************/
@ -109,6 +98,40 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn
/*******************************************************************************
**
*******************************************************************************/
@Override
public QueryInput clone() throws CloneNotSupportedException
{
QueryInput clone = (QueryInput) super.clone();
if(fieldsToTranslatePossibleValues != null)
{
clone.fieldsToTranslatePossibleValues = new HashSet<>(fieldsToTranslatePossibleValues);
}
if(queryJoins != null)
{
clone.queryJoins = new ArrayList<>(queryJoins);
}
if(clone.associationNamesToInclude != null)
{
clone.associationNamesToInclude = new HashSet<>(associationNamesToInclude);
}
if(queryHints != null)
{
clone.queryHints = EnumSet.noneOf(QueryHint.class);
clone.queryHints.addAll(queryHints);
}
return (clone);
}
/*******************************************************************************
** Getter for filter
**
@ -648,4 +671,19 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn
return (this);
}
/*******************************************************************************
** null-safely check if query hints map contains the specified hint
*******************************************************************************/
public boolean hasQueryHint(QueryHint queryHint)
{
if(this.queryHints == null)
{
return (false);
}
return (queryHints.contains(queryHint));
}
}

View File

@ -22,6 +22,8 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -49,6 +51,10 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
** specific joinMetaData to use must be set. The joinMetaData field can also be
** used instead of specify joinTable and baseTableOrAlias, but only for cases
** where the baseTable is not an alias.
**
** The securityCriteria member, in general, is meant to be populated when a
** JoinsContext is constructed before executing a query, and not meant to be set
** by users.
*******************************************************************************/
public class QueryJoin
{
@ -59,13 +65,30 @@ public class QueryJoin
private boolean select = false;
private Type type = Type.INNER;
private List<QFilterCriteria> securityCriteria = new ArrayList<>();
/*******************************************************************************
**
** define the types of joins - INNER, LEFT, RIGHT, or FULL.
*******************************************************************************/
public enum Type
{INNER, LEFT, RIGHT, FULL}
{
INNER,
LEFT,
RIGHT,
FULL;
/*******************************************************************************
** check if a join is an OUTER type (LEFT or RIGHT).
*******************************************************************************/
public static boolean isOuter(Type type)
{
return (LEFT == type || RIGHT == type);
}
}
@ -348,4 +371,66 @@ public class QueryJoin
return (this);
}
/*******************************************************************************
** Getter for securityCriteria
*******************************************************************************/
public List<QFilterCriteria> getSecurityCriteria()
{
return (this.securityCriteria);
}
/*******************************************************************************
** Setter for securityCriteria
*******************************************************************************/
public void setSecurityCriteria(List<QFilterCriteria> securityCriteria)
{
this.securityCriteria = securityCriteria;
}
/*******************************************************************************
** Fluent setter for securityCriteria
*******************************************************************************/
public QueryJoin withSecurityCriteria(List<QFilterCriteria> securityCriteria)
{
this.securityCriteria = securityCriteria;
return (this);
}
/*******************************************************************************
** Fluent setter for securityCriteria
*******************************************************************************/
public QueryJoin withSecurityCriteria(QFilterCriteria securityCriteria)
{
if(this.securityCriteria == null)
{
this.securityCriteria = new ArrayList<>();
}
this.securityCriteria.add(securityCriteria);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return "QueryJoin{base="
+ baseTableOrAlias + ", joinTable='"
+ joinTable + ", joinMetaData="
+ (joinMetaData == null ? null : joinMetaData.getName()) + ", alias='"
+ alias + ", select="
+ select + ", type="
+ type + '}';
}
}

View File

@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
/*******************************************************************************
@ -33,7 +35,17 @@ public abstract class AbstractFilterExpression<T extends Serializable> implement
/*******************************************************************************
**
*******************************************************************************/
public abstract T evaluate();
public abstract T evaluate() throws QException;
/*******************************************************************************
**
*******************************************************************************/
public T evaluateInputValues(Map<String, Serializable> inputValues) throws QException
{
return (T) this;
}

View File

@ -0,0 +1,214 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
**
*******************************************************************************/
public class FilterVariableExpression extends AbstractFilterExpression<Serializable>
{
private String variableName;
private String fieldName;
private String operator;
private int valueIndex = 0;
/*******************************************************************************
**
*******************************************************************************/
@Override
public Serializable evaluate() throws QException
{
throw (new QUserFacingException("Missing variable value."));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Serializable evaluateInputValues(Map<String, Serializable> inputValues) throws QException
{
if(!inputValues.containsKey(variableName) || "".equals(ValueUtils.getValueAsString(inputValues.get(variableName))))
{
throw (new QUserFacingException("Missing value for variable: " + variableName));
}
return (inputValues.get(variableName));
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public FilterVariableExpression()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
private FilterVariableExpression(String fieldName, int valueIndex)
{
this.fieldName = fieldName;
this.valueIndex = valueIndex;
}
/*******************************************************************************
** Getter for valueIndex
*******************************************************************************/
public int getValueIndex()
{
return (this.valueIndex);
}
/*******************************************************************************
** Setter for valueIndex
*******************************************************************************/
public void setValueIndex(int valueIndex)
{
this.valueIndex = valueIndex;
}
/*******************************************************************************
** Fluent setter for valueIndex
*******************************************************************************/
public FilterVariableExpression withValueIndex(int valueIndex)
{
this.valueIndex = valueIndex;
return (this);
}
/*******************************************************************************
** Getter for fieldName
*******************************************************************************/
public String getFieldName()
{
return (this.fieldName);
}
/*******************************************************************************
** Setter for fieldName
*******************************************************************************/
public void setFieldName(String fieldName)
{
this.fieldName = fieldName;
}
/*******************************************************************************
** Fluent setter for fieldName
*******************************************************************************/
public FilterVariableExpression withFieldName(String fieldName)
{
this.fieldName = fieldName;
return (this);
}
/*******************************************************************************
** Getter for variableName
*******************************************************************************/
public String getVariableName()
{
return (this.variableName);
}
/*******************************************************************************
** Setter for variableName
*******************************************************************************/
public void setVariableName(String variableName)
{
this.variableName = variableName;
}
/*******************************************************************************
** Fluent setter for variableName
*******************************************************************************/
public FilterVariableExpression withVariableName(String variableName)
{
this.variableName = variableName;
return (this);
}
/*******************************************************************************
** Getter for operator
*******************************************************************************/
public String getOperator()
{
return (this.operator);
}
/*******************************************************************************
** Setter for operator
*******************************************************************************/
public void setOperator(String operator)
{
this.operator = operator;
}
/*******************************************************************************
** Fluent setter for operator
*******************************************************************************/
public FilterVariableExpression withOperator(String operator)
{
this.operator = operator;
return (this);
}
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.exceptions.QException;
/*******************************************************************************
@ -35,7 +36,7 @@ public class Now extends AbstractFilterExpression<Instant>
**
*******************************************************************************/
@Override
public Instant evaluate()
public Instant evaluate() throws QException
{
return (Instant.now());
}

View File

@ -28,6 +28,7 @@ import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.exceptions.QException;
/*******************************************************************************
@ -119,7 +120,7 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
**
*******************************************************************************/
@Override
public Instant evaluate()
public Instant evaluate() throws QException
{
/////////////////////////////////////////////////////////////////////////////
// Instant doesn't let us plus/minus WEEK, MONTH, or YEAR... //

View File

@ -28,6 +28,7 @@ import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -95,7 +96,7 @@ public class ThisOrLastPeriod extends AbstractFilterExpression<Instant>
**
*******************************************************************************/
@Override
public Instant evaluate()
public Instant evaluate() throws QException
{
ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId();

View File

@ -39,7 +39,9 @@ public class ReplaceInput extends AbstractTableActionInput
private UniqueKey key;
private List<QRecord> records;
private QQueryFilter filter;
private boolean performDeletes = true;
private boolean performDeletes = true;
private boolean allowNullKeyValuesToEqual = false;
private boolean setPrimaryKeyInInsertedRecords = false;
private boolean omitDmlAudit = false;
@ -239,4 +241,66 @@ public class ReplaceInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for allowNullKeyValuesToEqual
*******************************************************************************/
public boolean getAllowNullKeyValuesToEqual()
{
return (this.allowNullKeyValuesToEqual);
}
/*******************************************************************************
** Setter for allowNullKeyValuesToEqual
*******************************************************************************/
public void setAllowNullKeyValuesToEqual(boolean allowNullKeyValuesToEqual)
{
this.allowNullKeyValuesToEqual = allowNullKeyValuesToEqual;
}
/*******************************************************************************
** Fluent setter for allowNullKeyValuesToEqual
*******************************************************************************/
public ReplaceInput withAllowNullKeyValuesToEqual(boolean allowNullKeyValuesToEqual)
{
this.allowNullKeyValuesToEqual = allowNullKeyValuesToEqual;
return (this);
}
/*******************************************************************************
** Getter for setPrimaryKeyInInsertedRecords
*******************************************************************************/
public boolean getSetPrimaryKeyInInsertedRecords()
{
return (this.setPrimaryKeyInInsertedRecords);
}
/*******************************************************************************
** Setter for setPrimaryKeyInInsertedRecords
*******************************************************************************/
public void setSetPrimaryKeyInInsertedRecords(boolean setPrimaryKeyInInsertedRecords)
{
this.setPrimaryKeyInInsertedRecords = setPrimaryKeyInInsertedRecords;
}
/*******************************************************************************
** Fluent setter for setPrimaryKeyInInsertedRecords
*******************************************************************************/
public ReplaceInput withSetPrimaryKeyInInsertedRecords(boolean setPrimaryKeyInInsertedRecords)
{
this.setPrimaryKeyInInsertedRecords = setPrimaryKeyInInsertedRecords;
return (this);
}
}

View File

@ -26,7 +26,7 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
/*******************************************************************************
**
** Input for Storage actions.
*******************************************************************************/
public class StorageInput extends AbstractTableActionInput
{

View File

@ -120,7 +120,7 @@ public class UpdateInput extends AbstractTableActionInput
/*******************************************************************************
**
*******************************************************************************/
public UpdateInput withRecordEntities(List<QRecordEntity> recordEntityList)
public UpdateInput withRecordEntities(List<? extends QRecordEntity> recordEntityList)
{
for(QRecordEntity recordEntity : CollectionUtils.nonNullList(recordEntityList))
{

Some files were not shown because too many files have changed in this diff Show More