Compare commits

...

186 Commits

Author SHA1 Message Date
abc6331131 Fixed process responses in openapi.yaml -- they were a layer too low, w/ a wrapped "typedResponse" above them (and since they were being serialized directly by jackson, were missing the 'values' now that they were marked to be ignored by it... so going through our conversion method in here - this suggests some refactoring that should apply a change like this to all specs, in case they have overrides of handleOutput as well... 2024-12-11 15:27:33 -06:00
e84fe7eb18 Checkstyle! 2024-12-11 15:05:47 -06:00
63a48eeafa Avoid exceptions from jackson serialization of processValues that contain a map with a null key 2024-12-11 14:59:08 -06:00
5434721c8e Add NullKeyToEmptyStringSerializer - to allow jackson serialization of a map with a null key 2024-12-11 14:40:06 -06:00
f3546da8cc Updating to 0.24.0 2024-11-22 15:51:25 -06:00
cfd3100535 Merge tag 'version-0.23.0' into dev
Tag release
2024-11-22 15:51:21 -06:00
0dbac39ef5 Merge branch 'rel/0.23.0' 2024-11-22 15:48:22 -06:00
00b4708d80 Update for next development version 2024-11-22 15:27:52 -06:00
b5959b4b89 Update versions for release 2024-11-22 15:27:48 -06:00
243ffe81a5 Change base port - to make mvn verify more stable 2024-11-22 15:14:35 -06:00
76118bfca1 CE-1946: added boolean to let frontend know if it is running in a process 2024-11-22 11:40:44 -06:00
6e91149b0a Feedback from code review 2024-11-22 10:21:22 -06:00
cfeb71aa2f Merged dev into feature/pom-version-fixing 2024-11-21 19:21:04 -06:00
edaabc3523 Try to manage 'snapshot' versions ourselves, to avoid bom-pom causing floating versions to be included... 2024-11-21 15:55:15 -06:00
e53e00c520 Merge pull request #138 from Kingsrook/feature/CE-1887-mobile-android-app
Feature/ce 1887 mobile android app
2024-11-21 11:53:39 -06:00
e970d613a7 Merged dev into feature/CE-1887-mobile-android-app 2024-11-21 10:55:16 -06:00
f5c1573102 Merge pull request #139 from Kingsrook/feature/CE-1946-process-to-allow-post-wms-carton-contents-adjustments
Feature/ce 1946 process to allow post wms carton contents adjustments
2024-11-21 10:33:48 -06:00
2103d578b3 Merge pull request #140 from Kingsrook/feature/CE-1772-generate-labels-poc
Feature/ce 1772 generate labels poc
2024-11-21 10:31:32 -06:00
daad8a720a CE-1946: added more props to child record list data 2024-11-19 20:41:16 -06:00
0ef01efcaa CE-1772: updates to alert widgets 2024-11-19 15:03:02 -06:00
389f0ad16f Merged dev into feature/CE-1887-mobile-android-app 2024-11-04 07:58:14 -06:00
6ef0a89533 CE-1772: fix aws expecting content type if object metadata is given 2024-11-03 21:53:50 -06:00
ce50120234 CE-1772: s3 updates to allow content type specifications among other things 2024-11-03 21:34:50 -06:00
50ef9420f6 CE-1887 - Rebuilt to get stable set of capabilities in example 2024-10-31 14:34:28 -05:00
c9fefb45a5 CE-1887 - Rebuilt per changes in this latest iteration 2024-10-31 14:19:14 -05:00
bf97d757b0 CE-1887 - add call to build and run ValidateApiVersions 2024-10-31 12:35:29 -05:00
c481940736 CE-1887 - javadoc / checkstyle 2024-10-31 12:06:53 -05:00
5f0f4cdab3 CE-1887 - WIP on doing verification of MW API during CI 2024-10-31 11:51:18 -05:00
8356fc3f12 CE-1887 - Add QFrontendWidgetMetaData to meta-data responses 2024-10-31 11:45:37 -05:00
8534a2ca55 CE-1887 - update to map some process values using their versioned-api responses (specifically, blockWidgetData) 2024-10-31 11:45:37 -05:00
46e54ed8e3 CE-1887 - update some block attributes as needed for working-version of android mobile scan apps 2024-10-31 11:45:37 -05:00
45439d7596 CE-1887 - Update to return full icon object, not just name 2024-10-31 11:19:24 -05:00
f01301e993 CE-1887 - Add frontend, middleware, and application name & version properties 2024-10-31 11:18:24 -05:00
59edb34c12 CE-1887 - Add frontend, middleware, and application name & version properties 2024-10-31 11:17:51 -05:00
74ea6a2d90 CE-1887 - Add MetaDataFilter to the QInstance and MetaDataAction 2024-10-31 11:17:18 -05:00
93ab08cbf1 CE-1887 expose to reset additional endpointGroups / javalinRoutes 2024-10-21 14:20:13 -05:00
a5051e559a CE-1887 Test on QAppJavalinServer 2024-10-21 13:41:54 -05:00
c5fdfceae6 CE-1887 Try to exclude full tools package 2024-10-21 13:07:12 -05:00
2965338e22 CE-1887 Increasing test coverage 2024-10-21 09:54:04 -05:00
5e37beabbe CE-1887 Fix tests (pass qinstance into spec) 2024-10-18 10:52:42 -05:00
428e188b4b CE-1887 Checkstyle! 2024-10-18 10:44:43 -05:00
533822973b CE-1887 Add copyright 2024-10-18 10:25:45 -05:00
5e9bd2a7e7 CE-1887 Adding test coverage! 2024-10-18 10:15:24 -05:00
7c54006985 CE-1887 Reoreder imports after moving MetaDataProducerInterface 2024-10-18 07:57:19 -05:00
678ecfd589 CE-1887 Add withRefToSchema; allow setType to set null 2024-10-17 20:27:11 -05:00
cc55b32206 CE-1887 Initial (not quite finished) version 1 middleware api spec 2024-10-17 20:26:54 -05:00
8dedc98866 CE-1887 rename method generate to generateOpenAPIModel 2024-10-17 12:40:08 -05:00
cde7a60ae0 CE-1887 Initial checkin 2024-10-17 12:39:41 -05:00
aef366e5fe CE-1887 Initial checkin 2024-10-17 12:36:21 -05:00
a46397df39 CE-1887 Initial checkin - dev-tools for middleware api 2024-10-17 12:34:41 -05:00
8500a2559c CE-1887 Initial checkin 2024-10-17 12:28:43 -05:00
872e125810 CE-1887 Updates for javalin 6.3.0; add method addJavalinRoutes(EndpointGroup); 2024-10-17 12:24:30 -05:00
eb754b42b9 CE-1887 Move methods deal with getting classes from packages into new ClassPathUtils 2024-10-17 12:22:58 -05:00
9ea92553e7 CE-1887 Fix spelling of withVales method 2024-10-17 12:22:10 -05:00
26a33eb1a0 CE-1887 Initial checkin of more formalized classes to define a qqq application 2024-10-17 12:22:10 -05:00
e40b35154e CE-1887 Move MetaDataProducerInterface into metadata package (where it always belonged...) 2024-10-17 12:22:10 -05:00
8436f2ab0a CE-1887 Updates for javalin 6.3.0; add method addJavalinRoutes(EndpointGroup); add getter & setter for QInstance; 2024-10-17 11:55:20 -05:00
7beea514d2 CE-1887 Upgrade javalin (5.6.1 to 6.3.0); Add dep on new qqq-openapi module 2024-10-17 11:52:13 -05:00
fc23718c4f CE-1887 Migrate openAPI model classes out of qqq-middleware-api, into new qqq-openapi module (for re-use within qqq-midleware-javalin) 2024-10-17 11:46:29 -05:00
efe89c7043 Merge pull request #137 from Kingsrook/feature/allow-basepull-override-values-from-jobs
hotfix: allow basepull override values from jobs
2024-10-11 09:27:24 -05:00
bbf4c2c2ff hotfix: allow basepull override values from jobs 2024-10-10 18:12:45 -05:00
ff1e022798 CE-1836: wasn't properly using boolean values in backend step input 2024-10-10 11:31:43 -05:00
f09735c811 Merged feature/CE-1836-create-order-checkers into dev 2024-10-10 10:56:30 -05:00
7ab9171998 Merged feature/CE-1821-veryify-shipped-orders-process into dev 2024-10-10 10:56:07 -05:00
b979f413c8 Merged feature/CE-1654-warehouse-security-key-all-access-left-join into dev 2024-10-10 10:53:25 -05:00
766881dee0 CE-1836: fixed npe if last basepull runtime hadnt been set 2024-10-10 09:59:20 -05:00
f65b16df60 Merge pull request #136 from Kingsrook/feature/CE-1836-create-order-checkers
Feature/ce 1836 create order checkers
2024-10-09 14:59:54 -05:00
e0597827ef CE-1836: updates from code review 2024-10-09 10:30:49 -05:00
10014f16ae CE-1836: fixed to check as boolean 2024-10-08 16:20:23 -05:00
526ba6ca30 CE-1836: added potential to log output 2024-10-08 15:46:47 -05:00
4f92fb2ae2 CE-1836: updates to allow getting basepull key value and sync config perform insert/updates from input 2024-10-07 22:33:16 -05:00
b687d07e46 CE-1836: update abstract table sync to make members and functions protected 2024-10-04 12:24:58 -05:00
b955a20e18 CE-1654 - Checkstyle! 2024-10-02 16:22:41 -05:00
eb8781db77 CE-1654 - Update joins built for security-purposes, that if they're for an all-access key, to be outer (LEFT); update tests to reflect this 2024-10-02 16:16:16 -05:00
febda51233 CE-1821: added static utility method for returning a list of entities rather than records 2024-09-26 15:16:23 -05:00
27dbc72db4 CE-1727 - Add standardColor and isAlert 2024-09-20 19:39:16 -05:00
983a93d38c CE-1727 - Remove un-intentional commit of bulk-load-mapping wip changes 2024-09-20 10:04:38 -05:00
28ad0661d1 CE-1727 - Mark as serializable 2024-09-20 10:00:11 -05:00
a8e235c155 CE-1727 - Update JSON serialization at the end of doProcessInitOrStep to specify to include null and empty values 2024-09-20 09:59:59 -05:00
c96bb9dda8 CE-1727 - Add support for QCodeReferenceLambda 2024-09-20 09:57:30 -05:00
e4bef88406 CE-1727 - Introduce concept of stepFlow to processes - LINEAR (the previous), and STATE_MACHINE (designed to be more flexible) 2024-09-20 09:57:02 -05:00
c18aa44010 CE-1727 - Initial checkin 2024-09-20 09:53:41 -05:00
47e95d74e3 CE-1727 - Add 'conditional' attribute 2024-09-20 09:47:35 -05:00
cf4c6d2144 CE-1727 - migrating from updatedFrontendStepList to processMetaDataAdjustment - so one can update fields in a process (original intent for inline PVS's) 2024-09-20 09:44:37 -05:00
780341b5cc CE-1727 - Initial checkin 2024-09-20 09:19:29 -05:00
20d4a9ffeb CE-1727 - new Layout (FLEX_ROW_CENTER) 2024-09-20 09:16:30 -05:00
4f1310ded9 CE-1727 - Initial checkin of new blocks 2024-09-20 09:15:59 -05:00
791b77b938 Merged feature/CE-1654-warehouse-security-key into dev 2024-09-18 16:48:29 -05:00
e6864b89c1 Merged feature/javalin-query-default-limit into dev 2024-09-18 16:48:14 -05:00
c3171c335f Update to always impose a limit on queries (they were getting lost if there was a defaultQueryFilter passed in) 2024-09-17 16:41:41 -05:00
bb548b78d9 updates to allow override api utils to disable or alter request details 2024-09-10 17:28:05 -05:00
161591405b CE-1654 - do chicken-egg session before the OTHER call to finalizeCustomizeSession too... 2024-09-10 10:51:21 -05:00
3cc0cfd86c CE-1654 - Just log, don't throw, if missing a security key value (should this be a setting??) 2024-09-10 09:34:46 -05:00
fb16a041fb CE-1727 - Add inlinePossibleValueSources option to fields 2024-09-09 11:53:01 -05:00
9bf9825132 Option (turned on by default, controlled via javalin metadata) to not allow query requests without a limit 2024-09-05 18:33:37 -05:00
a7ca34ec92 CE-1546 Switch auditTable.id and auditUser.id back to INTEGER (one isn't expected to have 2,000,000,000 of those) - fixes possible-value lookups 2024-09-05 14:17:45 -05:00
403227bae1 Merge tag 'version-0.22.1' into dev
Tag release
2024-09-05 13:40:57 -05:00
ab4837ff16 Merge branch 'rel/0.22.1' 2024-09-05 13:38:04 -05:00
107acb5685 Update for next development version 2024-09-05 13:28:56 -05:00
65166150e6 Update versions for release 2024-09-05 13:28:54 -05:00
c678a8159e Merged feature/CE-1546-support-migrating-audit-detail-to-big-int into dev 2024-09-05 13:17:40 -05:00
6673a8fc47 Updating to 0.23.0 2024-09-05 08:45:49 -05:00
c4f4faf32b Merge tag 'version-0.22.0' into dev
Tag release
2024-09-05 08:45:45 -05:00
9de08be978 Merge branch 'rel/0.22.0' 2024-09-05 08:43:09 -05:00
4349b37c8d Update for next development version 2024-09-05 07:56:20 -05:00
afb6aa3b89 Update versions for release 2024-09-05 07:56:16 -05:00
6c9ce41c7b Merge pull request #130 from Kingsrook/feature/CE-1646-possible-value-filter-bug
Feature/ce 1646 possible value filter bug
2024-09-04 16:23:05 -05:00
dc34e69c3c Merge pull request #131 from Kingsrook/feature/CE-1643-query-date-bugs-2
Feature/ce 1643 query date bugs 2
2024-09-04 16:21:03 -05:00
f457fd0860 CE-1654 activate chickenAndEggSession while calling customizer.finalCustomizeSession 2024-09-03 22:01:07 -05:00
c3834efad3 CE-1546 - fixing the use long for id in test 2024-08-27 13:05:24 -05:00
d513c8431b CE-1546 - fixing the use long for id in test 2024-08-27 10:01:34 -05:00
fc4e69f059 CE-1546 - feedback from code review 2024-08-26 12:14:01 -05:00
050208cdda CE-1643 Updated sig; added some local-date tests; made instant tests less dumb i hope 2024-08-26 11:00:26 -05:00
8f4146923b CE-1643 Update AbstractFilterExpression.evaluate to take in a QFieldMetaData - so that, in the temporal-based implementations, we can handle DATE_TIMEs differently from DATEs, where we were having RDBMS queries not return expected results, due to Instants being bound instead of LocalDates. 2024-08-26 11:00:20 -05:00
666f4a872d CE-1646 add use-cases to preserve the previous behavior for whether a report w/ missing input criteria values should fail or not 2024-08-23 14:36:23 -05:00
89e0fc566d Try to fix flaky test 2024-08-23 12:17:04 -05:00
42fd5a0cb3 Merged dev into feature/CE-1646-possible-value-filter-bug 2024-08-23 11:52:50 -05:00
89cf23a65a Updating to 0.22.0 2024-08-23 11:50:41 -05:00
57b0d6c29b Merge tag 'version-0.21.0' into dev
Tag release
2024-08-23 11:50:37 -05:00
6702c06ed0 Merge branch 'rel/0.21.0' 2024-08-23 11:47:47 -05:00
c90def42f5 Update for next development version 2024-08-23 11:39:10 -05:00
9dfbd839c8 Update versions for release 2024-08-23 11:39:07 -05:00
724d5779cc Merge pull request #127 from Kingsrook/feature/CE-1405-zero-day-ledger-billing
Feature/ce 1405 zero day ledger billing
2024-08-23 11:19:46 -05:00
1fef376e65 Merge pull request #128 from Kingsrook/feature/CE-1556-ops-overview-enhanced-tooltips
Feature/ce 1556 ops overview enhanced tooltips
2024-08-23 11:02:05 -05:00
ed1e251934 CE-1646 Fix expected message on one test 2024-08-23 10:01:20 -05:00
81248a8daf CE-1646 Accept 'useCase' parameter in possibleValues function, to pass to backend, to control how possible-value filters are applied when input values are missing 2024-08-23 09:57:08 -05:00
d3417a0652 CE-1405 Remove usage of SparseQRecord... not clear if we want it or not at this time 2024-08-21 20:09:36 -05:00
053d5f1058 CE-1405 Add getOldRecordMap 2024-08-21 17:01:55 -05:00
20a5130757 CE-1546 - Moving audit ids to longs and adding general support for long ids 2024-08-21 09:35:33 -05:00
47e27d5ffc CE-1554: updates to allow widget block overlays 2024-08-20 18:06:01 -05:00
59a70a4cb7 CE-1405 fix bug with fieldNamesToInclude for tables w/ no selected fields 2024-08-20 09:38:54 -05:00
fea757c46d Merged dev into feature/CE-1405-zero-day-ledger-billing 2024-08-16 16:57:26 -05:00
9a65ea81b2 CE-1405 / CE-1479 - add queryInput.fieldNamesToInclude 2024-08-15 08:53:19 -05:00
494ec00b84 CE-1556: updated to try to use composite block data within tooltips 2024-08-13 17:23:30 -05:00
9b4b61af38 Merged feature/CE-1472-add-extensivewms-orders into dev 2024-08-13 10:14:46 -05:00
f237b5e82d Merged feature/fix-formParam-exceptions-for-plaintext-body-with-percent into dev 2024-08-05 13:36:43 -05:00
207311eb0b Merged feature/qol-improvements-20240801 into dev 2024-08-05 13:36:24 -05:00
ab5af234af Merged feature/checkstyle-updates into dev 2024-08-05 13:35:21 -05:00
9baa7c32bf Add safety around most calls to formParam and/or queryParam, as they can throw if the request isn't formatted as expected, in ways that we may not want it to. 2024-08-02 12:32:36 -05:00
3eae3a5758 re-set queryStat startTimestamp to just before executeQuery, to avoid including time spent aquiring db connection 2024-08-01 15:12:59 -05:00
a11d584c8a Fix formatting of booleans when value is string (e.g., format based on QFieldMetaData type, not value object class) 2024-08-01 15:11:20 -05:00
ba3cf53c30 Update to throw QNotFoundException if view isn't found by id (rather than NPE) 2024-08-01 15:08:53 -05:00
d44790545d Add total # failures to message; remove unused c'tor 2024-08-01 15:04:03 -05:00
5aed59b9b1 Add implements AutoCloseable, so we could use in a try-with-resources 2024-08-01 15:02:38 -05:00
3bcc0a17bc Add a log info re: releasing lock 2024-08-01 15:02:20 -05:00
09c4d99612 Avoid NPE and return w/ noop in performValidations if null (or empty) input records 2024-08-01 15:02:06 -05:00
26fc4fb4e0 Initial checkin 2024-08-01 15:01:22 -05:00
d92be4e69b don't duplicate apikey=value in re-tries; mask api key in outboundApiLog urls 2024-08-01 15:00:36 -05:00
2de3306f95 Add c'tor that takes table name, and override withTableName 2024-08-01 14:41:55 -05:00
58b0936c50 Add details to Incorrect number of values given exception 2024-08-01 14:41:40 -05:00
51eb7d89be Take report format as input 2024-08-01 14:40:27 -05:00
0b5e97d596 Bugfix, where sheet contents could get out-of-sync with their labels (e.g., see use-case with some summary views before their corresponding table views) 2024-07-22 14:26:45 -05:00
2609bc801c CE-1405 Add dataSource as argument to ReportCustomRecordSourceInterface.execute 2024-07-22 14:25:49 -05:00
583d702355 Re-add getInstance and getSession (until qqq consumer apps stop using them) 2024-07-19 17:02:37 -05:00
06a69279a8 CE-1472 - Fix doUpdate to set URL 2024-07-19 16:38:06 -05:00
9a2276edf2 CE-1472 - Refactored to do variants a little more generically per different auth-types; made createOAuth2TokenRequest its own overrideable method 2024-07-19 16:38:06 -05:00
36307dba24 CE-1405 Updates to qqq-reports: support for ReportCustomRecordSourceInterface 2024-07-19 16:37:22 -05:00
fa2b1c0b8e Fix merge conflicts 2024-07-19 16:25:15 -05:00
840e1aada3 Applying checkstyle updates to test sources 2024-07-19 16:16:51 -05:00
22d5bc547c Add includeTestSourceDirectory=true to checkstyle config 2024-07-19 16:16:51 -05:00
b7cfea157d Checkstyle updates
- remove MagicNumber
- add MissingJavadocType
- remove rules about contents of javadocs
2024-07-19 16:16:51 -05:00
028751e23a more test coverage for javalin (for new anonymous inner TypeReference) 2024-07-19 16:16:27 -05:00
be0e1f9c0b add some test coverage (updates to eliminate warnings put us just under threshold) 2024-07-19 16:16:27 -05:00
912e40fe0b Eliminated all warnings. 2024-07-19 16:16:27 -05:00
f9af2ba983 Remove all calls to actionInput.getInstance and getSesssion, in favor of the equivallent methods from QContext 2024-07-19 16:16:16 -05:00
61ec57af02 Merge pull request #119 from Kingsrook/feature/CE-1460-export-and-join-bugs
Feature/ce 1460 export and join bugs
2024-07-18 13:39:43 -05:00
ccce1a3d1f Merged dev into feature/CE-1460-export-and-join-bugs 2024-07-09 11:35:59 -05:00
eb36630bcd CE-1406 Initial checkin 2024-07-09 11:34:56 -05:00
c3f702bb65 CE-1406 Initial checkin 2024-07-09 11:03:21 -05:00
31fa3c3921 CE-1406 Update to clone queryJoins... since our friend the JoinContext likes to mutate them, and break things! also cleaned up all warnings. 2024-07-08 15:19:33 -05:00
099fd27309 CE-1406 Initial checkin 2024-07-08 14:39:44 -05:00
95998b687b CE-1406 Add renderedReportId to output 2024-07-08 14:35:59 -05:00
1a6cc5bf3c CE-1406 Add Cloneable 2024-07-08 14:35:49 -05:00
27a6c0d53c CE-1406 in ensureRecordSecurityLockIsRepresented, getTable using table name, not a (potential) alias; avoid NPE on exposedJoins; whitespace; add cloneable in JoinOn 2024-07-08 14:35:14 -05:00
27c693f0c4 CE-1406 Fix orderInstructionsJoinOrder 2024-07-08 10:57:09 -05:00
a3433d60f7 CE-1406 remove tests that weren't ready for commit 2024-07-08 10:27:16 -05:00
c2a13b1ada Expose orderInstructionsJoinOrder on order table; flip orderInstructionsJoinOrder (to expose bug covered in testFlippedJoinForOnClause 2024-07-08 10:26:11 -05:00
576ca8a6df Add withCriteria overloads that match most common constructor signatures for QFilterCriteria 2024-07-08 10:24:39 -05:00
a9a988f221 Add missing overloads for debug,warn,error(LogPair ...) 2024-07-08 10:23:47 -05:00
7f23a0da79 Add LOG.info plus explicit QPermissionDeniedException for null inputs to various checkXPermissionThrowing methods (instead of null pointers) 2024-07-08 10:22:50 -05:00
0d2e6012a3 Remove "aurora" as literal value for rdbmsBackend vendor (in favor of VENDOR_AURORA_MYSQL constant) 2024-07-08 10:21:59 -05:00
bce9af06fb Move logSQL calls into finally blocks, to happen upon success or exception. 2024-07-08 10:20:45 -05:00
385f4c20e5 Add overload of executeStatement, that takes the SQL string, for including in an explicit LOG.warn upon SQLException. Add similar catch(SQLException) { LOG; throw } blocks to other execute methods. 2024-07-08 10:19:34 -05:00
6b7fb21d76 CE-1460 Initial checkin 2024-07-08 09:49:59 -05:00
172b25f33e CE-1460 Fix in makeFromClause, to flip join before getting names out of it. Fixes a case where the JoinContext can send a backward join this far. 2024-07-08 09:49:43 -05:00
8dbf7fe4cd CE-1460 Construct a new, clean QueryJoin object for the second Aggregate call (as JoinsContext changes the one it takes in during the first call, leading to different join conditions being in place, causing second query to potentially fail) 2024-07-05 12:57:07 -05:00
479 changed files with 33190 additions and 1693 deletions

View File

@ -1,23 +1,51 @@
#!/bin/bash
if [ -z "$CIRCLE_BRANCH" ] && [ -z "$CIRCLE_TAG" ]; then
echo "Error: env vars CIRCLE_BRANCH and CIRCLE_TAG were not set."
exit 1;
fi
############################################################################
## adjust-pom.version.sh
## During CircleCI builds - edit the qqq parent pom.xml, to set the
## <revision> value such that:
## - feature-branch builds, tagged as snapshot-*, deploy with a version
## number that includes that tag's name (minus the snapshot- part)
## - integration-branch builds deploy with a version number that includes
## the branch name slugified
## - we never deploy -SNAPSHOT versions any more - because we don't believe
## it is ever valid to not know exactly what versions you are getting
## (perhaps because we are too loose with our versioning?)
############################################################################
if [ "$CIRCLE_BRANCH" == "dev" ] || [ "$CIRCLE_BRANCH" == "staging" ] || [ "$CIRCLE_BRANCH" == "main" ] || [ \! -z $(echo "$CIRCLE_TAG" | grep "^version-") ]; then
echo "On a primary branch or tag [${CIRCLE_BRANCH}${CIRCLE_TAG}] - will not edit the pom version.";
POM=$(dirname $0)/../pom.xml
echo "On branch: $CIRCLE_BRANCH, tag: $CIRCLE_TAG..."
######################################################################
## ## only do anything if the committed pom has a -SNAPSHOT version ##
######################################################################
REVISION=$(grep '<revision>' $POM | sed 's/.*<revision>//;s/<.*//');
echo "<revision> in pom.xml is: $REVISION"
if [ \! $(echo "$REVISION" | grep SNAPSHOT) ]; then
echo "Not on a SNAPSHOT revision, so nothing to do here."
exit 0;
fi
if [ -n "$CIRCLE_BRANCH" ]; then
SLUG=$(echo $CIRCLE_BRANCH | sed 's/[^a-zA-Z0-9]/-/g')
else
SLUG=$(echo $CIRCLE_TAG | sed 's/^snapshot-//g')
##################################################################################
## ## figure out if we need a SLUG: a snapshot- tag, or an integration/ branch ##
##################################################################################
SLUG=""
if [ $(echo "$CIRCLE_TAG" | grep ^snapshot-) ]; then
SLUG=$(echo "$CIRCLE_TAG" | sed "s/^snapshot-//")-
echo "Using slug [$SLUG] from tag [$CIRCLE_TAG]"
elif [ $(echo "$CIRCLE_BRANCH" | grep ^integration/) ]; then
SLUG=$(echo "$CIRCLE_BRANCH" | sed "s,/,-,g")-
echo "Using slug [$SLUG] from branch [$CIRCLE_BRANCH]"
fi
POM=$(dirname $0)/../pom.xml
################################################################
## ## build the replcaement for -SNAPSHOT, and update the pom ##
################################################################
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
REPLACEMENT=${SLUG}${TIMESTAMP}
echo "Updating $POM <revision> to: $SLUG-SNAPSHOT"
sed -i "s/<revision>.*/<revision>$SLUG-SNAPSHOT<\/revision>/" $POM
echo "Updating $POM -SNAPSHOT to: -$REPLACEMENT"
sed -i "s/-SNAPSHOT<\/revision>/-$REPLACEMENT<\/revision>/" $POM
git diff $POM

View File

@ -79,6 +79,19 @@ commands:
- ~/.m2
key: v1-dependencies-{{ checksum "pom.xml" }}
check_middleware_api_versions:
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "pom.xml" }}
- run:
name: Build and Run ValidateApiVersions
command: |
mvn -s .circleci/mvn-settings.xml -T4 install -DskipTests
mvn -s .circleci/mvn-settings.xml -pl qqq-middleware-javalin package appassembler:assemble -DskipTests
qqq-middleware-javalin/target/appassembler/bin/ValidateApiVersions -r $(pwd)
mvn_jar_deploy:
steps:
- checkout
@ -130,6 +143,7 @@ jobs:
## - localstack/startup
- install_java17
- mvn_verify
- check_middleware_api_versions
mvn_deploy:
executor: localstack/default
@ -137,6 +151,7 @@ jobs:
## - localstack/startup
- install_java17
- mvn_verify
- check_middleware_api_versions
- mvn_jar_deploy
publish_asciidoc:

View File

@ -213,18 +213,6 @@
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
<module name="NonEmptyAtclauseDescription"/>
<!-- <module name="JavadocTagContinuationIndentation"/> -->
<!--
<module name="SummaryJavadoc">
<property name="forbiddenSummaryFragments" value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
</module>
-->
<!-- <module name="JavadocParagraph"/> -->
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
<property name="target" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<module name="JavadocMethod">
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
@ -233,23 +221,14 @@
<module name="MissingJavadocMethod">
<property name="scope" value="private"/>
</module>
<module name="MissingJavadocType">
<property name="scope" value="private"/>
</module>
<module name="MethodName">
<property name="format" value="^[a-z][a-zA-Z0-9_]*$"/>
<message key="name.invalidPattern"
value="Method name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="SingleLineJavadoc">
<property name="ignoreInlineTags" value="false"/>
</module>
<module name="MagicNumber">
<property name="severity" value="info"/>
<property name="tokens" value="NUM_DOUBLE, NUM_FLOAT, NUM_INT"/>
<property name="ignoreNumbers" value="0, 1, 2, 3, 4, 5, 6, 7, 8"/>
<property name="ignoreFieldDeclaration" value="true"/>
<property name="ignoreAnnotation" value="true"/>
</module>
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected"/>
</module>

View File

@ -33,9 +33,6 @@ If the {link-table} has a `POST_QUERY_CUSTOMIZER` defined, then after records ar
* `table` - *String, Required* - Name of the table being queried against.
* `filter` - *<<QQueryFilter>> object* - Specification for what records should be returned, based on *<<QFilterCriteria>>* objects, and how they should be sorted, based on *<<QFilterOrderBy>>* objects.
If a `filter` is not given, then all rows in the table will be returned by the query.
* `skip` - *Integer* - Optional number of records to be skipped at the beginning of the result set.
e.g., for implementing pagination.
* `limit` - *Integer* - Optional maximum number of records to be returned by the query.
* `transaction` - *QBackendTransaction object* - Optional transaction object.
** Behavior for this object is backend-dependant.
In an RDBMS backend, this object is generally needed if you want your query to see data that may have been modified within the same transaction.
@ -55,6 +52,14 @@ But if running a query to provide data as part of a process, then this can gener
* `shouldMaskPassword` - *boolean, default: true* - Controls whether or not fields with `type` = `PASSWORD` should be masked, or if their actual values should be returned.
* `queryJoins` - *List of <<QueryJoin>> objects* - Optional list of tables to be joined with the main table being queried.
See QueryJoin below for further details.
* `fieldNamesToInclude` - *Set of String* - Optional set of field names to be included in the records.
** Fields from a queryJoin must be prefixed by the join table's name or alias, and a period.
Field names from the table being queried should not have any sort of prefix.
** A `null` set here (default) means to include all fields from the table and any queryJoins set as select=true.
** An empty set will cause an error, as well any unrecognized field names.
** `QueryAction` will validate the set of field names, and throw an exception if any unrecognized names are given.
** _Note that this is an optional feature, which some backend modules may not implement.
Meaning, they would always return all fields._
==== QQueryFilter
A key component of *<<QueryInput>>*, a *QQueryFilter* defines both what records should be included in a query's results (e.g., an SQL `WHERE`), as well as how those results should be sorted (SQL `ORDER BY`).
@ -68,6 +73,9 @@ In general, multiple *orderBys* can be given (depending on backend implementatio
** Each *subFilter* can include its own additional *subFilters*.
** Each *subFilter* can specify a different *booleanOperator*.
** For example, consider the following *QQueryFilter*, that uses two *subFilters*, and a mix of *booleanOperators*
* `skip` - *Integer* - Optional number of records to be skipped at the beginning of the result set.
e.g., for implementing pagination.
* `limit` - *Integer* - Optional maximum number of records to be returned by the query.
[source,java]
----

View File

@ -36,6 +36,7 @@
<module>qqq-backend-module-rdbms</module>
<module>qqq-backend-module-mongodb</module>
<module>qqq-language-support-javascript</module>
<module>qqq-openapi</module>
<module>qqq-middleware-picocli</module>
<module>qqq-middleware-javalin</module>
<module>qqq-middleware-lambda</module>
@ -46,12 +47,11 @@
</modules>
<properties>
<revision>0.21.0-SNAPSHOT</revision>
<revision>0.24.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.release>17</maven.compiler.release>
<maven.compiler.showDeprecation>true</maven.compiler.showDeprecation>
<maven.compiler.showWarnings>true</maven.compiler.showWarnings>
<coverage.haltOnFailure>true</coverage.haltOnFailure>
@ -168,6 +168,7 @@
<violationSeverity>warning</violationSeverity>
<excludes>**/target/generated-sources/*.*</excludes>
<!-- <linkXRef>false</linkXRef> -->
<includeTestSourceDirectory>true</includeTestSourceDirectory>
</configuration>
<goals>
<goal>check</goal>

View File

@ -37,7 +37,7 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
**
** Note: One would imagine that this class shouldn't ever implement Serializable...
*******************************************************************************/
public class QBackendTransaction
public class QBackendTransaction implements AutoCloseable
{
/*******************************************************************************

View File

@ -225,7 +225,13 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
{
if(auditSingleInput.getSecurityKeyValues() == null || !auditSingleInput.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType()))
{
throw (new QException("Missing securityKeyValue [" + recordSecurityLock.getSecurityKeyType() + "] in audit request for table " + auditSingleInput.getAuditTableName()));
///////////////////////////////////////////////////////
// originally, this case threw... //
// but i think it's better to record the audit, just //
// missing its security key value, then to fail... //
///////////////////////////////////////////////////////
// throw (new QException("Missing securityKeyValue [" + recordSecurityLock.getSecurityKeyType() + "] in audit request for table " + auditSingleInput.getAuditTableName()));
LOG.info("Missing securityKeyValue in audit request", logPair("table", auditSingleInput.getAuditTableName()), logPair("securityKey", recordSecurityLock.getSecurityKeyType()));
}
}
@ -272,7 +278,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
List<QRecord> auditDetailRecords = new ArrayList<>();
for(AuditSingleInput auditSingleInput : CollectionUtils.nonNullList(input.getAuditSingleInputList()))
{
Integer auditId = insertOutput.getRecords().get(i++).getValueInteger("id");
Long auditId = insertOutput.getRecords().get(i++).getValueLong("id");
if(auditId == null)
{
LOG.warn("Missing an id for inserted audit - so won't be able to store its child details...");

View File

@ -344,6 +344,9 @@ public class RecordAutomationStatusUpdater
/***************************************************************************
**
***************************************************************************/
private record Key(QTableMetaData table, TriggerEvent triggerEvent) {}
}

View File

@ -55,10 +55,10 @@ public abstract class AbstractPreInsertCustomizer implements TableCustomizerInte
/////////////////////////////////////////////////////////////////////////////////
// allow the customizer to specify when it should be executed as part of the //
// insert action. default (per method in this class) is AFTER_ALL_VALIDATIONS //
/////////////////////////////////////////////////////////////////////////////////
/***************************************************************************
** allow the customizer to specify when it should be executed as part of the
** insert action. default (per method in this class) is AFTER_ALL_VALIDATIONS
***************************************************************************/
public enum WhenToRun
{
BEFORE_ALL_VALIDATIONS,

View File

@ -28,6 +28,7 @@ import java.util.Iterator;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
@ -49,6 +50,9 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
*******************************************************************************/
public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInsertCustomizer
{
/***************************************************************************
**
***************************************************************************/
public enum RelationshipType
{
PARENT_POINTS_AT_CHILD,
@ -97,7 +101,7 @@ public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInse
List<QRecord> rs = records;
List<QRecord> childrenToInsert = new ArrayList<>();
QTableMetaData table = getInsertInput().getTable();
QTableMetaData childTable = getInsertInput().getInstance().getTable(getChildTableName());
QTableMetaData childTable = QContext.getQInstance().getTable(getChildTableName());
////////////////////////////////////////////////////////////////////////////////
// iterate over the inserted records, building a list child records to insert //

View File

@ -24,10 +24,12 @@ package com.kingsrook.qqq.backend.core.actions.customizers;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@ -143,4 +145,19 @@ public interface RecordCustomizerUtilityInterface
}
}
/*******************************************************************************
**
*******************************************************************************/
default Map<Serializable, QRecord> getOldRecordMap(List<QRecord> oldRecordList, UpdateInput updateInput)
{
Map<Serializable, QRecord> oldRecordMap = new HashMap<>();
for(QRecord qRecord : oldRecordList)
{
oldRecordMap.put(qRecord.getValue(updateInput.getTable().getPrimaryKeyField()), qRecord);
}
return (oldRecordMap);
}
}

View File

@ -31,6 +31,7 @@ import java.util.Map;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.actions.values.SearchPossibleValueSourceAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
@ -102,7 +103,7 @@ public abstract class AbstractWidgetRenderer
String possibleValueSourceName = dropdownData.getPossibleValueSourceName();
if(possibleValueSourceName != null)
{
QPossibleValueSource possibleValueSource = input.getInstance().getPossibleValueSource(possibleValueSourceName);
QPossibleValueSource possibleValueSource = QContext.getQInstance().getPossibleValueSource(possibleValueSourceName);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this looks complicated, but is just look for a label in the dropdown data and if found use it, //

View File

@ -23,11 +23,11 @@ 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.MetaDataProducerInterface;
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;
@ -40,9 +40,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
** - 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 class AlertWidgetRenderer extends AbstractWidgetRenderer implements MetaDataProducerInterface<QWidgetMetaData>
{
public static final String NAME = "ProcessAlertWidget";
public static final String NAME = "AlertWidgetRenderer";

View File

@ -34,6 +34,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
@ -181,10 +182,10 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
{
String widgetLabel = input.getQueryParams().get("widgetLabel");
String joinName = input.getQueryParams().get("joinName");
QJoinMetaData join = input.getInstance().getJoin(joinName);
QJoinMetaData join = QContext.getQInstance().getJoin(joinName);
String id = input.getQueryParams().get("id");
QTableMetaData leftTable = input.getInstance().getTable(join.getLeftTable());
QTableMetaData rightTable = input.getInstance().getTable(join.getRightTable());
QTableMetaData leftTable = QContext.getQInstance().getTable(join.getLeftTable());
QTableMetaData rightTable = QContext.getQInstance().getTable(join.getRightTable());
Integer maxRows = null;
if(StringUtils.hasContent(input.getQueryParams().get("maxRows")))
@ -252,7 +253,7 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
}
}
String tablePath = input.getInstance().getTablePath(rightTable.getName());
String tablePath = QContext.getQInstance().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);
@ -278,7 +279,9 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
Map<String, Serializable> widgetValues = input.getWidgetMetaData().getDefaultValues();
if(widgetValues.containsKey("disabledFieldsForNewChildRecords"))
{
widgetData.setDisabledFieldsForNewChildRecords((Set<String>) widgetValues.get("disabledFieldsForNewChildRecords"));
@SuppressWarnings("unchecked")
Set<String> disabledFieldsForNewChildRecords = (Set<String>) widgetValues.get("disabledFieldsForNewChildRecords");
widgetData.setDisabledFieldsForNewChildRecords(disabledFieldsForNewChildRecords);
}
else
{
@ -298,6 +301,9 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
}
}
widgetData.setAllowRecordEdit(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("allowRecordEdit"))));
widgetData.setAllowRecordDelete(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("allowRecordDelete"))));
return (new RenderWidgetOutput(widgetData));
}
catch(Exception e)

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.util.HashMap;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
@ -57,7 +58,7 @@ public class ProcessWidgetRenderer extends AbstractWidgetRenderer
setupDropdowns(input, widgetMetaData, data);
String processName = (String) widgetMetaData.getDefaultValues().get(WIDGET_PROCESS_NAME);
QProcessMetaData processMetaData = input.getInstance().getProcess(processName);
QProcessMetaData processMetaData = QContext.getQInstance().getProcess(processName);
data.setProcessMetaData(processMetaData);
data.setDefaultValues(new HashMap<>(input.getQueryParams()));

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.actions.metadata;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** a default implementation of MetaDataFilterInterface, that allows all the things
*******************************************************************************/
public class AllowAllMetaDataFilter implements MetaDataFilterInterface
{
/***************************************************************************
**
***************************************************************************/
@Override
public boolean allowTable(MetaDataInput input, QTableMetaData table)
{
return (true);
}
/***************************************************************************
**
***************************************************************************/
@Override
public boolean allowProcess(MetaDataInput input, QProcessMetaData process)
{
return (true);
}
/***************************************************************************
**
***************************************************************************/
@Override
public boolean allowReport(MetaDataInput input, QReportMetaData report)
{
return (true);
}
/***************************************************************************
**
***************************************************************************/
@Override
public boolean allowApp(MetaDataInput input, QAppMetaData app)
{
return (true);
}
/***************************************************************************
**
***************************************************************************/
@Override
public boolean allowWidget(MetaDataInput input, QWidgetMetaDataInterface widget)
{
return (true);
}
}

View File

@ -28,12 +28,18 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
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.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData;
@ -48,6 +54,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
/*******************************************************************************
@ -56,6 +63,12 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
*******************************************************************************/
public class MetaDataAction
{
private static final QLogger LOG = QLogger.getLogger(MetaDataAction.class);
private static Memoization<QInstance, MetaDataFilterInterface> metaDataFilterMemoization = new Memoization<>();
/*******************************************************************************
**
*******************************************************************************/
@ -63,27 +76,32 @@ public class MetaDataAction
{
ActionHelper.validateSession(metaDataInput);
// todo pre-customization - just get to modify the request?
MetaDataOutput metaDataOutput = new MetaDataOutput();
MetaDataOutput metaDataOutput = new MetaDataOutput();
Map<String, AppTreeNode> treeNodes = new LinkedHashMap<>();
Map<String, AppTreeNode> treeNodes = new LinkedHashMap<>();
MetaDataFilterInterface filter = getMetaDataFilter();
/////////////////////////////////////
// map tables to frontend metadata //
/////////////////////////////////////
Map<String, QFrontendTableMetaData> tables = new LinkedHashMap<>();
for(Map.Entry<String, QTableMetaData> entry : metaDataInput.getInstance().getTables().entrySet())
for(Map.Entry<String, QTableMetaData> entry : QContext.getQInstance().getTables().entrySet())
{
String tableName = entry.getKey();
QTableMetaData table = entry.getValue();
if(!filter.allowTable(metaDataInput, table))
{
continue;
}
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, table);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
continue;
}
QBackendMetaData backendForTable = metaDataInput.getInstance().getBackendForTable(tableName);
QBackendMetaData backendForTable = QContext.getQInstance().getBackendForTable(tableName);
tables.put(tableName, new QFrontendTableMetaData(metaDataInput, backendForTable, table, false, false));
treeNodes.put(tableName, new AppTreeNode(table));
}
@ -96,11 +114,16 @@ public class MetaDataAction
// map processes to frontend metadata //
////////////////////////////////////////
Map<String, QFrontendProcessMetaData> processes = new LinkedHashMap<>();
for(Map.Entry<String, QProcessMetaData> entry : metaDataInput.getInstance().getProcesses().entrySet())
for(Map.Entry<String, QProcessMetaData> entry : QContext.getQInstance().getProcesses().entrySet())
{
String processName = entry.getKey();
QProcessMetaData process = entry.getValue();
if(!filter.allowProcess(metaDataInput, process))
{
continue;
}
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, process);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
@ -116,11 +139,16 @@ public class MetaDataAction
// map reports to frontend metadata //
//////////////////////////////////////
Map<String, QFrontendReportMetaData> reports = new LinkedHashMap<>();
for(Map.Entry<String, QReportMetaData> entry : metaDataInput.getInstance().getReports().entrySet())
for(Map.Entry<String, QReportMetaData> entry : QContext.getQInstance().getReports().entrySet())
{
String reportName = entry.getKey();
QReportMetaData report = entry.getValue();
if(!filter.allowReport(metaDataInput, report))
{
continue;
}
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, report);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
@ -136,11 +164,16 @@ public class MetaDataAction
// map widgets to frontend metadata //
//////////////////////////////////////
Map<String, QFrontendWidgetMetaData> widgets = new LinkedHashMap<>();
for(Map.Entry<String, QWidgetMetaDataInterface> entry : metaDataInput.getInstance().getWidgets().entrySet())
for(Map.Entry<String, QWidgetMetaDataInterface> entry : QContext.getQInstance().getWidgets().entrySet())
{
String widgetName = entry.getKey();
QWidgetMetaDataInterface widget = entry.getValue();
if(!filter.allowWidget(metaDataInput, widget))
{
continue;
}
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, widget);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
@ -154,7 +187,7 @@ public class MetaDataAction
///////////////////////////////////////////////////////
// sort apps - by sortOrder (integer), then by label //
///////////////////////////////////////////////////////
List<QAppMetaData> sortedApps = metaDataInput.getInstance().getApps().values().stream()
List<QAppMetaData> sortedApps = QContext.getQInstance().getApps().values().stream()
.sorted(Comparator.comparing((QAppMetaData a) -> a.getSortOrder())
.thenComparing((QAppMetaData a) -> a.getLabel()))
.toList();
@ -173,9 +206,19 @@ public class MetaDataAction
continue;
}
apps.put(appName, new QFrontendAppMetaData(app, metaDataOutput));
treeNodes.put(appName, new AppTreeNode(app));
if(!filter.allowApp(metaDataInput, app))
{
continue;
}
//////////////////////////////////////
// build the frontend-app meta-data //
//////////////////////////////////////
QFrontendAppMetaData frontendAppMetaData = new QFrontendAppMetaData(app, metaDataOutput);
/////////////////////////////////////////
// add children (if they're permitted) //
/////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(app.getChildren()))
{
for(QAppChildMetaData child : app.getChildren())
@ -189,9 +232,42 @@ public class MetaDataAction
}
}
apps.get(appName).addChild(new AppTreeNode(child));
//////////////////////////////////////////////////////////////////////////////////////////////////////
// if the child was filtered away, so it isn't in its corresponding map, then don't include it here //
//////////////////////////////////////////////////////////////////////////////////////////////////////
if(child instanceof QTableMetaData table && !tables.containsKey(table.getName()))
{
continue;
}
if(child instanceof QProcessMetaData process && !processes.containsKey(process.getName()))
{
continue;
}
if(child instanceof QReportMetaData report && !reports.containsKey(report.getName()))
{
continue;
}
if(child instanceof QAppMetaData childApp && !apps.containsKey(childApp.getName()))
{
// continue;
}
frontendAppMetaData.addChild(new AppTreeNode(child));
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
// if the app ended up having no children, then discard it //
// todo - i think this was wrong, because it didn't take into account ... something nested maybe... //
//////////////////////////////////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeIsEmpty(frontendAppMetaData.getChildren()) && CollectionUtils.nullSafeIsEmpty(frontendAppMetaData.getWidgets()))
{
// LOG.debug("Discarding empty app", logPair("name", frontendAppMetaData.getName()));
// continue;
}
apps.put(appName, frontendAppMetaData);
treeNodes.put(appName, new AppTreeNode(app));
}
metaDataOutput.setApps(apps);
@ -211,14 +287,14 @@ public class MetaDataAction
////////////////////////////////////
// add branding metadata if found //
////////////////////////////////////
if(metaDataInput.getInstance().getBranding() != null)
if(QContext.getQInstance().getBranding() != null)
{
metaDataOutput.setBranding(metaDataInput.getInstance().getBranding());
metaDataOutput.setBranding(QContext.getQInstance().getBranding());
}
metaDataOutput.setEnvironmentValues(metaDataInput.getInstance().getEnvironmentValues());
metaDataOutput.setEnvironmentValues(QContext.getQInstance().getEnvironmentValues());
metaDataOutput.setHelpContents(metaDataInput.getInstance().getHelpContent());
metaDataOutput.setHelpContents(QContext.getQInstance().getHelpContent());
// todo post-customization - can do whatever w/ the result if you want?
@ -227,6 +303,33 @@ public class MetaDataAction
/***************************************************************************
**
***************************************************************************/
private MetaDataFilterInterface getMetaDataFilter()
{
return metaDataFilterMemoization.getResult(QContext.getQInstance(), i ->
{
MetaDataFilterInterface filter = null;
QCodeReference metaDataFilterReference = QContext.getQInstance().getMetaDataFilter();
if(metaDataFilterReference != null)
{
filter = QCodeLoader.getAdHoc(MetaDataFilterInterface.class, metaDataFilterReference);
LOG.debug("Using new meta-data filter of type: " + filter.getClass().getSimpleName());
}
if(filter == null)
{
filter = new AllowAllMetaDataFilter();
LOG.debug("Using new default (allow-all) meta-data filter");
}
return (filter);
}).orElseThrow(() -> new QRuntimeException("Error getting metaDataFilter"));
}
/*******************************************************************************
**
*******************************************************************************/

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.metadata;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
**
*******************************************************************************/
public interface MetaDataFilterInterface
{
/***************************************************************************
**
***************************************************************************/
boolean allowTable(MetaDataInput input, QTableMetaData table);
/***************************************************************************
**
***************************************************************************/
boolean allowProcess(MetaDataInput input, QProcessMetaData process);
/***************************************************************************
**
***************************************************************************/
boolean allowReport(MetaDataInput input, QReportMetaData report);
/***************************************************************************
**
***************************************************************************/
boolean allowApp(MetaDataInput input, QAppMetaData app);
/***************************************************************************
**
***************************************************************************/
boolean allowWidget(MetaDataInput input, QWidgetMetaDataInterface widget);
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.metadata;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInput;
@ -47,7 +48,7 @@ public class ProcessMetaDataAction
// todo pre-customization - just get to modify the request?
ProcessMetaDataOutput processMetaDataOutput = new ProcessMetaDataOutput();
QProcessMetaData process = processMetaDataInput.getInstance().getProcess(processMetaDataInput.getProcessName());
QProcessMetaData process = QContext.getQInstance().getProcess(processMetaDataInput.getProcessName());
if(process == null)
{
throw (new QNotFoundException("Process [" + processMetaDataInput.getProcessName() + "] was not found."));

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.metadata;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput;
@ -48,12 +49,12 @@ public class TableMetaDataAction
// todo pre-customization - just get to modify the request?
TableMetaDataOutput tableMetaDataOutput = new TableMetaDataOutput();
QTableMetaData table = tableMetaDataInput.getInstance().getTable(tableMetaDataInput.getTableName());
QTableMetaData table = QContext.getQInstance().getTable(tableMetaDataInput.getTableName());
if(table == null)
{
throw (new QNotFoundException("Table [" + tableMetaDataInput.getTableName() + "] was not found."));
}
QBackendMetaData backendForTable = tableMetaDataInput.getInstance().getBackendForTable(table.getName());
QBackendMetaData backendForTable = QContext.getQInstance().getBackendForTable(table.getName());
tableMetaDataOutput.setTable(new QFrontendTableMetaData(tableMetaDataInput, backendForTable, table, true, true));
// todo post-customization - can do whatever w/ the result if you want

View File

@ -49,6 +49,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
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.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -78,6 +79,12 @@ public class PermissionsHelper
warnAboutPermissionSubTypeForTables(permissionSubType);
QTableMetaData table = QContext.getQInstance().getTable(tableName);
if(table == null)
{
LOG.info("Throwing a permission denied exception in response to a non-existent table name", logPair("tableName", tableName));
throw (new QPermissionDeniedException("Permission denied."));
}
commonCheckPermissionThrowing(getEffectivePermissionRules(table, QContext.getQInstance()), permissionSubType, table.getName());
}
@ -184,7 +191,14 @@ public class PermissionsHelper
*******************************************************************************/
public static void checkProcessPermissionThrowing(AbstractActionInput actionInput, String processName, Map<String, Serializable> processValues) throws QPermissionDeniedException
{
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
if(process == null)
{
LOG.info("Throwing a permission denied exception in response to a non-existent process name", logPair("processName", processName));
throw (new QPermissionDeniedException("Permission denied."));
}
QPermissionRules effectivePermissionRules = getEffectivePermissionRules(process, QContext.getQInstance());
if(effectivePermissionRules.getCustomPermissionChecker() != null)
@ -226,6 +240,13 @@ public class PermissionsHelper
public static void checkAppPermissionThrowing(AbstractActionInput actionInput, String appName) throws QPermissionDeniedException
{
QAppMetaData app = QContext.getQInstance().getApp(appName);
if(app == null)
{
LOG.info("Throwing a permission denied exception in response to a non-existent app name", logPair("appName", appName));
throw (new QPermissionDeniedException("Permission denied."));
}
commonCheckPermissionThrowing(getEffectivePermissionRules(app, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, app.getName());
}
@ -255,6 +276,13 @@ public class PermissionsHelper
public static void checkReportPermissionThrowing(AbstractActionInput actionInput, String reportName) throws QPermissionDeniedException
{
QReportMetaData report = QContext.getQInstance().getReport(reportName);
if(report == null)
{
LOG.info("Throwing a permission denied exception in response to a non-existent process name", logPair("reportName", reportName));
throw (new QPermissionDeniedException("Permission denied."));
}
commonCheckPermissionThrowing(getEffectivePermissionRules(report, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, report.getName());
}
@ -284,6 +312,13 @@ public class PermissionsHelper
public static void checkWidgetPermissionThrowing(AbstractActionInput actionInput, String widgetName) throws QPermissionDeniedException
{
QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
if(widget == null)
{
LOG.info("Throwing a permission denied exception in response to a non-existent widget name", logPair("widgetName", widgetName));
throw (new QPermissionDeniedException("Permission denied."));
}
commonCheckPermissionThrowing(getEffectivePermissionRules(widget, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, widget.getName());
}

View File

@ -25,6 +25,7 @@ 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.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
@ -54,7 +55,7 @@ public class CancelProcessAction extends RunProcessAction
{
ActionHelper.validateSession(runProcessInput);
QProcessMetaData process = runProcessInput.getInstance().getProcess(runProcessInput.getProcessName());
QProcessMetaData process = QContext.getQInstance().getProcess(runProcessInput.getProcessName());
if(process == null)
{
throw new QBadRequestException("Process [" + runProcessInput.getProcessName() + "] is not defined in this instance.");

View File

@ -30,6 +30,7 @@ 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;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
@ -39,6 +40,7 @@ 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;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceLambda;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
@ -64,7 +66,7 @@ public class RunBackendStepAction
{
ActionHelper.validateSession(runBackendStepInput);
QProcessMetaData process = runBackendStepInput.getInstance().getProcess(runBackendStepInput.getProcessName());
QProcessMetaData process = QContext.getQInstance().getProcess(runBackendStepInput.getProcessName());
if(process == null)
{
throw new QException("Process [" + runBackendStepInput.getProcessName() + "] is not defined in this instance.");
@ -256,11 +258,20 @@ public class RunBackendStepAction
{
runBackendStepOutput.seedFromRequest(runBackendStepInput);
Class<?> codeClass = Class.forName(code.getName());
Object codeObject = codeClass.getConstructor().newInstance();
Object codeObject;
if(code instanceof QCodeReferenceLambda<?> qCodeReferenceLambda)
{
codeObject = qCodeReferenceLambda.getLambda();
}
else
{
Class<?> codeClass = Class.forName(code.getName());
codeObject = codeClass.getConstructor().newInstance();
}
if(!(codeObject instanceof BackendStep backendStepCodeObject))
{
throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of BackendStep"));
throw (new QException("The supplied codeReference [" + code + "] is not a reference to a BackendStep"));
}
backendStepCodeObject.run(runBackendStepInput, runBackendStepOutput);

View File

@ -28,6 +28,7 @@ import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
@ -58,6 +59,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaD
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
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.processes.QStateMachineStep;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
@ -79,6 +81,7 @@ public class RunProcessAction
{
private static final QLogger LOG = QLogger.getLogger(RunProcessAction.class);
public static final String BASEPULL_KEY_VALUE = "basepullKeyValue";
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";
@ -99,7 +102,7 @@ public class RunProcessAction
{
ActionHelper.validateSession(runProcessInput);
QProcessMetaData process = runProcessInput.getInstance().getProcess(runProcessInput.getProcessName());
QProcessMetaData process = QContext.getQInstance().getProcess(runProcessInput.getProcessName());
if(process == null)
{
throw new QException("Process [" + runProcessInput.getProcessName() + "] is not defined in this instance.");
@ -133,90 +136,11 @@ public class RunProcessAction
try
{
String lastStepName = runProcessInput.getStartAfterStep();
STEP_LOOP:
while(true)
switch(Objects.requireNonNull(process.getStepFlow(), "Process [" + process.getName() + "] has a null stepFlow."))
{
///////////////////////////////////////////////////////////////////////////////////////////////////////
// always refresh the step list - as any step that runs can modify it (in the process state). //
// this is why we don't do a loop over the step list - as we'd get ConcurrentModificationExceptions. //
///////////////////////////////////////////////////////////////////////////////////////////////////////
List<QStepMetaData> stepList = getAvailableStepList(processState, process, lastStepName);
if(stepList.isEmpty())
{
break;
}
QStepMetaData step = stepList.get(0);
lastStepName = step.getName();
if(step instanceof QFrontendStepMetaData frontendStep)
{
////////////////////////////////////////////////////////////////
// Handle what to do with frontend steps, per request setting //
////////////////////////////////////////////////////////////////
switch(runProcessInput.getFrontendStepBehavior())
{
case BREAK ->
{
LOG.trace("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName());
processFrontendStepFieldDefaultValues(processState, frontendStep);
processFrontendComponents(processState, frontendStep);
processState.setNextStepName(step.getName());
break STEP_LOOP;
}
case SKIP ->
{
LOG.trace("Skipping frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
//////////////////////////////////////////////////////////////////////
// much less error prone in case this code changes in the future... //
//////////////////////////////////////////////////////////////////////
// noinspection UnnecessaryContinue
continue;
}
case FAIL ->
{
LOG.trace("Throwing error for frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
throw (new QException("Failing process at step " + step.getName() + " (as requested, to fail on frontend steps)"));
}
default -> throw new IllegalStateException("Unexpected value: " + runProcessInput.getFrontendStepBehavior());
}
}
else if(step instanceof QBackendStepMetaData backendStepMetaData)
{
///////////////////////
// Run backend steps //
///////////////////////
LOG.debug("Running backend step [" + step.getName() + "] in process [" + process.getName() + "]");
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
{
//////////////////////////////////////////////////
// in case we have a different step type, throw //
//////////////////////////////////////////////////
throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
}
case LINEAR -> runLinearStepLoop(process, processState, stateKey, runProcessInput, runProcessOutput);
case STATE_MACHINE -> runStateMachineStep(runProcessInput.getStartAfterStep(), process, processState, stateKey, runProcessInput, runProcessOutput, 0);
default -> throw (new QException("Unhandled process step flow: " + process.getStepFlow()));
}
///////////////////////////////////////////////////////////////////////////
@ -258,6 +182,270 @@ public class RunProcessAction
/***************************************************************************
**
***************************************************************************/
private void runLinearStepLoop(QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws Exception
{
String lastStepName = runProcessInput.getStartAfterStep();
while(true)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////
// always refresh the step list - as any step that runs can modify it (in the process state). //
// this is why we don't do a loop over the step list - as we'd get ConcurrentModificationExceptions. //
///////////////////////////////////////////////////////////////////////////////////////////////////////
List<QStepMetaData> stepList = getAvailableStepList(processState, process, lastStepName);
if(stepList.isEmpty())
{
break;
}
QStepMetaData step = stepList.get(0);
lastStepName = step.getName();
if(step instanceof QFrontendStepMetaData frontendStep)
{
LoopTodo loopTodo = prepareForFrontendStep(runProcessInput, process, frontendStep, processState);
if(loopTodo == LoopTodo.BREAK)
{
break;
}
}
else if(step instanceof QBackendStepMetaData backendStepMetaData)
{
RunBackendStepOutput runBackendStepOutput = runBackendStep(process, processState, stateKey, runProcessInput, runProcessOutput, backendStepMetaData, step);
/////////////////////////////////////////////////////////////////////////////////////////
// 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();
}
}
else
{
//////////////////////////////////////////////////
// in case we have a different step type, throw //
//////////////////////////////////////////////////
throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
}
}
}
/***************************************************************************
**
***************************************************************************/
private enum LoopTodo
{
BREAK,
CONTINUE
}
/***************************************************************************
**
***************************************************************************/
private LoopTodo prepareForFrontendStep(RunProcessInput runProcessInput, QProcessMetaData process, QFrontendStepMetaData step, ProcessState processState) throws QException
{
////////////////////////////////////////////////////////////////
// Handle what to do with frontend steps, per request setting //
////////////////////////////////////////////////////////////////
switch(runProcessInput.getFrontendStepBehavior())
{
case BREAK ->
{
LOG.trace("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName());
processFrontendStepFieldDefaultValues(processState, step);
processFrontendComponents(processState, step);
processState.setNextStepName(step.getName());
return LoopTodo.BREAK;
}
case SKIP ->
{
LOG.trace("Skipping frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
return LoopTodo.CONTINUE;
}
case FAIL ->
{
LOG.trace("Throwing error for frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
throw (new QException("Failing process at step " + step.getName() + " (as requested, to fail on frontend steps)"));
}
default -> throw new IllegalStateException("Unexpected value: " + runProcessInput.getFrontendStepBehavior());
}
}
/***************************************************************************
**
***************************************************************************/
private void runStateMachineStep(String lastStepName, QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, int stackDepth) throws Exception
{
//////////////////////////////
// check for stack-overflow //
//////////////////////////////
Integer maxStateMachineProcessStepFlowStackDepth = Objects.requireNonNullElse(runProcessInput.getValueInteger("maxStateMachineProcessStepFlowStackDepth"), 20);
if(stackDepth > maxStateMachineProcessStepFlowStackDepth)
{
throw (new QException("StateMachine process recurred too many times (exceeded maxStateMachineProcessStepFlowStackDepth of " + maxStateMachineProcessStepFlowStackDepth + ")"));
}
//////////////////////////////////
// figure out what step to run: //
//////////////////////////////////
QStepMetaData step = null;
if(!StringUtils.hasContent(lastStepName))
{
////////////////////////////////////////////////////////////////////
// if no lastStepName is given, start at the process's first step //
////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeIsEmpty(process.getStepList()))
{
throw (new QException("Process [" + process.getName() + "] does not have a step list defined."));
}
step = process.getStepList().get(0);
}
else
{
/////////////////////////////////////
// else run the given lastStepName //
/////////////////////////////////////
processState.clearNextStepName();
step = process.getStep(lastStepName);
if(step == null)
{
throw (new QException("Could not find step by name [" + lastStepName + "]"));
}
}
/////////////////////////////////////////////////////////////////////////
// for the flow of: //
// we were on a frontend step (as a sub-step of a state machine step), //
// and now we're here to run that state-step's backend step - //
// find the state-machine step containing this frontend step. //
/////////////////////////////////////////////////////////////////////////
String skipSubStepsUntil = null;
if(step instanceof QFrontendStepMetaData frontendStepMetaData)
{
QStateMachineStep stateMachineStep = getStateMachineStepContainingSubStep(process, frontendStepMetaData.getName());
if(stateMachineStep == null)
{
throw (new QException("Could not find stateMachineStep that contains last-frontend step: " + frontendStepMetaData.getName()));
}
step = stateMachineStep;
//////////////////////////////////////////////////////////////////////////////////
// set this flag, to know to skip this frontend step in the sub-step loop below //
//////////////////////////////////////////////////////////////////////////////////
skipSubStepsUntil = frontendStepMetaData.getName();
}
if(!(step instanceof QStateMachineStep stateMachineStep))
{
throw (new QException("Have a non-stateMachineStep in a process using stateMachine flow... " + step.getClass().getName()));
}
///////////////////////
// run the sub-steps //
///////////////////////
boolean ranAnySubSteps = false;
for(QStepMetaData subStep : stateMachineStep.getSubSteps())
{
///////////////////////////////////////////////////////////////////////////////////////////////
// ok, well, skip them if this flag is set (and clear the flag once we've hit this sub-step) //
///////////////////////////////////////////////////////////////////////////////////////////////
if(skipSubStepsUntil != null)
{
if(skipSubStepsUntil.equals(subStep.getName()))
{
skipSubStepsUntil = null;
}
continue;
}
ranAnySubSteps = true;
if(subStep instanceof QFrontendStepMetaData frontendStep)
{
LoopTodo loopTodo = prepareForFrontendStep(runProcessInput, process, frontendStep, processState);
if(loopTodo == LoopTodo.BREAK)
{
return;
}
}
else if(subStep instanceof QBackendStepMetaData backendStepMetaData)
{
RunBackendStepOutput runBackendStepOutput = runBackendStep(process, processState, stateKey, runProcessInput, runProcessOutput, backendStepMetaData, step);
Optional<String> nextStepName = runBackendStepOutput.getProcessState().getNextStepName();
if(nextStepName.isEmpty() && StringUtils.hasContent(stateMachineStep.getDefaultNextStepName()))
{
nextStepName = Optional.of(stateMachineStep.getDefaultNextStepName());
}
if(nextStepName.isPresent())
{
//////////////////////////////////////////////////////////////////////////////////////////////////////
// if we've been given a next-step-name, go to that step now. //
// it might be a backend-only stateMachineStep, in which case, we should run that backend step now. //
// or it might be a frontend-then-backend step, in which case, we want to go to that frontend step. //
// if we weren't given a next-step-name, then we should stay in the same state - either to finish //
// its sub-steps, or, to fall out of the loop and end the process. //
//////////////////////////////////////////////////////////////////////////////////////////////////////
processState.clearNextStepName();
runStateMachineStep(nextStepName.get(), process, processState, stateKey, runProcessInput, runProcessOutput, stackDepth + 1);
return;
}
}
else
{
//////////////////////////////////////////////////
// in case we have a different step type, throw //
//////////////////////////////////////////////////
throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
}
}
if(!ranAnySubSteps)
{
if(StringUtils.hasContent(stateMachineStep.getDefaultNextStepName()))
{
runStateMachineStep(stateMachineStep.getDefaultNextStepName(), process, processState, stateKey, runProcessInput, runProcessOutput, stackDepth + 1);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
public QStateMachineStep getStateMachineStepContainingSubStep(QProcessMetaData process, String stepName)
{
for(QStepMetaData step : process.getAllSteps().values())
{
if(step instanceof QStateMachineStep stateMachineStep)
{
for(QStepMetaData subStep : stateMachineStep.getSubSteps())
{
if(subStep.getName().equals(stepName))
{
return (stateMachineStep);
}
}
}
}
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
@ -335,12 +523,12 @@ 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 we're restoring an old state, we can discard a previously stored processMetaDataAdjustment - //
// it is only needed on the transitional edge from a backend-step to a frontend step, but not //
// in the other directly //
/////////////////////////////////////////////////////////////////////////////////////////////////////
optionalProcessState.get().setProcessMetaDataAdjustment(null);
///////////////////////////////////////////////////////////////////////////
// if there were values from the caller, put those (back) in the request //
@ -355,16 +543,40 @@ public class RunProcessAction
}
ProcessState processState = optionalProcessState.get();
processState.clearNextStepName();
return processState;
}
/***************************************************************************
**
***************************************************************************/
private RunBackendStepOutput runBackendStep(QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, QBackendStepMetaData backendStepMetaData, QStepMetaData step) throws Exception
{
///////////////////////
// Run backend steps //
///////////////////////
LOG.debug("Running backend step [" + step.getName() + "] in process [" + process.getName() + "]");
RunBackendStepOutput runBackendStepOutput = runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
//////////////////////////////////////////////////////////////////////////////////////////////
// similarly, if the step produced a processMetaDataAdjustment, propagate that data outward //
//////////////////////////////////////////////////////////////////////////////////////////////
if(runBackendStepOutput.getProcessMetaDataAdjustment() != null)
{
LOG.debug("Process step [" + step.getName() + "] generated a ProcessMetaDataAdjustment [" + runBackendStepOutput.getProcessMetaDataAdjustment() + "]!");
runProcessOutput.setProcessMetaDataAdjustment(runBackendStepOutput.getProcessMetaDataAdjustment());
}
return runBackendStepOutput;
}
/*******************************************************************************
** Run a single backend step.
*******************************************************************************/
protected RunBackendStepOutput runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
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());
@ -517,9 +729,13 @@ public class RunProcessAction
/*******************************************************************************
**
*******************************************************************************/
protected String determineBasepullKeyValue(QProcessMetaData process, BasepullConfiguration basepullConfiguration) throws QException
protected String determineBasepullKeyValue(QProcessMetaData process, RunProcessInput runProcessInput, BasepullConfiguration basepullConfiguration) throws QException
{
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
if(runProcessInput.getValueString(BASEPULL_KEY_VALUE) != null)
{
basepullKeyValue = runProcessInput.getValueString(BASEPULL_KEY_VALUE);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if process specifies that it uses variants, look for that data in the session and append to our basepull key //
@ -551,7 +767,7 @@ public class RunProcessAction
String basepullTableName = basepullConfiguration.getTableName();
String basepullKeyFieldName = basepullConfiguration.getKeyField();
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
String basepullKeyValue = determineBasepullKeyValue(process, basepullConfiguration);
String basepullKeyValue = determineBasepullKeyValue(process, runProcessInput, basepullConfiguration);
///////////////////////////////////////
// get the stored basepull timestamp //
@ -631,7 +847,7 @@ public class RunProcessAction
String basepullKeyFieldName = basepullConfiguration.getKeyField();
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
Integer basepullHoursBackForInitialTimestamp = basepullConfiguration.getHoursBackForInitialTimestamp();
String basepullKeyValue = determineBasepullKeyValue(process, basepullConfiguration);
String basepullKeyValue = determineBasepullKeyValue(process, runProcessInput, basepullConfiguration);
///////////////////////////////////////
// get the stored basepull timestamp //

View File

@ -0,0 +1,202 @@
/*
* 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.reporting;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.ReportDestination;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
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.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.Pair;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Utility for verifying that the ExportAction works for all tables, and all
** exposed joins.
**
** Meant for use within a unit test, or maybe as part of an instance's boot-up/
** validation.
*******************************************************************************/
public class ExportsFullInstanceVerifier
{
private static final QLogger LOG = QLogger.getLogger(ExportsFullInstanceVerifier.class);
private boolean filterForAtMostOneRowPerExport = true;
/*******************************************************************************
**
*******************************************************************************/
public void verify(Collection<QTableMetaData> tables) throws QException
{
Map<Pair<String, String>, Exception> caughtExceptions = new LinkedHashMap<>();
for(QTableMetaData table : tables)
{
if(table.isCapabilityEnabled(QContext.getQInstance().getBackendForTable(table.getName()), Capability.TABLE_QUERY))
{
LOG.info("Verifying Exports on table", logPair("tableName", table.getName()));
//////////////////////////////////////////////
// run the table by itself (no join fields) //
//////////////////////////////////////////////
runExport(table.getName(), Collections.emptyList(), "main-table-only", caughtExceptions);
///////////////////////////////////////////////////
// run once w/ the fields from each exposed join //
///////////////////////////////////////////////////
for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(table.getExposedJoins()))
{
runExport(table.getName(), List.of(exposedJoin), "join-" + exposedJoin.getLabel(), caughtExceptions);
}
/////////////////////////////////////////////////
// run w/ all exposed joins (if there are any) //
/////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(table.getExposedJoins()))
{
runExport(table.getName(), table.getExposedJoins(), "all-joins", caughtExceptions);
}
}
}
//////////////////////////////////
// log out an exceptions caught //
//////////////////////////////////
if(!caughtExceptions.isEmpty())
{
for(Map.Entry<Pair<String, String>, Exception> entry : caughtExceptions.entrySet())
{
LOG.info("Caught an exception verifying reports", entry.getValue(), logPair("tableName", entry.getKey().getA()), logPair("fieldName", entry.getKey().getB()));
}
throw (new QException("Reports Verification failed with " + caughtExceptions.size() + " exception" + StringUtils.plural(caughtExceptions.size())));
}
}
/*******************************************************************************
**
*******************************************************************************/
private void runExport(String tableName, List<ExposedJoin> exposedJoinList, String description, Map<Pair<String, String>, Exception> caughtExceptions)
{
try
{
////////////////////////////////////////////////////////////////////////////////////
// build the list of fieldNames to export - starting with all fields in the table //
////////////////////////////////////////////////////////////////////////////////////
List<String> fieldNames = new ArrayList<>();
for(QFieldMetaData field : QContext.getQInstance().getTable(tableName).getFields().values())
{
fieldNames.add(field.getName());
}
///////////////////////////////////////////////////
// add all fields from all exposed joins as well //
///////////////////////////////////////////////////
for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(exposedJoinList))
{
QTableMetaData joinTable = QContext.getQInstance().getTable(exposedJoin.getJoinTable());
for(QFieldMetaData field : joinTable.getFields().values())
{
fieldNames.add(joinTable.getName() + "." + field.getName());
}
}
LOG.info("Verifying export", logPair("description", description), logPair("fieldCount", fieldNames.size()));
QQueryFilter queryFilter = new QQueryFilter();
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if caller is okay with a filter that should limit the report to a small number of rows (could be more than 1 for to-many joins), then do so //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(filterForAtMostOneRowPerExport)
{
queryFilter.withCriteria(QContext.getQInstance().getTable(tableName).getPrimaryKeyField(), QCriteriaOperator.EQUALS, 1);
}
ExportInput exportInput = new ExportInput();
exportInput.setTableName(tableName);
exportInput.setFieldNames(fieldNames);
exportInput.setReportDestination(new ReportDestination()
.withReportOutputStream(new ByteArrayOutputStream())
.withReportFormat(ReportFormat.CSV));
exportInput.setQueryFilter(queryFilter);
new ExportAction().execute(exportInput);
}
catch(QException e)
{
caughtExceptions.put(Pair.of(tableName, description), e);
}
}
/*******************************************************************************
** Getter for filterForAtMostOneRowPerExport
*******************************************************************************/
public boolean getFilterForAtMostOneRowPerExport()
{
return (this.filterForAtMostOneRowPerExport);
}
/*******************************************************************************
** Setter for filterForAtMostOneRowPerExport
*******************************************************************************/
public void setFilterForAtMostOneRowPerExport(boolean filterForAtMostOneRowPerExport)
{
this.filterForAtMostOneRowPerExport = filterForAtMostOneRowPerExport;
}
/*******************************************************************************
** Fluent setter for filterForAtMostOneRowPerExport
*******************************************************************************/
public ExportsFullInstanceVerifier withFilterForAtMostOneRowPerExport(boolean filterForAtMostOneRowPerExport)
{
this.filterForAtMostOneRowPerExport = filterForAtMostOneRowPerExport;
return (this);
}
}

View File

@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.DataSourceQueryInputCustomizer;
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportCustomRecordSourceInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportViewCustomizer;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
@ -62,10 +63,13 @@ 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.CriteriaMissingInputValueBehavior;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.FilterUseCase;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
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.QueryJoin;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
@ -301,10 +305,19 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
JoinsContext joinsContext = null;
if(dataSource != null)
{
///////////////////////////////////////////////////////////////////////////////////////
// count records, if applicable, from the data source - for populating into the //
// countByDataSource map, as well as for checking if too many rows (e.g., for excel) //
///////////////////////////////////////////////////////////////////////////////////////
countDataSourceRecords(reportInput, dataSource, reportFormat);
///////////////////////////////////////////////////////////////////////////////////////////
// if there's a source table, set up a joins context, to use below for looking up fields //
///////////////////////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(dataSource.getSourceTable()))
{
joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), dataSource.getQueryFilter());
countDataSourceRecords(reportInput, dataSource, reportFormat);
QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
joinsContext = new JoinsContext(QContext.getQInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), queryFilter);
}
}
@ -328,6 +341,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
field.setName(column.getName());
if(StringUtils.hasContent(column.getLabel()))
{
field.setLabel(column.getLabel());
}
fields.add(field);
@ -345,23 +359,33 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
*******************************************************************************/
private void countDataSourceRecords(ReportInput reportInput, QReportDataSource dataSource, ReportFormat reportFormat) throws QException
{
QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
setInputValuesInQueryFilter(reportInput, queryFilter);
CountInput countInput = new CountInput();
countInput.setTableName(dataSource.getSourceTable());
countInput.setFilter(queryFilter);
countInput.setQueryJoins(dataSource.getQueryJoins());
CountOutput countOutput = new CountAction().execute(countInput);
if(countOutput.getCount() != null)
Integer count = null;
if(dataSource.getCustomRecordSource() != null)
{
countByDataSource.put(dataSource.getName(), countOutput.getCount());
// todo - add `count` method to interface?
}
else if(StringUtils.hasContent(dataSource.getSourceTable()))
{
QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
setInputValuesInQueryFilter(reportInput, queryFilter);
if(reportFormat.getMaxRows() != null && countOutput.getCount() > reportFormat.getMaxRows())
CountInput countInput = new CountInput();
countInput.setTableName(dataSource.getSourceTable());
countInput.setFilter(queryFilter);
countInput.setQueryJoins(cloneDataSourceQueryJoins(dataSource));
CountOutput countOutput = new CountAction().execute(countInput);
count = countOutput.getCount();
}
if(count != null)
{
countByDataSource.put(dataSource.getName(), count);
if(reportFormat.getMaxRows() != null && count > reportFormat.getMaxRows())
{
throw (new QUserFacingException("The requested report would include more rows ("
+ String.format("%,d", countOutput.getCount()) + ") than the maximum allowed ("
+ String.format("%,d", count) + ") than the maximum allowed ("
+ String.format("%,d", reportFormat.getMaxRows()) + ") for the selected file format (" + reportFormat + ")."));
}
}
@ -369,6 +393,26 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/*******************************************************************************
**
*******************************************************************************/
private static List<QueryJoin> cloneDataSourceQueryJoins(QReportDataSource dataSource)
{
if(dataSource == null || dataSource.getQueryJoins() == null)
{
return (null);
}
List<QueryJoin> rs = new ArrayList<>();
for(QueryJoin queryJoin : dataSource.getQueryJoins())
{
rs.add(queryJoin.clone());
}
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
@ -402,13 +446,19 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
String tableLabel = ObjectUtils.tryElse(() -> QContext.getQInstance().getTable(dataSource.getSourceTable()).getLabel(), Objects.requireNonNullElse(dataSource.getSourceTable(), ""));
AtomicInteger consumedCount = new AtomicInteger(0);
/////////////////////////////////////////////////////////////////
// run a record pipe loop, over the query for this data source //
/////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////
// run a record pipe loop, over the query (or other data-supplier/source) for this data source //
/////////////////////////////////////////////////////////////////////////////////////////////////
RecordPipe recordPipe = new BufferedRecordPipe(1000);
new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) ->
{
if(dataSource.getSourceTable() != null)
if(dataSource.getCustomRecordSource() != null)
{
ReportCustomRecordSourceInterface recordSource = QCodeLoader.getAdHoc(ReportCustomRecordSourceInterface.class, dataSource.getCustomRecordSource());
recordSource.execute(reportInput, dataSource, recordPipe);
return (true);
}
else if(dataSource.getSourceTable() != null)
{
QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
setInputValuesInQueryFilter(reportInput, queryFilter);
@ -417,12 +467,12 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
queryInput.setRecordPipe(recordPipe);
queryInput.setTableName(dataSource.getSourceTable());
queryInput.setFilter(queryFilter);
queryInput.setQueryJoins(dataSource.getQueryJoins());
queryInput.setQueryJoins(cloneDataSourceQueryJoins(dataSource));
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())));
queryInput.setFieldsToTranslatePossibleValues(setupFieldsToTranslatePossibleValues(reportInput, dataSource));
if(dataSource.getQueryInputCustomizer() != null)
{
@ -474,7 +524,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
}
consumedCount.getAndAdd(records.size());
return (consumeRecords(reportInput, dataSource, records, tableView, summaryViews, variantViews));
return (consumeRecords(dataSource, records, tableView, summaryViews, variantViews));
});
////////////////////////////////////////////////
@ -493,7 +543,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/*******************************************************************************
**
*******************************************************************************/
private Set<String> setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource, JoinsContext joinsContext) throws QException
private Set<String> setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource) throws QException
{
Set<String> fieldsToTranslatePossibleValues = new HashSet<>();
@ -566,7 +616,56 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
return;
}
queryFilter.interpretValues(reportInput.getInputValues());
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// for reports defined in meta-data, the established rule is, that missing input variable values are discarded. //
// but for non-meta-data reports (e.g., user-saved), we expect an exception for missing values. //
// so, set those use-cases up. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
FilterUseCase filterUseCase;
if(StringUtils.hasContent(reportInput.getReportName()) && QContext.getQInstance().getReport(reportInput.getReportName()) != null)
{
filterUseCase = new ReportFromMetaDataFilterUseCase();
}
else
{
filterUseCase = new ReportNotFromMetaDataFilterUseCase();
}
queryFilter.interpretValues(reportInput.getInputValues(), filterUseCase);
}
/***************************************************************************
**
***************************************************************************/
private static class ReportFromMetaDataFilterUseCase implements FilterUseCase
{
/***************************************************************************
**
***************************************************************************/
@Override
public CriteriaMissingInputValueBehavior getDefaultCriteriaMissingInputValueBehavior()
{
return CriteriaMissingInputValueBehavior.REMOVE_FROM_FILTER;
}
}
/***************************************************************************
**
***************************************************************************/
private static class ReportNotFromMetaDataFilterUseCase implements FilterUseCase
{
/***************************************************************************
**
***************************************************************************/
@Override
public CriteriaMissingInputValueBehavior getDefaultCriteriaMissingInputValueBehavior()
{
return CriteriaMissingInputValueBehavior.THROW_EXCEPTION;
}
}
@ -574,9 +673,9 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/*******************************************************************************
**
*******************************************************************************/
private Integer consumeRecords(ReportInput reportInput, QReportDataSource dataSource, List<QRecord> records, QReportView tableView, List<QReportView> summaryViews, List<QReportView> variantViews) throws QException
private Integer consumeRecords(QReportDataSource dataSource, List<QRecord> records, QReportView tableView, List<QReportView> summaryViews, List<QReportView> variantViews) throws QException
{
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
QTableMetaData table = QContext.getQInstance().getTable(dataSource.getSourceTable());
////////////////////////////////////////////////////////////////////////////
// if this record goes on a table view, add it to the report streamer now //
@ -687,7 +786,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/*******************************************************************************
**
*******************************************************************************/
private void addRecordToSummaryKeyAggregates(QTableMetaData table, QRecord record, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>> viewAggregates, SummaryKey key) throws QException
private void addRecordToSummaryKeyAggregates(QTableMetaData table, QRecord record, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>> viewAggregates, SummaryKey key)
{
Map<String, AggregatesInterface<?, ?>> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
addRecordToAggregatesMap(table, record, keyAggregates);
@ -698,7 +797,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/*******************************************************************************
**
*******************************************************************************/
private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map<String, AggregatesInterface<?, ?>> aggregatesMap) throws QException
private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map<String, AggregatesInterface<?, ?>> aggregatesMap)
{
//////////////////////////////////////////////////////////////////////////////////////
// todo - an optimization could be, to only compute aggregates that we'll need... //
@ -706,7 +805,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
//////////////////////////////////////////////////////////////////////////////////////
for(String fieldName : record.getValues().keySet())
{
QFieldMetaData field = null;
QFieldMetaData field;
try
{
//////////////////////////////////////////////////////
@ -779,9 +878,14 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
List<QReportView> reportViews = views.stream().filter(v -> v.getType().equals(ReportType.SUMMARY)).toList();
for(QReportView view : reportViews)
{
QReportDataSource dataSource = getDataSource(view.getDataSourceName());
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
SummaryOutput summaryOutput = computeSummaryRowsForView(reportInput, view, table);
QReportDataSource dataSource = getDataSource(view.getDataSourceName());
if(dataSource == null)
{
throw new QReportingException("Data source for summary view was not found (viewName=" + view.getName() + ", dataSourceName=" + view.getDataSourceName() + ").");
}
QTableMetaData table = QContext.getQInstance().getTable(dataSource.getSourceTable());
SummaryOutput summaryOutput = computeSummaryRowsForView(reportInput, view, table);
ExportInput exportInput = new ExportInput();
exportInput.setReportDestination(reportInput.getReportDestination());
@ -867,9 +971,8 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/*******************************************************************************
**
*******************************************************************************/
private SummaryOutput computeSummaryRowsForView(ReportInput reportInput, QReportView view, QTableMetaData table) throws QReportingException, QFormulaException
private SummaryOutput computeSummaryRowsForView(ReportInput reportInput, QReportView view, QTableMetaData table) throws QFormulaException
{
QValueFormatter valueFormatter = new QValueFormatter();
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
variableInterpreter.addValueMap("input", reportInput.getInputValues());
variableInterpreter.addValueMap("total", getSummaryValuesForInterpreter(totalAggregates));
@ -941,10 +1044,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
//////////////////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(view.getOrderByFields()))
{
summaryRows.sort((o1, o2) ->
{
return summaryRowComparator(view, o1, o2);
});
summaryRows.sort((o1, o2) -> summaryRowComparator(view, o1, o2));
}
////////////////
@ -979,8 +1079,6 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
Serializable serializable = getValueForColumn(variableInterpreter, column);
totalRow.setValue(column.getName(), serializable);
thisRowValues.put(column.getName(), serializable);
String formatted = valueFormatter.formatValue(column.getDisplayFormat(), serializable);
}
}
@ -1003,7 +1101,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
titleValues.add(variableInterpreter.interpret(titleField));
}
title = new QValueFormatter().formatStringWithValues(view.getTitleFormat(), titleValues);
title = QValueFormatter.formatStringWithValues(view.getTitleFormat(), titleValues);
}
else if(StringUtils.hasContent(view.getTitleFormat()))
{

View File

@ -0,0 +1,43 @@
/*
* 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.reporting.customizers;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
/*******************************************************************************
** Interface to be implemented to do a custom source of data for a report
** (instead of just a query against a table).
*******************************************************************************/
public interface ReportCustomRecordSourceInterface
{
/***************************************************************************
** Given the report input, put records into the pipe, for the report.
***************************************************************************/
void execute(ReportInput reportInput, QReportDataSource reportDataSource, RecordPipe recordPipe) throws QException;
}

View File

@ -124,7 +124,7 @@ public class ExcelFastexcelExportStreamer implements ExportStreamerInterface
if(workbook == null)
{
String appName = ObjectUtils.tryAndRequireNonNullElse(() -> QContext.getQInstance().getBranding().getAppName(), "QQQ");
QInstance instance = exportInput.getInstance();
QInstance instance = QContext.getQInstance();
if(instance != null && instance.getBranding() != null && instance.getBranding().getCompanyName() != null)
{
appName = instance.getBranding().getCompanyName();

View File

@ -124,10 +124,11 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
private Writer activeSheetWriter = null;
private StreamedSheetWriter sheetWriter = null;
private QReportView currentView = null;
private Map<String, List<QFieldMetaData>> fieldsPerView = new HashMap<>();
private Map<String, Integer> rowsPerView = new HashMap<>();
private Map<String, String> labelViewsByName = new HashMap<>();
private QReportView currentView = null;
private Map<String, List<QFieldMetaData>> fieldsPerView = new HashMap<>();
private Map<String, Integer> rowsPerView = new HashMap<>();
private Map<String, String> labelViewsByName = new HashMap<>();
private Map<String, String> sheetReferenceByViewName = new HashMap<>();
@ -180,6 +181,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
String sheetReference = sheet.getPackagePart().getPartName().getName().substring(1);
sheetMapByExcelReference.put(sheetReference, sheet);
sheetMapByViewName.put(view.getName(), sheet);
sheetReferenceByViewName.put(view.getName(), sheetReference);
sheetCounter++;
}
@ -446,7 +448,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
// - with a new output stream writer //
// - and with a SpreadsheetWriter //
//////////////////////////////////////////
zipOutputStream.putNextEntry(new ZipEntry("xl/worksheets/sheet" + this.sheetIndex++ + ".xml"));
zipOutputStream.putNextEntry(new ZipEntry(sheetReferenceByViewName.get(view.getName())));
activeSheetWriter = new OutputStreamWriter(zipOutputStream);
sheetWriter = new StreamedSheetWriter(activeSheetWriter);

View File

@ -161,7 +161,7 @@ public class StreamedSheetWriter
}
}
Map<String, Integer> m = new HashMap();
Map<String, Integer> m = new HashMap<>();
m.computeIfAbsent("s", (s) -> 3);
value = rs.toString();

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.BuildScriptLogAndScriptLogLineExecutionLogger;
@ -97,7 +98,7 @@ public class RecordScriptTestInterface implements TestScriptActionInterface
}
QueryOutput queryOutput = new QueryAction().execute(new QueryInput(tableName)
.withFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, recordPrimaryKeyList.split(","))))
.withFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, Arrays.stream(recordPrimaryKeyList.split(",")).toList())))
.withIncludeAssociations(true));
if(CollectionUtils.nullSafeIsEmpty(queryOutput.getRecords()))
{

View File

@ -154,8 +154,9 @@ public class RunAdHocRecordScriptAction
Method qRecordListToApiRecordList = apiScriptUtilsClass.getMethod("qRecordListToApiRecordList", List.class, String.class, String.class, String.class);
Object apiRecordList = qRecordListToApiRecordList.invoke(null, input.getRecordList(), input.getTableName(), scriptRevision.getApiName(), scriptRevision.getApiVersion());
// noinspection unchecked
return (ArrayList<? extends Serializable>) apiRecordList;
@SuppressWarnings("unchecked")
ArrayList<? extends Serializable> rs = (ArrayList<? extends Serializable>) apiRecordList;
return rs;
}
catch(ClassNotFoundException e)
{

View File

@ -352,7 +352,7 @@ public class GetAction
{
if(qPossibleValueTranslator == null)
{
qPossibleValueTranslator = new QPossibleValueTranslator(getInput.getInstance(), getInput.getSession());
qPossibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), QContext.getQSession());
}
qPossibleValueTranslator.translatePossibleValuesInRecords(getInput.getTable(), List.of(returnRecord));
}

View File

@ -227,6 +227,11 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
*******************************************************************************/
public void performValidations(InsertInput insertInput, boolean isPreview) throws QException
{
if(CollectionUtils.nullSafeIsEmpty(insertInput.getRecords()))
{
return;
}
QTableMetaData table = insertInput.getTable();
///////////////////////////////////////////////////////////////////
@ -241,7 +246,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
setDefaultValuesInRecords(table, insertInput.getRecords());
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, insertInput.getInstance(), table, insertInput.getRecords(), null);
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, QContext.getQInstance(), table, insertInput.getRecords(), null);
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_UNIQUE_KEY_CHECKS);
setErrorsIfUniqueKeyErrors(insertInput, table);

View File

@ -26,6 +26,7 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@ -50,8 +51,10 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperat
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
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.QueryJoin;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
@ -64,6 +67,7 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -101,6 +105,8 @@ public class QueryAction
throw (new QException("A table named [" + queryInput.getTableName() + "] was not found in the active QInstance"));
}
validateFieldNamesToInclude(queryInput);
QBackendMetaData backend = queryInput.getBackend();
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_QUERY_RECORD.getRole());
this.queryInput = queryInput;
@ -158,6 +164,125 @@ public class QueryAction
/***************************************************************************
** if QueryInput contains a set of FieldNamesToInclude, then validate that
** those are known field names in the table being queried, or a selected
** queryJoin.
***************************************************************************/
static void validateFieldNamesToInclude(QueryInput queryInput) throws QException
{
Set<String> fieldNamesToInclude = queryInput.getFieldNamesToInclude();
if(fieldNamesToInclude == null)
{
////////////////////////////////
// null set means select all. //
////////////////////////////////
return;
}
if(fieldNamesToInclude.isEmpty())
{
/////////////////////////////////////
// empty set, however, is an error //
/////////////////////////////////////
throw (new QException("An empty set of fieldNamesToInclude was given as queryInput, which is not allowed."));
}
List<String> unrecognizedFieldNames = new ArrayList<>();
Map<String, QTableMetaData> selectedQueryJoins = null;
for(String fieldName : fieldNamesToInclude)
{
if(fieldName.contains("."))
{
////////////////////////////////////////////////
// handle names with dots - fields from joins //
////////////////////////////////////////////////
String[] parts = fieldName.split("\\.");
if(parts.length != 2)
{
unrecognizedFieldNames.add(fieldName);
}
else
{
String tableOrAlias = parts[0];
String fieldNamePart = parts[1];
////////////////////////////////////////////
// build map of queryJoins being selected //
////////////////////////////////////////////
if(selectedQueryJoins == null)
{
selectedQueryJoins = new HashMap<>();
for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryInput.getQueryJoins()))
{
if(queryJoin.getSelect())
{
String joinTableOrAlias = queryJoin.getJoinTableOrItsAlias();
QTableMetaData joinTable = QContext.getQInstance().getTable(queryJoin.getJoinTable());
if(joinTable != null)
{
selectedQueryJoins.put(joinTableOrAlias, joinTable);
}
}
}
}
if(!selectedQueryJoins.containsKey(tableOrAlias))
{
///////////////////////////////////////////
// unrecognized tableOrAlias is an error //
///////////////////////////////////////////
unrecognizedFieldNames.add(fieldName);
}
else
{
QTableMetaData joinTable = selectedQueryJoins.get(tableOrAlias);
if(!joinTable.getFields().containsKey(fieldNamePart))
{
//////////////////////////////////////////////////////////
// unrecognized field within the join table is an error //
//////////////////////////////////////////////////////////
unrecognizedFieldNames.add(fieldName);
}
}
}
}
else
{
///////////////////////////////////////////////////////////////////////
// non-join fields - just ensure field name is in table's fields map //
///////////////////////////////////////////////////////////////////////
if(!queryInput.getTable().getFields().containsKey(fieldName))
{
unrecognizedFieldNames.add(fieldName);
}
}
}
if(!unrecognizedFieldNames.isEmpty())
{
throw (new QException("QueryInput contained " + unrecognizedFieldNames.size() + " unrecognized field name" + StringUtils.plural(unrecognizedFieldNames) + ": " + StringUtils.join(",", unrecognizedFieldNames)));
}
}
/*******************************************************************************
** shorthand way to call for the most common use-case, when you just want the
** entities to be returned, and you just want to pass in a table name and filter.
*******************************************************************************/
public static <T extends QRecordEntity> List<T> execute(String tableName, Class<T> entityClass, 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.getRecordEntities(entityClass));
}
/*******************************************************************************
** 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.
@ -297,7 +422,7 @@ public class QueryAction
{
if(qPossibleValueTranslator == null)
{
qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession());
qPossibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), QContext.getQSession());
}
qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records, queryInput.getQueryJoins(), queryInput.getFieldsToTranslatePossibleValues());
}

View File

@ -252,7 +252,7 @@ public class UpdateAction
behaviorsToOmit = Set.of(DynamicDefaultValueBehavior.MODIFY_DATE);
}
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, updateInput.getInstance(), table, updateInput.getRecords(), behaviorsToOmit);
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, QContext.getQInstance(), table, updateInput.getRecords(), behaviorsToOmit);
validatePrimaryKeysAreGiven(updateInput);
if(oldRecordList.isPresent())

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.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -68,7 +67,7 @@ public class QValueFormatter
*******************************************************************************/
public static String formatValue(QFieldMetaData field, Serializable value)
{
return (formatValue(field.getDisplayFormat(), field.getName(), value));
return (formatValue(field.getDisplayFormat(), field.getType(), field.getName(), value));
}
@ -78,7 +77,7 @@ public class QValueFormatter
*******************************************************************************/
public static String formatValue(String displayFormat, Serializable value)
{
return (formatValue(displayFormat, "", value));
return (formatValue(displayFormat, null, "", value));
}
@ -87,7 +86,7 @@ public class QValueFormatter
** For a display format string, an optional fieldName (only used for logging),
** and a value, apply the format.
*******************************************************************************/
private static String formatValue(String displayFormat, String fieldName, Serializable value)
private static String formatValue(String displayFormat, QFieldType fieldType, String fieldName, Serializable value)
{
//////////////////////////////////
// null values get null results //
@ -107,6 +106,11 @@ public class QValueFormatter
return formatBoolean(b);
}
if(QFieldType.BOOLEAN.equals(fieldType))
{
return formatBoolean(ValueUtils.getValueAsBoolean(value));
}
if(value instanceof LocalTime lt)
{
return formatLocalTime(lt);
@ -404,6 +408,7 @@ public class QValueFormatter
}
/*******************************************************************************
** 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).
@ -462,7 +467,8 @@ public class QValueFormatter
{
for(QFieldMetaData field : table.getFields().values())
{
if(field.getType().equals(QFieldType.BLOB))
Optional<FieldAdornment> fileDownloadAdornment = field.getAdornment(AdornmentType.FILE_DOWNLOAD);
if(fileDownloadAdornment.isPresent())
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// file name comes from: //
@ -472,20 +478,7 @@ public class QValueFormatter
// - tableLabel primaryKey fieldLabel //
// - and - if the FILE_DOWNLOAD adornment had a DEFAULT_EXTENSION, then it gets added (preceded by a dot) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Optional<FieldAdornment> fileDownloadAdornment = field.getAdornment(AdornmentType.FILE_DOWNLOAD);
Map<String, Serializable> adornmentValues = Collections.emptyMap();
if(fileDownloadAdornment.isPresent())
{
adornmentValues = fileDownloadAdornment.get().getValues();
}
else
{
///////////////////////////////////////////////////////
// don't change blobs unless they are file-downloads //
///////////////////////////////////////////////////////
continue;
}
Map<String, Serializable> adornmentValues = fileDownloadAdornment.get().getValues();
String fileNameField = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FIELD));
String fileNameFormat = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT));
@ -536,7 +529,13 @@ public class QValueFormatter
}
}
record.setValue(field.getName(), "/data/" + table.getName() + "/" + primaryKey + "/" + field.getName() + "/" + fileName);
/////////////////////////////////////////////
// if field type is blob, update its value //
/////////////////////////////////////////////
if(QFieldType.BLOB.equals(field.getType()))
{
record.setValue(field.getName(), "/data/" + table.getName() + "/" + primaryKey + "/" + field.getName() + "/" + fileName);
}
record.setDisplayValue(field.getName(), fileName);
}
}
@ -563,6 +562,7 @@ public class QValueFormatter
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// heavy fields that weren't fetched - they should have a backend-detail specifying their length (or null if null) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("unchecked")
Map<String, Serializable> heavyFieldLengths = (Map<String, Serializable>) record.getBackendDetail(QRecord.BACKEND_DETAILS_TYPE_HEAVY_FIELD_LENGTHS);
if(heavyFieldLengths != null)
{

View File

@ -30,6 +30,7 @@ import java.util.List;
import java.util.Objects;
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.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
@ -69,14 +70,14 @@ public class SearchPossibleValueSourceAction
*******************************************************************************/
public SearchPossibleValueSourceOutput execute(SearchPossibleValueSourceInput input) throws QException
{
QInstance qInstance = input.getInstance();
QInstance qInstance = QContext.getQInstance();
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(input.getPossibleValueSourceName());
if(possibleValueSource == null)
{
throw new QException("Missing possible value source named [" + input.getPossibleValueSourceName() + "]");
}
possibleValueTranslator = new QPossibleValueTranslator(input.getInstance(), input.getSession());
possibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), QContext.getQSession());
SearchPossibleValueSourceOutput output = null;
if(possibleValueSource.getType().equals(QPossibleValueSourceType.ENUM))
{
@ -199,7 +200,7 @@ public class SearchPossibleValueSourceAction
QueryInput queryInput = new QueryInput();
queryInput.setTableName(possibleValueSource.getTableName());
QTableMetaData table = input.getInstance().getTable(possibleValueSource.getTableName());
QTableMetaData table = QContext.getQInstance().getTable(possibleValueSource.getTableName());
QQueryFilter queryFilter = new QQueryFilter();
queryFilter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
@ -259,9 +260,6 @@ public class SearchPossibleValueSourceAction
}
}
// todo - skip & limit as params
queryFilter.setLimit(250);
///////////////////////////////////////////////////////////////////////////////////////////////////////
// if given a default filter, make it the 'top level' filter and the one we just created a subfilter //
///////////////////////////////////////////////////////////////////////////////////////////////////////
@ -271,6 +269,9 @@ public class SearchPossibleValueSourceAction
queryFilter = input.getDefaultQueryFilter();
}
// todo - skip & limit as params
queryFilter.setLimit(250);
queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
queryInput.setFilter(queryFilter);
@ -299,6 +300,7 @@ public class SearchPossibleValueSourceAction
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings({ "rawtypes", "unchecked" })
private SearchPossibleValueSourceOutput searchPossibleValueCustom(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource)
{
try

View File

@ -22,8 +22,8 @@
package com.kingsrook.qqq.backend.core.exceptions;
import java.util.Arrays;
import java.util.List;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -55,12 +55,11 @@ public class QInstanceValidationException extends QException
*******************************************************************************/
public QInstanceValidationException(List<String> reasons)
{
super(
(reasons != null && reasons.size() > 0)
? "Instance validation failed for the following reasons:\n - " + StringUtils.join("\n - ", reasons)
: "Validation failed, but no reasons were provided");
super((CollectionUtils.nullSafeHasContents(reasons))
? "Instance validation failed for the following reasons:\n - " + StringUtils.join("\n - ", reasons) + "\n(" + reasons.size() + " Total reason" + StringUtils.plural(reasons) + ")"
: "Validation failed, but no reasons were provided");
if(reasons != null && reasons.size() > 0)
if(CollectionUtils.nullSafeHasContents(reasons))
{
this.reasons = reasons;
}
@ -68,25 +67,6 @@ public class QInstanceValidationException extends QException
/*******************************************************************************
** Constructor of an array/varargs of reasons. They feed into the core exception message.
**
*******************************************************************************/
public QInstanceValidationException(String... reasons)
{
super(
(reasons != null && reasons.length > 0)
? "Instance validation failed for the following reasons: " + StringUtils.joinWithCommasAndAnd(Arrays.stream(reasons).toList())
: "Validation failed, but no reasons were provided");
if(reasons != null && reasons.length > 0)
{
this.reasons = Arrays.stream(reasons).toList();
}
}
/*******************************************************************************
** Constructor of message & cause - does not populate reasons!
**

View File

@ -0,0 +1,55 @@
/*
* 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.instances;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
** Version of AbstractQQQApplication that assumes all meta-data is produced
** by MetaDataProducers in a single package.
*******************************************************************************/
public abstract class AbstractMetaDataProducerBasedQQQApplication extends AbstractQQQApplication
{
/***************************************************************************
**
***************************************************************************/
public abstract String getMetaDataPackageName();
/***************************************************************************
**
***************************************************************************/
@Override
public QInstance defineQInstance() throws QException
{
QInstance qInstance = new QInstance();
MetaDataProducerHelper.processAllMetaDataProducersInPackage(qInstance, getMetaDataPackageName());
return (qInstance);
}
}

View File

@ -0,0 +1,78 @@
/*
* 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.instances;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.instances.validation.plugins.QInstanceValidatorPluginInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** Base class to provide the definition of a QQQ-based application.
**
** Essentially, just how to define its meta-data - in the form of a QInstance.
**
** Also provides means to define the instance validation plugins to be used.
*******************************************************************************/
public abstract class AbstractQQQApplication
{
/***************************************************************************
**
***************************************************************************/
public abstract QInstance defineQInstance() throws QException;
/***************************************************************************
**
***************************************************************************/
public QInstance defineValidatedQInstance() throws QException, QInstanceValidationException
{
QInstance qInstance = defineQInstance();
QInstanceValidator.removeAllValidatorPlugins();
for(QInstanceValidatorPluginInterface<?> validatorPlugin : CollectionUtils.nonNullList(getValidatorPlugins()))
{
QInstanceValidator.addValidatorPlugin(validatorPlugin);
}
QInstanceValidator qInstanceValidator = new QInstanceValidator();
qInstanceValidator.validate(qInstance);
return (qInstance);
}
/***************************************************************************
**
***************************************************************************/
protected List<QInstanceValidatorPluginInterface<?>> getValidatorPlugins()
{
return new ArrayList<>();
}
}

View File

@ -58,6 +58,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
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.processes.QStateMachineStep;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
@ -410,10 +411,27 @@ public class QInstanceEnricher
**
*******************************************************************************/
private void enrichStep(QStepMetaData step)
{
enrichStep(step, false);
}
/***************************************************************************
**
***************************************************************************/
private void enrichStep(QStepMetaData step, boolean isSubStep)
{
if(!StringUtils.hasContent(step.getLabel()))
{
step.setLabel(nameToLabel(step.getName()));
if(isSubStep && (step.getName().endsWith(".backend") || step.getName().endsWith(".frontend")))
{
step.setLabel(nameToLabel(step.getName().replaceFirst("\\.(backend|frontend)", "")));
}
else
{
step.setLabel(nameToLabel(step.getName()));
}
}
step.getInputFields().forEach(this::enrichField);
@ -434,6 +452,13 @@ public class QInstanceEnricher
frontendStepMetaData.getRecordListFields().forEach(this::enrichField);
}
}
else if(step instanceof QStateMachineStep stateMachineStep)
{
for(QStepMetaData subStep : CollectionUtils.nonNullList(stateMachineStep.getSubSteps()))
{
enrichStep(subStep, true);
}
}
}

View File

@ -43,7 +43,9 @@ import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataFilterInterface;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportCustomRecordSourceInterface;
import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
@ -73,6 +75,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
@ -184,6 +187,7 @@ public class QInstanceValidator
//////////////////////////////////////////////////////////////////////////
try
{
validateInstanceAttributes(qInstance);
validateBackends(qInstance);
validateAuthentication(qInstance);
validateAutomationProviders(qInstance);
@ -224,6 +228,19 @@ public class QInstanceValidator
/***************************************************************************
**
***************************************************************************/
private void validateInstanceAttributes(QInstance qInstance)
{
if(qInstance.getMetaDataFilter() != null)
{
validateSimpleCodeReference("Instance metaDataFilter ", qInstance.getMetaDataFilter(), MetaDataFilterInterface.class);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -922,18 +939,13 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private <T extends FieldBehavior<T>> void validateTableField(QInstance qInstance, String tableName, String fieldName, QTableMetaData table, QFieldMetaData field)
private 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() + ".");
if(field.getPossibleValueSourceName() != null)
{
assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null,
"Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + ".");
}
String prefix = "Field " + fieldName + " in table " + tableName + " ";
validateFieldPossibleValueSourceAttributes(qInstance, field, prefix);
///////////////////////////////////////////////////
// validate things we know about field behaviors //
@ -944,12 +956,13 @@ public class QInstanceValidator
assertCondition(field.getMaxLength() != null, prefix + "specifies a ValueTooLongBehavior, but not a maxLength.");
}
Set<Class<FieldBehavior<T>>> usedFieldBehaviorTypes = new HashSet<>();
Set<Class<FieldBehavior<?>>> usedFieldBehaviorTypes = new HashSet<>();
if(field.getBehaviors() != null)
{
for(FieldBehavior<?> fieldBehavior : field.getBehaviors())
{
Class<FieldBehavior<T>> behaviorClass = (Class<FieldBehavior<T>>) fieldBehavior.getClass();
@SuppressWarnings("unchecked")
Class<FieldBehavior<?>> behaviorClass = (Class<FieldBehavior<?>>) fieldBehavior.getClass();
errors.addAll(fieldBehavior.validateBehaviorConfiguration(table, field));
@ -1037,6 +1050,31 @@ public class QInstanceValidator
/***************************************************************************
**
***************************************************************************/
private void validateFieldPossibleValueSourceAttributes(QInstance qInstance, QFieldMetaData field, String prefix)
{
if(field.getPossibleValueSourceName() != null)
{
assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null,
prefix + "has an unrecognized possibleValueSourceName " + field.getPossibleValueSourceName());
assertCondition(field.getInlinePossibleValueSource() == null, prefix.trim() + " has both a possibleValueSourceName and an inlinePossibleValueSource, which is not allowed.");
}
if(field.getInlinePossibleValueSource() != null)
{
String name = "inlinePossibleValueSource for " + prefix.trim();
if(assertCondition(QPossibleValueSourceType.ENUM.equals(field.getInlinePossibleValueSource().getType()), name + " must have a type of ENUM."))
{
validatePossibleValueSource(qInstance, name, field.getInlinePossibleValueSource());
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -1544,6 +1582,16 @@ public class QInstanceValidator
}
}
for(QFieldMetaData field : process.getInputFields())
{
validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", input field " + field.getName());
}
for(QFieldMetaData field : process.getOutputFields())
{
validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", output field " + field.getName());
}
if(process.getCancelStep() != null)
{
if(assertCondition(process.getCancelStep().getCode() != null, "Cancel step is missing a code reference, in process " + processName))
@ -1659,9 +1707,12 @@ public class QInstanceValidator
String dataSourceErrorPrefix = "Report " + reportName + " data source " + dataSource.getName() + " ";
boolean hasASource = false;
if(StringUtils.hasContent(dataSource.getSourceTable()))
{
assertCondition(dataSource.getStaticDataSupplier() == null, dataSourceErrorPrefix + "has both a sourceTable and a staticDataSupplier (exactly 1 is required).");
hasASource = true;
assertCondition(dataSource.getStaticDataSupplier() == null, dataSourceErrorPrefix + "has both a sourceTable and a staticDataSupplier (not compatible together).");
if(assertCondition(qInstance.getTable(dataSource.getSourceTable()) != null, dataSourceErrorPrefix + "source table " + dataSource.getSourceTable() + " is not a table in this instance."))
{
if(dataSource.getQueryFilter() != null)
@ -1670,14 +1721,21 @@ public class QInstanceValidator
}
}
}
else if(dataSource.getStaticDataSupplier() != null)
if(dataSource.getStaticDataSupplier() != null)
{
assertCondition(dataSource.getCustomRecordSource() == null, dataSourceErrorPrefix + "has both a staticDataSupplier and a customRecordSource (not compatible together).");
hasASource = true;
validateSimpleCodeReference(dataSourceErrorPrefix, dataSource.getStaticDataSupplier(), Supplier.class);
}
else
if(dataSource.getCustomRecordSource() != null)
{
errors.add(dataSourceErrorPrefix + "does not have a sourceTable or a staticDataSupplier (exactly 1 is required).");
hasASource = true;
validateSimpleCodeReference(dataSourceErrorPrefix, dataSource.getCustomRecordSource(), ReportCustomRecordSourceInterface.class);
}
assertCondition(hasASource, dataSourceErrorPrefix + "does not have a sourceTable, customRecordSource, or a staticDataSupplier.");
}
}
@ -1936,78 +1994,88 @@ public class QInstanceValidator
qInstance.getPossibleValueSources().forEach((pvsName, possibleValueSource) ->
{
assertCondition(Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + ".");
if(assertCondition(possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName))
validatePossibleValueSource(qInstance, pvsName, possibleValueSource);
});
}
}
/***************************************************************************
**
***************************************************************************/
private void validatePossibleValueSource(QInstance qInstance, String name, QPossibleValueSource possibleValueSource)
{
if(assertCondition(possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + name))
{
////////////////////////////////////////////////////////////////////////////////////////////////
// assert about fields that should and should not be set, based on possible value source type //
// do additional type-specific validations as well //
////////////////////////////////////////////////////////////////////////////////////////////////
switch(possibleValueSource.getType())
{
case ENUM ->
{
////////////////////////////////////////////////////////////////////////////////////////////////
// assert about fields that should and should not be set, based on possible value source type //
// do additional type-specific validations as well //
////////////////////////////////////////////////////////////////////////////////////////////////
switch(possibleValueSource.getType())
assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + name + " should not have a tableName.");
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "enum-type possibleValueSource " + name + " should not have searchFields.");
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "enum-type possibleValueSource " + name + " should not have orderByFields.");
assertCondition(possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + name + " should not have a customCodeReference.");
assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + name + " is missing enum values");
}
case TABLE ->
{
assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + name + " should not have enum values.");
assertCondition(possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + name + " should not have a customCodeReference.");
QTableMetaData tableMetaData = null;
if(assertCondition(StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + name + " is missing a tableName."))
{
case ENUM ->
{
assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + pvsName + " should not have a tableName.");
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "enum-type possibleValueSource " + pvsName + " should not have searchFields.");
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "enum-type possibleValueSource " + pvsName + " should not have orderByFields.");
assertCondition(possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + pvsName + " should not have a customCodeReference.");
assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + pvsName + " is missing enum values");
}
case TABLE ->
{
assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + pvsName + " should not have enum values.");
assertCondition(possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + pvsName + " should not have a customCodeReference.");
QTableMetaData tableMetaData = null;
if(assertCondition(StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + pvsName + " is missing a tableName."))
{
tableMetaData = qInstance.getTable(possibleValueSource.getTableName());
assertCondition(tableMetaData != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + pvsName + ".");
}
if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "table-type possibleValueSource " + pvsName + " is missing searchFields."))
{
if(tableMetaData != null)
{
QTableMetaData finalTableMetaData = tableMetaData;
for(String searchField : possibleValueSource.getSearchFields())
{
assertNoException(() -> finalTableMetaData.getField(searchField), "possibleValueSource " + pvsName + " has an unrecognized searchField: " + searchField);
}
}
}
if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "table-type possibleValueSource " + pvsName + " is missing orderByFields."))
{
if(tableMetaData != null)
{
QTableMetaData finalTableMetaData = tableMetaData;
for(QFilterOrderBy orderByField : possibleValueSource.getOrderByFields())
{
assertNoException(() -> finalTableMetaData.getField(orderByField.getFieldName()), "possibleValueSource " + pvsName + " has an unrecognized orderByField: " + orderByField.getFieldName());
}
}
}
}
case CUSTOM ->
{
assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + pvsName + " should not have enum values.");
assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + pvsName + " should not have a tableName.");
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "custom-type possibleValueSource " + pvsName + " should not have searchFields.");
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "custom-type possibleValueSource " + pvsName + " should not have orderByFields.");
if(assertCondition(possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference."))
{
validateSimpleCodeReference("PossibleValueSource " + pvsName + " custom code reference: ", possibleValueSource.getCustomCodeReference(), QCustomPossibleValueProvider.class);
}
}
default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType());
tableMetaData = qInstance.getTable(possibleValueSource.getTableName());
assertCondition(tableMetaData != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + name + ".");
}
runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance);
if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "table-type possibleValueSource " + name + " is missing searchFields."))
{
if(tableMetaData != null)
{
QTableMetaData finalTableMetaData = tableMetaData;
for(String searchField : possibleValueSource.getSearchFields())
{
assertNoException(() -> finalTableMetaData.getField(searchField), "possibleValueSource " + name + " has an unrecognized searchField: " + searchField);
}
}
}
if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "table-type possibleValueSource " + name + " is missing orderByFields."))
{
if(tableMetaData != null)
{
QTableMetaData finalTableMetaData = tableMetaData;
for(QFilterOrderBy orderByField : possibleValueSource.getOrderByFields())
{
assertNoException(() -> finalTableMetaData.getField(orderByField.getFieldName()), "possibleValueSource " + name + " has an unrecognized orderByField: " + orderByField.getFieldName());
}
}
}
}
});
case CUSTOM ->
{
assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + name + " should not have enum values.");
assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + name + " should not have a tableName.");
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "custom-type possibleValueSource " + name + " should not have searchFields.");
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "custom-type possibleValueSource " + name + " should not have orderByFields.");
if(assertCondition(possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + name + " is missing a customCodeReference."))
{
validateSimpleCodeReference("PossibleValueSource " + name + " custom code reference: ", possibleValueSource.getCustomCodeReference(), QCustomPossibleValueProvider.class);
}
}
default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType());
}
runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance);
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.instances;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
@ -113,7 +114,7 @@ public class SecretsManagerUtils
dotEnv.renameTo(new File(".env.backup-" + System.currentTimeMillis()));
}
FileUtils.writeStringToFile(dotEnv, fullEnv.toString());
FileUtils.writeStringToFile(dotEnv, fullEnv.toString(), StandardCharsets.UTF_8);
}
else
{

View File

@ -280,6 +280,16 @@ public class QLogger
/*******************************************************************************
**
*******************************************************************************/
public void debug(LogPair... logPairs)
{
logger.warn(() -> makeJsonString(null, null, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
@ -420,6 +430,16 @@ public class QLogger
/*******************************************************************************
**
*******************************************************************************/
public void warn(LogPair... logPairs)
{
logger.warn(() -> makeJsonString(null, null, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
@ -480,6 +500,16 @@ public class QLogger
/*******************************************************************************
**
*******************************************************************************/
public void error(LogPair... logPairs)
{
logger.warn(() -> makeJsonString(null, null, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -32,7 +32,6 @@ import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
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.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
@ -93,17 +92,6 @@ public class AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
@JsonIgnore
public QAuthenticationMetaData getAuthenticationMetaData()
{
return (getInstance().getAuthentication());
}
/*******************************************************************************
** Getter for instance
**

View File

@ -152,5 +152,8 @@ public class AuditDetailAccumulator implements Serializable
}
/***************************************************************************
**
***************************************************************************/
private record TableNameAndPrimaryKey(String tableName, Serializable primaryKey) {}
}

View File

@ -31,6 +31,16 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
*******************************************************************************/
public class MetaDataInput extends AbstractActionInput
{
private String frontendName;
private String frontendVersion;
private String middlewareName;
private String middlewareVersion;
private String applicationName;
private String applicationVersion;
/*******************************************************************************
**
@ -39,4 +49,190 @@ public class MetaDataInput extends AbstractActionInput
{
}
/*******************************************************************************
** Getter for frontendName
*******************************************************************************/
public String getFrontendName()
{
return (this.frontendName);
}
/*******************************************************************************
** Setter for frontendName
*******************************************************************************/
public void setFrontendName(String frontendName)
{
this.frontendName = frontendName;
}
/*******************************************************************************
** Fluent setter for frontendName
*******************************************************************************/
public MetaDataInput withFrontendName(String frontendName)
{
this.frontendName = frontendName;
return (this);
}
/*******************************************************************************
** Getter for frontendVersion
*******************************************************************************/
public String getFrontendVersion()
{
return (this.frontendVersion);
}
/*******************************************************************************
** Setter for frontendVersion
*******************************************************************************/
public void setFrontendVersion(String frontendVersion)
{
this.frontendVersion = frontendVersion;
}
/*******************************************************************************
** Fluent setter for frontendVersion
*******************************************************************************/
public MetaDataInput withFrontendVersion(String frontendVersion)
{
this.frontendVersion = frontendVersion;
return (this);
}
/*******************************************************************************
** Getter for middlewareName
*******************************************************************************/
public String getMiddlewareName()
{
return (this.middlewareName);
}
/*******************************************************************************
** Setter for middlewareName
*******************************************************************************/
public void setMiddlewareName(String middlewareName)
{
this.middlewareName = middlewareName;
}
/*******************************************************************************
** Fluent setter for middlewareName
*******************************************************************************/
public MetaDataInput withMiddlewareName(String middlewareName)
{
this.middlewareName = middlewareName;
return (this);
}
/*******************************************************************************
** Getter for middlewareVersion
*******************************************************************************/
public String getMiddlewareVersion()
{
return (this.middlewareVersion);
}
/*******************************************************************************
** Setter for middlewareVersion
*******************************************************************************/
public void setMiddlewareVersion(String middlewareVersion)
{
this.middlewareVersion = middlewareVersion;
}
/*******************************************************************************
** Fluent setter for middlewareVersion
*******************************************************************************/
public MetaDataInput withMiddlewareVersion(String middlewareVersion)
{
this.middlewareVersion = middlewareVersion;
return (this);
}
/*******************************************************************************
** Getter for applicationName
*******************************************************************************/
public String getApplicationName()
{
return (this.applicationName);
}
/*******************************************************************************
** Setter for applicationName
*******************************************************************************/
public void setApplicationName(String applicationName)
{
this.applicationName = applicationName;
}
/*******************************************************************************
** Fluent setter for applicationName
*******************************************************************************/
public MetaDataInput withApplicationName(String applicationName)
{
this.applicationName = applicationName;
return (this);
}
/*******************************************************************************
** Getter for applicationVersion
*******************************************************************************/
public String getApplicationVersion()
{
return (this.applicationVersion);
}
/*******************************************************************************
** Setter for applicationVersion
*******************************************************************************/
public void setApplicationVersion(String applicationVersion)
{
this.applicationVersion = applicationVersion;
}
/*******************************************************************************
** Fluent setter for applicationVersion
*******************************************************************************/
public MetaDataInput withApplicationVersion(String applicationVersion)
{
this.applicationVersion = applicationVersion;
return (this);
}
}

View File

@ -0,0 +1,139 @@
/*
* 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.processes;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Object that stores adjustments that a process wants to make, at run-time,
** to its meta-data.
**
** e.g., changing the steps; updating fields (e.g., changing an inline PVS,
** or an isRequired attribute)
*******************************************************************************/
public class ProcessMetaDataAdjustment
{
private static final QLogger LOG = QLogger.getLogger(ProcessMetaDataAdjustment.class);
private List<QFrontendStepMetaData> updatedFrontendStepList = null;
private Map<String, QFieldMetaData> updatedFields = null;
/*******************************************************************************
**
*******************************************************************************/
public ProcessMetaDataAdjustment withUpdatedField(QFieldMetaData field)
{
if(updatedFields == null)
{
updatedFields = new LinkedHashMap<>();
}
if(!StringUtils.hasContent(field.getName()))
{
LOG.warn("Missing name on field in withUpdatedField - no update will happen.");
}
else
{
if(updatedFields.containsKey(field.getName()))
{
LOG.info("UpdatedFields map already contained a field with this name - overwriting it.", logPair("fieldName", field.getName()));
}
updatedFields.put(field.getName(), field);
}
return (this);
}
/*******************************************************************************
** 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 ProcessMetaDataAdjustment withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
return (this);
}
/*******************************************************************************
** Getter for updatedFields
*******************************************************************************/
public Map<String, QFieldMetaData> getUpdatedFields()
{
return (this.updatedFields);
}
/*******************************************************************************
** Setter for updatedFields
*******************************************************************************/
public void setUpdatedFields(Map<String, QFieldMetaData> updatedFields)
{
this.updatedFields = updatedFields;
}
/*******************************************************************************
** Fluent setter for updatedFields
*******************************************************************************/
public ProcessMetaDataAdjustment withUpdatedFields(Map<String, QFieldMetaData> updatedFields)
{
this.updatedFields = updatedFields;
return (this);
}
}

View File

@ -29,7 +29,6 @@ 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;
/*******************************************************************************
@ -42,10 +41,7 @@ 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;
private ProcessMetaDataAdjustment processMetaDataAdjustment = null;
@ -148,33 +144,36 @@ public class ProcessState implements Serializable
/*******************************************************************************
** Getter for updatedFrontendStepList
** Getter for processMetaDataAdjustment
*******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
public ProcessMetaDataAdjustment getProcessMetaDataAdjustment()
{
return (this.updatedFrontendStepList);
return (this.processMetaDataAdjustment);
}
/*******************************************************************************
** Setter for updatedFrontendStepList
** Setter for processMetaDataAdjustment
*******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
public void setProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment)
{
this.updatedFrontendStepList = updatedFrontendStepList;
this.processMetaDataAdjustment = processMetaDataAdjustment;
}
/*******************************************************************************
** Fluent setter for updatedFrontendStepList
** Fluent setter for processMetaDataAdjustment
*******************************************************************************/
public ProcessState withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
public ProcessState withProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment)
{
this.updatedFrontendStepList = updatedFrontendStepList;
this.processMetaDataAdjustment = processMetaDataAdjustment;
return (this);
}
}

View File

@ -374,7 +374,13 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
.map(step -> (QFrontendStepMetaData) step)
.toList());
setUpdatedFrontendStepList(updatedFrontendStepList);
ProcessMetaDataAdjustment processMetaDataAdjustment = getProcessMetaDataAdjustment();
if(processMetaDataAdjustment == null)
{
processMetaDataAdjustment = new ProcessMetaDataAdjustment();
}
processMetaDataAdjustment.setUpdatedFrontendStepList(updatedFrontendStepList);
setProcessMetaDataAdjustment(processMetaDataAdjustment);
}
@ -411,21 +417,21 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
/*******************************************************************************
** Getter for updatedFrontendStepList
** Getter for ProcessMetaDataAdjustment (pass-through to processState)
*******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
public ProcessMetaDataAdjustment getProcessMetaDataAdjustment()
{
return (this.processState.getUpdatedFrontendStepList());
return (this.processState.getProcessMetaDataAdjustment());
}
/*******************************************************************************
** Setter for updatedFrontendStepList
** Setter for updatedFrontendStepList (pass-through to processState)
*******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
public void setProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment)
{
this.processState.setUpdatedFrontendStepList(updatedFrontendStepList);
this.processState.setProcessMetaDataAdjustment(processMetaDataAdjustment);
}
}

View File

@ -33,6 +33,7 @@ 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.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -336,7 +337,12 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
*******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.processState.setUpdatedFrontendStepList(updatedFrontendStepList);
if(this.processState.getProcessMetaDataAdjustment() == null)
{
this.processState.setProcessMetaDataAdjustment(new ProcessMetaDataAdjustment());
}
this.processState.getProcessMetaDataAdjustment().setUpdatedFrontendStepList(updatedFrontendStepList);
}
@ -346,7 +352,27 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
*******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
{
return this.processState.getUpdatedFrontendStepList();
return ObjectUtils.tryElse(() -> this.processState.getProcessMetaDataAdjustment().getUpdatedFrontendStepList(), null);
}
/*******************************************************************************
** Getter for processMetaDataAdjustment
*******************************************************************************/
public ProcessMetaDataAdjustment getProcessMetaDataAdjustment()
{
return (this.processState.getProcessMetaDataAdjustment());
}
/*******************************************************************************
** Setter for processMetaDataAdjustment
*******************************************************************************/
public void setProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment)
{
this.processState.setProcessMetaDataAdjustment(processMetaDataAdjustment);
}
}

View File

@ -59,6 +59,30 @@ public class AggregateInput extends AbstractTableActionInput
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public AggregateInput(String tableName)
{
this();
setTableName(tableName);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public AggregateInput withTableName(String tableName)
{
super.withTableName(tableName);
return (this);
}
/*******************************************************************************
** Getter for filter
**

View File

@ -0,0 +1,66 @@
/*
* 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.query;
/***************************************************************************
** Possible behaviors for doing interpretValues on a filter, and a criteria
** has a variable value (either as a string-that-looks-like-a-variable,
** as in ${input.foreignId} for a PVS filter, or a FilterVariableExpression),
** and a value for that variable isn't available.
**
** Used in conjunction with FilterUseCase and its implementations, e.g.,
** PossibleValueSearchFilterUseCase.
***************************************************************************/
public enum CriteriaMissingInputValueBehavior
{
//////////////////////////////////////////////////////////////////////
// this was the original behavior, before we added this enum. but, //
// it doesn't ever seem entirely valid, and isn't currently used. //
//////////////////////////////////////////////////////////////////////
INTERPRET_AS_NULL_VALUE,
//////////////////////////////////////////////////////////////////////////
// make the criteria behave as though it's not in the filter at all. //
// effectively by changing its operator to TRUE, so it always matches. //
// original intended use is for possible-values on query screens, //
// where a foreign-id isn't present, so we want to show all PV options. //
//////////////////////////////////////////////////////////////////////////
REMOVE_FROM_FILTER,
//////////////////////////////////////////////////////////////////////////////////////
// make the criteria such that it makes no rows ever match. //
// e.g., changes it to a FALSE. I suppose, within an OR, that might //
// not be powerful enough... but, it solves the immediate use-case in //
// front of us, which is forms, where a PV field should show no values //
// until a foreign key field has a value. //
// Note that this use-case used to have the same end-effect by such //
// variables being interpreted as nulls - but this approach feels more intentional. //
//////////////////////////////////////////////////////////////////////////////////////
MAKE_NO_MATCHES,
///////////////////////////////////////////////////////////////////////////////////////////
// throw an exception if a value isn't available. This is the overall default, //
// and originally was what we did for FilterVariableExpressions, e.g., for saved reports //
///////////////////////////////////////////////////////////////////////////////////////////
THROW_EXCEPTION
}

View File

@ -0,0 +1,58 @@
/*
* 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.query;
/*******************************************************************************
** Interface where we can associate behaviors with various use cases for
** QQueryFilters - the original being, how to handle (in the interpretValues
** method) how to handle missing input values.
**
** Includes a default implementation, with a default behavior - which is to
** throw an exception upon missing criteria variable values.
*******************************************************************************/
public interface FilterUseCase
{
FilterUseCase DEFAULT = new DefaultFilterUseCase();
/***************************************************************************
**
***************************************************************************/
CriteriaMissingInputValueBehavior getDefaultCriteriaMissingInputValueBehavior();
/***************************************************************************
**
***************************************************************************/
class DefaultFilterUseCase implements FilterUseCase
{
/***************************************************************************
**
***************************************************************************/
@Override
public CriteriaMissingInputValueBehavior getDefaultCriteriaMissingInputValueBehavior()
{
return CriteriaMissingInputValueBehavior.THROW_EXCEPTION;
}
}
}

View File

@ -82,7 +82,7 @@ public class JoinsContext
/////////////////////////////////////////////////////////////////////////////
// we will get a TON of more output if this gets turned up, so be cautious //
/////////////////////////////////////////////////////////////////////////////
private Level logLevel = Level.OFF;
private Level logLevel = Level.OFF;
private Level logLevelForFilter = Level.OFF;
@ -378,7 +378,7 @@ public class JoinsContext
{
securityFieldTableAlias = matchedQueryJoin.getJoinTableOrItsAlias();
}
tmpTable = instance.getTable(securityFieldTableAlias);
tmpTable = instance.getTable(aliasToTableNameMap.getOrDefault(securityFieldTableAlias, securityFieldTableAlias));
////////////////////////////////////////////////////////////////////////////////////////
// set the baseTableOrAlias for the next iteration to be this join's joinTableOrAlias //
@ -404,6 +404,12 @@ public class JoinsContext
chainIsInner = false;
}
if(hasAllAccessKey(recordSecurityLock))
{
queryJoin.withType(QueryJoin.Type.LEFT);
chainIsInner = false;
}
addQueryJoin(queryJoin, "forRecordSecurityLock (non-flipped)", "- ");
addedQueryJoins.add(queryJoin);
tmpTable = instance.getTable(join.getRightTable());
@ -423,6 +429,12 @@ public class JoinsContext
chainIsInner = false;
}
if(hasAllAccessKey(recordSecurityLock))
{
queryJoin.withType(QueryJoin.Type.LEFT);
chainIsInner = false;
}
addQueryJoin(queryJoin, "forRecordSecurityLock (flipped)", "- ");
addedQueryJoins.add(queryJoin);
tmpTable = instance.getTable(join.getLeftTable());
@ -456,44 +468,53 @@ public class JoinsContext
/***************************************************************************
**
***************************************************************************/
private boolean hasAllAccessKey(RecordSecurityLock recordSecurityLock)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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());
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
{
QSession session = QContext.getQSession();
if(session.hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
{
return (true);
}
}
return (false);
}
/*******************************************************************************
**
*******************************************************************************/
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()))
boolean haveAllAccessKey = hasAllAccessKey(recordSecurityLock);
if(haveAllAccessKey)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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))
if(sourceQueryJoin != null)
{
haveAllAccessKey = true;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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(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;
}
////////////////////////////////////////////////////////////////////////////////////////
// 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;
}
}
@ -545,7 +566,7 @@ public class JoinsContext
}
else
{
List<Serializable> securityKeyValues = session.getSecurityKeyValues(recordSecurityLock.getSecurityKeyType(), type);
List<Serializable> securityKeyValues = QContext.getQSession().getSecurityKeyValues(recordSecurityLock.getSecurityKeyType(), type);
if(CollectionUtils.nullSafeIsEmpty(securityKeyValues))
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -1118,7 +1139,7 @@ public class JoinsContext
if(useExposedJoins)
{
QTableMetaData mainTable = QContext.getQInstance().getTable(mainTableName);
for(ExposedJoin exposedJoin : mainTable.getExposedJoins())
for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(mainTable.getExposedJoins()))
{
if(exposedJoin.getJoinTable().equals(joinTableName))
{
@ -1159,6 +1180,7 @@ public class JoinsContext
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -25,6 +25,7 @@ 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.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -237,6 +238,28 @@ public class QQueryFilter implements Serializable, Cloneable
/*******************************************************************************
** fluent method to add a new criteria
*******************************************************************************/
public QQueryFilter withCriteria(String fieldName, QCriteriaOperator operator, Collection<? extends Serializable> values)
{
addCriteria(new QFilterCriteria(fieldName, operator, values));
return (this);
}
/*******************************************************************************
** fluent method to add a new criteria
*******************************************************************************/
public QQueryFilter withCriteria(String fieldName, QCriteriaOperator operator, Serializable... values)
{
addCriteria(new QFilterCriteria(fieldName, operator, values));
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@ -505,8 +528,27 @@ public class QQueryFilter implements Serializable, Cloneable
** Note - it may be very important that you call this method on a clone of a
** QQueryFilter - e.g., if it's one that defined in metaData, and that we don't
** want to be (permanently) changed!!
*******************************************************************************/
**
** This overload does not take in a FilterUseCase - it uses FilterUseCase.DEFAULT
******************************************************************************/
public void interpretValues(Map<String, Serializable> inputValues) throws QException
{
interpretValues(inputValues, FilterUseCase.DEFAULT);
}
/*******************************************************************************
** Replace any criteria values that look like ${input.XXX} with the value of XXX
** from the supplied inputValues map - where the handling of missing values
** is specified in the inputted FilterUseCase parameter
**
** Note - it may be very important that you call this method on a clone of a
** 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, FilterUseCase useCase) throws QException
{
List<Exception> caughtExceptions = new ArrayList<>();
@ -522,6 +564,9 @@ public class QQueryFilter implements Serializable, Cloneable
{
try
{
Serializable interpretedValue = value;
Exception caughtException = null;
if(value instanceof AbstractFilterExpression<?>)
{
///////////////////////////////////////////////////////////////////////
@ -530,17 +575,54 @@ public class QQueryFilter implements Serializable, Cloneable
///////////////////////////////////////////////////////////////////////
if(value instanceof FilterVariableExpression filterVariableExpression)
{
newValues.add(filterVariableExpression.evaluateInputValues(inputValues));
}
else
{
newValues.add(value);
try
{
interpretedValue = filterVariableExpression.evaluateInputValues(inputValues);
}
catch(Exception e)
{
caughtException = e;
interpretedValue = InputNotFound.instance;
}
}
}
else
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// for non-expressions, cast the value to a string, and see if it can be resolved a variable. //
// there are 3 possible cases here: //
// 1: it doesn't look like a variable, so it just comes back as a string version of whatever went in. //
// 2: it was resolved from a variable to a value, e.g., ${input.someVar} => someValue //
// 3: it looked like a variable, but no value for that variable was present in the interpreter's value //
// map - so we'll get back the InputNotFound.instance. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
String valueAsString = ValueUtils.getValueAsString(value);
interpretedValue = variableInterpreter.interpretForObject(valueAsString, InputNotFound.instance);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if interpreting a value returned the not-found value, or an empty string, //
// then decide how to handle the missing value, based on the use-case input //
// Note: questionable, using "" here, but that's what reality is passing a lot for cases we want to treat as missing... //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(interpretedValue == InputNotFound.instance || "".equals(interpretedValue))
{
CriteriaMissingInputValueBehavior missingInputValueBehavior = getMissingInputValueBehavior(useCase);
switch(missingInputValueBehavior)
{
case REMOVE_FROM_FILTER -> criterion.setOperator(QCriteriaOperator.TRUE);
case MAKE_NO_MATCHES -> criterion.setOperator(QCriteriaOperator.FALSE);
case INTERPRET_AS_NULL_VALUE -> newValues.add(null);
/////////////////////////////////////////////////
// handle case in the default: THROW_EXCEPTION //
/////////////////////////////////////////////////
default -> throw (Objects.requireNonNullElseGet(caughtException, () -> new QUserFacingException("Missing value for criteria on field: " + criterion.getFieldName())));
}
}
else
{
String valueAsString = ValueUtils.getValueAsString(value);
Serializable interpretedValue = variableInterpreter.interpretForObject(valueAsString);
newValues.add(interpretedValue);
}
}
@ -563,6 +645,44 @@ public class QQueryFilter implements Serializable, Cloneable
/***************************************************************************
** Note: in the original build of this, it felt like we *might* want to be
** able to specify these behaviors at the individual criteria level, where
** the implementation would be to add to QFilterCriteria:
** - Map<FilterUseCase, CriteriaMissingInputValueBehavior> missingInputValueBehaviors;
** - CriteriaMissingInputValueBehavior getMissingInputValueBehaviorForUseCase(FilterUseCase useCase) {}
*
** (and maybe do that in a sub-class of QFilterCriteria, so it isn't always
** there? idk...) and then here we'd call:
** - CriteriaMissingInputValueBehavior missingInputValueBehavior = criterion.getMissingInputValueBehaviorForUseCase(useCase);
*
** But, we don't actually have that use-case at hand now, so - let's keep it
** just at the level we need for now.
**
***************************************************************************/
private CriteriaMissingInputValueBehavior getMissingInputValueBehavior(FilterUseCase useCase)
{
if(useCase == null)
{
useCase = FilterUseCase.DEFAULT;
}
CriteriaMissingInputValueBehavior missingInputValueBehavior = useCase.getDefaultCriteriaMissingInputValueBehavior();
if(missingInputValueBehavior == null)
{
missingInputValueBehavior = useCase.getDefaultCriteriaMissingInputValueBehavior();
}
if(missingInputValueBehavior == null)
{
missingInputValueBehavior = FilterUseCase.DEFAULT.getDefaultCriteriaMissingInputValueBehavior();
}
return (missingInputValueBehavior);
}
/*******************************************************************************
** Getter for skip
*******************************************************************************/
@ -655,4 +775,28 @@ public class QQueryFilter implements Serializable, Cloneable
{
return Objects.hash(criteria, orderBys, booleanOperator, subFilters, skip, limit);
}
/***************************************************************************
** "Token" object to be used as the defaultIfLooksLikeVariableButNotFound
** parameter to variableInterpreter.interpretForObject, so we can be
** very clear that we got this default back (e.g., instead of a null,
** which could maybe mean something else?)
***************************************************************************/
private static final class InputNotFound implements Serializable
{
private static InputNotFound instance = new InputNotFound();
/*******************************************************************************
** private singleton constructor
*******************************************************************************/
private InputNotFound()
{
}
}
}

View File

@ -66,6 +66,14 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn
private List<QueryJoin> queryJoins = null;
private boolean selectDistinct = false;
/////////////////////////////////////////////////////////////////////////////
// if this set is null, then the default (all fields) should be included //
// if it's an empty set, that should throw an error //
// or if there are any fields in it that aren't valid fields on the table, //
// or in a selected queryJoin. //
/////////////////////////////////////////////////////////////////////////////
private Set<String> fieldNamesToInclude;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if you say you want to includeAssociations, you can limit which ones by passing them in associationNamesToInclude. //
// if you leave it null, you get all associations defined on the table. if you pass it as empty, you get none. //
@ -686,4 +694,35 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn
return (queryHints.contains(queryHint));
}
/*******************************************************************************
** Getter for fieldNamesToInclude
*******************************************************************************/
public Set<String> getFieldNamesToInclude()
{
return (this.fieldNamesToInclude);
}
/*******************************************************************************
** Setter for fieldNamesToInclude
*******************************************************************************/
public void setFieldNamesToInclude(Set<String> fieldNamesToInclude)
{
this.fieldNamesToInclude = fieldNamesToInclude;
}
/*******************************************************************************
** Fluent setter for fieldNamesToInclude
*******************************************************************************/
public QueryInput withFieldNamesToInclude(Set<String> fieldNamesToInclude)
{
this.fieldNamesToInclude = fieldNamesToInclude;
return (this);
}
}

View File

@ -56,7 +56,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
** JoinsContext is constructed before executing a query, and not meant to be set
** by users.
*******************************************************************************/
public class QueryJoin
public class QueryJoin implements Cloneable
{
private String baseTableOrAlias;
private String joinTable;
@ -69,6 +69,40 @@ public class QueryJoin
/*******************************************************************************
**
*******************************************************************************/
@Override
public QueryJoin clone()
{
try
{
QueryJoin clone = (QueryJoin) super.clone();
if(joinMetaData != null)
{
clone.joinMetaData = joinMetaData.clone();
}
if(securityCriteria != null)
{
clone.securityCriteria = new ArrayList<>();
for(QFilterCriteria securityCriterion : securityCriteria)
{
clone.securityCriteria.add(securityCriterion.clone());
}
}
return clone;
}
catch(CloneNotSupportedException e)
{
throw new AssertionError();
}
}
/*******************************************************************************
** define the types of joins - INNER, LEFT, RIGHT, or FULL.
*******************************************************************************/

View File

@ -25,6 +25,7 @@ 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.model.metadata.fields.QFieldMetaData;
/*******************************************************************************
@ -35,16 +36,19 @@ public abstract class AbstractFilterExpression<T extends Serializable> implement
/*******************************************************************************
**
*******************************************************************************/
public abstract T evaluate() throws QException;
public abstract T evaluate(QFieldMetaData field) throws QException;
/*******************************************************************************
** Evaluate the expression, given a map of input values.
**
** By default, this will defer to the evaluate(void) method - but, a subclass
** (e.g., FilterVariableExpression) may react differently.
*******************************************************************************/
public T evaluateInputValues(Map<String, Serializable> inputValues) throws QException
{
return (T) this;
return evaluate(null);
}

View File

@ -26,6 +26,7 @@ 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.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -45,7 +46,7 @@ public class FilterVariableExpression extends AbstractFilterExpression<Serializa
**
*******************************************************************************/
@Override
public Serializable evaluate() throws QException
public Serializable evaluate(QFieldMetaData field) throws QException
{
throw (new QUserFacingException("Missing variable value."));
}

View File

@ -22,23 +22,42 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
import java.io.Serializable;
import java.time.Instant;
import java.time.ZoneId;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.utils.ValueUtils;
/*******************************************************************************
**
*******************************************************************************/
public class Now extends AbstractFilterExpression<Instant>
public class Now extends AbstractFilterExpression<Serializable>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public Instant evaluate() throws QException
public Serializable evaluate(QFieldMetaData field) throws QException
{
return (Instant.now());
QFieldType type = field == null ? QFieldType.DATE_TIME : field.getType();
if(type.equals(QFieldType.DATE_TIME))
{
return (Instant.now());
}
else if(type.equals(QFieldType.DATE))
{
ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId();
return (Instant.now().atZone(zoneId).toLocalDate());
}
else
{
throw (new QException("Unsupported field type [" + type + "]"));
}
}
}

View File

@ -22,19 +22,24 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
import java.io.Serializable;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
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;
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.utils.ValueUtils;
/*******************************************************************************
**
*******************************************************************************/
public class NowWithOffset extends AbstractFilterExpression<Instant>
public class NowWithOffset extends AbstractFilterExpression<Serializable>
{
private Operator operator;
private int amount;
@ -42,6 +47,9 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
/***************************************************************************
**
***************************************************************************/
public enum Operator
{PLUS, MINUS}
@ -120,7 +128,30 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
**
*******************************************************************************/
@Override
public Instant evaluate() throws QException
public Serializable evaluate(QFieldMetaData field) throws QException
{
QFieldType type = field == null ? QFieldType.DATE_TIME : field.getType();
if(type.equals(QFieldType.DATE_TIME))
{
return (evaluateForDateTime());
}
else if(type.equals(QFieldType.DATE))
{
return (evaluateForDate());
}
else
{
throw (new QException("Unsupported field type [" + type + "]"));
}
}
/***************************************************************************
**
***************************************************************************/
private Instant evaluateForDateTime()
{
/////////////////////////////////////////////////////////////////////////////
// Instant doesn't let us plus/minus WEEK, MONTH, or YEAR... //
@ -144,6 +175,26 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
/***************************************************************************
**
***************************************************************************/
private LocalDate evaluateForDate()
{
ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId();
LocalDate now = Instant.now().atZone(zoneId).toLocalDate();
if(operator.equals(Operator.PLUS))
{
return (now.plus(amount, timeUnit));
}
else
{
return (now.minus(amount, timeUnit));
}
}
/*******************************************************************************
** Getter for operator
**

View File

@ -22,27 +22,35 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
import java.io.Serializable;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
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.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
**
*******************************************************************************/
public class ThisOrLastPeriod extends AbstractFilterExpression<Instant>
public class ThisOrLastPeriod extends AbstractFilterExpression<Serializable>
{
private Operator operator;
private ChronoUnit timeUnit;
/***************************************************************************
**
***************************************************************************/
public enum Operator
{THIS, LAST}
@ -85,7 +93,7 @@ public class ThisOrLastPeriod extends AbstractFilterExpression<Instant>
** Factory
**
*******************************************************************************/
public static ThisOrLastPeriod last(int amount, ChronoUnit timeUnit)
public static ThisOrLastPeriod last(ChronoUnit timeUnit)
{
return (new ThisOrLastPeriod(Operator.LAST, timeUnit));
}
@ -96,7 +104,31 @@ public class ThisOrLastPeriod extends AbstractFilterExpression<Instant>
**
*******************************************************************************/
@Override
public Instant evaluate() throws QException
public Serializable evaluate(QFieldMetaData field) throws QException
{
QFieldType type = field == null ? QFieldType.DATE_TIME : field.getType();
if(type.equals(QFieldType.DATE_TIME))
{
return (evaluateForDateTime());
}
else if(type.equals(QFieldType.DATE))
{
// return (evaluateForDateTime());
return (evaluateForDate());
}
else
{
throw (new QException("Unsupported field type [" + type + "]"));
}
}
/***************************************************************************
**
***************************************************************************/
private Instant evaluateForDateTime()
{
ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId();
@ -151,7 +183,57 @@ public class ThisOrLastPeriod extends AbstractFilterExpression<Instant>
return operator.equals(Operator.THIS) ? startOfThisYear : startOfLastYear;
}
default -> throw (new QRuntimeException("Unsupported timeUnit: " + timeUnit));
default -> throw (new QRuntimeException("Unsupported unit: " + timeUnit));
}
}
/*******************************************************************************
**
*******************************************************************************/
public LocalDate evaluateForDate()
{
ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId();
LocalDate today = Instant.now().atZone(zoneId).toLocalDate();
switch(timeUnit)
{
case DAYS ->
{
return operator.equals(Operator.THIS) ? today : today.minusDays(1);
}
case WEEKS ->
{
LocalDate startOfThisWeek = today;
while(startOfThisWeek.get(ChronoField.DAY_OF_WEEK) != DayOfWeek.SUNDAY.getValue())
{
////////////////////////////////////////
// go backwards until sunday is found //
////////////////////////////////////////
startOfThisWeek = startOfThisWeek.minusDays(1);
}
return operator.equals(Operator.THIS) ? startOfThisWeek : startOfThisWeek.minusDays(7);
}
case MONTHS ->
{
Instant startOfThisMonth = ValueUtils.getStartOfMonthInZoneId(zoneId.getId());
LocalDateTime startOfThisMonthLDT = LocalDateTime.ofInstant(startOfThisMonth, ZoneId.of(zoneId.getId()));
LocalDateTime startOfLastMonthLDT = startOfThisMonthLDT.minusMonths(1);
Instant startOfLastMonth = startOfLastMonthLDT.toInstant(ZoneId.of(zoneId.getId()).getRules().getOffset(Instant.now()));
return (operator.equals(Operator.THIS) ? startOfThisMonth : startOfLastMonth).atZone(zoneId).toLocalDate();
}
case YEARS ->
{
Instant startOfThisYear = ValueUtils.getStartOfYearInZoneId(zoneId.getId());
LocalDateTime startOfThisYearLDT = LocalDateTime.ofInstant(startOfThisYear, zoneId);
LocalDateTime startOfLastYearLDT = startOfThisYearLDT.minusYears(1);
Instant startOfLastYear = startOfLastYearLDT.toInstant(zoneId.getRules().getOffset(Instant.now()));
return (operator.equals(Operator.THIS) ? startOfThisYear : startOfLastYear).atZone(zoneId).toLocalDate();
}
default -> throw (new QRuntimeException("Unsupported unit: " + timeUnit));
}
}

View File

@ -79,10 +79,11 @@ public class QFilterCriteriaDeserializer extends StdDeserializer<QFilterCriteria
/////////////////////////////////
// get values out of json node //
/////////////////////////////////
List<Serializable> values = objectMapper.treeToValue(node.get("values"), List.class);
String fieldName = objectMapper.treeToValue(node.get("fieldName"), String.class);
QCriteriaOperator operator = objectMapper.treeToValue(node.get("operator"), QCriteriaOperator.class);
String otherFieldName = objectMapper.treeToValue(node.get("otherFieldName"), String.class);
@SuppressWarnings("unchecked")
List<Serializable> values = objectMapper.treeToValue(node.get("values"), List.class);
String fieldName = objectMapper.treeToValue(node.get("fieldName"), String.class);
QCriteriaOperator operator = objectMapper.treeToValue(node.get("operator"), QCriteriaOperator.class);
String otherFieldName = objectMapper.treeToValue(node.get("otherFieldName"), String.class);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// look at all the values - if any of them are actually meant to be an Expression (instance of subclass of AbstractFilterExpression) //

View File

@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
public class StorageInput extends AbstractTableActionInput
{
private String reference;
private String contentType;
@ -74,4 +75,35 @@ public class StorageInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for contentType
*******************************************************************************/
public String getContentType()
{
return (this.contentType);
}
/*******************************************************************************
** Setter for contentType
*******************************************************************************/
public void setContentType(String contentType)
{
this.contentType = contentType;
}
/*******************************************************************************
** Fluent setter for contentType
*******************************************************************************/
public StorageInput withContentType(String contentType)
{
this.contentType = contentType;
return (this);
}
}

View File

@ -220,7 +220,7 @@ public class AuditsMetaDataProvider
.withRecordLabelFormat("%s %s")
.withRecordLabelFields("auditTableId", "recordId")
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("id", QFieldType.LONG))
.withField(new QFieldMetaData("auditTableId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT_TABLE))
.withField(new QFieldMetaData("auditUserId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT_USER))
.withField(new QFieldMetaData("recordId", QFieldType.INTEGER))
@ -243,8 +243,8 @@ public class AuditsMetaDataProvider
.withRecordLabelFormat("%s")
.withRecordLabelFields("id")
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("auditId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT))
.withField(new QFieldMetaData("id", QFieldType.LONG))
.withField(new QFieldMetaData("auditId", QFieldType.LONG).withPossibleValueSourceName(TABLE_NAME_AUDIT))
.withField(new QFieldMetaData("message", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS))
.withField(new QFieldMetaData("fieldName", QFieldType.STRING).withMaxLength(100).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS))
.withField(new QFieldMetaData("oldValue", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS))

View File

@ -22,12 +22,18 @@
package com.kingsrook.qqq.backend.core.model.dashboard.widgets;
import java.util.List;
/*******************************************************************************
** Model containing datastructure expected by frontend alert widget
**
*******************************************************************************/
public class AlertData extends QWidgetData
{
/***************************************************************************
**
***************************************************************************/
public enum AlertType
{
ERROR,
@ -37,8 +43,10 @@ public class AlertData extends QWidgetData
private String html;
private AlertType alertType;
private String html;
private AlertType alertType;
private Boolean hideWidget = false;
private List<String> bulletList;
@ -136,4 +144,66 @@ public class AlertData extends QWidgetData
return (this);
}
/*******************************************************************************
** Getter for hideWidget
*******************************************************************************/
public boolean getHideWidget()
{
return (this.hideWidget);
}
/*******************************************************************************
** Setter for hideWidget
*******************************************************************************/
public void setHideWidget(boolean hideWidget)
{
this.hideWidget = hideWidget;
}
/*******************************************************************************
** Fluent setter for hideWidget
*******************************************************************************/
public AlertData withHideWidget(boolean hideWidget)
{
this.hideWidget = hideWidget;
return (this);
}
/*******************************************************************************
** Getter for bulletList
*******************************************************************************/
public List<String> getBulletList()
{
return (this.bulletList);
}
/*******************************************************************************
** Setter for bulletList
*******************************************************************************/
public void setBulletList(List<String> bulletList)
{
this.bulletList = bulletList;
}
/*******************************************************************************
** Fluent setter for bulletList
*******************************************************************************/
public AlertData withBulletList(List<String> bulletList)
{
this.bulletList = bulletList;
return (this);
}
}

View File

@ -39,9 +39,14 @@ public class ChildRecordListData extends QWidgetData
private QueryOutput queryOutput;
private QTableMetaData childTableMetaData;
private String tableName;
private String tablePath;
private String viewAllLink;
private Integer totalRows;
private Boolean disableRowClick = false;
private Boolean allowRecordEdit = false;
private Boolean allowRecordDelete = false;
private Boolean isInProcess = false;
private boolean canAddChildRecord = false;
private Map<String, Serializable> defaultValuesForNewChildRecords;
@ -352,4 +357,173 @@ public class ChildRecordListData extends QWidgetData
return (this);
}
/*******************************************************************************
** Getter for tableName
*******************************************************************************/
public String getTableName()
{
return (this.tableName);
}
/*******************************************************************************
** Setter for tableName
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/*******************************************************************************
** Fluent setter for tableName
*******************************************************************************/
public ChildRecordListData withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Fluent setter for tablePath
*******************************************************************************/
public ChildRecordListData withTablePath(String tablePath)
{
this.tablePath = tablePath;
return (this);
}
/*******************************************************************************
** Getter for disableRowClick
*******************************************************************************/
public Boolean getDisableRowClick()
{
return (this.disableRowClick);
}
/*******************************************************************************
** Setter for disableRowClick
*******************************************************************************/
public void setDisableRowClick(Boolean disableRowClick)
{
this.disableRowClick = disableRowClick;
}
/*******************************************************************************
** Fluent setter for disableRowClick
*******************************************************************************/
public ChildRecordListData withDisableRowClick(Boolean disableRowClick)
{
this.disableRowClick = disableRowClick;
return (this);
}
/*******************************************************************************
** Getter for allowRecordEdit
*******************************************************************************/
public Boolean getAllowRecordEdit()
{
return (this.allowRecordEdit);
}
/*******************************************************************************
** Setter for allowRecordEdit
*******************************************************************************/
public void setAllowRecordEdit(Boolean allowRecordEdit)
{
this.allowRecordEdit = allowRecordEdit;
}
/*******************************************************************************
** Fluent setter for allowRecordEdit
*******************************************************************************/
public ChildRecordListData withAllowRecordEdit(Boolean allowRecordEdit)
{
this.allowRecordEdit = allowRecordEdit;
return (this);
}
/*******************************************************************************
** Getter for allowRecordDelete
*******************************************************************************/
public Boolean getAllowRecordDelete()
{
return (this.allowRecordDelete);
}
/*******************************************************************************
** Setter for allowRecordDelete
*******************************************************************************/
public void setAllowRecordDelete(Boolean allowRecordDelete)
{
this.allowRecordDelete = allowRecordDelete;
}
/*******************************************************************************
** Fluent setter for allowRecordDelete
*******************************************************************************/
public ChildRecordListData withAllowRecordDelete(Boolean allowRecordDelete)
{
this.allowRecordDelete = allowRecordDelete;
return (this);
}
/*******************************************************************************
** Getter for isInProcess
*******************************************************************************/
public Boolean getIsInProcess()
{
return (this.isInProcess);
}
/*******************************************************************************
** Setter for isInProcess
*******************************************************************************/
public void setIsInProcess(Boolean isInProcess)
{
this.isInProcess = isInProcess;
}
/*******************************************************************************
** Fluent setter for isInProcess
*******************************************************************************/
public ChildRecordListData withIsInProcess(Boolean isInProcess)
{
this.isInProcess = isInProcess;
return (this);
}
}

View File

@ -40,9 +40,24 @@ public class CompositeWidgetData extends AbstractBlockWidgetData<CompositeWidget
{
private List<AbstractBlockWidgetData<?, ?, ?, ?>> blocks = new ArrayList<>();
private Map<String, Serializable> styleOverrides = new HashMap<>();
private ModalMode modalMode;
private Layout layout;
/***************************************************************************
**
***************************************************************************/
public enum ModalMode
{
MODAL
}
private Layout layout;
private Map<String, Serializable> styleOverrides = new HashMap<>();
private String overlayHtml;
private Map<String, Serializable> overlayStyleOverrides = new HashMap<>();
@ -51,12 +66,14 @@ public class CompositeWidgetData extends AbstractBlockWidgetData<CompositeWidget
*******************************************************************************/
public enum Layout
{
/////////////////////////////////////////////////////////////
// note, these are used in QQQ FMD CompositeWidgetData.tsx //
/////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////
// note, these are used in QQQ FMD CompositeWidget.tsx //
// and qqq-android CompositeWidgetBlock.kt //
/////////////////////////////////////////////////////////
FLEX_COLUMN,
FLEX_ROW_WRAPPED,
FLEX_ROW_SPACE_BETWEEN,
FLEX_ROW_CENTER,
TABLE_SUB_ROW_DETAILS,
BADGES_WRAPPER
}
@ -218,4 +235,122 @@ public class CompositeWidgetData extends AbstractBlockWidgetData<CompositeWidget
return (this);
}
/*******************************************************************************
** Getter for overlayHtml
*******************************************************************************/
public String getOverlayHtml()
{
return (this.overlayHtml);
}
/*******************************************************************************
** Setter for overlayHtml
*******************************************************************************/
public void setOverlayHtml(String overlayHtml)
{
this.overlayHtml = overlayHtml;
}
/*******************************************************************************
** Fluent setter for overlayHtml
*******************************************************************************/
public CompositeWidgetData withOverlayHtml(String overlayHtml)
{
this.overlayHtml = overlayHtml;
return (this);
}
/*******************************************************************************
** Getter for overlayStyleOverrides
*******************************************************************************/
public Map<String, Serializable> getOverlayStyleOverrides()
{
return (this.overlayStyleOverrides);
}
/*******************************************************************************
** Setter for overlayStyleOverrides
*******************************************************************************/
public void setOverlayStyleOverrides(Map<String, Serializable> overlayStyleOverrides)
{
this.overlayStyleOverrides = overlayStyleOverrides;
}
/*******************************************************************************
** Fluent setter for overlayStyleOverrides
*******************************************************************************/
public CompositeWidgetData withOverlayStyleOverrides(Map<String, Serializable> overlayStyleOverrides)
{
this.overlayStyleOverrides = overlayStyleOverrides;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public CompositeWidgetData withOverlayStyleOverride(String key, Serializable value)
{
addOverlayStyleOverride(key, value);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void addOverlayStyleOverride(String key, Serializable value)
{
if(this.overlayStyleOverrides == null)
{
this.overlayStyleOverrides = new HashMap<>();
}
this.overlayStyleOverrides.put(key, value);
}
/*******************************************************************************
** Getter for modalMode
*******************************************************************************/
public ModalMode getModalMode()
{
return (this.modalMode);
}
/*******************************************************************************
** Setter for modalMode
*******************************************************************************/
public void setModalMode(ModalMode modalMode)
{
this.modalMode = modalMode;
}
/*******************************************************************************
** Fluent setter for modalMode
*******************************************************************************/
public CompositeWidgetData withModalMode(ModalMode modalMode)
{
this.modalMode = modalMode;
return (this);
}
}

View File

@ -31,7 +31,7 @@ import java.util.Map;
** Base class for the data returned by rendering a Widget.
**
*******************************************************************************/
public abstract class QWidgetData
public abstract class QWidgetData implements Serializable
{
private String label;
private String sublabel;

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks;
import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.CompositeWidgetData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
@ -51,6 +52,11 @@ public abstract class AbstractBlockWidgetData<
private V values;
private SX styles;
///////////////////////////////////////////////////////////////////////////////////
// optional field name to act as a 'guard' for the block - e.g., only include it //
// if the value for this field is true //
///////////////////////////////////////////////////////////////////////////////////
private String conditional;
/*******************************************************************************
@ -74,6 +80,7 @@ public abstract class AbstractBlockWidgetData<
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public T withTooltip(S key, String value)
{
addTooltip(key, value);
@ -99,6 +106,7 @@ public abstract class AbstractBlockWidgetData<
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public T withTooltip(S key, BlockTooltip value)
{
addTooltip(key, value);
@ -144,6 +152,7 @@ public abstract class AbstractBlockWidgetData<
/*******************************************************************************
** Fluent setter for tooltipMap
*******************************************************************************/
@SuppressWarnings("unchecked")
public T withTooltipMap(Map<S, BlockTooltip> tooltipMap)
{
this.tooltipMap = tooltipMap;
@ -178,6 +187,7 @@ public abstract class AbstractBlockWidgetData<
** Fluent setter for tooltip
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public T withTooltip(String tooltip)
{
this.tooltip = new BlockTooltip(tooltip);
@ -190,6 +200,7 @@ public abstract class AbstractBlockWidgetData<
** Fluent setter for tooltip
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public T withTooltip(BlockTooltip tooltip)
{
this.tooltip = tooltip;
@ -199,8 +210,22 @@ public abstract class AbstractBlockWidgetData<
/*******************************************************************************
** Fluent setter for tooltip
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public T withTooltip(CompositeWidgetData data)
{
this.tooltip = new BlockTooltip(data);
return (T) (this);
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public T withLink(S key, String value)
{
addLink(key, value);
@ -226,6 +251,7 @@ public abstract class AbstractBlockWidgetData<
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public T withLink(S key, BlockLink value)
{
addLink(key, value);
@ -271,6 +297,7 @@ public abstract class AbstractBlockWidgetData<
/*******************************************************************************
** Fluent setter for linkMap
*******************************************************************************/
@SuppressWarnings("unchecked")
public T withLinkMap(Map<S, BlockLink> linkMap)
{
this.linkMap = linkMap;
@ -305,6 +332,7 @@ public abstract class AbstractBlockWidgetData<
** Fluent setter for link
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public T withLink(String link)
{
this.link = new BlockLink(link);
@ -317,6 +345,7 @@ public abstract class AbstractBlockWidgetData<
** Fluent setter for link
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public T withLink(BlockLink link)
{
this.link = link;
@ -348,6 +377,7 @@ public abstract class AbstractBlockWidgetData<
/*******************************************************************************
** Fluent setter for values
*******************************************************************************/
@SuppressWarnings("unchecked")
public T withValues(V values)
{
this.values = values;
@ -379,6 +409,7 @@ public abstract class AbstractBlockWidgetData<
/*******************************************************************************
** Fluent setter for styles
*******************************************************************************/
@SuppressWarnings("unchecked")
public T withStyles(SX styles)
{
this.styles = styles;
@ -386,6 +417,7 @@ public abstract class AbstractBlockWidgetData<
}
/*******************************************************************************
** Getter for blockId
*******************************************************************************/
@ -409,6 +441,7 @@ public abstract class AbstractBlockWidgetData<
/*******************************************************************************
** Fluent setter for blockId
*******************************************************************************/
@SuppressWarnings("unchecked")
public T withBlockId(String blockId)
{
this.blockId = blockId;
@ -416,4 +449,34 @@ public abstract class AbstractBlockWidgetData<
}
/*******************************************************************************
** Getter for conditional
*******************************************************************************/
public String getConditional()
{
return (this.conditional);
}
/*******************************************************************************
** Setter for conditional
*******************************************************************************/
public void setConditional(String conditional)
{
this.conditional = conditional;
}
/*******************************************************************************
** Fluent setter for conditional
*******************************************************************************/
public AbstractBlockWidgetData withConditional(String conditional)
{
this.conditional = conditional;
return (this);
}
}

View File

@ -22,17 +22,24 @@
package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.CompositeWidgetData;
/*******************************************************************************
** A tooltip used within a (widget) block.
**
*******************************************************************************/
public class BlockTooltip
{
private String title;
private Placement placement = Placement.BOTTOM;
private CompositeWidgetData blockData;
private String title;
private Placement placement = Placement.BOTTOM;
/***************************************************************************
**
***************************************************************************/
public enum Placement
{BOTTOM, LEFT, RIGHT, TOP}
@ -59,6 +66,17 @@ public class BlockTooltip
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public BlockTooltip(CompositeWidgetData blockData)
{
this.blockData = blockData;
}
/*******************************************************************************
** Getter for title
*******************************************************************************/
@ -119,4 +137,35 @@ public class BlockTooltip
return (this);
}
/*******************************************************************************
** Getter for blockData
*******************************************************************************/
public CompositeWidgetData getBlockData()
{
return (this.blockData);
}
/*******************************************************************************
** Setter for blockData
*******************************************************************************/
public void setBlockData(CompositeWidgetData blockData)
{
this.blockData = blockData;
}
/*******************************************************************************
** Fluent setter for blockData
*******************************************************************************/
public BlockTooltip withBlockData(CompositeWidgetData blockData)
{
this.blockData = blockData;
return (this);
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.dashboard.widgets.blocks.audio;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles;
/*******************************************************************************
** block that plays an audio file
*******************************************************************************/
public class AudioBlockData extends AbstractBlockWidgetData<AudioBlockData, AudioValues, BaseSlots, BaseStyles>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getBlockTypeName()
{
return "AUDIO";
}
}

View File

@ -0,0 +1,130 @@
/*
* 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.dashboard.widgets.blocks.audio;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
/*******************************************************************************
**
*******************************************************************************/
public class AudioValues implements BlockValuesInterface
{
private String path;
private boolean showControls = false;
private boolean autoPlay = true;
/*******************************************************************************
** Getter for path
*******************************************************************************/
public String getPath()
{
return (this.path);
}
/*******************************************************************************
** Setter for path
*******************************************************************************/
public void setPath(String path)
{
this.path = path;
}
/*******************************************************************************
** Fluent setter for path
*******************************************************************************/
public AudioValues withPath(String path)
{
this.path = path;
return (this);
}
/*******************************************************************************
** Getter for showControls
*******************************************************************************/
public boolean getShowControls()
{
return (this.showControls);
}
/*******************************************************************************
** Setter for showControls
*******************************************************************************/
public void setShowControls(boolean showControls)
{
this.showControls = showControls;
}
/*******************************************************************************
** Fluent setter for showControls
*******************************************************************************/
public AudioValues withShowControls(boolean showControls)
{
this.showControls = showControls;
return (this);
}
/*******************************************************************************
** Getter for autoPlay
*******************************************************************************/
public boolean getAutoPlay()
{
return (this.autoPlay);
}
/*******************************************************************************
** Setter for autoPlay
*******************************************************************************/
public void setAutoPlay(boolean autoPlay)
{
this.autoPlay = autoPlay;
}
/*******************************************************************************
** Fluent setter for autoPlay
*******************************************************************************/
public AudioValues withAutoPlay(boolean autoPlay)
{
this.autoPlay = autoPlay;
return (this);
}
}

View File

@ -30,4 +30,335 @@ import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStyles
*******************************************************************************/
public class BaseStyles implements BlockStylesInterface
{
private Directional<String> padding;
private String backgroundColor;
/***************************************************************************
**
***************************************************************************/
public static class Directional<T>
{
private T top;
private T bottom;
private T left;
private T right;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public Directional()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public Directional(T top, T right, T bottom, T left)
{
this.top = top;
this.right = right;
this.bottom = bottom;
this.left = left;
}
/***************************************************************************
**
***************************************************************************/
public static <T> Directional<T> of(T top, T right, T bottom, T left)
{
return (new Directional<>(top, right, bottom, left));
}
/***************************************************************************
**
***************************************************************************/
public static <T> Directional<T> of(T value)
{
return (new Directional<>(value, value, value, value));
}
/***************************************************************************
**
***************************************************************************/
public static <T> Directional<T> ofTop(T top)
{
return (new Directional<>(top, null, null, null));
}
/***************************************************************************
**
***************************************************************************/
public static <T> Directional<T> ofRight(T right)
{
return (new Directional<>(null, right, null, null));
}
/***************************************************************************
**
***************************************************************************/
public static <T> Directional<T> ofBottom(T bottom)
{
return (new Directional<>(null, null, bottom, null));
}
/***************************************************************************
**
***************************************************************************/
public static <T> Directional<T> ofLeft(T left)
{
return (new Directional<>(null, null, null, left));
}
/***************************************************************************
**
***************************************************************************/
public static <T> Directional<T> ofX(T x)
{
return (new Directional<>(null, x, null, x));
}
/***************************************************************************
**
***************************************************************************/
public static <T> Directional<T> ofY(T y)
{
return (new Directional<>(y, null, y, null));
}
/***************************************************************************
**
***************************************************************************/
public static <T> Directional<T> ofXY(T x, T y)
{
return (new Directional<>(y, x, y, x));
}
/*******************************************************************************
** Getter for top
**
*******************************************************************************/
public T getTop()
{
return top;
}
/*******************************************************************************
** Setter for top
**
*******************************************************************************/
public void setTop(T top)
{
this.top = top;
}
/*******************************************************************************
** Fluent setter for top
**
*******************************************************************************/
public Directional<T> withTop(T top)
{
this.top = top;
return (this);
}
/*******************************************************************************
** Getter for bottom
**
*******************************************************************************/
public T getBottom()
{
return bottom;
}
/*******************************************************************************
** Setter for bottom
**
*******************************************************************************/
public void setBottom(T bottom)
{
this.bottom = bottom;
}
/*******************************************************************************
** Fluent setter for bottom
**
*******************************************************************************/
public Directional<T> withBottom(T bottom)
{
this.bottom = bottom;
return (this);
}
/*******************************************************************************
** Getter for left
**
*******************************************************************************/
public T getLeft()
{
return left;
}
/*******************************************************************************
** Setter for left
**
*******************************************************************************/
public void setLeft(T left)
{
this.left = left;
}
/*******************************************************************************
** Fluent setter for left
**
*******************************************************************************/
public Directional<T> withLeft(T left)
{
this.left = left;
return (this);
}
/*******************************************************************************
** Getter for right
**
*******************************************************************************/
public T getRight()
{
return right;
}
/*******************************************************************************
** Setter for right
**
*******************************************************************************/
public void setRight(T right)
{
this.right = right;
}
/*******************************************************************************
** Fluent setter for right
**
*******************************************************************************/
public Directional<T> withRight(T right)
{
this.right = right;
return (this);
}
}
/*******************************************************************************
** Getter for padding
*******************************************************************************/
public Directional<String> getPadding()
{
return (this.padding);
}
/*******************************************************************************
** Setter for padding
*******************************************************************************/
public void setPadding(Directional<String> padding)
{
this.padding = padding;
}
/*******************************************************************************
** Fluent setter for padding
*******************************************************************************/
public BaseStyles withPadding(Directional<String> padding)
{
this.padding = padding;
return (this);
}
/*******************************************************************************
** Getter for backgroundColor
*******************************************************************************/
public String getBackgroundColor()
{
return (this.backgroundColor);
}
/*******************************************************************************
** Setter for backgroundColor
*******************************************************************************/
public void setBackgroundColor(String backgroundColor)
{
this.backgroundColor = backgroundColor;
}
/*******************************************************************************
** Fluent setter for backgroundColor
*******************************************************************************/
public BaseStyles withBackgroundColor(String backgroundColor)
{
this.backgroundColor = backgroundColor;
return (this);
}
}

View File

@ -0,0 +1,46 @@
/*
* 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.dashboard.widgets.blocks.button;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots;
/*******************************************************************************
** a button (for a process - not sure yet what this could do in a standalone
** widget?) to submit the process screen to run a specific action (e.g., not just
** 'next'), or do other control-ish things
*******************************************************************************/
public class ButtonBlockData extends AbstractBlockWidgetData<ButtonBlockData, ButtonValues, BaseSlots, ButtonStyles>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getBlockTypeName()
{
return "BUTTON";
}
}

View File

@ -0,0 +1,143 @@
/*
* 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.dashboard.widgets.blocks.button;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStylesInterface;
/*******************************************************************************
**
*******************************************************************************/
public class ButtonStyles implements BlockStylesInterface
{
private String color;
private String format;
/***************************************************************************
**
***************************************************************************/
public enum StandardColor
{
SUCCESS,
WARNING,
ERROR,
INFO,
MUTED
}
/***************************************************************************
**
***************************************************************************/
public enum StandardFormat
{
OUTLINED,
FILLED,
TEXT
}
/*******************************************************************************
** Getter for color
*******************************************************************************/
public String getColor()
{
return (this.color);
}
/*******************************************************************************
** Setter for color
*******************************************************************************/
public void setColor(String color)
{
this.color = color;
}
/*******************************************************************************
** Fluent setter for color
*******************************************************************************/
public ButtonStyles withColor(String color)
{
this.color = color;
return (this);
}
/*******************************************************************************
** Getter for format
*******************************************************************************/
public String getFormat()
{
return (this.format);
}
/*******************************************************************************
** Setter for format
*******************************************************************************/
public void setFormat(String format)
{
this.format = format;
}
/*******************************************************************************
** Fluent setter for format
*******************************************************************************/
public ButtonStyles withFormat(String format)
{
this.format = format;
return (this);
}
/*******************************************************************************
** Setter for format
*******************************************************************************/
public void setFormat(StandardFormat format)
{
this.format = (format == null ? null : format.name().toLowerCase());
}
/*******************************************************************************
** Fluent setter for format
*******************************************************************************/
public ButtonStyles withFormat(StandardFormat format)
{
setFormat(format);
return (this);
}
}

View File

@ -0,0 +1,218 @@
/*
* 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.dashboard.widgets.blocks.button;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
/*******************************************************************************
**
*******************************************************************************/
public class ButtonValues implements BlockValuesInterface
{
private String label;
private String actionCode;
private String controlCode;
private QIcon startIcon;
private QIcon endIcon;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ButtonValues()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ButtonValues(String label, String actionCode)
{
setLabel(label);
setActionCode(actionCode);
}
/*******************************************************************************
** 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 ButtonValues withLabel(String label)
{
this.label = label;
return (this);
}
/*******************************************************************************
** Getter for actionCode
*******************************************************************************/
public String getActionCode()
{
return (this.actionCode);
}
/*******************************************************************************
** Setter for actionCode
*******************************************************************************/
public void setActionCode(String actionCode)
{
this.actionCode = actionCode;
}
/*******************************************************************************
** Fluent setter for actionCode
*******************************************************************************/
public ButtonValues withActionCode(String actionCode)
{
this.actionCode = actionCode;
return (this);
}
/*******************************************************************************
** Getter for startIcon
*******************************************************************************/
public QIcon getStartIcon()
{
return (this.startIcon);
}
/*******************************************************************************
** Setter for startIcon
*******************************************************************************/
public void setStartIcon(QIcon startIcon)
{
this.startIcon = startIcon;
}
/*******************************************************************************
** Fluent setter for startIcon
*******************************************************************************/
public ButtonValues withStartIcon(QIcon startIcon)
{
this.startIcon = startIcon;
return (this);
}
/*******************************************************************************
** Getter for endIcon
*******************************************************************************/
public QIcon getEndIcon()
{
return (this.endIcon);
}
/*******************************************************************************
** Setter for endIcon
*******************************************************************************/
public void setEndIcon(QIcon endIcon)
{
this.endIcon = endIcon;
}
/*******************************************************************************
** Fluent setter for endIcon
*******************************************************************************/
public ButtonValues withEndIcon(QIcon endIcon)
{
this.endIcon = endIcon;
return (this);
}
/*******************************************************************************
** Getter for controlCode
*******************************************************************************/
public String getControlCode()
{
return (this.controlCode);
}
/*******************************************************************************
** Setter for controlCode
*******************************************************************************/
public void setControlCode(String controlCode)
{
this.controlCode = controlCode;
}
/*******************************************************************************
** Fluent setter for controlCode
*******************************************************************************/
public ButtonValues withControlCode(String controlCode)
{
this.controlCode = controlCode;
return (this);
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.dashboard.widgets.blocks.image;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots;
/*******************************************************************************
** block to display an image
*******************************************************************************/
public class ImageBlockData extends AbstractBlockWidgetData<ImageBlockData, ImageValues, BaseSlots, ImageStyles>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getBlockTypeName()
{
return "IMAGE";
}
}

View File

@ -0,0 +1,108 @@
/*
* 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.dashboard.widgets.blocks.image;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles;
/*******************************************************************************
**
*******************************************************************************/
public class ImageStyles extends BaseStyles
{
private String width;
private String height;
/*******************************************************************************
** Fluent setter for padding
*******************************************************************************/
@Override
public ImageStyles withPadding(Directional<String> padding)
{
super.setPadding(padding);
return (this);
}
/*******************************************************************************
** Getter for width
*******************************************************************************/
public String getWidth()
{
return (this.width);
}
/*******************************************************************************
** Setter for width
*******************************************************************************/
public void setWidth(String width)
{
this.width = width;
}
/*******************************************************************************
** Fluent setter for width
*******************************************************************************/
public ImageStyles withWidth(String width)
{
this.width = width;
return (this);
}
/*******************************************************************************
** Getter for height
*******************************************************************************/
public String getHeight()
{
return (this.height);
}
/*******************************************************************************
** Setter for height
*******************************************************************************/
public void setHeight(String height)
{
this.height = height;
}
/*******************************************************************************
** Fluent setter for height
*******************************************************************************/
public ImageStyles withHeight(String height)
{
this.height = height;
return (this);
}
}

View File

@ -0,0 +1,98 @@
/*
* 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.dashboard.widgets.blocks.image;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
/*******************************************************************************
**
*******************************************************************************/
public class ImageValues implements BlockValuesInterface
{
private String path;
private String alt;
/*******************************************************************************
** Getter for path
*******************************************************************************/
public String getPath()
{
return (this.path);
}
/*******************************************************************************
** Setter for path
*******************************************************************************/
public void setPath(String path)
{
this.path = path;
}
/*******************************************************************************
** Fluent setter for path
*******************************************************************************/
public ImageValues withPath(String path)
{
this.path = path;
return (this);
}
/*******************************************************************************
** Getter for alt
*******************************************************************************/
public String getAlt()
{
return (this.alt);
}
/*******************************************************************************
** Setter for alt
*******************************************************************************/
public void setAlt(String alt)
{
this.alt = alt;
}
/*******************************************************************************
** Fluent setter for alt
*******************************************************************************/
public ImageValues withAlt(String alt)
{
this.alt = alt;
return (this);
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.dashboard.widgets.blocks.inputfield;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles;
/*******************************************************************************
** block to display an input field - initially targeted at widgets-in-processes
*******************************************************************************/
public class InputFieldBlockData extends AbstractBlockWidgetData<InputFieldBlockData, InputFieldValues, BaseSlots, BaseStyles>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getBlockTypeName()
{
return "INPUT_FIELD";
}
}

View File

@ -0,0 +1,217 @@
/*
* 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.dashboard.widgets.blocks.inputfield;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
/*******************************************************************************
**
*******************************************************************************/
public class InputFieldValues implements BlockValuesInterface
{
private QFieldMetaData fieldMetaData;
private Boolean autoFocus;
private Boolean submitOnEnter;
private Boolean hideSoftKeyboard;
private String placeholder;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public InputFieldValues()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public InputFieldValues(QFieldMetaData fieldMetaData)
{
setFieldMetaData(fieldMetaData);
}
/*******************************************************************************
** Getter for fieldMetaData
*******************************************************************************/
public QFieldMetaData getFieldMetaData()
{
return (this.fieldMetaData);
}
/*******************************************************************************
** Setter for fieldMetaData
*******************************************************************************/
public void setFieldMetaData(QFieldMetaData fieldMetaData)
{
this.fieldMetaData = fieldMetaData;
}
/*******************************************************************************
** Fluent setter for fieldMetaData
*******************************************************************************/
public InputFieldValues withFieldMetaData(QFieldMetaData fieldMetaData)
{
this.fieldMetaData = fieldMetaData;
return (this);
}
/*******************************************************************************
** Getter for autoFocus
*******************************************************************************/
public Boolean getAutoFocus()
{
return (this.autoFocus);
}
/*******************************************************************************
** Setter for autoFocus
*******************************************************************************/
public void setAutoFocus(Boolean autoFocus)
{
this.autoFocus = autoFocus;
}
/*******************************************************************************
** Fluent setter for autoFocus
*******************************************************************************/
public InputFieldValues withAutoFocus(Boolean autoFocus)
{
this.autoFocus = autoFocus;
return (this);
}
/*******************************************************************************
** Getter for submitOnEnter
*******************************************************************************/
public Boolean getSubmitOnEnter()
{
return (this.submitOnEnter);
}
/*******************************************************************************
** Setter for submitOnEnter
*******************************************************************************/
public void setSubmitOnEnter(Boolean submitOnEnter)
{
this.submitOnEnter = submitOnEnter;
}
/*******************************************************************************
** Fluent setter for submitOnEnter
*******************************************************************************/
public InputFieldValues withSubmitOnEnter(Boolean submitOnEnter)
{
this.submitOnEnter = submitOnEnter;
return (this);
}
/*******************************************************************************
** Getter for placeholder
*******************************************************************************/
public String getPlaceholder()
{
return (this.placeholder);
}
/*******************************************************************************
** Setter for placeholder
*******************************************************************************/
public void setPlaceholder(String placeholder)
{
this.placeholder = placeholder;
}
/*******************************************************************************
** Fluent setter for placeholder
*******************************************************************************/
public InputFieldValues withPlaceholder(String placeholder)
{
this.placeholder = placeholder;
return (this);
}
/*******************************************************************************
** Getter for hideSoftKeyboard
*******************************************************************************/
public Boolean getHideSoftKeyboard()
{
return (this.hideSoftKeyboard);
}
/*******************************************************************************
** Setter for hideSoftKeyboard
*******************************************************************************/
public void setHideSoftKeyboard(Boolean hideSoftKeyboard)
{
this.hideSoftKeyboard = hideSoftKeyboard;
}
/*******************************************************************************
** Fluent setter for hideSoftKeyboard
*******************************************************************************/
public InputFieldValues withHideSoftKeyboard(Boolean hideSoftKeyboard)
{
this.hideSoftKeyboard = hideSoftKeyboard;
return (this);
}
}

View File

@ -30,4 +30,325 @@ import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStyles
*******************************************************************************/
public class TextStyles implements BlockStylesInterface
{
private String color;
private String format;
private String weight;
private String size;
/***************************************************************************
**
***************************************************************************/
public enum StandardColor
{
SUCCESS,
WARNING,
ERROR,
INFO,
MUTED
}
/***************************************************************************
**
***************************************************************************/
public enum StandardFormat
{
DEFAULT,
ALERT,
BANNER
}
/***************************************************************************
**
***************************************************************************/
public enum StandardSize
{
LARGEST,
HEADLINE,
TITLE,
BODY,
SMALLEST
}
/***************************************************************************
**
***************************************************************************/
public enum StandardWeight
{
EXTRA_LIGHT("extralight"),
THIN("thin"),
MEDIUM("medium"),
SEMI_BOLD("semibold"),
BLACK("black"),
BOLD("bold"),
EXTRA_BOLD("extrabold"),
W100("100"),
W200("200"),
W300("300"),
W400("400"),
W500("500"),
W600("600"),
W700("700"),
W800("800"),
W900("900");
private final String value;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
StandardWeight(String value)
{
this.value = value;
}
/*******************************************************************************
** Getter for value
**
*******************************************************************************/
public String getValue()
{
return value;
}
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public TextStyles()
{
}
/***************************************************************************
**
***************************************************************************/
public TextStyles(StandardColor standardColor)
{
setColor(standardColor);
}
/*******************************************************************************
** Getter for format
*******************************************************************************/
public String getFormat()
{
return (this.format);
}
/*******************************************************************************
** Setter for format
*******************************************************************************/
public void setFormat(String format)
{
this.format = format;
}
/*******************************************************************************
** Fluent setter for format
*******************************************************************************/
public TextStyles withFormat(String format)
{
this.format = format;
return (this);
}
/*******************************************************************************
** Setter for format
*******************************************************************************/
public void setFormat(StandardFormat format)
{
this.format = format == null ? null : format.name().toLowerCase();
}
/*******************************************************************************
** Fluent setter for format
*******************************************************************************/
public TextStyles withFormat(StandardFormat format)
{
this.setFormat(format);
return (this);
}
/*******************************************************************************
** Getter for weight
*******************************************************************************/
public String getWeight()
{
return (this.weight);
}
/*******************************************************************************
** Setter for weight
*******************************************************************************/
public void setWeight(String weight)
{
this.weight = weight;
}
/*******************************************************************************
** Fluent setter for weight
*******************************************************************************/
public TextStyles withWeight(String weight)
{
this.weight = weight;
return (this);
}
/*******************************************************************************
** Setter for weight
*******************************************************************************/
public void setWeight(StandardWeight weight)
{
setWeight(weight == null ? null : weight.getValue());
}
/*******************************************************************************
** Fluent setter for weight
*******************************************************************************/
public TextStyles withWeight(StandardWeight weight)
{
setWeight(weight);
return (this);
}
/*******************************************************************************
** Getter for size
*******************************************************************************/
public String getSize()
{
return (this.size);
}
/*******************************************************************************
** Setter for size
*******************************************************************************/
public void setSize(String size)
{
this.size = size;
}
/*******************************************************************************
** Fluent setter for size
*******************************************************************************/
public TextStyles withSize(String size)
{
this.size = size;
return (this);
}
/*******************************************************************************
** Setter for size
*******************************************************************************/
public void setSize(StandardSize size)
{
this.size = (size == null ? null : size.name().toLowerCase());
}
/*******************************************************************************
** Fluent setter for size
*******************************************************************************/
public TextStyles withSize(StandardSize size)
{
setSize(size);
return (this);
}
/*******************************************************************************
** Getter for color
*******************************************************************************/
public String getColor()
{
return (this.color);
}
/*******************************************************************************
** Setter for color
*******************************************************************************/
public void setColor(String color)
{
this.color = color;
}
/*******************************************************************************
** Fluent setter for color
*******************************************************************************/
public TextStyles withColor(String color)
{
this.color = color;
return (this);
}
/*******************************************************************************
** Setter for color
*******************************************************************************/
public void setColor(StandardColor color)
{
this.color = color == null ? null : color.name();
}
/*******************************************************************************
** Fluent setter for color
*******************************************************************************/
public TextStyles withColor(StandardColor color)
{
setColor(color);
return (this);
}
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.text;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
/*******************************************************************************
@ -32,6 +33,9 @@ public class TextValues implements BlockValuesInterface
{
private String text;
private QIcon startIcon;
private QIcon endIcon;
/*******************************************************************************
@ -84,4 +88,66 @@ public class TextValues implements BlockValuesInterface
return (this);
}
/*******************************************************************************
** Getter for startIcon
*******************************************************************************/
public QIcon getStartIcon()
{
return (this.startIcon);
}
/*******************************************************************************
** Setter for startIcon
*******************************************************************************/
public void setStartIcon(QIcon startIcon)
{
this.startIcon = startIcon;
}
/*******************************************************************************
** Fluent setter for startIcon
*******************************************************************************/
public TextValues withStartIcon(QIcon startIcon)
{
this.startIcon = startIcon;
return (this);
}
/*******************************************************************************
** Getter for endIcon
*******************************************************************************/
public QIcon getEndIcon()
{
return (this.endIcon);
}
/*******************************************************************************
** Setter for endIcon
*******************************************************************************/
public void setEndIcon(QIcon endIcon)
{
this.endIcon = endIcon;
}
/*******************************************************************************
** Fluent setter for endIcon
*******************************************************************************/
public TextValues withEndIcon(QIcon endIcon)
{
this.endIcon = endIcon;
return (this);
}
}

View File

@ -192,6 +192,7 @@ public abstract class QRecordEntity
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
{
@SuppressWarnings("unchecked")
List<? extends QRecordEntity> associatedEntities = (List<? extends QRecordEntity>) qRecordEntityAssociation.getGetter().invoke(this);
String associationName = qRecordEntityAssociation.getAssociationAnnotation().name();
@ -245,6 +246,7 @@ public abstract class QRecordEntity
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
{
@SuppressWarnings("unchecked")
List<? extends QRecordEntity> associatedEntities = (List<? extends QRecordEntity>) qRecordEntityAssociation.getGetter().invoke(this);
String associationName = qRecordEntityAssociation.getAssociationAnnotation().name();
@ -346,6 +348,7 @@ public abstract class QRecordEntity
if(associationAnnotation.isPresent())
{
@SuppressWarnings("unchecked")
Class<? extends QRecordEntity> listTypeParam = (Class<? extends QRecordEntity>) getListTypeParam(possibleGetter.getReturnType(), possibleGetter.getAnnotatedReturnType());
associationList.add(new QRecordEntityAssociation(fieldName, possibleGetter, setter.get(), listTypeParam, associationAnnotation.orElse(null)));
}

View File

@ -22,9 +22,6 @@
package com.kingsrook.qqq.backend.core.model.metadata;
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
/*******************************************************************************
** Abstract class that knows how to produce meta data objects. Useful with
** MetaDataProducerHelper, to put point at a package full of these, and populate

View File

@ -22,7 +22,6 @@
package com.kingsrook.qqq.backend.core.model.metadata;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
@ -30,14 +29,12 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.common.collect.ImmutableSet;
import com.google.common.reflect.ClassPath;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -51,8 +48,6 @@ public class MetaDataProducerHelper
private static Map<Class<?>, Integer> comparatorValuesByType = new HashMap<>();
private static Integer defaultComparatorValue;
private static ImmutableSet<ClassPath.ClassInfo> topLevelClasses;
static
{
////////////////////////////////////////////////////////////////////////////////////////
@ -87,7 +82,7 @@ public class MetaDataProducerHelper
////////////////////////////////////////////////////////////////////////
// find all the meta data producer classes in (and under) the package //
////////////////////////////////////////////////////////////////////////
classesInPackage = getClassesInPackage(packageName);
classesInPackage = ClassPathUtils.getClassesInPackage(packageName);
}
catch(Exception e)
{
@ -176,51 +171,4 @@ public class MetaDataProducerHelper
}
/*******************************************************************************
** from https://stackoverflow.com/questions/520328/can-you-find-all-classes-in-a-package-using-reflection
** (since the original, from ChatGPT, didn't work in jars, despite GPT hallucinating that it would)
*******************************************************************************/
private static List<Class<?>> getClassesInPackage(String packageName) throws IOException
{
List<Class<?>> classes = new ArrayList<>();
ClassLoader loader = Thread.currentThread().getContextClassLoader();
for(ClassPath.ClassInfo info : getTopLevelClasses(loader))
{
if(info.getName().startsWith(packageName))
{
classes.add(info.load());
}
}
return (classes);
}
/*******************************************************************************
**
*******************************************************************************/
private static ImmutableSet<ClassPath.ClassInfo> getTopLevelClasses(ClassLoader loader) throws IOException
{
if(topLevelClasses == null)
{
topLevelClasses = ClassPath.from(loader).getTopLevelClasses();
}
return (topLevelClasses);
}
/*******************************************************************************
**
*******************************************************************************/
public static void clearTopLevelClassCache()
{
topLevelClasses = null;
}
}

View File

@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 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/
@ -19,12 +19,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model;
package com.kingsrook.qqq.backend.core.model.metadata;
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;
/*******************************************************************************

View File

@ -53,6 +53,8 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
private String variantOptionsTableUsernameField;
private String variantOptionsTablePasswordField;
private String variantOptionsTableApiKeyField;
private String variantOptionsTableClientIdField;
private String variantOptionsTableClientSecretField;
private String variantOptionsTableName;
// todo - at some point, we may want to apply this to secret properties on subclasses?
@ -648,4 +650,66 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
{
qInstance.addBackend(this);
}
/*******************************************************************************
** Getter for variantOptionsTableClientIdField
*******************************************************************************/
public String getVariantOptionsTableClientIdField()
{
return (this.variantOptionsTableClientIdField);
}
/*******************************************************************************
** Setter for variantOptionsTableClientIdField
*******************************************************************************/
public void setVariantOptionsTableClientIdField(String variantOptionsTableClientIdField)
{
this.variantOptionsTableClientIdField = variantOptionsTableClientIdField;
}
/*******************************************************************************
** Fluent setter for variantOptionsTableClientIdField
*******************************************************************************/
public QBackendMetaData withVariantOptionsTableClientIdField(String variantOptionsTableClientIdField)
{
this.variantOptionsTableClientIdField = variantOptionsTableClientIdField;
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableClientSecretField
*******************************************************************************/
public String getVariantOptionsTableClientSecretField()
{
return (this.variantOptionsTableClientSecretField);
}
/*******************************************************************************
** Setter for variantOptionsTableClientSecretField
*******************************************************************************/
public void setVariantOptionsTableClientSecretField(String variantOptionsTableClientSecretField)
{
this.variantOptionsTableClientSecretField = variantOptionsTableClientSecretField;
}
/*******************************************************************************
** Fluent setter for variantOptionsTableClientSecretField
*******************************************************************************/
public QBackendMetaData withVariantOptionsTableClientSecretField(String variantOptionsTableClientSecretField)
{
this.variantOptionsTableClientSecretField = variantOptionsTableClientSecretField;
return (this);
}
}

View File

@ -43,6 +43,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNodeType;
@ -113,6 +114,8 @@ public class QInstance
private QPermissionRules defaultPermissionRules = QPermissionRules.defaultInstance();
private QAuditRules defaultAuditRules = QAuditRules.defaultInstanceLevelNone();
private QCodeReference metaDataFilter = null;
//////////////////////////////////////////////////////////////////////////////////////
// todo - lock down the object (no more changes allowed) after it's been validated? //
// if doing so, may need to copy all of the collections into read-only versions... //
@ -1485,4 +1488,35 @@ public class QInstance
QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, listForSlot);
}
/*******************************************************************************
** Getter for metaDataFilter
*******************************************************************************/
public QCodeReference getMetaDataFilter()
{
return (this.metaDataFilter);
}
/*******************************************************************************
** Setter for metaDataFilter
*******************************************************************************/
public void setMetaDataFilter(QCodeReference metaDataFilter)
{
this.metaDataFilter = metaDataFilter;
}
/*******************************************************************************
** Fluent setter for metaDataFilter
*******************************************************************************/
public QInstance withMetaDataFilter(QCodeReference metaDataFilter)
{
this.metaDataFilter = metaDataFilter;
return (this);
}
}

View File

@ -177,7 +177,7 @@ public class QAuthenticationMetaData implements TopLevelMetaDataInterface
/*******************************************************************************
**
*******************************************************************************/
public QAuthenticationMetaData withVales(Map<String, String> values)
public QAuthenticationMetaData withValues(Map<String, String> values)
{
this.values = values;
return (this);

View File

@ -0,0 +1,58 @@
/*
* 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.metadata.code;
/*******************************************************************************
** Specialized type of QCodeReference that takes a lambda function object.
**
** Originally intended for more concise setup of backend steps in tests - but,
** may be generally useful.
*******************************************************************************/
public class QCodeReferenceLambda<T> extends QCodeReference
{
private final T lambda;
/***************************************************************************
**
***************************************************************************/
public QCodeReferenceLambda(T lambda)
{
this.lambda = lambda;
this.setCodeType(QCodeType.JAVA);
this.setName("[Lambda:" + lambda.toString() + "]");
}
/*******************************************************************************
** Getter for lambda
**
*******************************************************************************/
public T getLambda()
{
return lambda;
}
}

View File

@ -39,6 +39,9 @@ public class ParentWidgetMetaData extends QWidgetMetaData
/***************************************************************************
**
***************************************************************************/
public enum LayoutType
{
GRID,

View File

@ -188,7 +188,8 @@ public class FieldAdornment
** Fluent setter for values
**
*******************************************************************************/
public FieldAdornment withValues(Pair<String, Serializable>... values)
@SafeVarargs
public final FieldAdornment withValues(Pair<String, Serializable>... values)
{
for(Pair<String, Serializable> value : values)
{

View File

@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
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.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -73,10 +74,12 @@ public class QFieldMetaData implements Cloneable
// propose doing that in a secondary field, e.g., "onlyEditableOn=insert|update" //
///////////////////////////////////////////////////////////////////////////////////
private String displayFormat = "%s";
private String displayFormat = "%s";
private Serializable defaultValue;
private String possibleValueSourceName;
private QQueryFilter possibleValueSourceFilter;
private String possibleValueSourceName;
private QQueryFilter possibleValueSourceFilter;
private QPossibleValueSource inlinePossibleValueSource;
private Integer maxLength;
private Set<FieldBehavior<?>> behaviors;
@ -1058,4 +1061,35 @@ public class QFieldMetaData implements Cloneable
QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, this.helpContents);
}
/*******************************************************************************
** Getter for inlinePossibleValueSource
*******************************************************************************/
public QPossibleValueSource getInlinePossibleValueSource()
{
return (this.inlinePossibleValueSource);
}
/*******************************************************************************
** Setter for inlinePossibleValueSource
*******************************************************************************/
public void setInlinePossibleValueSource(QPossibleValueSource inlinePossibleValueSource)
{
this.inlinePossibleValueSource = inlinePossibleValueSource;
}
/*******************************************************************************
** Fluent setter for inlinePossibleValueSource
*******************************************************************************/
public QFieldMetaData withInlinePossibleValueSource(QPossibleValueSource inlinePossibleValueSource)
{
this.inlinePossibleValueSource = inlinePossibleValueSource;
return (this);
}
}

View File

@ -26,6 +26,7 @@ import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@ -45,7 +46,7 @@ public class AppTreeNode
private String label;
private List<AppTreeNode> children;
private String iconName;
private QIcon icon;
@ -82,7 +83,7 @@ public class AppTreeNode
if(appChildMetaData.getIcon() != null)
{
// todo - propagate icons from parents, if they aren't set here...
this.iconName = appChildMetaData.getIcon().getName();
this.icon = appChildMetaData.getIcon();
}
}
@ -138,7 +139,18 @@ public class AppTreeNode
*******************************************************************************/
public String getIconName()
{
return iconName;
return (icon == null ? null : icon.getName());
}
/*******************************************************************************
** Getter for icon
**
*******************************************************************************/
public QIcon getIcon()
{
return icon;
}

View File

@ -32,6 +32,7 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QSupplementalAppMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -45,7 +46,7 @@ public class QFrontendAppMetaData
{
private String name;
private String label;
private String iconName;
private QIcon icon;
private List<String> widgets = new ArrayList<>();
private List<AppTreeNode> children = new ArrayList<>();
@ -56,6 +57,7 @@ public class QFrontendAppMetaData
private Map<String, QSupplementalAppMetaData> supplementalAppMetaData;
/*******************************************************************************
**
*******************************************************************************/
@ -63,11 +65,7 @@ public class QFrontendAppMetaData
{
this.name = appMetaData.getName();
this.label = appMetaData.getLabel();
if(appMetaData.getIcon() != null)
{
this.iconName = appMetaData.getIcon().getName();
}
this.icon = appMetaData.getIcon();
List<String> filteredWidgets = CollectionUtils.nonNullList(appMetaData.getWidgets()).stream().filter(n -> metaDataOutput.getWidgets().containsKey(n)).toList();
if(CollectionUtils.nullSafeHasContents(filteredWidgets))
@ -81,6 +79,10 @@ public class QFrontendAppMetaData
List<String> filteredTables = CollectionUtils.nonNullList(section.getTables()).stream().filter(n -> metaDataOutput.getTables().containsKey(n)).toList();
List<String> filteredProcesses = CollectionUtils.nonNullList(section.getProcesses()).stream().filter(n -> metaDataOutput.getProcesses().containsKey(n)).toList();
List<String> filteredReports = CollectionUtils.nonNullList(section.getReports()).stream().filter(n -> metaDataOutput.getReports().containsKey(n)).toList();
//////////////////////////////////////////////////////
// only include the section if it has some contents //
//////////////////////////////////////////////////////
if(!filteredTables.isEmpty() || !filteredProcesses.isEmpty() || !filteredReports.isEmpty())
{
QAppSection clonedSection = section.clone();
@ -174,18 +176,7 @@ public class QFrontendAppMetaData
*******************************************************************************/
public String getIconName()
{
return iconName;
}
/*******************************************************************************
** Setter for iconName
**
*******************************************************************************/
public void setIconName(String iconName)
{
this.iconName = iconName;
return (icon == null ? null : icon.getName());
}
@ -235,4 +226,15 @@ public class QFrontendAppMetaData
{
return supplementalAppMetaData;
}
/*******************************************************************************
** Getter for icon
**
*******************************************************************************/
public QIcon getIcon()
{
return icon;
}
}

View File

@ -33,6 +33,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehaviorForFron
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.help.QHelpContent;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -56,6 +57,7 @@ public class QFrontendFieldMetaData
private List<FieldAdornment> adornments;
private List<QHelpContent> helpContents;
private QPossibleValueSource inlinePossibleValueSource;
private List<FieldBehaviorForFrontend> behaviors;
@ -81,6 +83,7 @@ public class QFrontendFieldMetaData
this.adornments = fieldMetaData.getAdornments();
this.defaultValue = fieldMetaData.getDefaultValue();
this.helpContents = fieldMetaData.getHelpContents();
this.inlinePossibleValueSource = fieldMetaData.getInlinePossibleValueSource();
for(FieldBehavior<?> behavior : CollectionUtils.nonNullCollection(fieldMetaData.getBehaviors()))
{
@ -218,6 +221,17 @@ public class QFrontendFieldMetaData
/*******************************************************************************
** Getter for inlinePossibleValueSource
**
*******************************************************************************/
public QPossibleValueSource getInlinePossibleValueSource()
{
return inlinePossibleValueSource;
}
/*******************************************************************************
** Getter for fieldBehaviors
**

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